典型题目来自: 【精挑细讲】这 9 道链表相关算法题,将助你告别链表问题,不信你打我 - 帅地的文章 - 知乎
注:我这个和原文不太一样哈,可以都看看
1 如何优雅地反转单链表
刷题链接: 206. 反转链表
递归做法1
定义 work(head) 操作一个链表节点,让它和它之后的指针箭头反转,返回反转后的链表的头结点(原尾节点)
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
def work(head):
if head.next is None: # 一个节点的情况
return head
# 两个以上节点。两个的时候最特殊,想的时候首选按两个想。
nxt = head.next
start = work(nxt)
nxt.next = head
# 在中间链的时候无所谓,因为会被覆盖。但最后一层递归的时候必须要置为None呀
head.next = None
return start
if head is None:
return head
return work(head)
递归做法2
定义 work(head, tail) 操作一个链表的头和尾巴,返回反转链表的头和尾巴。
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
def work(head, tail):
if head == tail:
return head, tail
# 两个以上
newhead = head.next
# 使得“去头部分”成为真正的链表。删除这句话会死循环
head.next = None
rehead, retail = work(newhead, tail)
retail.next = head
return rehead, head
if head is None:
return None
cur = head
while cur.next:
cur = cur.next
rehead, retail = work(head, cur)
return rehead
为什么递归做法1里,不考虑外部入边呢?因为外部入边对 work(head)根本不可见,且work(head)的返回值也不会作用于外部结点,且这个思路本身就是在一条直线上翻转箭头方向而已,并不会形成环。
与之相反, work(head, tail) 中有 tail 对外部节点的操作,就有出bug的可能。
可以看出,思路的简洁,引领操作的简洁,和 bug 的有效避免。
迭代做法
先断链,再操作。把尾巴用个变量存起来就放心了。不要拖着尾巴甩来甩去.
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
dummy = ListNode(-1)
if not head:
return None
# head not None
cur = head
while cur:
nxt = cur.next # 先断链!再操作!
cur.next = dummy.next
dummy.next = cur
cur = nxt
return dummy.next
举一反三:一趟扫描反转从位置 m 到 n 的链表
class Solution:
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
dummy = ListNode(-1) # 引入dummy是为了便于处理从第1个开始反转的情况
dummy.next = head
cur = dummy
for _ in range(m-1): # 到达第m个节点的前方节点
cur = cur.next
start = cur
# 第一个需要插入的节点,也是将来的局部尾节点
pur = cur.next
end = pur
# 倒序插入:保存下一个节点;摘下本节点插入;移至下一个节点
start.next = None
for _ in range(n-m+1):
# 摘除pur节点
nxt = pur.next
pur.next = None
# 在start后面插入pur
pur.next = start.next
start.next = pur
# 进入下一个pur节点
pur = nxt
end.next = pur
return dummy.next
举一反三:将单链表的每K个节点之间逆序
迭代方法
- 定义函数
work(pre) -> new_pre。 - 其中,pre是开始逆序的地方(它后面k个被逆序)
- new_pre是下一个开始被逆序的地方
- dummy(pre) -> [ 1,2,3,4,5 ] -> [6,7,8,9,10]
- dummy -> [ 5,4,3,2,1(new_pre) ] -> [6,7,8,9,10]
- 1(pre) -> [6,7,8,9,10] -> ...
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
def work(pre): # pre is not None and pre.next is not None
c1 = pre.next
# if not enought 这题贼讨厌 非得分类讨论 只好预先多扫一遍
for _ in range(k-1):
if c1.next:
c1 = c1.next
else:
return c1 # c1 is the tail
# if enough
c1 = pre.next
c2 = pre.next.next
for _ in range(k-1):
tmp = c2.next
c2.next = c1
c1 = c2
c2 = tmp
tail = pre.next
pre.next = c1
tail.next = c2 # 这一步非常关键,要把 tail连接到下一个node上
return tail
dummy = ListNode(-1)
dummy.next = head
pre = dummy
while pre.next is not None: # 至少后面有一个节点
pre = work(pre)
return dummy.next
递归方法
递归方法比较显然。 就是 reverseKGroup(all) => reverse(k) + reverseKGroup(all - k) 这样
2 找到环形链表的入口
双指针法1
- 通过快慢指针定位到一个环中点
- 计算环的长度n
- 让一个指针比另一个早n步出发,则一定会在入口处相遇
class Solution:
def detectCycle(self, head: ListNode) -> ListNode:
quick = slow = head
while True:
if not quick or not quick.next: # 无环
return None
quick = quick.next.next
slow = slow.next
if quick == slow: # 发现环!
met = quick
break
cur = met.next # 计算环的长度 n
n = 1
while cur != met:
cur = cur.next
n += 1
quick = slow = head # 一个指针比另一个早n步出发,则一定会在入口处相遇
for _ in range(n):
quick = quick.next
while quick != slow:
quick = quick.next
slow = slow.next
return quick
双指针法2
- 通过快慢指针定位到一个环中点
- 注意到此时,快指针距离是慢指针的2倍,快指针比慢指针领先整数圈数,则慢指针走了整数圈数kn!
- 相当于慢指针已经相对于head早出发了 k * n 步。头结点出发和慢指针在入口相遇。
双指针法2比1来说,有点不好想,需要激灵反应。但本质逻辑一样:领先整数圈数的快指针,将在入口与慢指针相遇
3 约瑟夫问题
环形单链表模拟法
这是最直观的方法,不过会超时就是了。。。
注意在初始化后继指针pre的时候,不要初始化成dummy(然后cur是dummy.next)
因为断环+重连时,默认pre在环上。既然代码是这样写的,就要按这套来全程维护!
如果初始化成dummy,在m=1时程序会死循环。
class Solution:
def lastRemaining(self, n: int, m: int) -> int:
# 造链表
dummy = Node(-1)
cur = dummy
for i in range(n):
cur.next = Node(i)
cur = cur.next
cur.next = dummy.next
pre = cur # 最后一个节点
cur = pre.next # 第一个节点
while cur.next != cur: # 停止条件:只剩一个人
for i in range(m-1):# pre和cur,向前走 m-1 步
pre = pre.next
cur = pre.next
pre.next = cur.next # 删除并重连环
cur = pre.next
return cur.val
队列模拟法
同样的高时间复杂度模拟法。
出队的元素,数到数字就杀死,不然重排队尾。
优势在于不用自己手动处理环形链表了。
公式法
不超时的好方法。不是链表,在此不多说。
- 简而言之,每杀一个人,如果从下一个人从0计数,就变成了一个规模更小的问题。这个从0开始的问题肯定是好算的,但是我们要的是最初的编号。
- 通过推导,可以得到从n-1个人的编号回推到这个人在n个人的情况下的编号的对应关系。然后递归求解即可。
4 优雅判断回文链表
栈 1
众所周知,回文:正序 = 倒序。 使用栈保存结果,再依次出栈,和链表比对。可惜空间复杂度为 O(n)
栈 2
众所周知,回文:左半边正序 = 右半边倒序。 因此可以只让半拉链表入栈。如何得到半拉链表?遍历求长度除以2,可以。快慢指针更便捷。
反转半链表
众所周知,回文:左半边链表反转 = 右半边链表正序。 在上一个方法中,我们已经定位到了中点node,那么在这里断开,反转左边链表再和右边比对即可。
这种做法空间复杂度只需要 O(1)。
5 复制含有随机指针节点的链表
138. 复制带随机指针的链表 非常巧妙的一个套路,记住就好。关键点在于:
- 对 “原地修改题目数据” 这种操作有意识
- 意识到next指针在复制新的链表主干之后,本质上属于闲置资源了
- 在感到急切需要 从老链表到新链表的映射 后,发现next指针这个闲置资源可用
- 操作问题:暂存辅助信息的域,一定要恢复到用于提交的要求内容!
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return None
# copy stem
dummy = Node(-1)
c1 = dummy
c2 = head
while c2:
tmp = c2.next
# 插入新节点
c1.next = Node(c2.val)
c1 = c1.next
# 连接主从节点
c1.random = c2
c2.next = c1
c2 = tmp # 下一个
c1 = dummy.next
while c1:
father = c1.random
if father.random:
# 也可能存在father没有随机指针的情况,不能连续 father.random.next
c1.random = father.random.next
else:
# 这一句话千万不能丢!!不然random指针仍然指向旧链表!!
# 辅助信息用完了之后,清理干净!!
c1.random = None
c1 = c1.next
return dummy.next