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

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


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

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

一、面试常考链表题调研

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

Leetcode 链表​leetcode-cn.com
ea2d9cc3dbd8d00a832bd251c87d09e6.png

0d2393da01437763680614b466474217.png

5b1614247f2dd44baf256e47b03c9474.png

77679c17ffea07c95d9e5f659a93d237.png

二、链表常见考察点

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

2c105b6b8d971a4dfd83c5597c8f8d46.gif

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

176ebe85b731d53b1ceb0e06eae98b20.gif

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

7609007d2dc8dd423842af74e510bf37.png

对比之下,链表由多个结点组成,每个结点包含了数据和指针。数据指向具体的内存块,而指针指向一个结点的内存地址。一般链表用一个 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
  • 两个链表的第一个公共节点,链表相交

4fc280195cd67b219dddcfe085bd1e4a.png

公共结点有一个特征是,它离尾部的距离是一样的。对于长链表 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 道题)

78061681de4c4399b084bfe62e3c8b32.png

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

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

d7911b93f9a017dbefe5fc4794d2960e.png

当我们要把重复节点全部去掉时,需要额外一个指针 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

两数相加

6867a9473dc91d2c0858a1dc4fc33bfd.png

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

 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 题)

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

  • 反转链表

b08248870513eb61c1e751ca18932ea4.gif
# 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

该题也有递归的解法:

71f7f40a3bf711d54ec72617d6345df7.gif
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

奇偶链表

83402523d9759dc395af306876752d7b.png
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

旋转链表

1f1b1b52b9cb79b72c4519688f61dcb5.png

这道题我们可以用返回倒数第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

对链表进行插入排序

0064da7f63478e93c769fe3c578ff8c8.gif

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

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

91300e7723ee2818b3636c352aa1400a.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/476605.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

感人至深的文章

http://bbs2.news.163.com/bbs/baoliao/75948727.html 转载于:https://www.cnblogs.com/shf/archive/2008/05/12/1192975.html

2022年度最佳开源软件榜单出炉!

源&#xff5c; OSC开源社区&#xff08;ID&#xff1a;oschina2013)InfoWorld 公布了 2022 年最佳开源软件榜单。InfoWorld 是致力于引领 IT 决策者走在科技前沿的国际科技媒体品牌&#xff0c;每年 InfoWorld 都会根据软件对开源界的贡献&#xff0c;以及在业界的影响力评选出…

程序员面试金典 - 面试题 16.13. 平分正方形(数学)

1. 题目 给定两个正方形及一个二维平面。请找出将这两个正方形分割成两半的一条直线。 假设正方形顶边和底边与 x 轴平行。 每个正方形的数据square包含3个数值&#xff0c;正方形的左下顶点坐标[X,Y] [square[0],square[1]]&#xff0c;以及正方形的边长square[2]。 所求直…

吵翻了!确认录取后导师和学生“互放鸽子”,网友:线上选拔太混乱

源 | 募格学术参考 | 中国科学报、科学网博客、知乎等导师和学生互相“放鸽子”是种怎样的体验&#xff1f;近日&#xff0c;《中国科学报》的一篇文章揭露了疫情下线上推免选拔的乱象。因为某些原因&#xff0c;产生了一些所谓“海王院校”&#xff08;指在夏令营或预推免中&a…

监控和剖析数据库操作P6Spy,SQL Profiler,SQL 使用简介

新一篇: 关于java类的动态装载 几乎 80% - 85% 的数据库性能问题是由于应用数据库的设计或者应用程序本身的代码所引起的。因此良好的事务处理能力需要在设计应用程序的时候&#xff0c;在设计数据库的时候就考虑到性能和伸缩性。 ---- DB2 Magazine 在我们 Java 开发应用程序的…

python新手练习项目_适合Python 新手的5大练手项目,你练了么?

已经学习了一段时间的Python&#xff0c;如果你看过之前W3Cschool的文章&#xff0c;就知道是时候该进去【项目】阶段了。 但是在练手项目的选择上&#xff0c;还存在疑问&#xff1f;不知道要从哪种项目先下手&#xff1f; W3Cschool首先有两点建议&#xff1a; 最好不要写太应…

LeetCode 866. 回文素数(除11外,偶数位的回文数都不是质数)

1. 题目 求出大于或等于 N 的最小回文素数。 回顾一下&#xff0c;如果一个数大于 1&#xff0c;且其因数只有 1 和它自身&#xff0c;那么这个数是素数。 例如&#xff0c;2&#xff0c;3&#xff0c;5&#xff0c;7&#xff0c;11 以及 13 是素数。 回顾一下&#xff0c;…

首个在ImageNet上精度超过80%的二值神经网络BNext问世,-1与+1的五年辛路历程

源 | 机器之心两年前&#xff0c;当 MeliusNet 问世时&#xff0c;机器之心曾发表过一篇技术文章《第一次胜过 MobileNet 的二值神经网络&#xff0c;-1 与 1 的三年艰苦跋涉》&#xff0c;回顾了 BNN 的发展历程。彼时&#xff0c;依靠早期 BNN 工作 XNOR-Net 起家的 XNOR.AI…

中countif函数_countif与countifs:单条件与多条件计数适用场景

countif&#xff08;&#xff09;的语法图丑~~图丑~~见谅~~Countif函数&#xff1a;对指定区域中符合指定条件的单元格计数。该函数的语法规则如下:countif(range&#xff0c;criteria)参数:range 要计算其中非空单元格数目的区域参数:criteria 以数字、表达式或文本形式定义的…

极大似然模型1

first_step.m%clear; syms rou fai2 k1 k2 k3 n rorn ii clc; ninput(观测时刻数 n); disp( dealing ); disp(Just wait for a few minutes............); k1sym((1-rou*rou)*(1-fai2*fai2)*Xmn(rorn,1)*Xmn(rorn,1)); k2sym((1-fai2^2)*(Xmn…

LeetCode 1177. 构建回文串检测(前缀和)

1. 题目 给你一个字符串 s&#xff0c;请你对 s 的子串进行检测。 每次检测&#xff0c;待检子串都可以表示为 queries[i] [left, right, k]。我们可以 重新排列 子串 s[left], ..., s[right]&#xff0c;并从中选择 最多 k 项替换成任何小写英文字母。 如果在上述检测过程…

张俊林:ChatGPT会成为下一代搜索引擎吗

文|张俊林知乎本文将从以下几个方面展开&#xff1a;引言ChatGPT的技术原理ChatGPT能否取代Google、百度等传统搜索引擎引言作为智能对话系统&#xff0c;ChatGPT最近两天爆火&#xff0c;都火出技术圈了&#xff0c;网上到处都在转ChatGPT相关的内容和测试例子&#xff0c;效果…

k8s dashboard_k8s集群部署Dashboard

部署Dashboard&#xff08;Web UI&#xff09;* dashboard-deployment.yaml // 部署Pod&#xff0c;提供Web服务 * dashboard-rbac.yaml // 授权访问apiserver获取信息 * dashboard-service.yaml // 发布服务&#xff0c;提供对外访问 ​…

.Net笔试题 有答案

在对SQL Server 数据库操作时应选用&#xff08;a&#xff09;。 a)SQL Server .NET Framework 数据提供程序&#xff1b; b)OLE DB .NET Framework 数据提供程序&#xff1b; c)ODBC .NET Framework 数据提供程序&#xff1b; d)Oracle .NET Framework数据提供程序&#x…

程序员面试金典 - 面试题 17.13. 恢复空格(DP+Trie树)

文章目录1. 题目2. 解题2.1 动态规划2.2 Trie树1. 题目 哦&#xff0c;不&#xff01;你不小心把一个长篇文章中的空格、标点都删掉了&#xff0c;并且大写也弄成了小写。 像句子"I reset the computer. It still didn’t boot!"已经变成了"iresetthecomputeri…

2022的结尾,对话系统起飞了

文 | 郑楚杰知乎编者记&#xff1a;近日来&#xff0c;ChatGPT的连续刷屏让人们重新看到了AI的希望&#xff0c;编者通过对ChatGPT的试用&#xff0c;发现其对话能力早已不同于两年前的对话系统了&#xff0c;可以说&#xff0c;有了质的飞跃&#xff0c;向着用户体验奇点迈进了…

python清除列表内容_Python 列表的清空方式

情况列表的操作&#xff1a; del list[:] list[] list[:][] def func(L): L.append(1) print L #L[:][] #del L[:] L [] print L L[] func(L) print L 输出结果&#xff1a; [1] [] [1] 分析&#xff1a;L是可变数据类型&#xff0c;L作为参数&#xff0c;函数内对L的改变&…

ASP.NET小收集:Word的编码是Unicode

Word的编码是Unicode&#xff0c;从Word连接的超链接会附带Unicode编码进行打开&#xff0c;所以&#xff0c;如果没有对链接页面设定编码&#xff0c;将出现乱码&#xff0c;解决方法之一&#xff1a;设置编码为UTF-8<meta http-equiv"Content-Type" content&quo…

程序员面试金典 - 面试题 05.08. 绘制直线(位运算)

1. 题目 绘制直线。有个单色屏幕存储在一个一维数组中&#xff0c;使得32个连续像素可以存放在一个 int 里。 屏幕宽度为w&#xff0c;且w可被32整除&#xff08;即一个 int 不会分布在两行上&#xff09;&#xff0c;屏幕高度可由数组长度及屏幕宽度推算得出。 请实现一个函数…

FarPoint Spread For .Net 4.0

FarPoint Spread For .Net 4.0 ftp://ftp.fpoint.com/Trials/SpreadWinForm/spwin.zip http://www.fpoint.com:8080/files/Trials/SpreadWinForm/spwinframework35.zip 转载于:https://www.cnblogs.com/Tonyyang/archive/2008/06/23/1228133.html