2.19~2.22 -链表

206. 反转链表 - 力扣(LeetCode)

经典问题,经典方法:

  1. 递归法
  2. 头插法
//递归
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
      if(!head || !head->next)  return head;
      ListNode* last = reverseList(head->next);
      head->next->next = head;
      head->next = NULL;

      return last;
    }
};
//头插
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
      ListNode* pre = NULL;
      ListNode* cur = head;
      while(cur) {
        ListNode* nxt = cur->next;
        cur->next = pre;
        pre = cur;
        cur = nxt;
      }
      return pre;
    }
};
//复杂度分析
//时间复杂度:O(n),其中n为链表节点个数。
//空间复杂度:O(1),仅用到若干额外变量。

234. 回文链表 - 力扣(LeetCode)

找链表中间节点+翻转链表

image-20250219155127320

⚠注意:第一张图中的2→3,在反转链表后,并不会断开。第一张图反转链表后,我们得到了两条链表,一条是1→2→3,另一条是5→4→3,所以不存在第一条链表长度比第二条小1的情况。

class Solution {
    // 876. 链表的中间结点
    ListNode* middleNode(ListNode* head) {
        ListNode* slow = head, *fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
        }
        return slow;
    }

    // 206. 反转链表
    ListNode* reverseList(ListNode* head) {
        ListNode* pre = nullptr, *cur = head;
        while (cur) {
            ListNode* nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

public:
    bool isPalindrome(ListNode* head) {
        ListNode* mid = middleNode(head);
        ListNode* head2 = reverseList(mid);
        while (head2) {
            if (head->val != head2->val) { // 不是回文链表
                return false;
            }
            head = head->next;
            head2 = head2->next;
        }
        return true;
    }
};

142. 环形链表 II - 力扣(LeetCode)

  1. 检测有无环
    • 有,递推公式得到2(x + y) = x + y + z + y --> x = z,让cur节点从头出发,slow节点从当前位置出发,相遇点即环入口。
    • 无,返回NULL
class Solution {
public:
    ListNode* detectCycle(ListNode* head) {
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            slow = slow->next;
            fast = fast->next->next;
            if (fast == slow) {
                while (slow != head) {
                    slow = slow->next;
                    head = head->next;
                }
                return slow;
            }
        }
        return nullptr;
    }
};

21. 合并两个有序链表 - 力扣(LeetCode)

  1. 迭代:

image-20250219162703873

  1. 递归:

image-20250219164150122

//迭代
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{}; // 用哨兵节点简化代码逻辑
        auto cur = &dummy; // cur 指向新链表的末尾
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1; // 把 list1 加到新链表中
                list1 = list1->next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur->next = list2; // 把 list2 加到新链表中
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2; // 拼接剩余链表
        return dummy.next;
    }
};
//递归
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        if (list1 == nullptr) return list2; // 注:如果都为空则返回空
        if (list2 == nullptr) return list1;
        if (list1->val < list2->val) {
            list1->next = mergeTwoLists(list1->next, list2);
            return list1;
        }
        list2->next = mergeTwoLists(list1, list2->next);
        return list2;
    }
};

2. 两数相加 - 力扣(LeetCode)

  1. 递归法

每次把两个节点值l1 .val, l2 .val与进位值carry相加,除以10的余数即为当前节点需要保存的数位,除以10的商即为新的进位值。

代码实现时,有一个简化代码的小技巧:如果递归中发现l 2 的长度比l 1 更长,那么可以交换l 1 和l 2 ,保证l 1 不是空节点,从而简化代码逻辑。

class Solution {
public:
    // l1 和 l2 为当前遍历的节点,carry 为进位
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2, int carry = 0) {
        if (l1 == nullptr && l2 == nullptr) { // 递归边界:l1 和 l2 都是空节点
            return carry ? new ListNode(carry) : nullptr; // 如果进位了,就额外创建一个节点
        }
        if (l1 == nullptr) { // 如果 l1 是空的,那么此时 l2 一定不是空节点
            swap(l1, l2); // 交换 l1 与 l2,保证 l1 非空,从而简化代码
        }
        int sum = carry + l1->val + (l2 ? l2->val : 0); // 节点值和进位加在一起
        l1->val = sum % 10; // 每个节点保存一个数位(直接修改原链表)
        l1->next = addTwoNumbers(l1->next, (l2 ? l2->next : nullptr), sum / 10); // 进位
        return l1;
    }
};

迭代的思路是,初始化答案为一个「空链表」,每次循环,向该链表末尾添加一个节点(保存一个数位)。循环即遍历链表 l1 和 l2 ,每次把两个节点值l1 .val, l2 .val与进位值carry相加,除以10的余数即为当前节点需要保存的数位,除以10的商即为新的进位值。

需要注意的是,在第一次循环时,我们无法往一个空节点的末尾添加节点。这里的技巧是,创建一个哨兵节点(dummy node),当成初始的「空链表」。循环结束后,哨兵节点的下一个节点就是最终要返回的链表头节点。

class Solution {
public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        ListNode dummy; // 哨兵节点
        ListNode* cur = &dummy;
        int carry = 0; // 进位
        while (l1 || l2 || carry) { // 有一个不是空节点,或者还有进位,就继续迭代
            if (l1) {
                carry += l1->val; // 节点值和进位加在一起
                l1 = l1->next; // 下一个节点
            }
            if (l2) {
                carry += l2->val; // 节点值和进位加在一起
                l2 = l2->next; // 下一个节点
            }  
            cur = cur->next = new ListNode(carry % 10); // 每个节点保存一个数位
            carry /= 10; // 新的进位
        }
        return dummy.next; // 哨兵节点的下一个节点就是头节点
    }
};

445. 两数相加 II - 力扣(LeetCode)

在上一题的基础上改成,链表从高位到低位保存数字

思路:链表翻转+两数相加

class Solution {
    ListNode* reverseList(ListNode* head) {//迭代法翻转
        ListNode* pre = nullptr, *cur = head;
        while (cur) {
            ListNode* nxt = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }

    ListNode* addTwo(ListNode* l1, ListNode* l2) {//迭代法相加
        ListNode dummy; // 哨兵节点
        auto cur = &dummy;
        int carry = 0; // 进位
        while (l1 || l2 || carry) { // 有一个不是空节点,或者还有进位,就继续迭代
            if (l1) carry += l1->val; // 节点值和进位加在一起
            if (l2) carry += l2->val; // 节点值和进位加在一起
            cur = cur->next = new ListNode(carry % 10); // 每个节点保存一个数位
            carry /= 10; // 新的进位
            if (l1) l1 = l1->next; // 下一个节点
            if (l2) l2 = l2->next; // 下一个节点
        }
        return dummy.next; // 哨兵节点的下一个节点就是头节点
    }

public:
    ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
        l1 = reverseList(l1);
        l2 = reverseList(l2);
        auto l3 = addTwo(l1, l2);
        return reverseList(l3);
    }
};

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

快慢指针思路,先让fast走n步,再让fast走到底,此时slow下一个就是待删节点,令slow->next = slow->next->next。

注意点:在先让fast走n步时,可能走到空节点(如果n = list.size(),此时删除头结点),因此定义虚拟头结点dummy,最后返回的也是dummy->next

class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        // 由于可能会删除链表头部,用哨兵节点简化代码
        ListNode dummy{0, head};
        ListNode* left = &dummy;
        ListNode* right = &dummy;
        while (n--) {
            right = right->next; // 右指针先向右走 n 步
        }
        while (right->next) {
            left = left->next;
            right = right->next; // 左右指针一起走
        }
        // 左指针的下一个节点就是倒数第 n 个节点
        ListNode* nxt = left->next;
        left->next = left->next->next;
        delete nxt;//注意回收空间
        return dummy.next;
    }
};

24. 两两交换链表中的节点 - 力扣(LeetCode)

lc24-c.png

class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode dummy(0, head); // 用哨兵节点简化代码逻辑
        ListNode* node0 = &dummy;
        ListNode* node1 = head;
        while (node1 && node1->next) { // 至少有两个节点
            ListNode* node2 = node1->next;
            ListNode* node3 = node2->next;

            node0->next = node2; // 0 -> 2
            node2->next = node1; // 2 -> 1
            node1->next = node3; // 1 -> 3

            node0 = node1; // 下一轮交换,0 是 1
            node1 = node3; // 下一轮交换,1 是 3
        }
        return dummy.next; // 返回新链表的头节点
    }
};

92. 反转链表 II - 力扣(LeetCode)

由于left可能等于头结点,因此需要定义dummyhead

class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        ListNode* dummyHead = new ListNode(0,head);
        ListNode* p0 = dummyHead;
        //p0从dummyHead走l - 1步来到待翻转部分的前一个节点
        for (int i = 0; i < left - 1; i++) {
           p0 = p0->next;
        }

        ListNode* pre = nullptr;
        ListNode* cur = p0->next;
        //翻转r - l 次,最后pre停在反转后的头结点上,cur停在下一个节点。
        for (int i = 0; i < right - left + 1; i++) {
           ListNode* nxt = cur->next;
           cur->next = pre;
           pre = cur;
           cur = nxt;
        }
        //此时已经l->r翻转完毕,l->next = nullptr但是p0->next并没有断
        p0->next->next = cur;
        p0->next = pre;
        return dummyHead->next;
    }
};

25. K 个一组翻转链表 - 力扣(LeetCode)

image-20250220203850679

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        // 统计节点个数
        int n = 0;
        for (ListNode* cur = head; cur; cur = cur->next) {
            n++;
        }

        ListNode dummy(0, head);
        ListNode* p0 = &dummy;
        ListNode* pre = nullptr;
        ListNode* cur = head;

        // k 个一组处理
        for (; n >= k; n -= k) {
            for (int i = 0; i < k; i++) { // 同 92 题
                ListNode* nxt = cur->next;
                cur->next = pre; // 每次循环只修改一个 next,方便大家理解
                pre = cur;
                cur = nxt;
            }

            // 见视频
            ListNode* nxt = p0->next;//翻转后的末尾
            p0->next->next = cur;
            p0->next = pre;//p0接到翻转后的头
            p0 = nxt;//翻转后的末尾是下一个循环的前驱结点
        }
        return dummy.next;
    }
};

前置题:328. 奇偶链表 - 力扣(LeetCode)

image-20240731111427114.png

  1. 如果是空链表,evenList的初始节点就不存在,因此特殊处理,直接返回;
  2. 初始化奇数索引节点链表oddList初始化为头节点,偶数索引节点链表evenList初始化为次节点;
  3. 记录偶数索引节点链表头节点evenList,用于之后的拼接;【奇数索引节点的头节点就是head,不需要在额外记录了】
  4. 更新奇偶索引链表的节点;
  5. 最后一个奇数索引节点和首个偶数索引节点拼接起来;
    返回头节点head;
class Solution {
public:
    ListNode* oddEvenList(ListNode* head) {
        if(!head)return head;   // 空链表,直接返回
        ListNode* odd = head, *even = head->next;   // 奇数索引节点链表odd初始化为头节点,偶数索引节点链表even初始化为次节点
        ListNode* evenhead = even;      // 记录偶数索引节点链表头节点
        // 当前偶数节点不为空且当前偶数节点之后还有节点
        while(even && even->next){
            odd->next = even->next;     // 下一个奇数索引节点是当前偶数索引节点的下一个节点
            odd = odd->next;            // 更新当前奇数索引节点
            even->next = odd->next;     // 下一个偶数索引节点是新的奇数索引节点的下一个节点
            even = even->next;          // 更新偶数索引节点
        }
        odd->next = evenhead;   // 最后一个奇数索引节点和首个偶数索引节点拼接起来
        return head;
    }
};

138. 随机链表的复制 - 力扣(LeetCode)

题意

深拷贝一个链表,要求新链表中的每个节点都是新创建的,并且这些节点的random指针都指向新链表中的相应节点。

思路

如果没有random指针,只需在遍历链表的同时,依次复制每个节点(创建新节点并复制val),添加在新链表的末尾。

random指针,问题就变得复杂了,我们需要知道random指向的那个节点,在新链表中是哪个节点。

所以必须记录原链表节点到新链表节点的映射(map)。这样可以通过原链表random指向的节点,知道新链表的random应该指向哪个节点。

难道要用哈希表吗?不需要,我们可以把新链表和旧链表「混在一起」。

例如链表1→2→3,依次复制每个节点(创建新节点并复制val和next),把新节点直接插到原节点的后面,形成一个交错链表1→1 ′ →2→2 ′ →3→3 ′

如此一来,原链表节点的下一个节点,就是其对应的新链表节点了!

然后遍历这个交错链表,假如节点1的random指向节点3,那么就把节点1 ′ 的random指向节点3的下一个节点3 ′ ,这样就完成了对random指针的复制。

cur->next->random = cur->random->next;

最后,从交错链表链表中分离出1 ′ →2 ′ →3 ′ ,即为深拷贝后的链表。做法类似328. 奇偶链表

⚠注意:不能只删除节点1,2,3,因为题目要求原链表的next不能修改。(但 Python 可以这么做)

class Solution {
public:
    Node* copyRandomList(Node* head) {
        if (head == nullptr) {
            return nullptr;
        }

        // 复制每个节点,把新节点直接插到原节点的后面
        for (Node* cur = head; cur; cur = cur->next->next) {
            cur->next = new Node(cur->val, cur->next, nullptr);
        }

        // 遍历交错链表中的原链表节点
        for (Node* cur = head; cur; cur = cur->next->next) {
            if (cur->random) {
                // 要复制的 random 是 cur->random 的下一个节点
                cur->next->random = cur->random->next;
            }
        }

        // 把交错链表分离成两个链表
        Node* new_head = head->next;
        Node* cur = head;
        for (; cur->next->next; cur = cur->next) {
            Node* copy = cur->next;
            cur->next = copy->next; // 恢复原节点的 next
            copy->next = copy->next->next; // 设置新节点的 next
        }
        cur->next = nullptr; // 恢复原节点的 next
        return new_head;
    }
};

148. 排序链表 - 力扣(LeetCode)

前置题目

img

迭代法:

class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1 , ListNode* list2){
      ListNode dummy;
      ListNode* cur = &dummy;
      while(list1 && list2){
        if(list1->val < list2->val){
          cur->next = list1;
          list1 = list1->next;
        }
        else{
          cur->next = list2;
          list2 = list2->next;
        }
        cur = cur->next;
      }
      cur->next = list1 ? list1 : list2;
      return dummy.next;
    }
};

递归法:

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

回归本题:

方法一:归并排序(分治)

  1. 找到链表的中间结点 head 2 的前一个节点,并断开head 2 与其前一个节点的连接。这样我们就把原链表均分成了两段更短的链表。
  2. 分治,递归调用sortList,分别排序head(只有前一半)和head 2
  3. 排序后,我们得到了两个有序链表,那么合并两个有序链表,得到排序后的链表,返回链表头节点。
class Solution {
    // 876. 链表的中间结点(快慢指针)
    ListNode* middleNode(ListNode* head) {
        ListNode* pre = head;
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast && fast->next) {
            pre = slow; // 记录 slow 的前一个节点
            slow = slow->next;
            fast = fast->next->next;
        }
        pre->next = nullptr; // 断开 slow 的前一个节点和 slow 的连接
        return slow;
    }

    // 21. 合并两个有序链表(双指针)
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy; // 用哨兵节点简化代码逻辑
        ListNode* cur = &dummy; // cur 指向新链表的末尾
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1; // 把 list1 加到新链表中
                list1 = list1->next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur->next = list2; // 把 list2 加到新链表中
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2; // 拼接剩余链表
        return dummy.next;
    }

public:
    ListNode* sortList(ListNode* head) {
        // 如果链表为空或者只有一个节点,无需排序
        if (head == nullptr || head->next == nullptr) {
            return head;
        }
        // 找到中间节点,并断开 head2 与其前一个节点的连接
        // 比如 head=[4,2,1,3],那么 middleNode 调用结束后 head=[4,2] head2=[1,3]
        ListNode* head2 = middleNode(head);
        // 分治
        head = sortList(head);
        head2 = sortList(head2);
        // 合并
        return mergeTwoLists(head, head2);
    }
};

复杂度分析

  • 时间复杂度:O(nlogn),其中n是链表长度。递归式\(T(n)=2T(n/2)+O(n)\),由主定理可得时间复杂度为O(nlogn)
  • 空间复杂度:O(logn)。递归需要O(logn)的栈开销。

方法二:归并排序(迭代)

方法一的归并是自顶向下计算,需要O(logn)的递归栈开销。

方法二将其改成自底向上计算,空间复杂度优化成O(1)。

自底向上的意思是:

  1. 首先,归并长度为1的子链表。例如[4,2,1,3],把第一个节点和第二个节点归并,第三个节点和第四个节点归并,得到[2,4,1,3]。

  2. 然后,归并长度为2的子链表。例如[2,4,1,3],把前两个节点和后两个节点归并,得到[1,2,3,4]。

  3. 然后,归并长度为4的子链表。

  4. 依此类推,直到归并的长度大于等于链表长度为止,此时链表已经是有序的了。

  5. 具体算法:

    1. 遍历链表,获取链表长度length。
    2. 初始化步长step=1。
    3. 循环直到step≥length。
    4. 每轮循环,从链表头节点开始。
    5. 分割出两段长为step的链表,合并,把合并后的链表插到新链表的末尾。重复该步骤,直到链表遍历完毕。
    6. 把step扩大一倍。回到第4步。
class Solution {
    // 获取链表长度
    int getListLength(ListNode* head) {
        int length = 0;
        while (head) {
            length++;
            head = head->next;
        }
        return length;
    }

    // 分割链表
    // 如果链表长度 <= size,不做任何操作,返回空节点
    // 如果链表长度 > size,把链表的前 size 个节点分割出来(断开连接),并返回剩余链表的头节点
    ListNode* splitList(ListNode* head, int size) {
        // 先找到 next_head 的前一个节点
        ListNode* cur = head;
        for (int i = 0; i < size - 1 && cur; i++) {
            cur = cur->next;
        }

        // 如果链表长度 <= size
        if (cur == nullptr || cur->next == nullptr) {
            return nullptr; // 不做任何操作,返回空节点
        }

        ListNode* next_head = cur->next;
        cur->next = nullptr; // 断开 next_head 的前一个节点和 next_head 的连接
        return next_head;
    }

    // 21. 合并两个有序链表(双指针)
    // 返回合并后的链表的头节点和尾节点
    pair<ListNode*, ListNode*> mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy; // 用哨兵节点简化代码逻辑
        ListNode* cur = &dummy; // cur 指向新链表的末尾
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1; // 把 list1 加到新链表中
                list1 = list1->next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur->next = list2; // 把 list2 加到新链表中
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2; // 拼接剩余链表
        while (cur->next) {
            cur = cur->next;
        }
        // 循环结束后,cur 是合并后的链表的尾节点
        return {dummy.next, cur};
    }

public:
    ListNode* sortList(ListNode* head) {
        int length = getListLength(head); // 获取链表长度
        ListNode dummy(0, head); // 用哨兵节点简化代码逻辑
        // step 为步长,即参与合并的链表长度
        for (int step = 1; step < length; step *= 2) {
            ListNode* new_list_tail = &dummy; // 新链表的末尾
            ListNode* cur = dummy.next; // 每轮循环的起始节点
            while (cur) {
                // 从 cur 开始,分割出两段长为 step 的链表,头节点分别为 head1 和 head2
                ListNode* head1 = cur;
                ListNode* head2 = splitList(head1, step);
                cur = splitList(head2, step); // 下一轮循环的起始节点
                // 合并两段长为 step 的链表
                auto [head, tail] = mergeTwoLists(head1, head2);
                // 合并后的头节点 head,插到 new_list_tail 的后面
                new_list_tail->next = head;
                new_list_tail = tail; // tail 现在是新链表的末尾
            }
        }
        return dummy.next;
    }
};

复杂度分析

  • 时间复杂度:O(nlogn),其中n是链表长度。
  • 空间复杂度:O(1)。

23. 合并 K 个升序链表 - 力扣(LeetCode)

方法一:最小堆

合并后的第一个节点first,一定是某个链表的头节点(因为链表已按升序排列)。

合并后的第二个节点,可能是某个链表的头节点,也可能是first的下一个节点。

例如有三个链表1->2->5, 3->4->6, 4->5->6,找到第一个节点1之后,第二个节点不是另一个链表的头节点,而是节点1的下一个节点2。

按照这个过程继续思考,每当我们找到一个节点值最小的节点x,就把节点x.next加入「可能是最小节点」的集合中。

因此,我们需要一个数据结构,它支持:

从数据结构中找到并移除最小节点。插入节点。这可以用最小堆实现。

初始把所有链表的头节点入堆,然后不断弹出堆中最小节点x,如果x.next不为空就加入堆中。循环直到堆为空。把弹出的节点按顺序拼接起来,就得到了答案。

实现时,可以用哨兵节点简化代码。

函数priority_queue中关键点解释

1. 为什么需要 decltype(cmp)

  • priority_queue 的模板参数需要明确三个信息:
    • 元素类型ListNode*(堆中存储的是链表节点指针)。
    • 底层容器类型vector<ListNode*>(默认用 vector 实现堆)。
    • 比较器类型decltype(cmp)(用来定义堆中元素的比较规则)。
  • 由于 cmp 是一个 lambda 表达式,它的类型是编译器自动生成的、唯一的匿名类型。直接用decltype(cmp) 可以避免手动写出这个复杂的类型名。这种写法是 C++11 后对 lambda 表达式类型的标准处理方式,避免了手动编写复杂的比较器类型。

2. 为什么不用 std::greaterstd::less

  • 标准库的比较器(如 std::greater)是按节点指针的地址大小比较的,而我们需要按节点的 val 值比较。
  • 因此必须自定义一个比较规则(lambda 表达式 cmp),让堆按照节点的 val 值构造最小堆。
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode* > &lists) {
        auto cmp = [](const ListNode* a, const ListNode* b) {
            return a->val > b->val; //  最小堆:a 的 val 比 b 的 val 大时,a 下沉
        };
        priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq;//    元素类型  ^^^^^^^^^  底层容器类型 ^^^^^^^^^^  比较器类型 ^^^^^^
        for (auto head: lists) {
            if (head) {
                pq.push(head);
            }
        }

        ListNode dummy{}; // 哨兵节点,作为合并后链表头节点的前一个节点
        auto cur = &dummy;
        while (!pq.empty()) { // 循环直到堆为空
            auto node = pq.top(); // 剩余节点中的最小节点
            pq.pop();
            if (node->next) { // 下一个节点不为空
                pq.push(node->next); // 下一个节点有可能是最小节点,入堆
            }
            cur->next = node; // 合并到新链表中
            cur = cur->next; // 准备合并下一个节点
        }
        return dummy.next; // 哨兵节点的下一个节点就是新链表的头节点
    }
};
/*复杂度分析
时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。
空间复杂度:O(k)。堆中至多有k个元素。
*/

方法二:分治

暴力做法是,按照21. 合并两个有序链表的题解思路,先合并前两个链表,再把得到的新链表和第三个链表合并,再和第四个链表合并,依此类推。

但是这种做法,平均每个节点会参与到O(k)次合并中(用(1+2+⋯+k)/k粗略估计),所以总的时间复杂度为O(nk)

一个巧妙的思路是,把lists一分为二(尽量均分),先合并前一半的链表,再合并后一半的链表,然后把这两个链表合并成最终的链表。如何合并前一半的链表呢?我们可以继续一分为二。如此分下去直到只有一个链表,此时无需合并。

我们可以写一个递归来完成上述逻辑,按照一分为二再合并的逻辑,递归像是在后序遍历一棵平衡二叉树。由于平衡树的高度是O(logk),所以每个链表节点只会出现在O(logk)次合并中!这样就做到了更快的O(nlogk)时间。

假设输入链表为:这个例子中,合并三个链表的分治过程如下:

  • List1: 1 → 4 → 5
  • List2: 1 → 3 → 4
  • List3: 2 → 6

分治步骤

  1. 分割:将三个链表分成左半部分(List1)和右半部分(List2、List3)。
  2. 合并右半部分
    • 进一步分割为 List2 和 List3。
    • 合并 List2 和 List3 → 1 → 2 → 3 → 4 → 6。
  3. 合并左半部分与右半结果
    • 合并 List1(1→4→5)和上一步的结果(1→2→3→4→6)→ 最终合并结果为 1→1→2→3→4→4→5→6。

最终结果
1 → 1 → 2 → 3 → 4 → 4 → 5 → 6

代码执行流程

  • 递归分割链表数组,逐层合并相邻链表,最终将所有链表合并为一个有序链表。
class Solution {
    // 21. 合并两个有序链表
    ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
        ListNode dummy{}; // 用哨兵节点简化代码逻辑
        auto cur = &dummy; // cur 指向新链表的末尾
        while (list1 && list2) {
            if (list1->val < list2->val) {
                cur->next = list1; // 把 list1 加到新链表中
                list1 = list1->next;
            } else { // 注:相等的情况加哪个节点都是可以的
                cur->next = list2; // 把 list2 加到新链表中
                list2 = list2->next;
            }
            cur = cur->next;
        }
        cur->next = list1 ? list1 : list2; // 拼接剩余链表
        return dummy.next;
    }

    // 合并从 lists[i] 到 lists[j-1] 的链表
    ListNode* mergeKLists(vector<ListNode*>& lists, int i, int j) {
        int m = j - i;
        if (m == 0) {
            return nullptr; // 注意输入的 lists 可能是空的
        }
        if (m == 1) {
            return lists[i]; // 无需合并,直接返回
        }
        auto left = mergeKLists(lists, i, i + m / 2); // 合并左半部分
        auto right = mergeKLists(lists, i + m / 2, j); // 合并右半部分
        return mergeTwoLists(left, right); // 最后把左半和右半合并
    }

public:
    ListNode* mergeKLists(vector<ListNode* > &lists) {
        return mergeKLists(lists, 0, lists.size());
    }
};
/*复杂度分析
时间复杂度:O(nlogk),其中k为lists的长度,n为所有链表的节点数之和。每个节点参与链表合并的次数为O(logk)次,一共有n个节点,所以总的时间复杂度为O(nlogk)。
空间复杂度:O(logk)。递归深度为O(logk),需要用到O(logk)的栈空间。Python 忽略切片产生的额外空间。

146. LRU 缓存 - 力扣(LeetCode)

img

问:需要几个哨兵节点?

答:一个就够了。一开始哨兵节点dummyprevnext都指向dummy。随着节点的插入,dummynext指向链表的第一个节点(最上面的书),prev指向链表的最后一个节点(最下面的书)。

问:为什么节点要把key也存下来?

答:在删除链表末尾节点时,也要删除哈希表中的记录,这需要知道末尾节点的key。

class Node {
public:
    int key;
    int value;
    Node* prev;
    Node* next;

    Node(int k = 0, int v = 0) : key(k), value(v) {}
};

class LRUCache {
private:
    int capacity;
    Node* dummy; // 哨兵节点
    unordered_map<int, Node*> key_to_node;

    // 删除一个节点(抽出一本书)
    void remove(Node* x) {
        x->prev->next = x->next;
        x->next->prev = x->prev;
    }

    // 在链表头添加一个节点(把一本书放在最上面)
    void push_front(Node* x) {
        x->prev = dummy;
        x->next = dummy->next;
        x->prev->next = x;
        x->next->prev = x;
    }

    // 获取 key 对应的节点,同时把该节点移到链表头部
    Node* get_node(int key) {
        auto it = key_to_node.find(key);
        if (it == key_to_node.end()) { // 没有这本书
            return nullptr;
        }
        Node* node = it->second; // 有这本书
        remove(node); // 把这本书抽出来
        push_front(node); // 放在最上面
        return node;
    }

public:
    LRUCache(int capacity) : capacity(capacity), dummy(new Node()) {
        dummy->prev = dummy;
        dummy->next = dummy;
    }

    int get(int key) {
        Node* node = get_node(key);
        return node ? node->value : -1;
    }

    void put(int key, int value) {
        Node* node = get_node(key);
        if (node) { // 有这本书
            node->value = value; // 更新 value
            return;
        }
        key_to_node[key] = node = new Node(key, value); // 新书
        push_front(node); // 放在最上面
        if (key_to_node.size() > capacity) { // 书太多了
            Node* back_node = dummy->prev;
            key_to_node.erase(back_node->key);
            remove(back_node); // 去掉最后一本书
            delete back_node; // 释放内存
        }
    }
};
posted @ 2025-02-22 21:21  七龙猪  阅读(1)  评论(0)    收藏  举报
-->