1. 基础知识
链表基础
链表定义(有构造函数版本):
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数
};
如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!
删除结点:
2.移除链表元素
leetcode链接
思路一: 本题是无头节点的链表,未方便对首元结点进行操作,可以构造一个头节点指向首元结点,对整个链表进行迭代删除操作。(每次删除操作需释放空间)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 设置一个虚拟头节点,本题的链表是无头结点链表
ListNode *dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode *p = dummyHead;
while (p->next != nullptr) {
if (p->next->val == val) { // 如果p->next的val满足条件,则删除p->next
ListNode *tmp = p->next;
p->next = tmp->next;
delete tmp; // 释放被删除结点空间
}
else
p = p->next;
}
head = dummyHead->next;
delete dummyHead;
return head;
}
};
思路二: 递归方法,每次递归使当前链表符合条件。每次对首元结点进行判断,若等于val,则删除当前结点;反之,则保留。removeElements得到一个满足题目的子链表。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) { // 每次递归得到一个符合条件的子链表
if (head == nullptr) // 跳出递归条件
return nullptr;
if (head->val == val) { //若当前首元结点符合删除条件,删除当前结点
ListNode *newHead = removeElements(head->next, val);
delete head;
return newHead;
}
else { // 若不需要删除,则保留当前结点,判断后续子序列
head->next = removeElements(head->next, val);
return head;
}
}
};
3.设计链表
leetcode链接
本题需要自行构造链表和私有成员
使用单链表实现,其他均为链表基本操作:
class MyLinkedList {
public:
struct ListNode {
int val;
struct ListNode* next;
ListNode(int val) :val(val), next(nullptr) {} // 设置缺省值
};
MyLinkedList() {
_size = 0;
_dummyHead = new ListNode(0); // 生成一个空结点作头节点
}
int get(int index) {
if (index > (_size - 1) || index < 0)
return -1;
ListNode *cur = _dummyHead->next;
int curIndex = 0;
while (curIndex++ != index)
cur = cur->next;
return cur->val;
}
void addAtHead(int val) { // 头插
ListNode *cur = new ListNode(val);
cur->next = _dummyHead->next;
_dummyHead->next = cur;
_size++;
return;
}
void addAtTail(int val) { // 尾插
ListNode *tail = _dummyHead;
while (tail->next != nullptr)
tail = tail->next;
ListNode *cur = new ListNode(val);
tail->next = cur;
_size++;
return;
}
void addAtIndex(int index, int val) {
if (index > _size || index < 0)
return;
ListNode *cur = new ListNode(val);
ListNode *ins = _dummyHead; // ins指向插入位置前一个结点(尾插)
int i = -1;
while (i++ != (index - 1))
ins = ins->next;
cur->next = ins->next;
ins->next = cur;
_size++;
return;
}
void deleteAtIndex(int index) {
if (index > (_size - 1) || index < 0)
return;
ListNode *pre = _dummyHead;
ListNode *cur = _dummyHead->next;
int i = 0;
while (i++ != index) {
pre = cur;
cur = cur->next;
}
pre->next = cur->next;
delete cur;
_size--;
return;
}
private: // 设置 "_" 开头,表明是私有成员(是一种代码风格)
int _size;
ListNode* _dummyHead; // 虚拟头节点
};
4. 反转链表
leetcode链接
思路1: 创建一个新的链表,从之前的链表中每次摘出首元元素,头插法插入新链表(使用哨兵结点)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 分别创建两个头节点,为反转前后的两个头节点
ListNode *dummyHead1 = new ListNode(0,head);
ListNode *dummyHead2 = new ListNode(0);
ListNode *cur = head;
// 每次从链表1摘出一个元素,使用头插法插入链表2
while (cur != nullptr) {
dummyHead1->next = cur->next;
cur->next = dummyHead2->next;
dummyHead2->next = cur;
cur = dummyHead1->next;
}
cur = dummyHead2->next;
delete dummyHead1;
delete dummyHead2;
return cur;
}
};
思路2: 通过双指针直接将链表反转(pre表示已翻转的部分链表,cur指向待反转剩余链表的首元元素。每次将首元元素摘下头插),不使用哨兵头节点
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// pre必须初始化,不然要报错
ListNode *pre = NULL;
ListNode *cur = head;
ListNode *tmp; // tmp保存cur->next
while (cur) {
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
思路3: 使用递归的方法,思路与思路2一致。pre每次得到已反转子链表,cur指向未反转首元元素
class Solution {
public:
ListNode* reverse(ListNode* pre, ListNode* cur) {
if (cur == NULL)
return pre;
ListNode *tmp = cur->next;
cur->next = pre;
return reverse(cur, tmp);
}
ListNode* reverseList(ListNode* head) {
return reverse(NULL, head);
}
};
5.两两交换链表中的节点
leetcode链接
思路一:递归,每轮递归交换两个元素,并添加进已交换的链表中
class Solution {
public:
ListNode* swap(ListNode* pre, ListNode* cur) {
if (cur == NULL || cur->next == NULL)
return cur;
ListNode *rear = cur->next;
ListNode *tmp = rear->next;
cur->next = rear->next;
rear->next = cur;
pre->next = rear;
return swap(cur, tmp);
}
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
swap(dummyHead, head);
return dummyHead->next;
}
};
思路二:递归写法2,swapPairs每次得到一个满足条件的子链表(链表的后半部分),交换该子链表的前两个元素,并继续返回新的子链表
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr)
return head;
// 将head与head->next交换
ListNode *newHead = head->next;
head->next = swapPairs(newHead->next); // 将后续子链表操作,使其符合条件,接入在head之后
newHead->next = head;
return newHead;
}
};
6. 删除链表的倒数第N个结点
leetcode链接
思路:使用快慢指针,快指针比慢指针快N个元素,当快指针遍历到尾结点时,慢指针到达应删除节点,删除该节点。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
if (head->next == nullptr)
return nullptr;
// 创建一个虚拟头节点,方便操作
ListNode *dummyHead = new ListNode(0,head);
ListNode *pre = dummyHead;
ListNode *curPre = dummyHead;
ListNode *cur = dummyHead->next; // cur为慢指针,最后指向应删除节点
while (n--) // 将快指针移动到第N个位置
pre = pre->next;
while (pre->next) { // 向后遍历快指针
pre = pre->next;
curPre = cur;
cur = cur->next;
}
curPre->next = cur->next;
delete cur;
head = dummyHead->next;
delete dummyHead;
return head;
}
};
也可将快指针移动n+1个位置,则不需要多设置删除节点的前置节点的指针,而是直接找到该前置节点指针
7. 链表相交
思路: 定义两个指针分别指向两个链表的头节点,将长链表指针移动两链表的差值,使二者剩余结点相同。循环比较两个指针是否相等,直至遍历到尾结点或得到相同指针。
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
// 为A,B创建虚拟头结点
ListNode *dummyHeadA = new ListNode(0);
dummyHeadA->next = headA;
ListNode *dummyHeadB = new ListNode(0);
dummyHeadB->next = headB;
int countA=0, countB=0;
ListNode *cur = dummyHeadA;
while (cur->next) {
countA++;
cur = cur->next;
}
cur = dummyHeadB;
while (cur->next) {
countB++;
cur = cur->next;
}
int gap = abs(countA - countB);
// 将长的链表的当前指针移动二者之间的差值,保证后续长度相等
if (countA > countB) {
cur = dummyHeadA->next;
while (gap--)
cur = cur->next;
}
else {
cur = dummyHeadB->next;
while (gap--)
cur = cur->next;
}
// 找到短链表
ListNode *shortCur = countA <= countB ? dummyHeadA->next : dummyHeadB->next;
while (cur) { // 循环比较指针是否相等
if (cur == shortCur)
break;
// 将两边的指针向后移
cur = cur->next;
shortCur = shortCur->next;
}
delete dummyHeadA, dummyHeadB;
return cur;
}
};
8.环形链表Ⅱ
leetcode链接
学习链接:把环形链表讲清楚
判断是否有环: 使用快慢指针,快指针每次移动两格,慢指针每次移动一格。当快慢指针相遇时,则说明有环,则必定在环中相遇。
问:为什么若有环,则快慢指针必定相遇?
答:因为快指针先进入环,在环中若干圈后,慢指针进入环。而快指针每次移动两格,慢指针每次移动一格,则快指针每次以相对1格的速度接近慢指针。因为1必定是环中元素数量的因数,所以必定会相遇。
找到环的入口: 利用数学公式,判断环的入口。假设头节点到环的入口节点的节点数为x(包含头节点,不包含入口节点),入口节点到快慢指针相遇节点的节点数为(既不包含入口节点,又不包含相遇节点),相遇节点到入口节点的节点数为z(即包含相遇节点,又包含入口节点)。
那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:
即2 * (x+y) = x + y+n(y+z)
经整理: x = (n - 1) (y + z) + z
表示快指针转了n-1圈后,相遇节点到入口节点的距离 = 慢指针到入口节点的距离
问:为什么slow指针相遇时,移动的距离是x+y,而不是x+y+n(y+z),即多转了n圈
答:考虑到slow指针走一圈的位移,fast指针能位移2圈,因此,slow指针未走完一圈即会相遇。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
// 定义快慢指针
ListNode *fast = head;
ListNode *slow = head;
while(fast != nullptr && fast->next != nullptr){
fast = fast->next->next;
slow = slow->next;
if (fast == slow) { //若是有环,则会相等
ListNode *cur = head;
// 同时从起点和相遇节点开始遍历,若二者相遇,则一定是在入口节点
while (cur != slow) {
cur = cur->next;
slow = slow->next;
}
return cur;
}
}
return nullptr;
}
};
9.总结
链表总结
图片来源: 代码随想录知识星球 (opens new window)成员:海螺人