通关算法题之 ⌈链表⌋

链表

删除元素

203. 移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例

img
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if(!head) return head;
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* p = dummy;
        while(p && p->next){
            if(p->next->val == val){
                p->next = p->next->next;
            }else{
                p = p->next;
            }
        }
        return dummy->next;
    }
};

83. 删除排序链表中的重复元素

给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 ,返回 已排序的链表 。

示例:

img
输入:head = [1,1,2,3,3]
输出:[1,2,3]
class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        if(!head) return head;
        ListNode* p = head;
        while(p->next){
            if(p->next->val == p->val){
                p->next = p->next->next;
            }else{
                p = p->next;
            }
        }
        return head;
    }
};

相交链表

160. 相交链表

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null。图示两个链表在节点 c1 开始相交。题目数据 保证 整个链式结构中不存在环。注意,函数返回结果后,链表必须 保持其原始结构

img

这题难点在于,由于两条链表的长度可能不同,两条链表之间的节点无法对应:

image-20220505184443179

如果用两个指针 p1p2 分别在两条链表上前进,并不能同时走到公共节点,也就无法得到相交节点 c1解决这个问题的关键是,通过某些方式,让 p1p2 能够同时到达相交节点 c1 —— 使用双指针。

如果用两个指针 p1p2 分别在两条链表上前进,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。

如果这样进行拼接,就可以让 p1p2 同时进入公共部分,也就是同时到达相交节点 c1

image-20220505184819556

那如果两个链表没有相交点,是否能够正确的返回 null 呢?这个逻辑可以覆盖这种情况的,相当于 c1 节点是 null 空指针,可以正确返回 null。

class Solution {
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
        ListNode* p1 = headA, * p2 = headB;
        while(p1 != p2){
            if(p1) p1 = p1->next;
            else p1 = headB;
            if(p2) p2 = p2->next;
            else p2 = headA;
        }
        return p1;
    }
};

合并链表

21、合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:

img
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

用指针分别访问两个链表,根据题意比较节点的大小,赋值到新的链表。

1

这个算法的逻辑类似于「拉拉链」,l1, l2 类似于拉链两侧的锯齿,指针 p 就好像拉链的拉索,将两个有序链表合并。合并后 list1 和 list2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可。

代码中还用到一个链表的算法题中是很常见的「虚拟头结点」技巧,也就是 dummy 节点,它相当于是个占位符,可以避免处理空指针的情况,降低代码的复杂性。

struct ListNode{
    int val;
    ListNode* next;
    ListNode(int x): val(x), next(nullptr){}
}

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        //虚拟头节点
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy, *p1 = list1, *p2 = list2;
        while(p1 && p2){
            // 比较 p1 和 p2 两个指针,将值较小的的节点接到 p 指针
            if(p1->val > p2->val){
                p->next = p2;
                p2 = p2->next;
            }else{
                p->next = p1;
                p1 = p1->next;
            }
            p = p->next;
        }
        p->next = !p1 ? p2 : p1;
        return dummy->next;
    }
};

23、合并 k 个有序链表

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

本题是上一道题目21. 合并两个有序链表 的延伸,利用 优先级队列(二叉堆) 进行节点排序即可。

具体步骤

  1. 新建虚拟节点dummy,指向合并后的链表, 新建一个优先级队列priority_queue
  2. 将所有的链表的第一个元素加入队列;
  3. 将队列的第一元素出队列,插入到新链表的尾部;
  4. 将第一元素的下一个元素入队,比较剩下的链表的第1个元素和第i个链表的第2个元素;
  5. 重复3-4步, 得到新的队列。
class Solution {
public:
    struct Comp{
        //小根堆,从小到大排序
        bool operator()(ListNode* l1, ListNode* l2){
            return l1->val > l2->val;
        }
    }; 
    
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        //优先级队列,最小堆
        priority_queue<ListNode*, vector<ListNode*>, Comp> pq;
        //虚拟节点
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;
        // 将所有的链表的第一个元素加入队列
        for(ListNode* head : lists){
            if(head) pq.push(head);
        }
        while(!pq.empty()){
            // 获取最小节点,接到结果链表中
            ListNode *node = pq.top(); pq.pop();
            p->next = node;
            p = p->next;
            if(node->next) pq.push(node->next);
        }
        return dummy->next;
    }
};

这个算法是面试常考题,它的时间复杂度是多少呢?

优先级队列 pq 中的元素个数最多是 k,所以一次 poll 或者 add 方法的时间复杂度是 O(logk);所有的链表节点都会被加入和弹出 pq所以算法整体的时间复杂度是 O(Nlogk),其中 k 是链表的条数,N 是这些链表的节点总数

86. 分隔链表

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对位置。

示例

img
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

只需要遍历链表的所有节点,小于x的放到一个小的链表中,大于等于x的放到一个大的链表中,最后再把这两个链表串起来即可。

image.png
class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        ListNode* smaller = new ListNode(0);
        ListNode* larger = new ListNode(0);
        ListNode* p1 = smaller, * p2 = larger, * p = head;
        while(p){
            if(p->val < x){
                p1->next = p;
                p1 = p1->next;
            }else{
                p2->next = p;
                p2 = p2->next;
            }
            p = p->next;
        }
        p1->next = larger->next;
        p2->next = nullptr; // 注意这一步
        return smaller->next;
    }
};

328. 奇偶链表

给定单链表的头节点 head ,将所有索引为奇数的节点和索引为偶数的节点分别组合在一起,然后返回重新排序的列表。第一个节点的索引被认为是奇数 , 第二个节点的索引为偶数 ,以此类推。

请注意,偶数组和奇数组内部的相对顺序应该与输入时保持一致。你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。

示例

img
输入: head = [1,2,3,4,5]
输出: [1,3,5,2,4]

分别维护两个链表,一个链表接入索引为奇数的节点,另一个链表接入索引为偶数的节点,然后组合两个链表。

class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        ListNode* odd = new ListNode(0);
        ListNode* even = new ListNode(0);
        ListNode* p1 = odd, * p2 = even, * p = head;
        int index = 1;
        while(p){
            if(index % 2 == 1){
                p1->next = p;
                p1 = p1->next;
            }else{
                p2->next = p;
                p2 = p2->next;
            }
            p = p->next;
            index++;
        }
        p1->next = even->next;
        p2->next = nullptr;
        return odd->next;
    }
};

倒数节点

剑指 Offer 22. 链表中倒数第k个节点

输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。

例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。

示例:

给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.

如何只遍历一次链表,就算出倒数第 k 个节点?可以做到的,如果是面试问到这道题,面试官肯定也是希望你给出只需遍历一次链表的解法。

首先,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步:

image-20220505145454803

趁这个时候,再用一个指针 p2 指向链表头节点 head,此时若把p1当作链表尾部的NULL,则p2 就是倒数第k个节点。:

image-20220505145900194

那如何让p1成为真正的尾节点而且p1p2之间的位置关系保持不变呢?俩指针一同向后移动呗,直到p1 == NULL为止,此时p1就是真正的倒数第k个节点。

image-20220505150130107

这道题目重点在于理解双指针移动的原理与方法,方法理解了,代码实现就非常简单了。

class Solution {
public:
    ListNode* getKthFromEnd(ListNode* head, int k) {
        ListNode* p1 = head;
        //p1 移动 k 步
        for(int i = 0; i < k; i++){
            p1 = p1->next;
        }
        ListNode* p2 = head;
        //p1 和 p2 一同向后移动
        while(p1){
            p1 = p1->next;
            p2 = p2->next;
        }
        return p2;
    }
};

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例:

img
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

要删除倒数第 n 个节点,就得获得倒数第 n + 1 个节点的指针。获取单链表的倒数第 k 个节点,就是想考察 双指针技巧 中快慢指针的运用,一般都会要求只遍历一次链表,就算出倒数第 k 个节点。

注意:使用了虚拟头结点的技巧,也是为了防止出现空指针的情况。比如说链表总共有 5 个节点,题目就让你删除倒数第 5 个节点,也就是第一个节点,那按照算法逻辑,应该首先找到倒数第 6 个节点,但第一个节点前面已经没有节点了,这就会出错。但有了虚拟头节点 dummy 的存在,就避免了这个问题,能够对这种情况进行正确的删除。

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummy = new ListNode(-1);
        dummy->next = head;
        // 删除倒数第 n 个,要先找倒数第 n + 1 个节点
        ListNode* p = find(dummy, n + 1);
        // 删掉倒数第 n 个节点
        p->next = p->next->next;
        return dummy->next;
    }

    ListNode* find(ListNode* head, int n){
        ListNode* p1 = head;
        for(int i = 0; i < n; i++){
            p1 = p1->next;
        }
        ListNode* p2 = head;
        while(p1){
            p1 = p1->next;
            p2 = p2->next;
        }
        return p2;
    }
};

环形链表

876. 链表的中间结点

题目:给定一个头结点为 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

如果想一次遍历就得到中间节点,就要使用「快慢指针」的技巧:

我们让两个指针 slowfast 分别指向链表头结点 head每当慢指针 slow 前进一步,快指针 fast 就前进两步,这样当 fast 走到链表末尾时,slow 就指向了链表中点

上述思路的代码实现如下:

class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode* fast = head, * slow = head;
        // 快指针走到末尾时停止
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
};

需要注意的是,如果链表长度为偶数,也就是说中点有两个的时候,我们这个解法返回的节点是靠后的那个节点。另外,这段代码稍加修改就可以直接用到判断链表成环的算法题上。

141. 环形链表

给你一个链表的头节点 head ,判断链表中是否有环。如果链表中存在环,则返回 true ; 否则,返回 false

示例:

img
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。

经典题目了,要使用双指针技巧中的快慢指针,每当慢指针 slow 前进一步,快指针 fast 就前进两步。

如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode* fast = head, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            // 快慢指针相遇,说明含有环
            if(fast == slow) return true;
        }
        // 不含有环
        return false;
    }
};

142. 环形链表 II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null不允许修改 链表。

示例:

img
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

**当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。**为什么要这样呢?

假设快慢指针相遇时,慢指针 slow 走了 k 步,那么快指针 fast 一定走了 2k 步:

image-20220505155433363

fast 一定比 slow 多走了 k 步,这多走的 k 步其实就是 fast 指针在环里转圈圈,所以 k 的值就是环长度的「整数倍」。

假设相遇点距环的起点的距离为 m,那么结合上图的 slow 指针,环的起点距头结点 head 的距离为 k - m,也就是说如果从 head 前进 k - m 步就能到达环起点。

巧的是,如果从相遇点继续前进 k - m 步,也恰好到达环起点。因为结合上图的 fast 指针,从相遇点开始走k步可以转回到相遇点,那走 k - m 步肯定就走到环起点了:

image-20220505155537778

所以,只要我们把快慢指针中的任一个重新指向 head,然后两个指针同速前进,k - m 步后一定会相遇,相遇之处就是环的起点了。

class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
            if(fast == slow) break;
        }
        // fast 遇到空指针说明没有环
        if(!fast || !fast->next) return NULL;
        // 重新指向头结点
        slow = head;
        // 快慢指针同步前进,相交点就是环起点
        while(slow != fast){
            fast = fast->next;
            slow = slow->next;
        }
        return slow;
    }
};

翻转链表

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例:

img
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

解法一:递归法

class Solution {
public:
    //定义:将以head为起点的链表反转,并返回反转之后的头结点
    ListNode* reverseList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* last = reverseList(head->next);
        head->next->next = head;
        head->next = nullptr;
        return last;
    }
};

对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 reverse 函数定义是这样的:

输入一个节点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点

image-20220508092702168

那么输入 reverse(head) 后,会在这里进行递归:

ListNode* last = reverseList(head->next);
image-20220508092851331

reverseList(head->next) 执行完成后,整个链表就成了这样:

image-20220508093035486

并且根据函数定义,reverse 函数会返回反转之后的头结点,我们用变量 last 接收了。

现在再来看下面的代码:

head->next->next = head;
image-20220508093159593

接下来:

head->next = NULL;
return last;
image-20220508093304789

这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意:

1、递归函数要有 base case,也就是这句:

if(!head || !head->next) return head;

意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。

2、当链表递归反转之后,新的头结点是 last,而之前的 head 变成了最后一个节点,别忘了链表的末尾要指向 null:

head->next = NULL;

理解了这两点后,我们就可以进一步深入了,接下来的问题其实都是在这个算法上的扩展。

解法二:迭代法,双指针法

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, * cur = head, * tmp = head;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

注意:

  • precur指针都要向后移动,所以还要记录cur后一个节点的指针tmp,因为cur->next = pre改变了cur的指向,cur下一个节点无法获得,所以要在该语句之前用tmp提前记录;
  • while循环结束后,cur指向NULLpre指向翻转后链表的头节点,所以要返回pre
  • 还可理解为在[head, NULL)区间(左闭右开)内翻转链表。

92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

示例:

img
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

首先思考一下:如何反转链表前 N 个节点

比如说对于下图链表,执行 reverseN(head, 3)

image-20220508094103751

解决思路和反转整个链表差不多,只要稍加修改即可:

ListNode* successor = NULL; // 后驱节点

// 定义:反转以 head 为起点的 n 个节点,返回新的头结点
ListNode* reverseN(ListNode* head, int n) {
    if (n == 1) {
        // 记录第 n + 1 个节点
        successor = head->next;
        return head;
    }
    // 以 head->next 为起点,需要反转前 n - 1 个节点
    ListNode* newHead = reverseN(head->next, n - 1);
    head->next->next = head;
    // 让反转之后的 head 节点和后面的节点连起来
    head->next = successor;
    return newHead;
}

具体的区别:

1、base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点

2、刚才我们直接把 head->next 设置为 NULL,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。

image-20220508094450123

这个函数能看懂,就离实现「反转一部分链表」不远了。

首先,如果 left == 1,就相当于反转链表开头的 right 个元素,也就是我们刚才实现的功能;

如果 right != 1 怎么办?如果我们把 head 的索引视为 1,那么我们是想从第 left 个元素开始反转;如果把 head->next 的索引视为 1 呢?那么相对于 head->next,反转的区间应该是从第 left - 1 个元素开始的;那么对于 head->next->next 呢……

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        if(left == 1) return reverseN(head, right);
        // 前进到反转的起点触发 base case
        head->next = reverseBetween(head->next, left - 1, right - 1);
        return head;
    }
    
    ListNode* successor = nullptr;
    ListNode* reverseN(ListNode* head, int n){
        if(n == 1){
            successor = head->next;
            return head;
        }
        ListNode* last = reverseN(head->next, n - 1);
        head->next->next = head;
        head->next = successor;
        return last;
    }
};

24. 两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例

img
输入:head = [1,2,3,4]
输出:[2,1,4,3]

解法一:递归法

class Solution {
public:
    // 定义:返回链表节点两两交换后的头节点
    ListNode* swapPairs(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* newHead = head->next;
        head->next = swapPairs(newHead->next);
        newHead->next = head;
        return newHead;
    }
};

解法二:迭代法

初始时,cur指向虚拟头结点,然后进行如下三步:

image-20220510111404759

打羊胎素展开链表:

image-20220510111531466
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* p = dummy;
        while(p->next && p->next->next){
            ListNode* tmp1 = p->next;
            ListNode* tmp2 = tmp1->next;
            ListNode* tmp3 = tmp2->next;
            p->next = tmp2;
            tmp2->next = tmp1;
            tmp1->next = tmp3;
            p = tmp1;
        }
        return dummy->next;
    }
};

25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例:

img
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

输入 headreverseKGroup 函数能够把以 head 为头的这条链表进行翻转。我们要充分利用这个递归函数的定义,把原问题分解成规模更小的子问题进行求解。

1、先反转以 head 开头的 k 个元素

image-20220508113128936

2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数

image-20220508113301946

3、将上述两个过程的结果连接起来

image-20220508113408376

4、最后函数递归完成之后就是这个结果,完全符合题意:

image-20220508113522192
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if(!head) return nullptr;
        ListNode* a = head, * b = head;
        // 区间 [a, b) 包含 k 个待反转元素
        for(int i = 0; i < k; i++){
            // 不足 k 个,不需要反转,base case
            if(!b) return head;
            b = b->next;
        }
        // 反转前 k 个元素
        ListNode* newHead = reverse(a, b);
        // 递归反转后续链表并连接起来
        a->next = reverseKGroup(b, k);
        return newHead;
    }
	// 定义:反转区间 [a, b) 的元素,注意是左闭右开
    ListNode* reverse(ListNode* a, ListNode* b){
        ListNode* pre =nullptr, * cur = a, * nxt = a;
        while(cur != b){
            nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
};

这里使用迭代法来写翻转链表reverse函数,其中三个指针的移动过程如图所示:

8

61. 旋转链表

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

img

示例

输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]

假设链表的长度为len,为了将链表每个节点向右移动 k 个位置,只需要将链表的后 k % len个节点移动到链表的最前面,然后将链表的后k % len个节点和前 len - k个节点连接到一块即可。

1、首先遍历整个链表,求出链表的长度n,并找出链表的尾节点tail

image-20220510114658985

2、由于k可能很大,所以我们令 k = k % n,然后再次从头节点head开始遍历,找到第n - k个节点p,那么1 ~ p是链表的前 n - k个节点,p+1 ~ n是链表的后k个节点。

image-20220510114840024

3、接下来就是依次执行 tail->next = headhead = p->nextp->next = nullptr,将链表的后k个节点和前 n - k个节点拼接到一块,并让head指向新的头节点p->next,新的尾节点即p节点的next指针指向null

image-20220510115248903

4、最后返回链表的新的头节点head

class Solution {
public:
    ListNode* rotateRight(ListNode* head, int k) {
        if(!head || !k) return head;
        ListNode* p = head;
        int n = 1;
        while(p->next){
            n++;
            p = p->next;
        }
        k = k % n;
        ListNode* tail = head;
        for(int i = 1; i < n - k; i++){
            tail = tail->next;
        }
        p->next = head;
        ListNode* newHead = tail->next;
        tail->next = nullptr;
        return newHead;
    }
};

链表重排

143. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:L0 → L1 → … → Ln - 1 → Ln
请将其重新排列后变为:L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …
不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例

输入:head = [1,2,3,4]
输出:[1,4,2,3]

注意观察链表是怎么变化的,方法:寻找链表中点 + 链表逆序 + 合并链表

目标链表即为将原链表的左半端和反转后的右半端合并后的结果,这样我们的任务即可划分为三步:

  • 找到原链表的中点(参考「876. 链表的中间结点」),使用快慢指针来 O(N) 地找到链表的中间节点。
  • 将原链表的右半端反转(参考「206. 反转链表」),使用迭代法实现链表的反转。
  • 将原链表的两端合并,因为两链表长度相差不超过 1,因此直接合并即可。
image-20220510210809294 image-20220510211017122
class Solution {
public:
    void reorderList(ListNode* head) {
        if(!head) return;
        ListNode* mid = middleNode(head);
        ListNode* l1 = head;
        ListNode* l2 = mid->next;
        mid->next = nullptr;
        l2 = reverse(l2);
        merge(l1, l2);
    }

    ListNode* middleNode(ListNode* head){
        ListNode* slow = head, * fast = head->next;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

    ListNode* reverse(ListNode* head){
        ListNode* pre = nullptr, * cur = head;
        while(cur){
            ListNode* tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }

    void merge(ListNode* l1, ListNode* l2){
        ListNode* p1, * p2;
        while(l1 && l2){
            p1 = l1->next;
            p2 = l2->next;
            l1->next = l2;
            l1 = p1;
            l2->next = l1;
            l2 = p2;
        }
    }
};

148. 排序链表

给你链表的头结点 head ,请将其按 升序 排列并返回排序后的链表 。

示例

img
输入:head = [4,2,1,3]
输出:[1,2,3,4]

通过递归实现链表归并排序,有以下两个环节:

Picture2.png
class Solution {
public:
    // 定义:排序链表
    ListNode* sortList(ListNode* head) {
        if(!head || !head->next) return head;
        ListNode* mid = middleNode(head);
        ListNode* l1 = head, * l2 = mid->next;
        mid->next = nullptr;
        // 注意:两链表排序之后再合并
        return merge(sortList(l1), sortList(l2));
    }
	// 定义:找链表的中心节点
    ListNode* middleNode(ListNode* head){
        // 注意 fast = head->next,若有两个中心节点则找到靠左的那个
        ListNode* fast = head->next, * slow = head;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }
	// 定义:合并两个链表
    ListNode* merge(ListNode* list1, ListNode* list2) {
        if(!list1 && !list2) return list1;
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy, *p1 = list1, *p2 = list2;
        while(p1 && p2){
            if(p1->val > p2->val){
                p->next = p2;
                p2 = p2->next;
            }else{
                p->next = p1;
                p1 = p1->next;
            }
            p = p->next;
        }
        p->next = !p1 ? p2 : p1;
        return dummy->next;
    }
};

234. 回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

示例:

img
输入:head = [1,2,2,1]
输出:true

双指针技巧,从两端向中间逼近即可:

bool isPalindrome(string s) {
    int left = 0, right = s.length - 1;
    while (left < right) {
        if (s[left] != s[right]) return false;
        left++; right--;
    }
    return true;
}

以上代码很好理解,因为回文串是对称的,所以正着读和倒着读应该是一样的,这一特点是解决回文串问题的关键

解法一:利用后序遍历

如果想正序打印链表中的 val 值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作。

class Solution {
public:
    ListNode* left;
    bool res = true;
    bool isPalindrome(ListNode* head) {
        left = head;
        traverse(head);
        return res;
    }

    void traverse(ListNode* right){
        if(!right) return;
        traverse(right->next);
        // 后序遍历位置
        if(left->val != right->val) res = false;
        left = left->next;
    }
};

利用后序遍历,算法的时间和空间复杂度都是 O(N)。能不能不用额外的空间,解决这个问题呢?

解法二:优化空间复杂度

1、先通过 ⌈双指针技巧⌋ 中的快慢指针来找到链表的中点:

image-20220509115340350

2、如果fast指针没有指向null,说明链表长度为奇数,slow还要再前进一步:

image-20220509115524911

3、从slow开始反转后面的链表,现在就可以开始比较回文串了:

image-20220509115621642
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode* left = head;
        ListNode* right = middleNode(head);
        right = reverseList(right);
        while(right){
            if(left->val != right->val) return false;
            left = left->next;
            right = right->next;
        }
        return true;
    }

    ListNode* middleNode(ListNode* head){
        ListNode* slow = head, * fast = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        // 链表长度为奇数,`slow还要再前进一步
        if(fast) slow = slow->next;
        return slow;
    }

    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, * cur = head, * tmp = head;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

算法总体的时间复杂度 O(N),空间复杂度 O(1),已经是最优的了。

另一个版本,找中间节点函数middleNode另一种写法:

class Solution {
public:
    bool isPalindrome(ListNode* head) {
        ListNode* mid = middleNode(head);
        ListNode* left = head, * right = mid->next;
        mid->next = nullptr;
        right = reverse(right);
        while(right){
            if(left->val != right->val) return false;
            left = left->next;
            right = right->next;
        }
        return true;
    }
    
    ListNode* middleNode(ListNode* head){
        ListNode* fast = head->next, * slow = head;
        while(fast && fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }

    ListNode* reverse(ListNode* head){
        ListNode* pre = nullptr, * cur = head, * tmp;
        while(cur){
            tmp = cur->next;
            cur->next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
};

两数之和

445. 两数相加 II

给定两个 非空链表 l1l2 来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。可以假设除了数字 0 之外,这两个数字都不会以零开头。

示例

img
输入:l1 = [7,2,4,3], l2 = [5,6,4]
输出:[7,8,0,7]

本题的主要难点在于链表中数位的顺序与我们做加法的顺序是相反的,为了逆序处理所有数位,可以使用:把所有数字压入栈中,再依次取出相加,计算过程中需要注意进位的情况

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        stack<int> s1, s2;
        while(l1){
            s1.push(l1->val);
            l1 = l1->next;
        }
        while(l2){
            s2.push(l2->val);
            l2 = l2->next;
        }
        int carry = 0, cur = 0; //进位,本位
        ListNode* res = nullptr;
        while(!s1.empty() || !s2.empty() || carry){
            int a = s1.empty() ? 0 : s1.top();
            int b = s2.empty() ? 0 : s2.top();
            if(!s1.empty()) s1.pop();
            if(!s2.empty()) s2.pop();
            int sum = a + b + carry;
            carry = sum / 10;
            cur = sum % 10;
            ListNode* node = new ListNode(cur);
            node->next = res;
            res = node;
        }
        return res;
    }
};

2. 两数之和

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

注意要根据进位的结果,判断是否需要在链表的结尾添加结点。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode* p1 = l1, * p2 = l2;
        ListNode* dummy = new ListNode(-1);
        ListNode* p = dummy;
        int cur = 0, add = 0;
        while(p1 || p2 || add){
            int val1 = p1 ? p1->val : 0;
            int val2 = p2 ? p2->val : 0;
            if(p1) p1 = p1->next;
            if(p2) p2 = p2->next;
            int sum = val1 + val2 + add;
            cur = sum % 10;
            add = sum / 10;
            p->next = new ListNode(cur);
            p = p->next;
            // 若最后的进位是1,则还要加上去
            if(add == 1){
                p->next = new ListNode(1);
            }
        }
        return dummy->next;
    }
};
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海岸星的清风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值