本文是根据穷码农的LeetCode刷题建议而进行专项练习时记录的心得。
文章转自自己在知乎原创的同名文章https://zhuanlan.zhihu.com/p/107457778
快慢指针,作为双指针系列的一个分支,因为有着自己的特点,所以我在练习时把这一类题单独拿出来记录。今天的笔记包含快慢指针(Two Points)类型下的5个题目,它们在leetcode上的编号和题名分别是:
下面将根据以上顺序分别记录代码和对应心得,使用的编译器为Python3。
Given a linked list, determine if it has a cycle in it.
To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.
Example 1:
Input: head = [3,2,0,-4], pos = 1
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the second node.
Example 2:
Input: head = [1,2], pos = 0
Output: true
Explanation: There is a cycle in the linked list, where tail connects to the first node.
Example 3:
Input: head = [1], pos = -1
Output: false
Explanation: There is no cycle in the linked list.
这道题我认为属于快指针的一个基础使用场景。在一开始做的时候,我原本以为快慢指针就是一个指针在前,一个指针在后,然后快指针在前面不断循环,若循环了一圈还不等于慢指针,就移动一次慢指针。咋一看,还挺适合这道题的。利用一个set存储快指针走过的节点,最后比对即可,但最后我发现这压根就不是快慢指针,而是单纯的双指针思路。所谓快慢指针,就是在同一个循环里,快慢指针同时移动,但快指针永远比慢指针多走几步(通常是2倍关系)。在知道了这个原则后,我便将它们放置于同一个循环里进行移动,只不过慢指针走一步,快指针走两步,直到快慢指针相遇/即将相遇或快指针走到尾部为止:
class Solution:
def hasCycle(self, head: ListNode) -> bool:
# my solution: 快慢指针。其中快指针永远比慢指针多走两步,如果快指针的下一个节点或者它本身的节点与慢指针相同,即一定有循环
if head is None:
return False
slow = head
fast = head
if fast.next is None:
return False
while fast is not None and fast.next is not None:
if fast.next == slow:
return True
else:
# the fast pointer will always move 2 steps (1-step faster) than the slow pointer
fast = fast.next.next
slow = slow.next
if fast == slow:
return True
return False
Given a non-empty, singly linked list with head node head, return a middle node of linked list. If there are two middle nodes, return the second middle node.
Example 1:
Input: [1,2,3,4,5]
Output: Node 3 from this list (Serialization: [3,4,5])
The returned node has value 3. (The judge's serialization of this node is [3,4,5]).
Note that we returned a ListNode object ans, such that:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, and ans.next.next.next = NULL.
Example 2:
Input: [1,2,3,4,5,6]
Output: Node 4 from this list (Serialization: [4,5,6])
Since the list has two middle nodes with values 3 and 4, we return the second one.
Note:
The number of nodes in the given list will be between 1 and 100.
这道题就是针对我在上一题所总结的规律的典型应用。利用快指针永远比慢指针多走2n(n通常等于1)的特点,只要快指针走到了链表尾部,那慢指针就一定到了链表的中间:
def middleNode(self, head: ListNode) -> ListNode:
# my solution: 快慢指针。借助 f = 2s 这个特性找中间节点
slow = head
fast = head
try:
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next
fast = fast.next
except Exception as e:
# when the linked list has even elements
return slow
# when the linked list has odd elements
return slow
不过,需要考虑节点数的奇偶情况。此题我采用异常抛出解决,即一旦抛出了便是偶数个节点,反之为奇数个。
Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.
Note: Do not modify the linked list.
Example 1:
Input: head = [3,2,0,-4], pos = 1
Output: tail connects to node index 1
Explanation: There is a cycle in the linked list, where tail connects to the second node.
Example 2:
Input: head = [1,2], pos = 0
Output: tail connects to node index 0
Explanation: There is a cycle in the linked list, where tail connects to the first node.
Example 3:
Input: head = [1], pos = -1
Output: no cycle
Explanation: There is no cycle in the linked list.
这道题咋一看觉得还挺容易的,实际上若需要采用快慢指针,编码确实会很容易,但它的规律我看了半天实在没能找到,直到看到了解析。它的解法分两种:第一就是单指针,每走一次便记录所指向节点,走完一圈之后若成环,一定能发现重复记录的节点,然后返回即可;第二种便是快慢指针。但在运用之前,我们需要察觉到此题循环的一个规律:当快慢指针相遇时,它们距离入环节点的距离等于从头节点到入环节点的距离。证明过程利用了Floyd算法,具体如下:
在知晓了这个规律后,可在快慢指针相遇时创建双指针:一个指向head,一个指向相遇的节点。两者同时出发,直至相遇时便可求得入环节点:
def detectCycle(self, head: ListNode) -> ListNode:
# solutuon: Floyd算法(快慢指针+双指针)。通过快慢指针记录相遇时的位置,双指针记录入环节点位置,进而返回入环节点
if head is None:
# Empty node
return None
slow = head
fast = head
if fast.next is None:
# only one node
return None
# get the node info when two pointers meet each other
intersect = self.getIntersection(fast, slow)
if intersect is None:
# No circle
return None
else:
start = head
while start != intersect:
start = start.next
intersect = intersect.next
return intersect
def getIntersection(self, fast: ListNode, slow: ListNode):
while fast is not None and fast.next is not None:
fast = fast.next.next
slow = slow.next
if fast == slow:
return slow
return None
Write an algorithm to determine if a number is "happy".
A happy number is a number defined by the following process: Starting with any positive integer, replace the number by the sum of the squares of its digits, and repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1. Those numbers for which this process ends in 1 are happy numbers.
Example:
Input: 19
Output: true
Explanation:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
这是我目前见过的最有意思的题。
在我所做过的所有快慢指针题目中,它们几乎都是以链表为背景。毕竟链表容易构造循环体,从而让两个指针在循环中有相遇的可能性。所以脱离了链表,我会本能回避使用快慢指针去解题,但这道题让我十分意外。一眼看去,这是一个需要在不断地加法运算中寻找数字规律的题,看那些能加到1的数字背后有着怎样的模式。但这样想极大地限制了我的思维方式,万万没想到关键之处是在于“1”这个点。要知道,一旦加到了1,说明以后再怎么加都会是1了,这不就是一个循环的点吗?把数字本身看作是一个指针指向的节点,如果它是快乐数的话,连续相加两次,和只相加一次,那最后的结果不都会一直等于1吗?由此可见,这道题其实可以巧妙地利用快慢指针思路解决:
def isHappy(self, n: int) -> bool:
# Solution: 巧妙利用happy number最后数字和始终为1的特点,构建循环思想,在不断相加的过程中,逐渐变成了一个“环”。
# 神奇的是,所有数字都会在相加到最后产生循环,只不过循环值不一定等于1
fastSum = 0
slowSum = 0
# First round, fast goes two steps, slow goes one step
fastSum = self.getSum(n)
fastSum = self.getSum(fastSum)
slowSum = self.getSum(n)
while fastSum != slowSum:
fastSum = self.getSum(fastSum)
fastSum = self.getSum(fastSum)
slowSum = self.getSum(slowSum)
return True if slowSum == 1 else False
# get the sum from its digits
def getSum(self, n: int):
sum = 0
while n >= 1:
# start by the right number
sum += int(pow(n % 10, 2))
n = math.floor(n / 10)
return sum
但有一点我还比较疑惑:所有的数字最后都将加到某个固定值就成环了,是不过不一定等于1。这一点解析并未提出(至少我还没看到),不知道大佬们是如何观察到的。
Given a singly linked list L: L0→L1→…→Ln-1→Ln,
reorder it to: L0→Ln→L1→Ln-1→L2→Ln-2→…
You may not modify the values in the list's nodes, only nodes itself may be changed.
Example 1:
Given 1->2->3->4, reorder it to 1->4->2->3.
Example 2:
Given 1->2->3->4->5, reorder it to 1->5->2->4->3.
这道题我认为非常考察处理链表的综合能力。如果要利用快慢指针,首先就是要观察出链表的中间节点在重组后变为了尾节点和链表的后半段都将被反向插入前半段这两个规律;其次,需要让代码找到中间节点,并将后半段链表进行逆序处理;最后,将逆序的链表合并到原链表的前半段上:
def reorderList(self, head: ListNode) -> None:
# solution: 快慢指针。通过观察可知,链表正中间的元素会被排到最后去,并且偶数个元素会被倒序
# Special considerations
if head is None:
return None
if head.next is None:
return head
# Get the middle pointer
cur = head
middle = self.middleNode(cur)
# break the middle link
while cur.next != middle:
cur = cur.next
cur.next = None
# Reverse the next-half linked list
reverse = self.reverseList(middle)
# Merge two linked lists
newHead = head
while head.next is not None and reverse.next is not None:
# Insert elements into a linked list
# Remember, when two pointers point to the same node, one pointer changes the 'next' attribute of
# the node, the other pointer will be influenced as well (they point to the same node)
temp = reverse.next
reverse.next = head.next
head.next = reverse
head = head.next.next
reverse = temp
if head.next is None:
head.next = reverse
elif reverse.next is None:
reverse.next = head
#return newHead
def middleNode(self, head: ListNode) -> ListNode:
# my solution: 快慢指针。借助 f = 2s 这个特性找中间节点
slow = head
fast = head
try:
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next
fast = fast.next
except Exception as e:
# when the linked list has even elements
return slow
# when the linked list has odd elements
return slow
def reverseList(self, head: ListNode) -> ListNode:
# solution: 三指针:无需创建任何非指针变量
# empty condition
if head is None:
return None
# one element
cur = head
if head.next is None:
return head
# two elements
pre = head.next
if pre.next is None:
pre.next = cur
cur.next = None
return pre
else:
# more than two elements
sup = pre.next
while sup is not None:
pre.next = cur
if cur == head:
cur.next = None
cur = pre
pre = sup
sup = sup.next
pre.next = cur
return pre
查找中间节点,反向链表,以及链表的合并,在我看来是链表处理最基础,也是非常重要的知识点。其中,关于链表合并,这里的三指针方法是我自己琢磨的,可能存有缺陷。核心思路是每移动一次节点,改变本节点的next指向,同时不丢失原本next指向的节点信息和本节点之前的节点信息(另外两个指针)。
快慢指针的题目做的不是很多,但它的规律比双指针更加明显:
如果笔记存在一些问题,发现后我会尽快纠正。
*注:本文的所有题目均来源于leetcode