列表 模版题单 12
链表
基本技能
链表相关的核心点
- null/nil 异常处理
- dummy node 哑巴节点
- 快慢指针
- 插入一个节点到排序链表
- 从一个链表中移除一个节点
- 翻转链表
- 合并两个链表
- 找到链表的中间节点
常见题型
remove-duplicates-from-sorted-list
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
class Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:if head is None:return headcurrent = headwhile current.next is not None:if current.next.val == current.val:current.next = current.next.nextelse:current = current.nextreturn head
remove-duplicates-from-sorted-list-ii
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现的数字。
- 思路:链表头结点可能被删除,所以用 dummy node 辅助删除
class Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:if head is None:return headdummy = ListNode(next=head)current, peek = dummy, headfind_dup = Falsewhile peek.next is not None:if peek.next.val == peek.val:find_dup = Truepeek.next = peek.next.nextelse:if find_dup:find_dup = Falsecurrent.next = current.next.nextelse:current = current.nextpeek = peek.nextif find_dup:current.next = current.next.nextreturn dummy.next
注意点
• A->B->C 删除 B,A.next = C
• 删除用一个 Dummy Node 节点辅助(允许头节点可变)
• 访问 X.next 、X.value 一定要保证 X != nil
reverse-linked-list
反转一个单链表。
- 思路:将当前结点放置到头结点
class Solution:def reverseList(self, head: ListNode) -> ListNode:if head is None:return headtail = headwhile tail.next is not None:# put tail.next to head tmp = tail.nexttail.next = tail.next.nexttmp.next = headhead = tmpreturn head
- Recursive method is tricky
class Solution:def reverseList(self, head: ListNode) -> ListNode:if head is None or head.next is None:return headrev_next = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn rev_next
reverse-linked-list-ii
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
- 思路:先找到 m 处, 再反转 n - m 次即可
class Solution:def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:if head is None:return headn -= m # number of times of reversecurr = dummy = ListNode(next=head)while m > 1: # find node at m - 1curr = curr.nextm -= 1start = curr.nextwhile n > 0: # reverse n - m timestmp = start.nextstart.next = tmp.nexttmp.next = curr.nextcurr.next = tmpn -= 1return dummy.next
merge-two-sorted-lists
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
- 思路:通过 dummy node 链表,连接各个元素
class Solution:def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:tail = dummy = ListNode()while l1 is not None and l2 is not None:if l1.val > l2.val:tail.next = l2l2 = l2.nextelse:tail.next = l1l1 = l1.nexttail = tail.nextif l1 is None:tail.next = l2else:tail.next = l1return dummy.next
partition-list
给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。
- 思路:将大于 x 的节点,放到另外一个链表,最后连接这两个链表
class Solution:def partition(self, head: ListNode, x: int) -> ListNode:p = l = ListNode()q = s = ListNode(next=head)while q.next is not None:if q.next.val < x:q = q.nextelse:p.next = q.nextq.next = q.next.nextp = p.nextp.next = Noneq.next = l.nextreturn s.next
哑巴节点使用场景
当头节点不确定的时候,使用哑巴节点
sort-list
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
- 思路:归并排序,slow-fast找中点
class Solution:def _merge(self, l1, l2):tail = l_merge = ListNode()while l1 is not None and l2 is not None:if l1.val > l2.val:tail.next = l2l2 = l2.nextelse:tail.next = l1l1 = l1.nexttail = tail.nextif l1 is not None:tail.next = l1else:tail.next = l2return l_merge.nextdef _findmid(self, head):slow, fast = head, head.nextwhile fast is not None and fast.next is not None:fast = fast.next.nextslow = slow.nextreturn slow# 找到中点,断开再合并def sortList(self, head: ListNode) -> ListNode:if head is None or head.next is None:return headmid = self._findmid(head)tail = mid.nextmid.next = None # break from middlereturn self._merge(self.sortList(head), self.sortList(tail))
注意点
- 快慢指针 判断 fast 及 fast.Next 是否为 nil 值
- 递归 mergeSort 需要断开中间节点
- 递归返回条件为 head 为 nil 或者 head.Next 为 nil
reorder-list
给定一个单链表 L:L→L→…→L__n→L
将其重新排列后变为: L→L__n→L→L__n→L→L__n→…
- 思路:找到中点断开,翻转后面部分,然后合并前后两个链表
class Solution:def reverseList(self, head: ListNode) -> ListNode:prev, curr = None, headwhile curr is not None:curr.next, prev, curr = prev, curr, curr.nextreturn prevdef reorderList(self, head: ListNode) -> None:"""Do not return anything, modify head in-place instead."""if head is None or head.next is None or head.next.next is None:returnslow, fast = head, head.nextwhile fast is not None and fast.next is not None:fast = fast.next.nextslow = slow.nexth, m = head, slow.nextslow.next = Nonem = self.reverseList(m)while h is not None and m is not None:p = m.nextm.next = h.nexth.next = mh = h.next.nextm = preturn
linked-list-cycle
给定一个链表,判断链表中是否有环。
- 思路1:Hash Table 记录所有结点判断重复,空间复杂度 O(n) 非最优,时间复杂度 O(n) 但必然需要 n 次循环
- 思路2:快慢指针,快慢指针相同则有环,证明:如果有环每走一步快慢指针距离会减 1,空间复杂度 O(1) 最优,时间复杂度 O(n) 但循环次数小于等于 n
class Solution:def hasCycle(self, head: ListNode) -> bool:slow = fast = headwhile fast is not None and fast.next is not None:slow = slow.nextfast = fast.next.nextif fast == slow:return Truereturn False
linked-list-cycle-ii
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回
null
。
- 思路:快慢指针,快慢相遇之后,慢指针回到头,快慢指针步调一致一起移动,相遇点即为入环点。
class Solution:def detectCycle(self, head: ListNode) -> ListNode:slow = fast = headwhile fast is not None and fast.next is not None:slow = slow.nextfast = fast.next.nextif slow == fast:slow = headwhile fast != slow:fast = fast.nextslow = slow.nextreturn slowreturn None
坑点
- 指针比较时直接比较对象,不要用值比较,链表中有可能存在重复值情况
- 第一次相交后,快指针需要从下一个节点开始和头指针一起匀速移动
注意,此题中使用 slow = fast = head 是为了保证最后找环起始点时移动步数相同,但是作为找中点使用时一般用 fast=head.Next 较多,因为这样可以知道中点的上一个节点,可以用来删除等操作。
- fast 如果初始化为 head.Next 则中点在 slow.Next
- fast 初始化为 head,则中点在 slow
palindrome-linked-list
请判断一个链表是否为回文链表。
- 思路:O(1) 空间复杂度的解法需要破坏原链表(找中点 -> 反转后半个list -> 判断回文),在实际应用中往往还需要复原(后半个list再反转一次后拼接),操作比较复杂,这里给出更工程化的做法
class Solution:def isPalindrome(self, head: ListNode) -> bool:s = []slow = fast = headwhile fast is not None and fast.next is not None:s.append(slow.val)slow = slow.nextfast = fast.next.nextif fast is not None:slow = slow.nextwhile len(s) > 0:if slow.val != s.pop():return Falseslow = slow.nextreturn True
copy-list-with-random-pointer
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的 深拷贝。
- 思路1:hash table 存储 random 指针的连接关系
class Solution:def copyRandomList(self, head: 'Node') -> 'Node':if head is None:return Noneparent = collections.defaultdict(list)out = Node(0)o, n = head, outwhile o is not None:n.next = Node(o.val)n = n.nextif o.random is not None:parent[o.random].append(n)o = o.nexto, n = head, out.nextwhile o is not None:if o in parent:for p in parent[o]:p.random = no = o.nextn = n.nextreturn out.next
- 思路2:复制结点跟在原结点后面,间接维护连接关系,优化空间复杂度,建立好新 list 的 random 链接后分离
class Solution:def copyRandomList(self, head: 'Node') -> 'Node':if head is None:return Nonep = headwhile p is not None:p.next = Node(p.val, p.next)p = p.next.nextp = headwhile p is not None:if p.random is not None:p.next.random = p.random.nextp = p.next.nextnew = head.nexto, n = head, newwhile n.next is not None:o.next = n.nextn.next = n.next.nexto = o.nextn = n.nexto.next = Nonereturn new
总结
链表必须要掌握的一些点,通过下面练习题,基本大部分的链表类的题目都是手到擒来~
- null/nil 异常处理
- dummy node 哑巴节点
- 快慢指针
- 插入一个节点到排序链表
- 从一个链表中移除一个节点
- 翻转链表
- 合并两个链表
- 找到链表的中间节点
练习
- remove-duplicates-from-sorted-list
- remove-duplicates-from-sorted-list-ii
- reverse-linked-list
- reverse-linked-list-ii
- merge-two-sorted-lists
- partition-list
- sort-list
- reorder-list
- linked-list-cycle
- linked-list-cycle-ii
- palindrome-linked-list
- copy-list-with-random-pointer