【编程之路】面试必刷TOP101:链表(01-05,Python实现)

【面试必刷TOP101】系列包含:


1.反转链表

1.1 迭代法

class Solution:
    def ReverseList(self , head: ListNode) -> ListNode:
        if head == None:
            return None
        p = head
        q = None
        while p:
            tmp = p.next
            p.next = q
            q = p
            p = tmp
        return q

时间复杂度: O ( n ) O(n) O(n),遍历链表一次
空间复杂度: O ( 1 ) O(1) O(1),无额外空间使用

1.2 递归法

这里的递归法,其实就相当于走到链表尾,再将每一个结点的指针 “掉头”。

class Solution:
    def ReverseList(self , head: ListNode) -> ListNode:
        if head == None or head.next == None:
            return head
        res = self.ReverseList(head.next)
        head.next.next = head
        # 防止出现环
        head.next = None
        return res

时间复杂度: O ( n ) O(n) O(n),相当于递归遍历链表
空间复杂度: O ( n ) O(n) O(n),递归栈深度为链表长度 n n n

2.链表内指定区间反转

2.1 头插迭代法

class Solution:
    def reverseBetween(self , head: ListNode, m: int, n: int) -> ListNode:
        res = ListNode(-1) # 设置虚拟头结点
        res.next = head
        pre = res          # 前序结点
        cur = head         # 当前结点
        for i in range(1,m):
            pre = cur
            cur = cur.next
        # 注意,这里的 pre 和 cur 指向的结点其实是一直没有发生变化的
        for i in range(m,n):
            tmp = cur.next
            cur.next = tmp.next
            tmp.next = pre.next
            pre.next = tmp
        return res.next

时间复杂度:O(n),最坏情况下遍历全部链表
空间复杂度:O(1),无额外空间使用

2.2 递归法

如果 m == 1,就相当于反转链表的前 n 个元素;
如果 m != 1,我们把 head 的索引视为1,那么我们是想从第 m 个元素开始反转;如果把 head.next 的索引视为1,那么相对于 head.next 的反转区间应该是从第 m-1 个元素开始的。以此类推,就是一个递归问题。

对于每次反转,如果 n == 1,相当于只颠倒第一个节点;如果 n != 1,则进入后续节点,也是一个递归问题。

class Solution:
    def __init__(self):
        self.temp = None
    def reverse(self, head: ListNode, n: int) -> ListNode:
        if n == 1:
            self.temp = head.next
            return head
        node = self.reverse(head.next, n-1)
        # 反转
        head.next.next = head
        # 每个子问题反转后的尾拼接第 n 个位置后的结点(注意理解)
        head.next = self.temp
        return node
    def reverseBetween(self , head: ListNode, m: int, n: int) -> ListNode:
        if m == 1:
            return self.reverse(head,n)
        node = self.reverseBetween(head.next, m-1, n-1)
        # 拼接已翻转
        head.next = node
        return head

时间复杂度:O(n),最坏情况下遍历全部链表
空间复杂度:O(n),递归栈深度最坏为 n

3.链表中的节点每 k 个一组翻转

3.1 递归法

现在我们想一想,如果拿到一个链表,想要像上述一样分组翻转应该做些什么?首先肯定是分段吧,至少我们要先分成一组一组,才能够在组内翻转,之后就是组内翻转,最后是将反转后的分组连接。

但是连接的时候遇到问题了:首先如果能够翻转,链表第一个元素一定是第一组,它翻转之后就跑到后面去了,而第一组的末尾元素才是新的链表首,我们要返回的也是这个元素,而原本的链表首要连接下一组翻转后的头部,即翻转前的尾部,如果不建立新的链表,看起来就会非常难。但是如果我们从最后的一个组开始翻转,得到了最后一个组的链表首,是不是可以直接连在倒数第二个组翻转后的尾(即翻转前的头)后面,这样从后往前是不是看起来就容易多了。

怎样从后往前呢?我们这时候可以用到自上而下再自下而上的递归或者说栈。接下来我们说说为什么能用递归?如果这个链表有 n n n 个分组可以反转,我们首先对第一个分组反转,那么是不是接下来将剩余 n − 1 n-1 n1 个分组反转后的结果接在第一组后面就行了,那这剩余的n−1n-1n−1组就是一个子问题。我们来看看递归的三段式模版:

  • 终止条件: 当进行到最后一个分组,即不足 k 次遍历到链表尾(0次也算),就将剩余的部分直接返回。
  • 返回值: 每一级要返回的就是翻转后的这一分组的头,以及连接好它后面所有翻转好的分组链表。
  • 本级任务: 对于每个子问题,先遍历 k k k 次,找到该组结尾在哪里,然后从这一组开头遍历到结尾,依次翻转,结尾就可以作为下一个分组的开头,而先前指向开头的元素已经跑到了这一分组的最后,可以用它来连接它后面的子问题,即后面分组的头。

step 1:每次从进入函数的头节点优先遍历链表 k k k 次,分出一组,若是后续不足 k k k 个节点,不用反转直接返回头。
step 2:从进入函数的头节点开始,依次反转接下来的一组链表,反转过程同 反转链表
step 3:这一组经过反转后,原来的头变成了尾,后面接下一组的反转结果,下一组采用上述递归继续。

class Solution:
    def reverseKGroup(self , head: ListNode, k: int) -> ListNode:
        tail = head
        for i in range(0,k):
            # 不是 k 的倍数,则保持原样,直接返回头结点
            if tail == None:
                return head
            tail = tail.next
        # ---------------------------------------反转链表
        pre = None
        cur = head
        while cur != tail:
            temp = cur.next
            cur.next = pre
            pre = cur
            cur = temp
        # ---------------------------------------反转链表
        head.next = self.reverseKGroup(tail, k)
        return pre

时间复杂度:O(n),一共遍历链表 n 个结点
空间复杂度:O(n),递归栈最大深度为 n / k

4.合并两个排序的链表

4.1 迭代法

class Solution:
    def Merge(self , pHead1: ListNode, pHead2: ListNode) -> ListNode:
        if pHead1 == None:
            return pHead2
        if pHead2 == None:
            return pHead1
        head = ListNode(-1)
        cur = head
        while pHead1 != None and pHead2 != None:
            if pHead1.val <= pHead2.val:
                cur.next = pHead1
                pHead1 = pHead1.next
            else:
                cur.next = pHead2
                pHead2 = pHead2.next
            cur = cur.next
        if pHead1 == None:
            cur.next = pHead2
        if pHead2 == None:
            cur.next = pHead1
        return head.next

时间复杂度:O(n),最坏情况遍历 2 n 2n 2n 个结点。
空间复杂度:O(1),无额外使用空间,新建的链表属于返回必要空间。

4.2 递归法

class Solution:
    def Merge(self , pHead1: ListNode, pHead2: ListNode) -> ListNode:
        if pHead1 == None:
            return pHead2
        if pHead2 == None:
            return pHead1
        if pHead1.val <= pHead2.val:
            pHead1.next = self.Merge(pHead1.next, pHead2)
            return pHead1
        else:
            pHead2.next = self.Merge(pHead1, pHead2.next)
            return pHead2

时间复杂度:O(n),最坏相当于遍历两个链表每个结点一次
空间复杂度:O(n),递归栈最大长度为 n

5.合并 k 个已排序的链表

5.1 归并排序思想

如果是两个有序链表合并,我们可能会利用归并排序合并阶段的思想:准备双指针分别放在两个链表头,每次取出较小的一个元素加入新的大链表,将其指针后移,继续比较,这样我们出去的都是最小的元素,自然就完成了排序。

其实这道题我们也可以两两比较啊,只要遍历链表数组,取出开头的两个链表,按照上述思路合并,然后新链表再与后一个继续合并,如此循环,知道全部合并完成。但是,这样太浪费时间了。

既然都是归并排序的思想了,那我们可不可以直接归并的分治来做,而不是顺序遍历合并链表呢?答案是可以的!

归并排序是什么?简单来说就是将一个数组每次划分成等长的两部分,对两部分进行排序即是子问题。对子问题继续划分,直到子问题只有1个元素。还原的时候呢,将每个子问题和它相邻的另一个子问题利用上述双指针的方式,1 个与 1 个合并成 2 个,2 个与 2 个合并成 4 个,因为这每个单独的子问题合并好的都是有序的,直到合并成原本长度的数组。

对于这 k 个链表,就相当于上述合并阶段的 k 个子问题,需要划分为链表数量更少的子问题,直到每一组合并时是两两合并,然后继续往上合并,这个过程基于递归:

  • 终止条件: 划分的时候直到左右区间相等或左边大于右边。
  • 返回值: 每级返回已经合并好的子问题链表。
  • 本级任务: 对半划分,将划分后的子问题合并成新的链表。

step 1:从链表数组的首和尾开始,每次划分从中间开始划分,划分成两半,得到左边 n/2 个链表和右边 n/2 个链表。
step 2:继续不断递归划分,直到每部分链表数为 1。
step 3:将划分好的相邻两部分链表,按照两个有序链表合并的方式合并,合并好的两部分继续往上合并,直到最终合并成一个链表。

class Solution:
    def Merge(self , pHead1: ListNode, pHead2: ListNode) -> ListNode:
        if pHead1 == None:
            return pHead2
        if pHead2 == None:
            return pHead1
        head = ListNode(-1)
        cur = head
        while pHead1 != None and pHead2 != None:
            if pHead1.val <= pHead2.val:
                cur.next = pHead1
                pHead1 = pHead1.next
            else:
                cur.next = pHead2
                pHead2 = pHead2.next
            cur = cur.next
        if pHead1 == None:
            cur.next = pHead2
        if pHead2 == None:
            cur.next = pHead1
        return head.next
    
    def divideMerge(self, lists: List[ListNode], left: int, right: int) -> ListNode:
        if left > right:
            return None
        elif left == right:
            return lists[left]
        mid = (int)((left+right)/2)
        return self.Merge(self.divideMerge(lists, left, mid), self.divideMerge(lists, mid+1, right))
            
    def mergeKLists(self , lists: List[ListNode]) -> ListNode:
        return self.divideMerge(lists, 0, len(lists)-1)

时间复杂度: O ( n l o g k ) O(nlogk) O(nlogk) n n n 表示列表中所有链表的结点数量, k k k 表示链表的数量
空间复杂度: O ( l o g k ) O(logk) O(logk),递归会使用到 O ( l o g k ) O(logk) O(logk) 空间代价的栈空间

5.2 优先队列

如果非要按照归并排序的合并思路,双指针不够用,我们可以直接准备 k 个指针,每次比较得出 k 个数字中的最小值。为了快速比较 k 个数字得到最小值,我们可以利用Java提供的PriorityQueue或者C++SLT提供的优先队列或者Python提供的PriorityQueue可以实现,它是一种参照堆排序的容器,容器中的元素是有序的,如果是小顶堆,顶部元素就是最小的,每次可以直接取出最小的元素。也就是说

每次该容器中有 k 个元素,我们可以直接拿出最小的元素,再插入下一个元素,相当于每次都是链表的 k 个指针在比较大小,只移动最小元素的指针。

step 1:不管是Java还是C++都需要重载比较方法,构造一个比较链表节点大小的小顶堆。(Python版本直接加入节点值)
step 2:先遍历 k 个链表头,将不是空节点的节点加入优先队列。
step 3:每次依次弹出优先队列中的最小元素,将其连接在合并后的链表后面,然后将这个节点在原本链表中的后一个节点(如果不为空的话)加入队列,类似上述归并排序双指针的过程。

from queue import PriorityQueue
class Solution:
    def mergeKLists(self , lists: List[ListNode]) -> ListNode:
        # 小顶堆
        pq = PriorityQueue()
        # 遍历所有链表的第一个元素
        for i in range(len(lists)):
            # 不为空则加入小顶堆
            if lists[i]:
                pq.put((lists[i].val,i))
                lists[i] = lists[i].next
        # 添加一个表头
        res = ListNode(-1)
        head = res
        # 直到小顶堆为空
        while not pq.empty():
            # 取出最小元素
            val, idx = pq.get()
            # 链接
            head.next = ListNode(val)
            head = head.next
            # 链表若不为空,每次取出后一个元素加入小顶堆,注意 idx 代表的是什么
            if lists[idx]:
                pq.put((lists[idx].val,idx))
                lists[idx] = lists[idx].next
        return res.next

也可以直接用堆实现。

import heapq
class Solution:
    def mergeKLists(self , lists: List[ListNode]) -> ListNode:
        dummy = ListNode(0)
        p = dummy
        head = []
        for i in range(len(lists)):
            if lists[i]:
                heapq.heappush(head, (lists[i].val, i))
                lists[i] = lists[i].next
        while head:
            val, idx = heapq.heappop(head)
            p.next = ListNode(val)
            p = p.next
            if lists[idx]:
                heapq.heappush(head, (lists[idx].val, idx))
                lists[idx] = lists[idx].next
        return dummy.next

时间复杂度: O ( n l o g k ) O(nlogk) O(nlogk) n n n 表示列表中所有链表的结点数量, k k k 表示链表的数量,优先队列中的元素不超过 k k k 个,那么插入和删除的时间代价为 O ( l o g k ) O(logk) O(logk),这里最多有 n n n 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O ( n l o g k ) O(nlogk) O(nlogk)
空间复杂度: O ( k ) O(k) O(k),优先队列中的元素不超过 k k k 个,故渐进空间复杂度为 O ( k ) O(k) O(k)

5.3 辅助数组

class Solution:
    def mergeKLists(self , lists: List[ListNode]) -> ListNode:
        tmp = []
        for head in lists:
            while head:
                tmp.append(head.val)
                head = head.next
        if not tmp:
            return None
        tmp.sort()
        res = ListNode(-1)
        cur = res
        for i in range(len(tmp)):
            cur.next = ListNode(tmp[i])
            cur = cur.next
        return res.next

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn) n n n 表示列表中所有链表的结点数量,首先遍历所有结点 O ( n ) O(n) O(n),排序 O ( n l o g n ) O(nlogn) O(nlogn)
空间复杂度: O ( n ) O(n) O(n),辅助数组空间。

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

G皮T

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

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

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

打赏作者

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

抵扣说明:

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

余额充值