【LeetCode Cookbook(C++描述)】一刷链表(上)

本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典链表算法题,以后会更新“二刷”“三刷”等等。考虑到上篇文章过长,链表专题将分几篇文章完成。

LeetCode #21: Merge Tow Sorted Lists 合并两个有序链表

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

迭代解法

这是最基本的链表技巧。遍历两条链表,每次循环都要比较一下链表上对应节点的大小,将较小的节点接到结果链上。

值得注意的是,当我们需要创建一条新链表(当然也包括分解链表的情况)的时候,可以使用「虚拟头节点」简化边界情况的处理,也就是 dummy 节点。如果不使用 dummy 节点,我们需要额外处理新链表的指针 p 为空的情况;而有了 dummy 这一占位符,可以避免处理空指针的情况,降低代码复杂性。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode* dummy = new ListNode(-1), *p = dummy;

        while (list1 != nullptr && list2 != nullptr) {
            if (list1->val < list2->val) {
                p->next = list1;
                list1 = list1->next;
            } else {
                p->next = list2;
                list2 = list2->next;
            }
            p = p->next;
        }
        // 合并后 l1 和 l2 最多只有一个还未被合并完,直接将链表末尾指向未合并完的链表
        p->next = list1 == nullptr ? list2 : list1;

        return dummy->next;
    }
};

假设 nm 分别为两条链表的长度,则该算法的时间复杂度为   O ( n + m ) \ O(n+m)  O(n+m) ,空间复杂度为   O ( 1 ) \ O(1)  O(1)

递归解法

我们要比较 list1list2 两条链表的节点大小,递归地决定下一个添加到结果链的节点。

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (list1 == nullptr) return list2;
        else if (list2 == nullptr) return list1;
        else if (list1->val < list2->val) {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        } else {
            list2->next = mergeTwoLists(list1, list2->next);
            return list2;
        }
    }
};

假设 nm 分别为两条链表的长度,则该算法的时间复杂度为   O ( n + m ) \ O(n+m)  O(n+m) ,递归调用需要消耗栈空间,栈空间大小取决于递归深度,因此空间复杂度为   O ( n + m ) \ O(n+m)  O(n+m)

更多例子

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

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

依旧利用「虚拟头节点」的技巧,我们可以将原链表分解为二,一条链表中的元素大小都小于 x ,另一条则都大于等于 x ,最后再将两条子链表连接在一起,就得到了符合题意的结果链。

这种情况下,我们需要把原链表的的节点连接到新的链表上(并非 new 新节点来组成新链表),断开节点与原链表的连接是必要的,否则结果链将包含一个环,不符合题意。

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        //小于 x 的子链
        ListNode* dummy1 = new ListNode(-1), *p1 = dummy1;
        //大于等于 x 的子链
        ListNode* dummy2 = new ListNode(-1), *p2 = dummy2;

        while (head != nullptr) {
            if (head->val < x) {
                p1->next = head;
                p1 = p1->next;
            }
            else {
                p2->next = head;
                p2 = p2->next;
            }
            //断开原链表中每个节点的 next 指针
            ListNode* temp = head->next;
            head->next = nullptr;
            head = temp;
        }
        //连接两个子链
        p1->next = dummy2->next;
        return dummy1->next;
    }
};

LeetCode #19:Remove Nth Node From End of List 删除链表的倒数第 N 个节点

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

从前往后寻找单链表的第 k 个节点很简单,一个 for 循环遍历过去就找到了,但是如何寻找从后往前数的第 k 个节点呢?

单链表是单向性的,一般只会给出一个 ListNode 头节点代表一整条单链表,最容易想出的解法是先遍历一遍链表求出其长度 n ,然后再遍历一遍链表计算第 n - k + 1 个节点,总共两次遍历。

然而,我们完全可以利用双指针技巧,只需要一次遍历即可达成目的。思路如下:

  1. 先让一个指针 p1 指向链表的头节点 head ,走 k 步;
  2. 此时的 p1 只需要再走 n - k 步即可走到链表末尾的空指针。我们再用新的指针p2 指向 head
  3. p1p2 同时走,p1 走到链表末尾的空指针时恰好走了 n - k 步,而 p2 则走了 n - k + 1 步,即恰好走到链表的倒数第 k 个节点上。

代码实现是这样的:

// 返回链表的倒数第 k 个节点
ListNode* findFromEnd(ListNode* head, int k) {
    ListNode* p1 = head;
    // p1 先走 k 步
    for (int i = 0; i < k; i++) {
        p1 = p1 -> next;
    }
    ListNode* p2 = head;
    // p1 和 p2 同时走 n - k 步
    while (p1 != nullptr) {
        p2 = p2 -> next;
        p1 = p1 -> next;
    }
    // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
    return p2;
}

那么,对于 #19 题,我们通过一次遍历就可删除倒数第 n 个节点:

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

        return dummy->next;
    }

private:
    ListNode* findFromEnd(ListNode* head, int k) {
        ListNode* p1 = head;
        // p1 先走 k 步
        for (int i = 0; i < k; i++) {
            p1 = p1 -> next;
        }
        ListNode* p2 = head;
        // p1 和 p2 同时走 n - k 步
        while (p1 != nullptr) {
            p2 = p2 -> next;
            p1 = p1 -> next;
        }
        // p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
        return p2;
    }
};

要删除倒数第 n 个节点,就得获得倒数第 n + 1 个节点的引用,可以用我们实现的 findFromEnd 来操作。

我们又使用了「虚拟头节点」的技巧,也是为了防止出现空指针的情况,比如说链表总共有 5 个节点,删除倒数第 5 个节点,也就是第一个节点,那么就应该首先找到倒数第 6 个节点。但第一个节点前面已经没有节点了,此时就会出错;但有了 dummy 虚拟节点的存在,很好地规避了这一情况的发生。

LeetCode #206:Reverse Linked List 反转链表

#206

迭代解法

这一解法非常容易,只需要在遍历列表的同时,把每个节点的 next 指向它的前驱节点(需要两个指针,p 指向前驱节点,q 指向当前节点),并利用新的指针 temp 来存储当前节点的后继节点,最后两个指针 pq 同时后移……以此类推,实现整条链表的反转。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* p = nullptr, *q = head;

        while (q) {
            ListNode* temp = q->next;
            q->next = p;
            p = q;
            q = temp;
        }
        
        return p;  // q 指向 nullptr,返回 p 作为新的头指针
    }
};

该算法的时间复杂度为   O ( n ) \ O(n)  O(n),空间复杂度为   O ( 1 ) \ O(1)  O(1)

递归解法

链表是一种兼具递归和迭代性质的数据结构,这一算法便是说明递归方法的简洁优美的经典案例。针对 head->next 进行递归,使得当前节点( head->next )的后继节点指向当前节点(此时当前节点与后继节点互相指向),并断开当前节点指向后继节点的指针。考虑到单链表的末尾为空指针,我们应当加入 base case ,递归到最后一个节点时返回其自身,避免栈溢出。
在这里插入图片描述
在这里插入图片描述

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        if (head == nullptr || head->next == nullptr) return head;

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

        return last;
    }
};

更多例子之反转链表前 N 个节点

解决思路与反转整个链表差不多,具体有以下区别:

  • base case 变为 n == 1 ,反转一个元素就是它本身,同时要存储后继节点 successor
  • 现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后继 successor(第 n + 1 个节点),反转之后与 head 连接。
ListNode* successor = nullptr;
//反转以 head 为起点的 n 个节点,返回新的头节点
ListNode* reverseN(ListNode* head, int n) {
    if (n == 1) {
        //记录第 n + 1 个节点
        successor = head->next;
        return head;
    }
    //以 head->next 为起点,需要反转前 n - 1 个节点
    ListNode* last = reverseN(head->next, n - 1);
    head->next->next = head;
    //让反转之后的 head 节点和后面的节点连起来
    head->next = successor;
    return last;
}

更多例子之反转链表的一部分

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

此题相当于给出索引区间(令 head 的索引为 1   [ l e f t , r i g h t ] \ [left, right]  [left,right] ,仅仅反转这一区间内的链表:

  • 如果 left == 1 ,相当于反转前 N 个元素,与之前的例子 reverseN 实现的功能一致。
  • 如果 left != 1 ,以 head->next 的索引为 1 ,反转区间应当从 left - 1 开始,……,直到相对于 head->next->...->next ,反转区间从 1 开始,此时套用 left == 1 的递归模式即可。
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        // base case
        if (left == 1) {
            return reverseN(head, right);
        }
        //前进到反转的起点触发 base case
        head->next = reverseBetween(head->next, left - 1, right - 1);
        return head;
    }

private:
    ListNode* successor = nullptr;
    //反转以 head 为起点的 n 个节点,返回新的头节点
    ListNode* reverseN(ListNode* head, int n) {
        if (n == 1) {
            //记录第 n + 1 个节点
            successor = head->next;
            return head;
        }
        ListNode* last = reverseN(head->next, n - 1);

        head->next->next = head;
        head->next = successor;
        return last;
    }
};

LeetCode #25:Reverse Nodes in k-Group - K 个一组翻转链表

#25
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

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

这也是链表递归的问题。假设我们有 reverseKGroup 函数来实现如题的功能,我们对这个链表调用 reverseKGroup(head, k) 将前 k 个节点反转,后面的节点也构成一条子链表(子问题);随后把原先的 head 指针移动到后面一段子链表的开头,由于子问题与原问题结构上完全一致,形成递归性质,就可以反复递归调用 reverseKGroup(head, k) ,从而解决问题。因此,大致的算法流程如下:

  1. 先反转以 head 开头的前 k 个元素;
  2. 将第 k + 1 个元素作为 head 递归调用 reverseKGroup(head, k) 函数;
  3. 将上述两个过程的结果连接起来。

而这道题的 base case ,就是如果最后剩下的元素不足 k 个,则保持不变。每次递归时,我们需要遍历子问题中前 k 个节点(新建指针 temp),保证有足够的节点来进行反转操作,如果不够则直接返回 head 。进一步地,我们也可以利用先前反转链表前 n 个元素的函数 reverseN

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if (head == nullptr) return nullptr;
        //遍历前 k 个元素,检查是否有足够节点
        ListNode* temp = head;
        for (int i = 0; i < k; i++) {
            // base case
            if (temp == nullptr) return head;
            temp = temp->next;
        }
        //反转前 k 个元素
        ListNode* newHead = reverseN(head, k);
        //递归反转后续链表并连接起来
        head->next = reverseKGroup(temp, k);

        return newHead;
    }

private:
    ListNode* successor = nullptr;
    //反转以 head 为起点的 n 个节点,返回新的头节点
    ListNode* reverseN(ListNode* head, int n) {
        if (n == 1) {
            //记录第 n + 1 个节点
            successor = head->next;
            return head;
        }
        ListNode* last = reverseN(head->next, n - 1);

        head->next->next = head;
        head->next = successor;
        return last;
    }
};

呜啊?

  • 27
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值