链表(Linked List)是一种常见的数据结构,它可以动态地插入和删除元素,不需要像数组那样预先分配固定大小的内存。链表中的每个元素称为节点(Node),每个节点包含一个数据值和一个指向下一个节点的指针。本教学将涵盖以下知识点:
单向链表中的每个节点只有一个指向下一个节点的指针。定义一个节点结构体:
struct Node {
int data; // 数据域
Node* next; // 指向下一个节点的指针
};
双向链表中的每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。定义一个节点结构体:
struct DoublyNode {
int data; // 数据域
DoublyNode* prev; // 指向前一个节点的指针
DoublyNode* next; // 指向下一个节点的指针
};
以下是链表的一些基本操作:
循环链表是另一种链表类型,其最后一个节点的下一个指针指向头节点,形成一个环。循环链表可以是单向的也可以是双向的。
struct CircularNode {
int data;
CircularNode* next;
};
struct DoublyCircularNode {
int data;
DoublyCircularNode* prev;
DoublyCircularNode* next;
};
这仅仅是关于链表的一些基本知识点。为了真正理解和掌握链表,你需要实际编写代码并实现链表及其基本操作。
链表是一种线性数据结构,它由一系列节点组成,每个节点包含两个部分:数据部分和指针部分。数据部分用来存储节点的数据,指针部分用来指向下一个节点。在C++中,可以使用以下方式定义链表节点的结构体:
struct ListNode {
int val; // 数据部分
ListNode* next; // 指针部分,指向下一个节点
ListNode(int x) : val(x), next(nullptr) {} // 构造函数
};
其中,val
表示节点的数据,next
指向下一个节点的指针。构造函数用来初始化节点的数据和指针部分。
链表的创建指的是将一系列节点按照一定的顺序组成链表。可以使用循环语句和指针来创建链表,例如,下面的代码创建了一个包含3个节点的链表:
ListNode* head = nullptr; // 链表头指针
ListNode* tail = nullptr; // 链表尾指针
for (int i = 1; i <= 3; i++) {
ListNode* node = new ListNode(i); // 创建一个新节点
if (head == nullptr) { // 如果链表为空,则将头指针和尾指针都指向新节点
head = node;
tail = node;
} else { // 如果链表不为空,则将新节点插入到尾部,并更新尾指针
tail->next = node;//原尾节点的next域指向下一节点
tail = node;//更新
}
}
在创建链表时,需要注意链表的头指针和尾指针的初始化,以及新节点的插入方式。
链表的访问指的是根据节点的位置或数据来访问链表中的节点。可以使用循环语句和指针来遍历链表,例如,下面的代码遍历了上面创建的链表,并输出了每个节点的数据:
ListNode* p = head; // 指向链表头节点的指针
while (p != nullptr) {
cout << p->val << " ";
p = p->next; // 移动指针到下一个节点
}
在访问链表节点时,需要注意指针的移动方式,以避免访问空指针或越界。
链表的插入指的是将一个新节点插入到链表中的某个位置。可以使用指针来找到要插入的位置,并将新节点插入到该位置的后面,例如,下面的代码将一个新节点插入到上面创建的链表的第2个位置:
ListNode* node = new ListNode(10); // 创建一个新节点
ListNode* p = head; // 指向链表头节点的指针
for (int i = 1; i < 2; i++) { // 移动指针到要插入的位置的前面
p = p->next;
}
node->next = p->next; // 将新节点插入到该位置的后面
p->next = node;
在插入新节点时,需要注意指针的移动方式,以及新节点的插入位置。
链表的删除指的是将链表中的某个节点删除。可以使用指针来找到要删除的节点,并将该节点从链表中删除,例如,下面的代码将上面创建的链表的第2个节点删除:
ListNode* p = head; // 指向链表头节点的指针
for (int i = 1; i < 2; i++) { // 移动指针到要删除的节点的前面
p = p->next;
}
链表的反转指的是将链表中的所有节点按照相反的顺序重新排列。可以使用三个指针来实现链表的反转,例如,下面的代码实现了上面创建的链表的反转:
ListNode* prev = nullptr; // 指向前一个节点的指针
ListNode* curr = head; // 指向当前节点的指针
while (curr != nullptr) {
ListNode* next = curr->next; // 指向下一个节点的指针
curr->next = prev; // 将当前节点的指针指向前一个节点
prev = curr; // 移动指针到下一个节点
curr = next;
}
head = prev; // 将反转后的链表头指针指向最后一个节点
在反转链表时,需要注意指针的移动方式,以及链表头指针的更新。
链表的合并指的是将两个已经排好序的链表合并成一个新的链表。可以使用两个指针来遍历两个链表,并将它们的节点按照一定的顺序合并到一个新的链表中,例如,下面的代码将两个已经排好序的链表合并成一个新的链表:
ListNode* p = head1; // 指向第一个链表的指针
ListNode* q = head2; // 指向第二个链表的指针
ListNode* dummy = new ListNode(0); // 创建一个虚拟节点
ListNode* r = dummy; // 指向新链表的指针
while (p != nullptr && q != nullptr) {
if (p->val < q->val) {
r->next = p; // 将第一个链表中的节点插入到新链表中
p = p->next; // 移动指针到下一个节点
} else {
r->next = q; // 将第二个链表中的节点插入到新链表中
q = q->next; // 移动指针到下一个节点
}
r = r->next; // 移动指针到新链表的最后一个节点
}
if (p != nullptr) {
r->next = p; // 将第一个链表中剩余的节点插入到新链表中
} else {
r->next = q; // 将第二个链表中剩余的节点插入到新链表中
}
head = dummy->next; // 将新链表头指针指向第一个节点
在合并链表时,需要注意指针的移动方式,以及新链表的头指针的更新。
链表的长度指的是链表中节点的个数。可以使用循环语句和指针来遍历链表,并计算链表中节点的个数,例如,下面的代码计算了上面创建的链表的长度:
ListNode* p = head; // 指向链表头节点的指针
int len = 0; // 链表长度
while (p != nullptr) {
len++;
p = p->next; // 移动指针到下一个节点
}
cout << "链表的长度为:" << len << endl;
在计算链表长度时,需要注意指针的移动方式,以避免访问空指针或越界。
链表的清空指的是将链表中所有节点删除,并释放它们占用的内存空间。可以使用循环语句和指针来遍历链表,并释放每个节点的内存空间,例如,下面的代码清空了上面创建的链表:
ListNode* p = head; // 指向链表头节点的指针
while (p != nullptr) {
ListNode* tmp = p; // 指向当前节点的指针