LeetCode上链表相关的题目,刷题技巧(俗称刷题套路)都是比较固定的,即使是一些Hard的题,其实也只是多种技巧的结合而已。 本文将会介绍所有的这些技巧,提供一些题解,并标注这些题解所使用的刷题技巧。
链表的数据结构:
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
一、刷链表题到底有哪些技巧
对于LeetCode上跟数据结构相关的题来说,其刷题技巧都是跟数据结构本身的特性绑定的,本质上就是该数据结构的增删改查的技巧,链表题也不例外。所以,本文会根据对链表结构的增删改查对刷题技巧进行分类。
1.1 链表节点的删除操作
删除链表中的某个节点,一般有几种场景:
- 只知道当前节点,删除当前节点
- 知道前一个节点,删除当前节点
1.1.1 只知道当前节点时,删除当前节点
删除某个节点,只需要知道当前节点就够了,删除操作主要就两步:
- 把当前节点的值设置为下一个节点的值
- 把当前节点的next指针,指向下一个节点的next指针的指向节点
这个操作实际上是删除下一个节点,然后把下一个节点的val和next赋值给当前的节点,就达到了删除当前节点的目的。
# 这两个题都是一样的,知道当前节点,删除当前节点
def deleteNode(self, node):
node.val, node.next = node.next.val, node.next.next
1.1.2 知道前一个节点,删除当前节点
删除操作如下:
def deleteNode(self, pre):
## pre表示前一个节点,直接设置pre的next指针往后跳一个节点即可
pre.next = pre.next.next
def deleteDuplicates(self, head: ListNode) -> ListNode:
# 边界条件
if not head or not head.next:
return head
cur = head
while cur.next:
if cur.val == cur.next.val:
# 删除cur.next节点,可以用pre.next=pre.next.next的技巧
cur.next = cur.next.next
else:
# 不用删除就直接往后移动指针
cur = cur.next
return head
# 这个本83题不同的在于,要把重复的数字全删了,一个都不留
def deleteDuplicates(self, head: ListNode) -> ListNode:
# 边界条件
if not head or not head.next:
return head
cur = dummy = ListNode(-1, head)
while cur.next and cur.next.next:
if cur.next.val == cur.next.next.val:
x = cur.next.val
# 持续的把后面相同的值都删掉,这一小段其实跟83的相同
while cur.next and cur.next.val == x:
cur.next = cur.next.next
else:
cur = cur.next
return dummy.next
# 可以看到,这里就是一个简单的遍历找到节点然后运用删除链表的即可
def removeElements(self, head: ListNode, val: int) -> ListNode:
# 去掉边界条件
if not head:
return head
# 哑节点,这也是一个技巧,考虑这题的场景,当你想要删除的节点
# 是head节点怎么办??这时候用哑节点可以很轻易的解决问题
# 一般链表题,使用哑节点的场景是:当头结点会动的时候就需要用哑节点,比如换位置,删除
pre = dummy = ListNode(-1, head)
while head:
if head.val == val:
# 利用上文所述的删除节点的方法
pre.next = pre.next.next
else:
# 如果不删除,则pre指针往后移动一步
pre = pre.next
head = head.next
# 使用哑节点的好处,是这里可以轻易的找到结果链表的第一个节点
return dummy.next
- 剑指 Offer 18. 删除链表的节点 这题就是想办法找到待删除节点的前一个节点,然后删掉目标节点。
# 这题其实和上一题是一样的,不过就是这个题目本身的条件,
# 保证了链表中只有一个节点满足node.val == val,所以只需要删除一个节点即可
# 所以可以看到删除了一个节点以后,就可以直接break跳出循环
def deleteNode(self, head: ListNode, val: int) -> ListNode:
if head is None:
return head
pre = dummy = ListNode(-1, head)
while head:
if head.val == val:
pre.next = pre.next.next
# 当删除了要删除的这一个节点以后,就可以直接跳出循环了
break
else:
pre = pre.next
head = head.next
return dummy.next
# 一个简单空间换时间,加上删除节点的技巧
def removeDuplicateNodes(self, head: ListNode) -> ListNode:
# 边界条件
if not head or not head.next:
return head
occurred, pre, cur = {head.val}, head, head.next
while cur:
if cur.val in occurred:
# 如果出现过,使用删除节点的技巧删除当前cur节点
pre.next = cur.next
else:
# 没出现过,则保留cur节点,指针往后移
pre = pre.next
occurred.add(cur.val)
cur = cur.next
return head
1.2 链表节点的查询操作
在给定的链表中,查询某个节点,其实就是对链表的遍历操作。最简单的当然是从前往后一个节点一个节点的遍历,但在刷LeetCode的时候,用的最多的技巧是双指针遍历。
ps: 普通的一个一个节点遍历的技巧这里就不提了,太简单了。
双指针一半用来查找节点,也可以扩展到多指针,比如三指针、四指针等等,其原理都是一样的。
1.2.1 查找中间节点
def middleNode(head: ListNode) -> ListNode:
# 边界条件最先排除
# head 是 None,或者整个链表只有 head 一个节点
if not head or not head.next:
return head
# 快慢指针
slow, fast = head, head.next
# 快慢指针需要注意的边界条件,一定是 fast and fast.next
# 如果是一次性走三步,就是 fast and fast.next and fast.next.next
# 确定 while 循环体里面,不会有空指针异常即可
while fast and fast.next:
slow, fast = slow.next, fast.next.next
return slow
链表有可能是奇数个节点,也有可能是偶数个节点,对于奇数个节点的链表来说,其中间节点是确定的,对于偶数个节点的链表来说,其中间节点就不一定了。 比如:1->2->3->4,有些题要求的中间节点是2,有些题要求的中间节点是3,对于这种问题来说,其实变换一下双指针的起始节点的位置就可以了。
# 还是上面那个题,我们结合哑节点的技巧,找中间节点的pre节点,然后再返回pre.next就是题目要求的结果
# 以1->2->3->4为例,上一种解法是直接找到slow=3
# 这里的解法是,找到pre=2,然后返回pre.next(3)
def middleNode(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
# 关键在这里,前移了一个节点
pre, fast = ListNode(-1, head), head
while fast and fast.next:
pre, fast = pre.next, fast.next.next
# 如果题目要求的是返回1->2->3->4中的2节点,直接返回pre即可
# return pre
return pre.next
def sortedListToBST(self, head: Optional[ListNode]) -> Optional[TreeNode]:
# 这里的主要思路是一个树的递归思想,就不解释了
# 跟链表相关的是,利用了快慢指针找链表中点,因为树的root节点是链表的中间节点
if head is None:
return head
if head.next is None:
return TreeNode(head.val)
preMiddle = self.middlePreNode(head)
middle = preMiddle.next
preMiddle.next = None
root = TreeNode(middle.val)
root.left = self.sortedListToBST(head)
root.right = self.sortedListToBST(middle.next)
return root
def middlePreNode(self, head: ListNode) -> ListNode:
# 快慢指针找中点,slow初始值指向head的前一个节点
# 所有最终的结果,slow也等于中间节点的前一个节点
slow, fast = ListNode(-1, head), head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
return slow
1.2.2 查找链表中的第K个节点
双指针再扩展一下,两个指针之间,不一定要一个走一步一个走两步的,也不定一开始的时候,两个指针之间只相隔一个节点的。
来看下面这两个题:
# 剑指 Offer 22. 链表中倒数第k个节点
def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
if head is None:
return head
slow, fast = head, head
# 先在slow和fast之间拉开k的距离
# 然后双指针一起向链表尾部移动,知道fast为None的时候,slow就是倒数第K个节点
for _ in range(k):
fast = fast.next
while fast:
# slow, fast往后走的速度是一样的,都是一个节点
slow, fast = slow.next, fast.next
return slow
# 面试题 02.02. 返回倒数第 k 个节点
def kthToLast(self, head: ListNode, k: int) -> int:
slow, fast = head, head
for _ in range(k):
fast = fast.next
while fast:
slow, fast = slow.next, fast.next
# 跟上面那题基本一模一样,只是返回的是节点值
return slow.val
# 其实就是一个双指针找正数第K个节点和倒数第K个节点的问题
def swapNodes(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 去掉边界条件
if head is None or head.next is None:
return head
# 只交换节点的值就很简单了,只要双指针找到两个节点即可
slow, fast = head, head
# 注意这里用的是 k-1,后面用的是fast.next不为空
# 跟上面两题(面试题 02.02. 返回倒数第 k 个节点)是有区别的
# 主要是为了找到正数第K个节点,因为正数都是从1开始而非0开始
# 所以这里需要用k-1
for _ in range(k - 1):
fast = fast.next
# 找到正数第k个节点
p1 = fast
while fast.next:
slow, fast = slow.next, fast.next
# slow就是倒数第k个节点,直接交换值即可
p1.val, slow.val = slow.val, p1.val
return head
1.2.3 双指针判断链表状态(查找链表相交点、判断链表成环等)
双指针另一个比较巧妙的应用,是用于判断链表的状态:
- 链表成环:两个指针一快一慢的走,如果是链表是个环,那么快慢两个指针必然会相遇
- 判断链表是否相交:同样是两个指针,走的速度一样,但是从两条链表的两端出发,如果两条链表有交点,那么两个指针必然会相遇在交点的节点上
先来看链表成环的题:
# 141. 环形链表
def hasCycle(self, head: Optional[ListNode]) -> bool:
# 慢指针每次走一步,快指针每次走两步
# 如果链表有环,那么他们必定会相遇在某个节点
# (想象一下操场跑圈,只要一直在跑,跑的快的人,必定能够超跑的慢的人一圈,也就是他们必定会在某个点相遇)
slow, fast = head, head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow == fast:
return True
return False
# 142. 环形链表 II
# 面试题 02.08. 环路检测
# 剑指 Offer II 022. 链表中环的入口节点
# 这几个题是一样的,寻找成环的那个点
def detectCycle(self, head: ListNode) -> ListNode:
slow, fast = head, head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow == fast:
# 假设成环的那个点为A,相遇点为B,head到A的距离为x,A到B的距离为y
# 由于有环存在,那么B是可以到A的,距离为z
# 那么,第一次相遇的时候,slow走过的距离为 x + y,fast走过的距离为 x + y + z + y
# slow和fast速度不同,时间相同,所以可以得到等式 2(x+y) = x+y+z+y,则求出z=x
# 所以再来一次双指针,从head节点和B点开始走,他们相遇的地方就是A点
s, f = head, slow
while s != f:
s, f = s.next, f.next
return s
# 如果不成环,slow和fast不回相遇,直接返回None
return None
然后看链表相交的题:
# 这些题都是一样的
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
# 边界条件
if not headA or not headB:
return None
# 双指针
p1, p2 = headA, headB
# 核心逻辑是:假设链表headA长度为a,链表headB长度为b,那么
# 当p1指针从headA跑到结尾时,就从headB继续跑
# 当p2指针从headB跑到结尾时,就从headA继续跑
# 如果headA和headB有交点,那么p1和p2就肯定会相遇在交点
# 如果headA和headB没有交点,那么p1和p2就肯定相遇在None,就是刚好都跑到终点,
# 且他们跑过的路径总长就是a+b
while p1 != p2:
p1 = p1.next if p1 else headB
p2 = p2.next if p2 else headA
return p1
1.3 链表节点的插入操作
链表节点总共就只有两个字段,一个是值字段val,一个是指向下一个节点的指针字段next。一般来说,改next字段的题比较多。
1.3.1 反转链表节点
反转一整条链表的节点,其实只需要了解两个链表节点之间怎么反转就行,然后只需要循环的对链表节点两两交换即可。
# 这种反转链表的方式是需要不断的移动 pre 和 cur 指针的
# 本质上就是不断的往pre节点后插入新的节点
def reverse(pre: ListNode, cur: ListNode):
# 保留下一个节点,避免丢失
tmp = cur.next
# 当前节点的next指向前一个节点
cur.next = pre
# pre和cur的指针都向后移动,开始准备下一轮的反转链表节点
pre, cur = cur, tmp
# 头插法反转链表,可以不用显式的去移动 pre 和 cur 指针
# 本质上就是不断的往pre节点插入插入新的节点,但也可以看作是不断的往链表头部插入节点
def reverse(pre: ListNode, cur: ListNode):
tmp = cur.next
cur.next = tmp.next
tmp.next = pre.next
pre.next = tmp
# 这种做法的好处是,不需要显式的移动pre和cur指针了
# 其中,cur指针回隐式的随着反转两个节点之后被移动,
# pre指针则一直处于头部,并不会移动
# 三个题完全一样的
# 206. 反转链表 剑指 Offer 24. 反转链表 剑指 Offer II 024. 反转链表
def reverseList(self, head: ListNode) -> ListNode:
# 去除边界条件,链表为空或者只有单个节点时直接返回,不需要反转
if not head or not head.next:
return head
# 考虑到第一个节点,反转后,next 的指向是 None,所以这里把 pre 设置成 None
pre, cur = None, head
while cur:
# 当前节点的next指针要指向前一个
tmp = cur.next
cur.next = pre
# pre和cur的指针都向后移动,开始准备下一轮的反转链表节点
pre, cur = cur, tmp
return pre
# 头插法也可以解决这几个题
def reverseList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
pre = ListNode(-1, head)
cur = head
while cur.next:
tmp = cur.next
cur.next = tmp.next
# 这里注意赋值一定要是pre.next,因为是插入到链表的头部
# 要注意走了几步以后,pre.next不是等于cur的
tmp.next = pre.next
pre.next = tmp
return pre.next
# 使用头插法,相对来说比较简单
def reverseBetween(self, head: ListNode, left: int, right: int) -> ListNode:
dummy = ListNode(-1, head)
# 先找left位置的前一个节点
pre = dummy
for _ in range(left - 1):
pre = pre.next
# 然后开始反转
cur = pre.next
# 头插法反转链表
for _ in range(right - left):
tmp = cur.next
cur.next = tmp.next
tmp.next = pre.next
pre.next = tmp
return dummy.next
1.3.2 交换节点
交换节点的技巧:
# 非常的类似于头插法
# 需要知道至少三个节点 pre, first, second
# 首先保存下最后一个节点 post,这样我们就知道有 pre first second post 四个节点了
# 然后只需要把 first second 换个位置就行
post = second.next
# 交换位置
pre.next = second
second.next = first
first.next = post
def swapPairs(self, head: ListNode) -> ListNode:
# 先去除边界条件
if not head or not head.next:
return head
dummy = ListNode(-1, head)
# 这里是类似于双指针的思路,四指针并行跑
pre, first, second, post = dummy, head, head.next, head.next.next
while True:
# 这里直接使用交换节点的技巧
pre.next = second
second.next = first
first.next = post
# 先判断是否已经到链表最后,如果是则直接跳出循环
# 如果没到最后则继续移动指针
if not post or not post.next:
break
# 移动指针
pre = pre.next.next
first, second = post, post.next
post = post.next.next
return dummy.next
1.3.3 纯插入操作
def flatten(self, head: 'Node') -> 'Node':
# 思路比较简单,就是从左到右遍历,发现某个节点有child,就先处理child节点
# 把下一层的节点都扫描完然后接到当前的节点的后面
# 处理完后,指针再往后移动一格,重复之前的操作
cur = head
while cur:
if cur.child:
# 其实这里就是一个插入操作的技巧
# 先保存一下某些关键节点的指针
tmp, child = cur.next, cur.child
# 然后开始修改cur和child节点的next以及prev指针,相当于在cur后面插入child节点
# 同时把cur.child置为None,因为这个节点的child已经处理完了
cur.next, child.prev, cur.child = child, cur, None
if tmp:
# 如果原来的cur.next是存在的话,要把下一层的链表的最后的节点和这个节点连接上
while child.next:
child = child.next
tmp.prev, child.next = child, tmp
# 处理完以后,指针向后走一格
cur = cur.next
return head
```## 二、多种技巧结合的链表题
### 2.1 双指针与删除链表节点结合
- [剑指 Offer II 021. 删除链表的倒数第 n 个结点](https://leetcode-cn.com/problems/SLwz0R/)
- [19. 删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/)
```python
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
if not head:
return head
dummy = ListNode(-1, head)
# 双指针,找到倒数第N个节点的pre节点
slow, fast = dummy, head
# 先让fast走n步
for _ in range(n):
fast = fast.next
# 然后一直走到底,这时候slow就是倒数第N个节点的pre节点
while fast:
slow, fast = slow.next, fast.next
# 知道当前节点的前驱pre节点,删除当前节点的技巧
slow.next = slow.next.next
return dummy.next
def deleteMiddle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 双指针找中间节点的前一个节点
dummy = ListNode(-1, head)
# 这边稍微变通一下,本来找中间节点时,是slow,fast=head,head
# 这边把slow设置为head的前一个节点,所以最后就能找到中间节点的前一个节点
slow, fast = dummy, head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
# 知道pre节点,删除节点的技巧
slow.next = slow.next.next
return dummy.next
2.2 双指针与反转链表技巧相结合
def isPalindrome(self, head: ListNode) -> bool:
if not head or not head.next:
return True
# 先用双指针找中点
slow, fast = head, head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
# 再反转后半部分链表
pre = None
# 链表节点数是奇数时,fast肯定不是None,且slz进一个,如果是偶数,则不需要
# 1 -> 2 -> 3 -> 2 -> 1,此时中点slow是3,需要从下一个2开始反转
# 1 -> 2 -> 2 -> 1,此时中点slow是2,直接开始反转即可
if fast:
slow = slow.next
while slow:
tmp = slow.next
slow.next = pre
pre = slow
slow = tmp
# 最后循环对比
while pre:
if pre.val != head.val:
return False
pre, head = pre.next, head.next
return True
def pairSum(self, head: Optional[ListNode]) -> int:
# 思路比较简单,就是先反转后半部分的链表,然后从两头开始遍历,比较相加的值即可
# 技巧是双指针+反转链表
if head is None:
return head
# 先双指针找中间节点,注意这里对起始位置的设置
# 可以做到循环结束以后,slow是中间节点的前一个节点
# 比如 1->2->3->4,循环结束时slow指向2
slow, fast = head, head.next
while fast.next:
slow, fast = slow.next, fast.next.next
# 从中间断开链表,然后反转链表,最后pre是后半段链表的头指针
pre, cur = None, slow.next
slow.next = None
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
# 然后开始对比即可
ans = 0
p1, p2 = head, pre
while p1:
ans = max(ans, p1.val + p2.val)
p1, p2 = p1.next, p2.next
return ans
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
if not head or not head.next:
return head
pre = dummy = ListNode(-1, head)
while head:
cur = pre
# 先检查是否大于等于K
for _ in range(k):
cur = cur.next
if not cur:
return dummy.next
# 此时cur是需要反转的链表的tail节点
# pre.next是需要反转的链表的head节点
tmp = cur.next
h, t = self.reverse(pre.next, cur)
# 把链表接上
pre.next, t.next = h, tmp
# 重新调整指针位置
pre, head = t, t.next
return dummy.next
def reverse(self, head: ListNode, tail: ListNode):
pre, cur = None, head
while pre != tail:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return tail, head
def reorderList(self, head: ListNode) -> None:
"""
Do not return anything, modify head in-place instead.
"""
# 边界条件
if not head or not head.next:
return
# 基本思路是,双指针找中点,然后反转后半部分链表,最后合并链表
slow, fast = ListNode(-1, head), head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
# 此时,slow是中点的前一个节点
tmp = slow.next
slow.next = None
p1, p2 = head, self.reverse(tmp)
self.merge(p1, p2)
def merge(self, p1: ListNode, p2: ListNode) -> ListNode:
head = dummy = ListNode(-1)
# 这里因为题目能确定,前半部分链表长度肯定小于等于后半部分
# 所以有一些边界条件就不考虑了
while p1:
head.next = p1
p1 = p1.next
head = head.next
head.next = p2
p2 = p2.next
head = head.next
if p2:
head.next = p2
return dummy.next
def reverse(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
pre, cur = None, head
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
2.3 双指针与插入链表技巧结合
def partition(self, head: ListNode, x: int) -> ListNode:
# 边界条件
if not head or not head.next:
return head
# 类似于双指针技巧吧,其实是搞了两个链表,
# small表示小于x的节点,按照原链表顺序排列
# large表示大于等于x的节点,按照原链表顺序排列
# 最后把large接到samll的尾节点之后即可
small = smallHead = ListNode(0)
large = largeHead = ListNode(0)
while head:
# 插入链表的技巧
if head.val < x:
small.next = head
small = small.next
else:
large.next = head
large = large.next
head = head.next
# 这里注意一下把最后那个节点设置为next=None,插入链表时常需要注意的点
# 因为这个节点可能是原链表中的某一个中间节点
large.next = None
# 最后把large接到samll的尾节点之后
small.next = largeHead.next
return smallHead.next
def copyRandomList(self, head: 'Node') -> 'Node':
# 边界条件
if head is None:
return None
# 先遍历一边,把每个节点都copy一下并插入到当前节点后面
cur = head
while cur:
tmp = Node(cur.val)
tmp.next = cur.next
cur.next = tmp
cur = tmp.next
# 然后把random拷贝一下
cur = head
while cur:
if cur.random:
cur.next.random = cur.random.next
cur = cur.next.next
# 最后开始拆链表,把之前copy出来的节点从链表里拆出来
slow, fast = head, head.next
dummy = fast
while fast and fast.next:
slow.next, fast.next = slow.next.next, fast.next.next
slow, fast = slow.next, fast.next
slow.next = None
return dummy
def insertionSortList(self, head: ListNode) -> ListNode:
# 边界条件
if not head or not head.next:
return head
dummy = ListNode(float("-inf"), head)
# 就是一个双指针模拟一下就可以
slow, fast = dummy, dummy.next
while fast:
# 如果已经按照顺序排列好了,就都往后走一步
if fast.val >= slow.val:
slow, fast = slow.next, fast.next
else:
# 如果需要把fast节点插入到前面的节点
# 就从头开始遍历,找到可插入的位置
pre = dummy
while pre.next.val < fast.val:
pre = pre.next
# 需要处理两个节点,一个是把fast从原来的地方删掉,所以用删除节点的技巧
slow.next = slow.next.next
# 这里要把fast节点插入到对应的位置,用插入节点的技巧
tmp = pre.next
pre.next = fast
fast.next = tmp
# 最后为了能继续循环后面的,需要把fast节点重新指向slow节点的后续
fast = slow.next
return dummy.next
``
- [328. 奇偶链表](https://leetcode.cn/problems/odd-even-linked-list/)
```python
def oddEvenList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 边界条件
if not head or not head.next:
return head
# 思路比较简单,双指针遍历一遍,把奇数节点和偶数节点分成两个链表
# 然后拼接上去就行
slow, fast = head, head.next
tmp = head.next
while fast and fast.next:
slow.next, fast.next = slow.next.next, fast.next.next
slow, fast = slow.next, fast.next
slow.next = tmp
return head
2.4 双指针与排序算法相结合
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 使用递归实现归并排序
# 思路就是,先用双指针找中点,从中间断开链表,合并左右两条链表,不断的递归即可
# 时间复杂度是O(N*logN)空间复杂度是O(logN)
if not head or not head.next:
# 边界条件
return head
# 双指针找中点
slow, fast = ListNode(-1, head), head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
# slow是中点的前一个节点,然后从中间截断链表
mid, slow.next = slow.next, None
# 递归的对两遍的链表排序
left, right = self.sortList(head), self.sortList(mid)
# 合并两条链表
cur = dummy = ListNode(-1)
while left and right:
if left.val <= right.val:
cur.next = left
left = left.next
else:
cur.next = right
right = right.next
cur = cur.next
cur.next = left if left else right
return dummy.next
# 但是这个题,还有个更好的解法,可以把空间复杂度优化到O(1)
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
# 边界条件
if not lists:
return None
length = len(lists)
if length == 0:
return None
# 其实是对数组做了一个归并的操作
return self.mergeAndSort(lists, 0, length - 1)
def mergeAndSort(self, lists: List[ListNode], left: int, right: int) -> ListNode:
if left == right:
return lists[left]
middle = (left + right) // 2
l = self.mergeAndSort(lists, left, middle)
r = self.mergeAndSort(lists, middle + 1, right)
return self.merge(l, r)
def merge(self, p1: ListNode, p2: ListNode) -> ListNode:
if not p1:
return p2
if not p2:
return p1
pre = dummy = ListNode(-1)
while p1 and p2:
if p1.val <= p2.val:
pre.next, p1 = p1, p1.next
else:
pre.next, p2 = p2, p2.next
pre = pre.next
pre.next = p1 if p1 else p2
return dummy.next
2.5 链表和其他数据结构结合
这种就是利用链表数据结构的特性来做一些事情,链表数据结构有如下的特性:
- 插入、删除操作O(1)
- 查询操作O(n)
- 有顺序
2.5.1 双向链表跟哈希表结合
哈希表的数据结构特性:
- 插入、删除、查询操作O(1)
- 没有顺序
哈希表跟链表数据结构结合起来使用,可以取长补短,做到:
-
插入、删除、查询操作O(1)
-
有顺序
# 主要的思路是:
# 1. 利用哈希表的查找操作为O(1),可以直接找到key对应的链表节点
# 2. 利用双向链表的插入、删除节点操作为O(1),可以很方便的新增、删除数据
# 3. 利用双向链表保持顺序,头部的节点是新的,尾部的节点是旧的
class Node:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cache = dict()
self.head = Node(-1)
self.tail = Node(-1)
# 这里用哑节点,这样后面插入/删除链表节点操作就不用考虑刚好
# 要操作的节点在两端了
self.head.next, self.tail.prev = self.tail, self.head
self.capacity = capacity
self.size = 0
def get(self, key: int) -> int:
if key not in self.cache:
return -1
# 如果key存在,则需要把这个节点移动到头部
node = self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key in self.cache:
node = self.cache[key]
# 如果存在,就修改value并把节点移动到头部
node.value = value
self.moveToHead(node)
else:
# 如果不存在,则在头部插入一个新节点
node = Node(key, value)
self.cache[key] = node
self.addToHead(node)
# 检查size
if self.size >= self.capacity:
# 超容量了先删除尾部节点
removed = self.removeTail()
self.cache.pop(removed.key)
else:
self.size += 1
def moveToHead(self, node: Node):
self.removeNode(node)
self.addToHead(node)
def removeNode(self, node: Node):
node.prev.next = node.next
node.next.prev = node.prev
def addToHead(self, node: Node):
self.head.next.prev = node
node.next = self.head.next
self.head.next = node
node.prev = self.head
def removeTail(self) -> Node:
node = self.tail.prev
self.removeNode(node)
return node
三、剩下的一些其实跟链表关系不大的题
剩下的还有一部分虽然打了链表的tag,但是实际上跟链表关系不是特别大的题目。这些题里可能只是使用了链表这个数据结构而已,并不涉及到以上提到的一些链表特有的答题技巧。
3.1 一些简单的只需要遍历链表的题
# 这两题也是一个简单的遍历,一遍遍历一遍选择节点就行
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
cur = dum = ListNode(0)
while l1 and l2:
if l1.val < l2.val:
cur.next, l1 = l1, l1.next
else:
cur.next, l2 = l2, l2.next
cur = cur.next
# 需要注意的是这里,l1 或者 l2 其中某一个遍历完了以后,
# 剩下的那个链表的所有节点,还需要接到结果的链表上去
cur.next = l1 if l1 else l2
return dum.next
# 这个就是简单的遍历,没有什么特别的技巧
def getDecimalValue(self, head: ListNode) -> int:
cur, ans = head, 0
while cur:
ans = ans * 2 + cur.val
cur = cur.next
return ans
# 遍历一遍就行,实在不行可以用反转链表
def reversePrint(self, head: ListNode) -> List[int]:
res = []
while head:
res.append(head.val)
head = head.next
# 这里其实用了python的语法糖
return res[::-1]
# 也就是一个简单的遍历
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
pre = dummy = ListNode(-1)
carry = 0
while l1 or l2:
val = carry
if l1:
val += l1.val
l1 = l1.next
if l2:
val += l2.val
l2 = l2.next
pre.next = ListNode(val % 10)
carry = val // 10
pre = pre.next
# 需要注意的是这边,最后如果还有进位的话,要多加一个节点
if carry != 0:
pre.next = ListNode(carry)
return dummy.next
# 445. 两数相加 II
# 剑指 Offer II 025. 链表中的两数相加
# 题目要求不能反转链表,就只能用栈解决了,空间复杂度O(m+n)
# 反而是使用反转链表的解法可以把空间压缩到 O(1)
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
s1, s2 = [], []
while l1:
s1.append(l1.val)
l1 = l1.next
while l2:
s2.append(l2.val)
l2 = l2.next
ans = None
carry = 0
while s1 or s2 or carry != 0:
a = 0 if not s1 else s1.pop()
b = 0 if not s2 else s2.pop()
cur = a + b + carry
carry = cur // 10
cur %= 10
curnode = ListNode(cur)
curnode.next = ans
ans = curnode
return ans
# 这个就是简单遍历,然后把链表接上就行
def mergeInBetween(self, list1: ListNode, a: int, b: int, list2: ListNode) -> ListNode:
preA = pB = dummy = ListNode(-1, list1)
# 先找到a位置节点的pre节点(preA)
for _ in range(a):
preA = preA.next
# 再找b位置的节点(pB)
for _ in range(b+1):
pB = pB.next
# 然后把list2接到a,b之间
preA.next = list2
while preA.next:
preA = preA.next
preA.next = pB.next
return dummy.next
def rotateRight(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
# 思路:现将头尾连起来,然后找合适的地方断开即可
# 这题的难点其实是个数学计算,要把断开的点算对,画个图就会比较清晰
if head is None or head.next is None or k == 0:
return head
# 先找最后一个节点
tail, count = head, 1
while tail.next:
count += 1
tail = tail.next
# 可以提早结束
step = k % count
if step == 0:
return head
# 头尾相连
tail.next = head
for _ in range(count - step):
tail = tail.next
# 找到合适的点,然后断开
res = tail.next
tail.next = None
return res
# 这题就是一个简单的双向链表模拟一下
class Node:
def __init__(self, val: str):
self.val = val
self.pre = None
self.next = None
class BrowserHistory:
def __init__(self, homepage: str):
self.cur = Node(homepage)
def visit(self, url: str) -> None:
node = Node(url)
node.pre = self.cur
self.cur.next = node
self.cur = self.cur.next
def back(self, steps: int) -> str:
while steps > 0 and self.cur.pre:
self.cur = self.cur.pre
steps -= 1
return self.cur.val
def forward(self, steps: int) -> str:
while steps > 0 and self.cur.next:
self.cur = self.cur.next
steps -= 1
return self.cur.val
def mergeNodes(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 排除边界条件
if head is None:
return None
# 这题的思路其实很简单,从左到右一遍循环,
# 不断的累加值,只要遇到0就停下来,新建节点,重置累加的值,
# 然后继续循环即可
dummy = pre = ListNode(-1)
cur = head.next
acc = 0
while cur:
if cur.val == 0:
node = ListNode(acc)
pre.next = node
pre = pre.next
acc = 0
else:
acc += cur.val
cur = cur.next
return dummy.next
def splitListToParts(self, head: ListNode, k: int) -> List[ListNode]:
# 思路比较简单,就是先计算平均长度,比如链表总长为5, k=3时
# 平均长度5//3=1,如果三个链表,每个链表都分配1个节点的话,就剩下5%3=2个节点未分配
# 这时候就把剩下的2个节点都分配给前面的链表,所以答案就是[2, 2, 1]
res, length = [], self.calLength(head)
quotient, remainder = length // k, length % k
res = [None for _ in range(k)]
i, cur = 0, head
while i < k and cur:
res[i] = cur
size = quotient + (1 if i < remainder else 0)
for _ in range(size - 1):
cur = cur.next
tmp = cur.next
cur.next = None
cur = tmp
i += 1
return res
def calLength(self, head: ListNode) -> int:
cur, length = head, 0
while cur:
cur, length = cur.next, length + 1
return length
def nodesBetweenCriticalPoints(self, head: Optional[ListNode]) -> List[int]:
# 思路比较简单,最小距离就是两个相邻的极值点之间的距离,最大距离就是第一个和最后一个极值点的距离
# 0. 用 first 和 last 保存第一个和最后一个极值点的位置
# 1. 先从左往右遍历,cur,cur.next, cur.next.next 三个点之间判断cur.next是不是极值点
# 2. 如果不是极值点,更新cur指针,直接往后遍历即可
# 3. 如果是极值点:
# 3.1 当first没有被初始化时,需要初始化first
# 3.2 last没有被初始化时,初始化last
# 3.3 当last已经被初始化时,说明可以开始更新minDist和maxDist的值了,最后也要更新last的值
minDist = maxDist = -1
first = last = -1
cur, pos = head, 0
while cur and cur.next and cur.next.next:
a, b, c = cur.val, cur.next.val, cur.next.next.val
if b > max(a, c) or b < min(a, c):
if last != -1:
maxDist = max(pos - first, maxDist)
minDist = (pos - last if minDist == -1 else min(minDist, pos - last))
if first == -1:
first = pos
# last的值在每次发现新的极值时都需要更新
last = pos
cur, pos = cur.next, pos + 1
return [minDist, maxDist]
# 这题其实跟链表关系不大,只要遍历一遍找到能插入的点就行
# 思路比较简单,分为三种情况:
# 1. 如果 head 是 None,那就创建一个节点,让这个节点的next指针指向自己即可
# 2. 开始循环,寻找边界,边界会出现p.next.val < p.val的情况,这时候p.next就是最小值节点,p就是最大值节点
# 2.1 如果要插入的值是大于最大值,小于最小值,就是要在边界点插入
# 2.2 如果要插入的值是大于等于p.val小于等于p.next.val,就说明找到了插入的点
def insert(self, head: 'Node', insertVal: int) -> 'Node':
if not head:
tmp = Node(insertVal)
tmp.next = tmp
return tmp
p = head
while p.next != head:
if p.val > p.next.val:
# 说明到达了边界点,p.next就是这个链表的起点
if p.val < insertVal or p.next.val > insertVal:
break
if p.val <= insertVal and insertVal <= p.next.val:
break
p = p.next
p.next = Node(insertVal, p.next)
return head
def numComponents(self, head: Optional[ListNode], nums: List[int]) -> int:
# 用一个set保存nums的值,然后遍历的时候检查下即可,需要注意ans加一的条件:
# 1. 当前节点的值在numSet里,而下一个节点的值不在numSet里
# 2. 当前节点的值在numSet里,而下一个节点为None
numSet, cur, ans = set(nums), head, 0
while cur:
if cur.val in numSet:
if cur.next:
if cur.next.val not in numSet:
ans += 1
else:
ans += 1
cur = cur.next
return ans
3.2 通用刷题技巧:前缀和
# 核心思路就是,如果两个节点的前缀和相等,则意味着两个节点之间的节点和为0
# 前缀和也是一种常见的刷题技巧,可以用在任意的线性数据结构之上,比如数组
def removeZeroSumSublists(self, head: ListNode) -> ListNode:
lookup = dict()
cur = dummy = ListNode(0, head)
preSum = 0
# 先建立前缀和跟节点的映射,注意后面的节点如果前缀和跟前面的节点一样
# 在map里就会覆盖掉前一个节点
# 然后下一次遍历的时候就能找到前后两个前缀和一样的节点,只要删除这两个节点中间的全部节点即可
while cur:
preSum += cur.val
lookup[preSum] = cur
cur = cur.next
# 开始一轮新的遍历
cur, preSum = dummy, 0
while cur:
preSum += cur.val
# lookup[preSum] 这时候要么找到cur节点自己,要么就是找到跟他一样前缀和的那个节点
cur.next = lookup[preSum].next
cur = cur.next
return dummy.next
3.3 随机算法
class Solution:
def __init__(self, head: Optional[ListNode]):
self.head = head
def getRandom(self) -> int:
# 蓄水池抽样算法可以以解决一个经典的面试算法题:当内存无法加载全部数据时,
# 如何从包含未知大小的数据流中随机选取k个数据,
# 并且要保证每个数据被抽取到的概率相等
# 其实跟这个题就是一样的
cur, i, ans = self.head, i, None
while cur:
# 每一轮都有 i/1 机会被选中
if randrange(i) == 0:
ans = cur.val
i = i + 1
cur = cur.next
return ans
其实在设计跳表的时候,也有一个随机算法,用于选择层数:
# 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层
# 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
# 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
# 50%的概率返回 1
# 25%的概率返回 2
# 12.5%的概率返回 3 ...
def randomLevel():
# p 表示每一层被选到的概率
level, p, maxLevel = 1, 0.5, 16
# pytyon的random.random()用于从[0, 1)生成一个随机的浮点数
while random.random() < 0.5 and level < 16:
level += 1
return level
3.4 通用刷题技巧:单调栈
单调栈 是一种比较通用的刷题技巧,它是有模版的:
def template(self, nums: List[int]) -> List[int]:
stack, res = [], [0] * len(nums)
# 反向遍历数组
for i in range(len(nums)-1, -1, -1):
# 这一步是把所有的比当前元素小的数都弹出栈
while len(stack) > 0 and stack[-1] <= nums[i]:
stack.pop()
# 更新结果,要么是栈顶元素,要么找不到就赋值0
res[i] = stack[-1] if len(stack) > 0 else 0
# 把当前的元素入栈
stack.append(nums[i])
return res
# 这个题比较简单的解法是,直接转成数组,然后套用单调栈模版
def nextLargerNodes(self, head: Optional[ListNode]) -> List[int]:
# 先转成数组
array = []
while head:
array.append(head.val)
head = head.next
# 套用单调栈模版
length = len(array)
stack, ans = [], [0] * length
# 从后往前逆序遍历
for i in range(length - 1, -1, -1):
while len(stack) > 0 and stack[-1] <= array[i]:
stack.pop()
ans[i] = stack[-1] if len(stack) > 0 else 0
stack.append(array[i])
return ans