参考链接: https://leetcode-cn.com/leetbook/detail/linked-list/
与数组类似,链表也是一种线性数据结构。链表中的每个元素实际上都是一个单独的对象,而所有对象都通过每个元素中的引用字段链接在一起。
链表有两种类型:单链表和双链表。
单链表
单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。
1. 结点结构定义:
// 单链表的定义
struct SinglyListNode {
int val; // 当前结点的值
SinglyListNode * next; // 指向下一个结点的指针/引用
SinglyListNode(int x): val(x), next(NULL) {}
}
多数情况下,将使用头结点(第一个结点)来表示整个列表。
2. 单链表操作:
访问:
与数组不同,我们无法在常量时间内访问单链表中的随机元素。如果我们想要获得第i个元素,必须从头结点逐个遍历。因此,按索引来访问元素平均花费O(N)时间,其中N是链表长度。
添加:
与数组不同,我们不需要将所有元素移动到插入元素之后。因此可以在O(1)时间复杂度中将新结点插入到链表中,这非常高效。
如果想在给定的结点p之后添加新值val,做法为:
- 使用新值val初始化新结点cur;
- 将新结点cur的next字段链接到p的下一个结点q;
- 将p中的next字段链接到cur。
由于我们使用头结点代表整个链表,因此在链表开头添加新结点时需要更新头结点head,做法为:
- 初始化一个新结点cur;
- 将新结点链接到我们的原始头结点head;
- 将cur指定为head。
删除:
如果从单链表中删除现有结点cur,分两步完成:
- 找到cur的上一个结点p和下一个结点q;
- 接下来链接p到cur的下一个结点q。
需要注意的是,使用cur的参考字段很容易找到q,但是必须从头结点遍历链表来找到p,它的平均时间为O(N)。因此,删除结点的时间复杂度为O(N),空间复杂度为O(1)。
当我们想删除头结点时,可以简单地将下一个结点分配给head来实现。
设计链表:
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
代码实现:
// 代码参考链接:https://leetcode-cn.com/problems/design-linked-list/solution/cdan-lian-biao-shi-xian-xiang-xi-zhu-shi-by-carlsu/
#include<iostream>
using namespace std;
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int x) : val(x), next(nullptr) {}
};
int len;
LinkedNode* dummyhead; // 虚拟头结点,不是真正的头结点,其下一个结点表示头结点head
// 初始化链表
MyLinkedList() {
dummyhead = new LinkedNode(0);
len = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index >= len || index < 0)
return -1;
LinkedNode* cur = dummyhead->next; // cur初始化为头结点head=dummyhead->next
while (index--) { // 遍历链表,得到第index个节点cur
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = dummyhead->next;
dummyhead->next = newNode;
len++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = dummyhead; // cur初始化为虚拟头节点
while (cur->next != nullptr) { // 遍历链表,直到cur的next节点为空
cur = cur->next;
}
cur->next = newNode;
len++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
void addAtIndex(int index, int val) {
if (index > len)
return;
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = dummyhead;
while (index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
len++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= len || index < 0)
return;
LinkedNode* cur = dummyhead;
while (index--) {
cur = cur->next;
}
LinkedNode* temp = cur->next;
cur->next = cur->next->next;
delete temp;
len--;
}
// 打印链表方式
void printLinkedList() {
LinkedNode* cur = dummyhead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
};
int main() {
int index = 1;
MyLinkedList* obj = new MyLinkedList(); // 实例化MyLinkedList对象obj并调用
int param_1 = obj->get(index); // 链表为空,返回-1
obj->addAtHead(1); // 链表变为1
obj->addAtTail(3); // 链表变为1->3
obj->addAtIndex(1, 2); // 链表变为1->2->3
int param_2 = obj->get(1); // 返回2
obj->deleteAtIndex(1); //现在链表是1->3
int param_3 = obj->get(1); // 返回3
cout << param_1 << " " << param_2 << " " << param_3 << endl; // 输出为-1 2 3
}
3. 链表中的双指针:
从一个经典的链表问题开始:
给定一个链表,判断链表中是否有环。
虽然该问题可以使用哈希表来解决,但是使用双指针的空间复杂度更低。
当我们在链表中使用两个速度不同的指针时:
- 如果没有环,快指针将停在链表的末尾;
- 如果有环,快指针最终将与慢指针相遇。
对于两个指针的速度:一个安全的选择是每次移动慢指针一步,移动快指针两步。每次迭代,快指针将多移动一步。如果环的长度为M,经过M次迭代后,快指针会多绕环一周,并赶上慢指针。
相关的LeetCode例题有:
- 141.环形链表
- 142.环形链表II
- 160.相交链表
以142题环形链表II为例,使用双指针的代码实现如下:
class Solution {
public:
// 定义链表节点结构体
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
// 快慢指针第一次相遇时,slow节点与环入口节点的距离正好和链表头节点与环入口节点的距离相等
ListNode* detectCycle(ListNode* head) {
ListNode* slow = head; // 慢指针
ListNode* fast = head; // 快指针
while (true) {
if (fast == NULL || fast->next == NULL)
return NULL;
slow = slow->next;
fast = fast->next->next;
if (slow == fast) // 找到第一次相遇节点,停止循环
break;
}
ListNode* temp = head;
while (slow != temp) {
slow = slow->next;
temp = temp->next;
}
return slow;
}
};
4. 单链表的常见考题:
206.反转链表
描述:反转一个单链表。
// 方法:双指针迭代
class Solution {
public:
// 定义链表节点结构体
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* reverseList(ListNode* head) {
// 申请两个指针prev和cur,其中prev初始为nullptr
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
ListNode* tmp = cur->next; // 记录当前节点的下一个节点
cur->next = prev; // 将当前节点指向prev
// prev和cur都前进一步
prev = cur;
cur = tmp;
}
return prev;
}
};
21.合并两个有序链表
描述:将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
// 方法:迭代
class Solution {
public:
// 定义链表节点结构体
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dummyHead = new ListNode(-1); // 定义一个虚拟头结点
ListNode* prev = dummyHead; // prev指针
while (l1 != nullptr && l2 != nullptr) {
if (l1->val < l2->val) {
prev->next = l1;
l1 = l1->next;
}
else {
prev->next = l2;
l2 = l2->next;
}
prev = prev->next;
}
// 循环结束后最多还有一个未合并完,可直接将链表末尾指向未合并完的链表即可
prev->next = l1 == nullptr ? l2 : l1;
return dummyHead->next;
}
};