目录
逆序打印链表
只需要打印的话,先递归地遍历到最后,再打印
反转链表
需要递归实现
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode* reverse(ListNode* head)
{
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* last = reverse(head->next);
head->next->next = head;
head->next = nullptr;
return last;
}
算法细节
reverse函数返回的是反转后的链表的最后一个结点
head是原来的链表中这个节点的前驱结点
head.next.next就是把这个节点的next设置为其前驱结点
再将其前驱结点的后继设置为null
反转链表前N个节点
需要记录后继节点successor
类似于反转链表,只需要稍作修改
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
ListNode* reverseN(ListNode* head, int n)
{
if (n == 1) {
successor = head->next;
return head;
}
ListNode* last = reverse(head->next, n-1);
head->next->next = head;
head->next = successor;
return last;
}
反转链表的一部分
给一个索引区间 [m, n]
(索引从 1 开始),仅仅反转区间中的链表元素
实际上是在求head的next节点为头结点的链表的[m-1,n-1]区间反转后的结果,一直递归下去直到m=1
// 注意:cpp 代码由 chatGPT🤖 根据我的 java 代码翻译,旨在帮助不同背景的读者理解算法逻辑。
// 本代码还未经过力扣测试,仅供参考,如有疑惑,可以参照我写的 java 代码对比查看。
ListNode* reverseBetween(ListNode* head, int m, int n) {
// base case
if (m == 1) {
return reverseN(head, n);
}
// 前进到反转的起点触发 base case
head->next = reverseBetween(head->next, m - 1, n - 1);
return head;
}
合并两个有序链表
类似于归并排序的合并操作
需要创造一条新链表的时候,可以使用虚拟头结点dummy简化边界情况的处理
有了dummy节点这个占位符,可以避免处理空指针的情况,降低代码的复杂性
dummy作为结果的头节点的虚拟的头节点
-1对一个节点来说是一个不太可能的值,标记着这个节点是一个dummy
指针p负责生成结果链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2)
{
ListNode* dummy = new ListNode(-1);
ListNode* p = dummy;
ListNode* p1 = list1;
ListNode* p2 = list2;
while (p1 != NULL && p2 != NULL)
{
if (p1->val > p2->val)
{
p->next = p2;
p2 = p2->next;
}
else
{
p->next = p1;
p1 = p1->next;
}
p = p->next;
}
if (p2 != NULL)
{
p->next = p2;
}
if (p1 != NULL)
{
p->next = p1;
}
return dummy->next;
}
};
单链表的分解
按照给定值x将链表分为小于x和大于等于x的前后两个部分
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* partition(ListNode* head, int x)
{
ListNode* dummy1 = new ListNode(-1);
ListNode* dummy2 = new ListNode(-1);
ListNode* p1 = dummy1;
ListNode* p2 = dummy2;
ListNode* p = head;
while (p != NULL)
{
if (p->val < x)
{
p1->next = p;
p1 = p1->next;
}
else
{
p2->next = p;
p2 = p2->next;
}
// p = p->next;
// 断开原链表中每个结点的next指针
ListNode* tmp = p->next;
p->next = nullptr;
p = tmp;
}
p1->next = dummy2->next;
return dummy1->next;
}
};
注意其中的插入到新的链表后需要断开原链表中的节点的next指针
但是需要将p->next指针保存下来,用于下一步的遍历
合并K个有序链表
难点在于如何快速得到 k
个节点中的最小节点,接到结果链表上
将链表节点放入一个最小堆,每次从堆顶pop出一个节点后,再push一个当前节点的next进来
复杂度分析
维护k个元素的最小堆的pop和push操作,本质上是每次将插入的元素下降到应该在的位置。最大下降的高度就是logk
所以维护的时间复杂度为O(logk),k为链表的条数
一共有n个节点,所以时间复杂度为O(nlogk)
单链表的倒数第k个节点
难点在于只知道头节点,并且要求只遍历一次
类似于移动窗口
两个指针一个遍历,一个指向另一个指针的倒数第k个前驱结点
删除节点
将x节点的前驱结点和x的后继节点相接
引入dummy防止删除第一个头节点的时候,没有前驱结点的错误
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode* dummy = new ListNode(-1);
dummy->next = head;
ListNode* p1 = dummy;
for (int i = 0; i < (n+1); i++)
{
p1 = p1->next;
}
ListNode* p2 = dummy;
while (p1 != NULL)
{
p1 = p1->next;
p2 = p2->next;
}
p2->next = p2->next->next;
return dummy->next;
}
};
单链表的中点
使用「快慢指针」的技巧:
我们让两个指针 slow
和 fast
分别指向链表头结点 head
每当慢指针 slow
前进一步,快指针 fast
就前进两步,这样,当 fast
走到链表末尾时,slow
就指向了链表中点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* middleNode(ListNode* head)
{
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
};
判断链表是否包含环
快慢指针:
fast和slow相遇说明两个指针进入了环,并且fast超过了slow一圈
如何计算这个环的起点?
当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置
因为假设相遇点离环的起点为m,走的距离为k和2k
k是环的长度 的倍数
k-m是起点和头结点的距离,同时也从相遇点走到环的起点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head)
{
ListNode* slow = head;
ListNode* fast = head;
while (fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
if (fast == slow)
{
break;
}
}
if (fast == NULL || fast->next == NULL)
{
return NULL;
}
slow = head;
while (fast != slow)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
};
链表相交
链表A由n1加上m的公共节点组成
链表B由n2加上m的公共部分组成
所以有n1+m+n2=n2+m+n1
我们可以让 p1
遍历完链表 A
之后开始遍历链表 B
,让 p2
遍历完链表 B
之后开始遍历链表 A
,这样相当于「逻辑上」两条链表接在了一起
如果这样进行拼接,就可以让 p1
和 p2
同时进入公共部分
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
ListNode* p1 = headA;
ListNode* p2 = headB;
while (p1 != p2)
{
if (p1 == NULL)
{
p1 = headB;
}
else
{
p1 = p1->next;
}
if (p2 == NULL)
{
p2 = headA;
}
else
{
p2 = p2->next;
}
}
return p1;
}
};
如果不存在交点,则最后都同时到达null