前言:只照着常考题去刷题确实是一种方法。但调研之后发现自己还是考虑不周,刷题刷的不应该是题,而是解题的思路和熟练程度。于是我决定重新组织一下刷题笔记的讲解顺序,不再以面试常考题来刷。而是以面试出题频率,方法思路类型,和难度等要素来综合排序。每一次分享我们会沿着一个主题展开它的各种变体,确保一次性能吃透与它类似的题目。往后我们会确定主题,对该主题下问题一步步分解,然后确定模板、举一反三去做题。而不是简单无脑地刷题。


这一次,我们的主题是链表。本文会覆盖以下三个内容:

  1. Leetcode 中文上所有链表题的题型和难易分布一览
  2. 链表题的大致考察点,常规解题思路和技巧,
  3. 题海将解题模板熟练化

一、面试常考链表题调研

首先,我们点进 Leetcode 页面,依次点题库,标签,链表,出现频率,便可以看到以下的题目排序。我们可以发现,链表类题目相对都是比较容易,简单中等题五五开,很少有困难题目。所以它这里更多的是考察一个代码的熟练度。比如对边界的处理,对指针的操作等。

Leetcode 链表​leetcode-cn.com


二、链表常见考察点

链表和数组统称为线性表。数组所有元素都储存在一段连续的内存中,具有通过下标访问数据的能力 O(1)。但这也让它扩容和删除元素的成本变得很高 O(n)。

因为扩容要新申请一块更大的内存,复制所有元素,再删除原来的内存。

而删除插入元素需要把该元素位置之后的其它元素都往前或往后挪一个位置,若插入时刚好分配的内存满了,还要重新进行扩容操作。

对比之下,链表由多个结点组成,每个结点包含了数据和指针。数据指向具体的内存块,而指针指向一个结点的内存地址。一般链表用一个 head 头结点指针来表示。

链表的好处是插入删除的空间复杂度是 O(1),但是访问某个结点的空间复杂度是 O(n)。

设计链表

class ListNode:def __init__(self, x):self.val = xself.next = Noneclass MyLinkedList:def __init__(self):"""Initialize your data structure here."""self.head = Noneself.length = 0def print(self):node = self.headwhile node:print(node.val, end = " ")node = node.nextprint()def get(self, index: int) -> int:"""Get the value of the index-th node in the linked list. If the index is invalid, return -1."""if index >= self.length:return -1prev = self.headcurr = self.headfor _ in range(index):prev = currcurr = curr.nextreturn curr.valdef addAtHead(self, val: int) -> None:"""Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list."""self.length += 1if not self.head:self.head = ListNode(val)else:new_head = ListNode(val)new_head.next = self.headself.head = new_headdef addAtTail(self, val: int) -> None:"""Append a node of value val to the last element of the linked list."""self.length += 1if not self.head:self.head = ListNode(val)else:tail = self.headwhile tail.next:tail = tail.nexttail.next = ListNode(val)def addAtIndex(self, index: int, val: int) -> None:"""Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted."""if index > self.length:returnif index == 0:new = ListNode(val)new.next = self.headself.head = newself.length += 1returnif index == self.length:self.addAtTail(val)returnprev = self.headcurr = self.headfor _ in range(index):prev = currcurr = curr.nextprev.next = ListNode(val)prev = prev.nextprev.next = currself.length += 1def deleteAtIndex(self, index: int) -> None:"""Delete the index-th node in the linked list, if the index is valid."""if index < self.length:self.length -= 1if index == 0:prev = self.headself.head = self.head.nextprev.next = Nonedel prevreturnprev = self.headcurr = self.headfor _ in range(index):prev = currcurr = curr.nextprev.next = curr.nextcurr.next = Nonedel curr


1. 节点的返回 (全 8 道题)

题目一般要你返回链表中满足某个条件的节点,大都可以使用双指针的思想。

  • 返回倒数第 k 个节点的值

倒数第 k 个节点,意味着某个指针要再走 k 步才会到结尾。怎样知道一个结点它的位置距离尾部有 k 步呢?我们可以用两个指针before,after 分别指向头结点。其中 before 结点不动,after 结点走 k 步后,才让 before 结点开始往后走。二者的步伐都一致,二者就会相隔 k 步。当 after 结点走到结尾的时候,before 结点所在的位置刚好是要返回的结点。代码如下:

# Definition for singly-linked list.

  • 链表中倒数第 k 个节点往后的链表

该题与上一题无差别,只是返回的内容是指针而不是值。这里要注意一下,以防 k 过大溢出, k 的取值要与链表的长度取膜。


# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:n = 0node = headwhile node:node = node.nextn += 1k = k % (n+1)if n < 2:return headbefore = after = headfor i in range(k):after = after.nextwhile after:after = after.nextbefore = before.nextreturn before

  • 链表的中间结点

这里同样是两个指针,不过是一快一慢。快指针每次走两步,慢指针每次走一步。两个指针同时从 head 开始走。如果链表长度为奇数,中点节点两边节点个数相等,最终快指针的下一个为空时停止,直接返回慢指针。如果链表长度为偶数,则没有中间节点能平分链表,我们取右边的最左边节点作为返回,即此时快指针为空时,返回慢指针。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def middleNode(self, head: ListNode) -> ListNode:if not head:return headfast, slow = head, headwhile fast and fast.next:fast = fast.next.nextslow = slow.nextreturn slow

环形链表

也是双指针。我们考虑让快指针比慢指针的步伐快走一步,即快指针每次走两步,慢指针每次走一步。这样快指针会先进入环中,当慢指针叶进入环后,与快指针刚好相差 k 个结点,设 n1 为环外的结点个数,n2 为构成环的结点个数,则很容易得到 n2 - n1 = k。当它们继续在环中一快一慢的步伐一直前行,由于每次快指针都能追上慢指针一个结点,所以它们之间的差距每次迭代都会小一步,即 k 会越来越小。直到 k == 0 时,二者相遇,说明有环。反过来想,若快指针能走到一个尽头,即它走到的位置下一个结点为空,则说明没有环。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def detectCycle(self, head: ListNode) -> ListNode:slow, fast = head, headwhile 1:if not fast or not fast.next:return Falsefast, slow  = fast.next.next, slow.nextif fast == slow:breakreturn True

环形链表 Ⅱ

如果我们要找入环点的位置,根据前面的公式,慢指针刚进入环中的时候,满足 n2 - n1 = k。若快指针降到每次一步的速度往前走 n2 - k 步,则刚好获得 head 到入环口的结点个数 n1。第一次快慢指针相遇时,慢指针走了 k + n1 = n2 步,所以要返回入环口,我们只需要在快慢指针相遇的时候,让快慢指针再以每次一步的步频往前走,直到二者第二次相遇。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def detectCycle(self, head: ListNode) -> ListNode:slow, fast = head, headwhile 1:if not (fast and fast.next):return Nonefast, slow = fast.next.next, slow.nextif fast == slow:breakslow = headwhile fast != slow:fast = fast.nextslow = slow.nextreturn slow

  • 两个链表的第一个公共节点,链表相交

公共结点有一个特征是,它离尾部的距离是一样的。对于长链表 B 来说,它要比短链表 A 事先多走 k 步才能到公共结点。这个 K 步,刚好是链表 B 比 链表 A 多出来的长度。要怎样获得这个 K 呢?一种简单方法是分别算两个链表的长度,相减便是。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:if not headA or not headB:return Nonedef get_length(head):n = 0while head:head = head.nextn += 1return nA_length, B_length = get_length(headA), get_length(headB)if A_length <= B_length:headA, headB = headB, headAfor _ in range(abs(A_length - B_length)):headA = headA.nextwhile headA and headB and headA != headB:headA = headA.nextheadB = headB.nextif not headA or not headB:return Nonereturn headA

一般人很少会想到这题也可以用快慢指针来做。一开始从两个链表以1的步伐分别各走一个指针。当其中一个先到达尾部时,让它下一步往后跳到另一个链表的头结点继续走,当另一个指针走到尾部时,同理。最终当它们第一次重叠时,返回的便是最开头的公共节点。问题是我们要如何判断它没有公共交点呢?也简单。当其中一个节点能够两次走到结尾,则说明没有公共交点。我们设立两个 Bool 变量去分别记录两个指针是否走过结尾。若走过,就返回 None。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:if not headA or not headB:return Nonep1, p2 = headA, headBp1_first_None, p2_first_None = False, Falsewhile p1 != p2:p1, p2 = p1.next, p2.nextif not p1:if p1_first_None:return Nonep1 = headBp1_first_None = Trueif not p2:if p2_first_None:return Nonep2 = headAp2_first_None = Truereturn p1

  • 2. 节点的删除 (全 9 道题)

Leetcode 还会考核满足某一条件的节点的删除。如上图所示。节点删除考察的是要如何改变链表的指针域,以达到删除的目的。

删除链表中的节点(只给要删除节点),删除中间节点

我们先从最简单的来。一种删除节点的题是它不给你链表,只给你某个要删除当前节点。最直接的方法是,把当前节点的下一个节点的值,复制到当前节点上,然后再将当前节点连接到它的下下个节点。从内存上看,它删掉的是当前节点的下一个节点。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def deleteNode(self, node):""":type node: ListNode:rtype: void Do not return anything, modify node in-place instead."""node.val = node.next.valnode.next = node.next.next

删除链表中的节点(给了整个链表,保证不重复)

另一种要删除的是其内存。前面那种复制的方法就不行了。这种题一般会给你整个链表的头节点。考虑到我们可能会删掉头节点。我们会用创建一个连向头节点的虚拟节点的方式来解。当你熟悉了这一技巧,往后的变体就都大同小异。无疑是改变了一下要删除节点的条件。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None
class Solution:def deleteNode(self, head: ListNode, val: int) -> ListNode:dummy = ListNode(float("inf"))dummy.next = headnode = dummywhile node.next and node.next.val != val:node = node.nextnode.next = node.next.nextreturn dummy.next

删除链表中的节点(给了整个链表,可能会重复)

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def removeElements(self, head: ListNode, val: int) -> ListNode:dummy = ListNode(float("inf"))dummy.next = headnode = dummywhile node.next:if node.next.val == val:node.next = node.next.nextelse:node = node.nextreturn dummy.next

删除链表的倒数第N个节点

这题结合了之前的双指针找倒数第K个节点的思路,先让快指针先走 n+1 步,再一起走到结尾,则慢指针刚好位于倒数第 n+1 个节点。这时执行删除操作。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:dummy = ListNode(0)dummy.next = headfirst, second = dummy, dummyfor i in range(n+1):first = first.nextwhile first:first = first.nextsecond = second.nextsecond.next = second.next.nextreturn dummy.next

这往后的变体还可以是:删除链表 N 到 M个节点。删除链表第M个节点后的 N 个节点。等等。方法都大同小异。用双指针找位置,用 dummy 虚拟节点避免删除头。

删除重复节点 I

你可以看到,这段代码和上面代码唯一的区别就是判定条件那里改动了一下。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:dummy = ListNode(float("inf"))dummy.next = headnode = dummywhile node.next:if node.val == node.next.val:node.next = node.next.nextelse:node = node.nextreturn dummy.next

删除重复节点 Ⅱ (不包含重复节点)

当我们要把重复节点全部去掉时,需要额外一个指针 pre 来作为辅助。如果存在两个值相同的节点,当前指针 curr 就会一直往后走,直到跑到一个与该值不同的节点位置。让pre .next = cur 完成节点的删除。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def deleteDuplicates(self, head: ListNode) -> ListNode:dummy = ListNode(float('inf'))dummy.next = headcur = dummywhile cur:pre = curcur = cur.nextwhile cur and cur.next and cur.val == cur.next.val:val = cur.valwhile cur and cur.val == val:cur = cur.nextpre.next = curreturn dummy.next

  • 删除重复节点 Ⅲ (未排序链表)

链表单调递增时,能保证前面操作了某个值的删除后,后面不会再操作与之一样的删除。但单调性不能保证时,原来的思路就不能照搬了。这时有两种做法。一种是用哈希表,储存重复过的数值。时间和空间复杂的都为 O(n)。另一种是两遍循环操作,时间复杂度为 O(n²),空间复杂度为 O(1)

class Solution:def removeDuplicateNodes(self, head: ListNode) -> ListNode:if not head:return headoccurred = set()occurred.add(head.val)pre = headwhile pre.next:cur = pre.nextif cur.val not in occurred:occurred.add(cur.val)pre = pre.nextelse:pre.next = pre.next.nextreturn head

class Solution {
public:ListNode* removeDuplicateNodes(ListNode* head) {ListNode* p1 = head;while (node) {ListNode* p2 = p1 ;while (p2->next) {if (p2->next->val == p1->val) {p2->next = p2->next->next;} else {p2 = p2->next;}}p1= p1->next;}return head;}
};

从链表中删去总和值为零的连续节点

这道题我们可以用前缀数组来解。第一遍遍历时,我们用一个字典储存前 k 个节点和 - 第k个节点的 pair。第二遍遍历时,我们把当前节点的下一个都赋值为前缀字典中最后储存的节点。

举个例子说:

head = [1,2,-3,3,1]

前缀字典每次变化:

sum 操作

0 {0: dummy}

1 {0: dummy, 1: node[0]}

3 {0: dummy, 1: node[0], 3: node[1]}

0 {0: node[2], 1: node[0], 3: node[1]}

3 {0: node[2], 1: node[0], 3: node[3]}

4 {0: node[2], 1: node[0], 3: node[3], 4: node[4]}

第二次遍历,链表变化:

sum 操作

0 dummy -> node[3]

1 node[0]-> node[1]

3 node[1] -> node[4]

0 node[2] -> node[3]

3 node[3] -> node[4]

4 node[4] -> None

汇总后,最终虚拟节点之后接的为:

dummy -> node[3] -> node[4] -> None

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def removeZeroSumSublists(self, head: ListNode) -> ListNode:hash_map = dict()dummy = ListNode(0)dummy.next = headnode = dummycurr_sum = 0while node:curr_sum += node.valhash_map[curr_sum] = nodenode = node.nextnode = dummycurr_sum = 0while node:curr_sum += node.valnode.next = hash_map[curr_sum].nextnode = node.nextreturn dummy.next


3. 节点的求和 (全 4 道题)

这里主要用的是节点直接位置和值的关系。

  • 二进制链表转整数

直接每进一位,把之前的结果乘上2,再加上当前节点的值便可

class Solution:def getDecimalValue(self, head: ListNode) -> int:res = 0node = headwhile node:res = res*2 + node.val;node = node.nextreturn res

两数相加

该题与我们小学学加法竖式计算是一样的。从低位到高位,用一个中间变量存进位。

 Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:dummy = ListNode(0)p1, p2, cur = l1, l2, dummyoverflow = 0while p1 or p2:x = p1.val if p1 else 0y = p2.val if p2 else 0curr_sum = overflow + x + yoverflow = curr_sum // 10cur.next = ListNode( curr_sum % 10)cur = cur.nextif p1:p1 = p1.nextif p2:p2 = p2.nextif overflow > 0:cur.next = ListNode(overflow)return dummy.next

两数相加 II

如果我们要反过来相加,则需要借助栈这个数据结构。

class Solution:def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:s1, s2 = [], []while l1:s1.append(l1.val)l1 = l1.nextwhile l2:s2.append(l2.val)l2 = l2.nextres = Noneoverflow = 0while s1 or s2 or overflow != 0:p1_val = 0 if not s1 else s1.pop()p2_val = 0 if not s2 else s2.pop()curr_sum = p1_val + p2_val + overflowoverflow = curr_sum // 10curr_sum %= 10cur = ListNode(curr_sum)cur.next = resres = curreturn res

链表求和

上面一题如果不使用额外空间要怎么做呢?方式是,我们用其中一个链表作为临时变量存我们当前位的临时结果。

class Solution:def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:dummy = ListNode(-1)cur = dummyoverflow = 0while l1 and l2:curr_sum = l1.val + l2.val + overflowl1.val = curr_sum % 10overflow = curr_sum // 10cur.next = l1cur = cur.nextl1, l2 = l1.next, l2.nextleft = Noneif l1:left=l1else:left=l2while left and overflow >= 0:curr_sum = left.val + overflowleft.val = curr_sum % 10overflow = curr_sum // 10cur.next = leftcur = cur.nextleft = left.nextif overflow > 0:cur.next = ListNode(overflow)return dummy.next

4. 节点的位置调整 (全 11 题)

这类题目通常需要我们用多个指针,根据题目条件,控制某个节点前中后三个位置的转换。

  • 反转链表
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = Noneclass Solution:def reverseList(self, head: ListNode) -> ListNode:prev = Nonecurr = headwhile curr:next_ = curr.nextcurr.next = prevprev = currcurr = next_return prev

该题也有递归的解法:

class Solution(object):def reverseList(self, head):""":type head: ListNode:rtype: ListNode"""if not head or not head.next:return headcur = self.reverseList(head.next)head.next.next = headhead.next = Nonereturn cur

  • 反转链表 II (指定m到n反转)

比上题多两个步骤,一个是先走m-1步,找到要反转的链表部分的起点的前一个节点A,另一个是用上述算法反转 n-m 次后,让A的下一个连到反转后的链表,反转后的链表的头连到第n个节点。

class Solution:def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:dummy = ListNode(0)dummy.next = headA = dummyfor i in range(m-1):A = A.nextprev = Nonecurr = A.nextB = A.nextfor i in range(n-m+1):next_ = curr.nextcurr.next = prevprev = currcurr = next_A.next = prevB.next = next_return dummy.next

递归法

class Solution(object):def reverseBetween(self, head, m, n):""":type head: ListNode:type m: int:type n: int:rtype: ListNode"""def reverseN(head,n):if n == 1:return head# 以 head.next 为起点,需要反转前 n - 1 个节点last = reverseN(head.next, n-1)successor = head.next.next # 以head.next为开头的链表已经完成翻转,那么head.next.next正确指向后继节点head.next.next = headhead.next = successorreturn lastif m == 1: return reverseN(head,n)head.next = self.reverseBetween(head.next,m-1,n-1)return head

  • 回文链表

当你熟悉了如何翻转链表,你还可以用它来检查回文。首先,我们用快慢指针的方式找到中点。然后对中点往后部分翻转链表。再看看翻转的后半部分和前半部分是否相等。

class 

两两交换链表中的节点

交换两个节点的技巧之前在反转链表中写过。这里可以照用。不同在我们每次反转局部的两个节点后,往后要把 prev 移动到当前的第一个节点上。

class Solution:def swapPairs(self, head: ListNode) -> ListNode:if not head or not head.next:return headdummy = ListNode(0)dummy.next = headprev = dummywhile prev.next and prev.next.next:first = prev.nextsecond = prev.next.nextprev.next = secondfirst.next = second.nextsecond.next = firstprev = firstreturn dummy.next

奇偶链表

class Solution:def oddEvenList(self, head: ListNode) -> ListNode:if not head:return headodd, even = head, head.nextoddHead, evenHead = odd, evenwhile even and even.next:odd.next = even.nextodd = odd.nexteven.next = odd.nexteven = even.nextodd.next = evenHeadreturn oddHead

旋转链表

这道题我们可以用返回倒数第k个节点的双指针方法找到旋转链表的新头,然后将结尾给连上旧头,让新头与前面的节点断开,返回新头,就完成了旋转。

class Solution:def rotateRight(self, head: 'ListNode', k: 'int') -> 'ListNode':if not head or not head.next:return headnode = headn = 0while node:node = node.nextn += 1k = k % nif not k:return headfast, slow = head, headfor _ in range(k-1):fast = fast.nextslow_prev = Nonewhile fast.next:slow_prev = slowslow = slow.nextfast = fast.nextfast.next = headslow_prev.next = Nonereturn slow

排序链表

比起 array 版本,不同的是我们要用 next 来做 i++ 的位移操作。空节点或单节点作为递归条件返回。一开始创建三个空节点,left,mid,right,并用对应的新指针 left_tail, mid_tail, right_tail 分别指向它们。接着我们遍历链表,对于当前节点值比头节点小的,就放在 left_tail 后面,若等于,就放在 mid_tail 后面,若小于,则放在 right_tail 后面。每次插入完,要后移其坐标。遍历完后我们把结尾都归 None。然后开始递归调用快排。把mid部分的快排结果放在链表 left 的右边,把 right 的快排结果,放在链表 mid 的右边。最后返回的是虚拟节点 left 右边的节点,为最终排序好的整个链表。代码如下:

class Solution:def get_tail(self, head):while head.next: head = head.nextreturn headdef sortList(self, head: ListNode) -> ListNode:if not head or not head.next:return headleft, mid, right = ListNode(-1), ListNode(-1), ListNode(-1)left_tail, mid_tail, right_tail = left, mid, rightval = head.valp = headwhile p:if p.val < val:left_tail.next = pleft_tail = left_tail.nextelif p.val == val:mid_tail.next = pmid_tail = mid_tail.nextelse:right_tail.next = pright_tail = right_tail.nextp = p.nextleft_tail.next = mid_tail.next = right_tail.next = Noneleft.next = self.sortList(left.next)right.next = self.sortList(right.next)self.get_tail(left).next = mid.nextself.get_tail(mid.next).next = right.nextres = left.nextreturn res

从尾到头打印链表

使用栈,先把遍历链表把节点值放进栈,再一个个出栈。

class Solution:def reversePrint(self, head: ListNode) -> List[int]:stk = []node = headwhile node:stk.append(node.val)node = node.nextres = []while stk:res.append(stk.pop())return res

重排链表

这道可以结合之前题的技巧,把问题分解成三步。第一步,用双指针方法找到中点节点。第二步,用反转链表方法把后半段反转。第三步,合并两个链表。

class Solution:def reverseList(self, head: ListNode) -> ListNode:prev = Nonecurr = headwhile curr:next_ = curr.nextcurr.next = prevprev = currcurr = next_return prevdef reorderList(self, head: ListNode) -> None:"""Do not return anything, modify head in-place instead."""if not head or not head.next:return head# 找中点slow, fast = head, headslow_prev = Nonewhile fast and fast.next:fast = fast.next.nextslow_prev = slowslow = slow.next# 中点后面的部分,翻转链表slow_prev.next = Nonereversed_right = self.reverseList(slow)# 合并两个链表first, second = head, reversed_rightwhile first and second:first_next, second_next = first.next, second.nextfirst.next, second.next = second, first_nextif not first_next and second_next:second.next = second_nextbreakfirst, second = first_next, second_nextreturn head

对链表进行插入排序

如动图所示,每次取出一个节点 curr,去和最开始的节点,往后一个个比较,如果发现它小于当前节点,就继续右移,直到它大于等于当前节点,则执行插入操作。

class Solution:def insertionSortList(self, head: ListNode) -> ListNode:if not head or not head.next:return headdummy = ListNode(float("-inf"))dummy.next = headprev = dummycurr = dummy.nextwhile curr:if curr.val < prev.val:tmp = dummywhile tmp.next.val < curr.val:tmp = tmp.nextprev.next = curr.nextcurr.next = tmp.nexttmp.next = currcurr = prev.nextelse:prev, curr = prev.next, curr.nextreturn dummy.next

K 个一组翻转链表

这道题的难点是需要常数空间。我们可以先写好一个反转链表。然后用三个指针 lastHead, currHead, nextHead 来分别表示要反转链表的上一个尾节点,头节点以及下一个要反转的链表的头节点。迭代时,部分反转链表,再补连上。

class Solution:def reverse(self, head):prev = Nonecurr = headwhile curr:next_ = curr.nextcurr.next = prevprev = currcurr = next_return prevdef reverseKGroup(self, head: ListNode, k: int) -> ListNode:dummy = ListNode(0)dummy.next = headnode = dummylast_head = dummycurr_head = node.nextto_end = Falsewhile node:for _ in range(k):node = node.nextif not node:to_end = Truebreakif to_end:breaknext_head = node.nextnode.next = Nonereversed_head = self.reverse(curr_head)last_head.next = reversed_headlast_head = curr_headcurr_head.next = next_headnode = curr_headcurr_head = next_headreturn dummy.next


5. 链表的分割 (全 2 道题)

分隔链表

左右两边各设置一个虚拟指针,一个用来接小于x的数,一个用来接大于等于x的数,最后再把两段接起来,返回。

class Solution:def partition(self, head: ListNode, x: int) -> ListNode:dummy_left, dummy_right = ListNode(float("-inf")), ListNode(float("inf"))left, right = dummy_left, dummy_rightcurr = headwhile curr:if curr.val < x:left.next = currleft = left.nextelse:right.next = currright = right.nextcurr = curr.nextleft.next = dummy_right.nextright.next = Nonereturn dummy_left.next

分隔链表

这道题就有点偏技巧性,需要先用余数预估长的有多少段,短的有多少段。

class Solution:def splitListToParts(self, root: ListNode, k: int) -> List[ListNode]:n = 0node = rootwhile node:n += 1node = node.nextnode = rootout = []remain = n % kshort_size = n // k long_size = short_size + 1for i in range(k):out.append(node)if remain > 0:size = long_sizeelse:size = short_sizenext_head = nodetail = Nonefor _ in range(size):tail = next_headnext_head = next_head.nextif tail:tail.next = Nonenode = next_headremain -= 1return out

6. 链表的合并 (全 2 道)

两个有序链表合并,看代码比较简单。需要用到虚拟节点 dummy 的技巧。

  • 合并两个有序链表
class Solution:def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:dummy = ListNode(0)node = dummywhile l1 and l2:if l1.val <= l2.val:node.next = l1node, l1 = node.next, l1.nextelse:node.next = l2node, l2 = node.next, l2.nextif l1:node.next = l1else:node.next = l2return dummy.next

  • 合并K个排序链表

这其实就是搜索引擎中的 MERGE 算法的变体。倒排索引中,每一个词项会对应一个包含该词项的文章ID,是一个长长的链表。我们如何把匹配到超过2个以上关键词的篇章合并?实际会用到跳表去优化。但这道题相对简单,只需要输出合并后的排序序列便可。

核心思想和合并2个排序链表一致,但不同在我们需要维持一个最大容量为k的 buffer 来存放当前的中间节点值。我们可以用一个 heap 用来对 buffer 中的节点进行排序。每次出一个值最小的节点,放在要合并的链表的后面。时间复杂度为 O(nlogk),空间复杂度为 O(k)

class 


7. 链表的复制 (就 1 道)

  • 复杂链表的复制,复制带随机指针的链表

剑指 Offer 经典题目。先每个一个节点插入一个 node,然后利用 节点的下一个节点的随机节点等于节点的随机节点的下一个节点特性,来完成中间插入 node 的随机节点连接的复制。最后再两两拆分便复制完成。

"""
# Definition for a Node.
class Node:def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):self.val = int(x)self.next = nextself.random = random
"""
class Solution:def copyRandomList(self, head: 'Node') -> 'Node':if not head:return headp = headwhile p:node = Node(p.val)next_ = p.nextp.next, node.next = node, next_p = next_p = headwhile p:if p.random:p.next.random = p.random.nextp = p.next.nextdummy = Node(0)dummy.next = headp1, p2 = dummy, head while p1 and p2:p1.next =p2.nextp1 = p1.nextp2.next = p1.nextp2 = p2.nextreturn dummy.next


8. 与其它数据结构交叉 (全部 5 道)

  • 二叉树中的列表 (与二叉树的 subTree 一样)

解法上,我们需要些一个dfs,一旦存在当前链表节点和树节点的值不匹配,就返回 False。否则,继续往下遍历找节点。时间复杂度

,空间复杂度为函数栈的调用,检查链表长度不会超过树的高度,因为超过就会返回 False,所以是
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Solution:def isSubPath(self, head: ListNode, root: TreeNode) -> bool:if not root:return Falseif not head:return Truedef dfs(head, root):if not head:return Trueif not root:return Falseif root.val != head.val:return Falsereturn dfs(head.next, root.left) or dfs(head.next, root.right)return dfs(head, root) or self.isSubPath(head, root.left) or self.isSubPath(head, root.right)

  • 有序链表转换二叉搜索树 (与二叉树相关)

思路是用中序遍历是顺序的。构建二叉树时,用中序遍历,每次节点就刚刚好与生成的树对齐。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, x):
#         self.val = x
#         self.next = None# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = Noneclass Solution:def sortedListToBST(self, head):""":type head: ListNode:rtype: TreeNode"""node = headn = 0while node:node = node.nextn += 1def dfs(l, r):nonlocal headif l >= r:return Nonemid = (l + r) // 2left = dfs(l, mid)root = TreeNode(head.val)root.left = left head = head.nextroot.right = dfs(mid+1, r)return rootreturn dfs(0, n)

  • 扁平化多级双向链表 (与 bfs,dfs 相关)
"""
# Definition for a Node.
class Node:def __init__(self, val, prev, next, child):self.val = valself.prev = prevself.next = nextself.child = child
"""class Solution:def flatten(self, head: 'Node') -> 'Node':if not head:return Nonedummy = Node(0, None, head, None)prev = dummystk = [head]while stk:curr = stk.pop()prev.next = currcurr.prev = previf curr.next:stk.append(curr.next)if curr.child:stk.append(curr.child)curr.child = Noneprev = currdummy.next.prev = Nonereturn dummy.next

  • 链表组件 (使用 Set 作为去重技巧)
class Solution(object):def numComponents(self, head, G):Gset = set(G)curr = headans = 0while curr:if curr.val in Gset:if not curr.next or curr.next.val not in Gset:ans += 1curr = curr.nextreturn ans

  • 链表中的下一个更大节点 (链表与单调栈)
class Solution:def nextLargerNodes(self, head: ListNode) -> List[int]:node = headstk = []ans = []i = 0while node:ans.append(0)while stk and stk[-1][0].val < node.val:ans[stk[-1][1]] = node.valstk.pop()stk.append((node, i))i += 1node = node.nextreturn ans


总结:一共 45 道题,刷了大概一周时间。一些常用的技巧不好用语言去总结,而是更多看代码、自己去写才会明白。大部分链表的基本思路就是虚拟节点,双指针。其中双指针用的最灵活。掌握了这几个技巧,会发现很多题都是可以分解成几个基本题的原型去做。另外链表与其它数据结构的综合题,涉及的都是其它知识。日后会再一一总结。但愿在正式秋招之前能总结完头部的专题。

最后附上这一专题的脑图:

写出一段代码将链表中的两个节点位置互换位置_面试 leetcode 算法专题系列(二)—— 链表...相关推荐

  1. 写出一段代码将链表中的两个节点位置互换位置_干货||链表的技巧和算法总结...

    链表的操作总结   链表反转 这是一个简单的链表操作问题,在leetcode上面有52.7%的通过率,难度是简单.但是还是想在这里基于python做一下总结,顺便总结一下链表的各种操作. 首先先看一下 ...

  2. 链表中每两个节点交换位置

    LeetCode上的一道两两交换链表节点的题目:如下 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表. 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换. 示例: 给定 1-&g ...

  3. 请写出一段 python 代码实现删除一个 list 里面的重复元素

    请写出一段 python 代码实现删除一个 list 里面的重复元素 方法一:利用set集合实现 info = [2017,1,16,9,2017,1,16,9] result = list(set( ...

  4. Java中有关日期的操作,昨天晚上赴约,搞到12点多才回来,今天写这一小段代码都花了一段漫长的时间,哎。。...

    Java中有关日期的操作,昨天晚上赴约,搞到12点多才回来,今天写这一小段代码都花了一段漫长的时间,哎.. 代码奉上: /** * * @param date * @return which mont ...

  5. 如何写出漂亮的代码:七个法则

    如何写出漂亮的代码:七个法则. 首先我想说明我本文阐述的是纯粹从美学的角度来写出代码,而非技术.逻辑等.以下为写出漂亮代码的七种方法: 1, 尽快结束 if语句 例如下面这个JavaScript语句, ...

  6. python用什么软件编程1001python用什么软件编程-怎样才能写出 Pythonic 的代码 #P1001#...

    L = [ i*i fori inrange(5) ] forindex, data inenumerate(L, 1):print(index, ':', data) 去除 import 语句和列表 ...

  7. python好学吗1001python好学吗-怎样才能写出 Pythonic 的代码 #P1001#

    L = [ i*i fori inrange(5) ] forindex, data inenumerate(L, 1):print(index, ':', data) 去除 import 语句和列表 ...

  8. 如何写出健壮的代码?

    简介:关于代码的健壮性,其重要性不言而喻.那么如何才能写出健壮的代码?阿里文娱技术专家长统将从防御式编程.如何正确使用异常和 DRY 原则等三个方面,并结合代码实例,分享自己的看法心得,希望对同学们有 ...

  9. python open方法1001python open方法_怎样才能写出 Pythonic 的代码 #P1001#

    L = [ i*i fori inrange(5) ] forindex, data inenumerate(L, 1):print(index, ':', data) 去除 import 语句和列表 ...

最新文章

  1. 2018-4-17论文《狼群算法的研究与应用》笔记2 :高维复杂单目标连续优化问题的改进狼群算法
  2. RHEL5.6配置本地yum源
  3. 【双100%解法】剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
  4. 使用Eclipse进行PHP的服务器端调试
  5. Java IO: 并发IO
  6. oracle仲裁磁盘是一块磁盘吗,基于ASM冗余设计的架构,仲裁磁盘组应该如何去规划?...
  7. V210调整根分区大小
  8. python txt转json_实战篇 | 用Python来找你喜欢的妹子(二)
  9. 基于CSE的微服务架构实践-轻量级架构技术选型
  10. WIN7 运行“计算机管理”出现c:\windows\system32\compmgmt.msc没有被指定在...”错误 解决办法...
  11. linux软件安装方法
  12. wpf之界面控件MaterialDesignInXAML
  13. 什么,又是流程有问题?
  14. 导入资料的预览与修改
  15. linux 命令 tar 打包压缩命令 date时间 命令实践
  16. Java变量命名规范
  17. 前端布局layout总结,左右布局,上中下布局
  18. 李沐论文精度系列之八:视频理解论文串讲
  19. 【Aladdin-Unity3D-Shader编程】之六-模型实时阴影
  20. 一段用c#操作datatable的代码

热门文章

  1. VSS 请求程序和 SharePoint 2013
  2. 仿Jquery链式操作的xml操作类
  3. VS2008制作安装包
  4. 智能硬件开发神器免费送!距离产品智能化,只差一个“三明治”的距离
  5. ./4.sh: No such file or directory
  6. Android wakelock 自上而下浅析
  7. java实现itchat_GitHub - Xiazki/itchat4j: wechatbot 的java实现,简单搭建了基本框架和实现了扫码登陆,具体网页微信api请参考...
  8. webmvcconfigurer配置跨域_为什么加了 Spring Security 会导致 Spring Boot 跨域失效呢?...
  9. Python3 —— 列表
  10. 51单片机——LCD1602