【面试必刷TOP101】系列包含:
- 面试必刷TOP101:链表(01-05,Python实现)
- 面试必刷TOP101:链表(06-10,Python实现)
- 面试必刷TOP101:链表(11-16,Python实现)
- 面试必刷TOP101:二分查找/排序(17-22,Python实现)
- 面试必刷TOP101:二叉树系列(23-30,Python实现)
- 面试必刷TOP101:二叉树系列(31-36,Python实现)
- 面试必刷TOP101:二叉树系列(37-41,Python实现)
- 面试必刷TOP101:堆、栈、队列(42-49,Python实现)
- 面试必刷TOP101:哈希表(50-54,Python实现)
- 面试必刷TOP101:递归 / 回溯(55-61,Python实现)
- 面试必刷TOP101:动态规划(入门)(62-66,Python实现)
- 面试必刷TOP101:动态规划(67-71,Python实现)
- 面试必刷TOP101:动态规划(72-77,Python实现)
- 面试必刷TOP101:动态规划(78-82,Python实现)
- 面试必刷TOP101:字符串(83-86,Python实现)
- 面试必刷TOP101:双指针(87-94,Python实现)
- 面试必刷TOP101:贪心算法(95-96,Python实现)
- 面试必刷TOP101:模拟(97-99,Python实现)
面试必刷TOP101:链表(01-05,Python实现)
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 n−1 个分组反转后的结果接在第一组后面就行了,那这剩余的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),辅助数组空间。