链表
链表是一种常见的数据结构,用于存储线性序列。与数组不同,链表中的元素在内存中不是连续存储的,而是通过指针相连。链表由一个头指针指向链表的第一个节点,每个节点包含两个部分:数据域和指针域。数据域存储节点的数据,指针域指向下一个节点的地址。
链表有多种类型:
- 单向链表中每个节点只有一个指针域,指向下一个节点;
- 双向链表中每个节点有两个指针域,一个指向前一个节点,一个指向后一个节点;
- 环形链表中最后一个节点的指针域指向头节点,形成一个环。
环形链表
环形链表问题是一类非常经典的链表问题,给定一个单向链表,判断这个链表是否存在环或者环的状态。
这类问题通常有两种解法。
-
快慢指针法:使用两个指针,一个快指针和一个慢指针,快指针每次移动两个节点,慢指针每次移动一个节点,如果链表中存在环,则快指针最终会追上慢指针,否则快指针会到达链表末尾结束。这个算法的时间复杂度是 O(n),其中 n 是链表的长度。
-
哈希表法:遍历链表中的每个节点,每访问一个节点就将它存储到哈希表中,如果访问到了哈希表中已经存在的节点,说明链表中存在环。这个算法的时间复杂度也是 O(n),其中 n 是链表的长度,但需要额外的空间来存储哈希表。
快慢指针法其实就是 Floyd 判圈算法,接下来我们详细介绍一下:
Floyd判圈算法
Floyd判圈算法(又称龟兔赛跑算法)是一种快速检测链表中是否存在环的算法。该算法使用两个指针,一个慢指针和一个快指针,从链表头部开始同时向后遍历链表,每次快指针比慢指针多走一步,直到快指针走到链表末尾或者两个指针相遇。
如果链表中不存在环,那么快指针最终会走到链表的末尾,算法结束。如果链表中存在环,那么快指针最终会在环内与慢指针相遇,算法会返回存在环的结论。
该算法的时间复杂度为 ,其中 是链表的长度。由于该算法使用的是常数级别的额外空间,因此它是一种空间复杂度比较优秀的算法。
Floyd 判圈算法的正确性可以通过数学归纳法来证明。
假设链表中存在环,且环的长度为 。设慢指针走了 步后进入环,快指针走了 步后进入环。设快指针比慢指针多走了 圈(),则有:
其中 表示慢指针在环内走的步数。因为快指针的速度是慢指针的两倍,所以可以得到:
也就是说,慢指针在环内走了 步。由于 ,因此 ,所以 且 。
因为快指针每次走两步,所以 的取值只有 种可能。当 时,表示快指针与慢指针相遇,此时算法会返回存在环的结论;当 时,表示快指针和慢指针在环内继续前进,由于快指针比慢指针多走了一圈,因此可以将 的值更新为 ,这样快指针和慢指针都在环内距离环入口的距离为 ,再次相遇的位置就是环的入口。
由此可见,Floyd 判圈算法的正确性是得到了保证的。
题目
接下来就看几道LeetCode上的相关题目:
141. 环形链表 - 力扣(LeetCode)
给你一个链表的头节点 head
,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos
不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true
。 否则,返回 false
。
示例 1:
输入: head = [3,2,0,-4], pos = 1
输出: true
解释: 链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入: head = [1,2], pos = 0
输出: true
解释: 链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入: head = [1], pos = -1
输出: false
解释: 链表中没有环。
提示:
- 链表中节点的数目范围是
[0, 104]
-105 <= Node.val <= 105
pos
为-1
或者链表中的一个 有效索引 。
进阶: 你能用 O(1)
(即,常量)内存解决此问题吗?
class Solution:
def hasCycle(self, head: ListNode) -> bool:
slow,fast = head,head
while True:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
if fast == slow:
return True
使用了快慢指针的方法:
slow
和fast
指针都被初始化为链表的头节点head
。- 进入一个无限循环,循环条件为
while True
,即一直循环直到函数中的某个条件满足返回。 - 在循环内部,首先检查
fast
指针是否为空或者fast.next
是否为空,如果是,说明链表已经到达末尾,没有环,直接返回False
。 - 否则,
slow
指针向前移动一步,fast
指针向前移动两步。 - 然后,检查
slow
和fast
指针是否相遇,如果相遇,说明链表中有环,返回True
。 - 如果循环继续执行,重复步骤3到步骤5。
这个解决方法使用了快慢指针的技巧,如果链表中有环,快慢指针最终会相遇;如果链表中没有环,快指针会最先到达链表末尾,返回False
。
这种解法的时间复杂度是O(N),其中N是链表中的节点数,因为在最坏情况下,fast
指针会遍历整个链表。空间复杂度是O(1),因为只使用了两个指针。
142. 环形链表 II - 力扣(LeetCode)
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入: head = [3,2,0,-4], pos = 1
输出: 返回索引为 1 的链表节点
解释: 链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入: head = [1,2], pos = 0
输出: 返回索引为 0 的链表节点
解释: 链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入: head = [1], pos = -1
输出: 返回 null
解释: 链表中没有环。
提示:
- 链表中节点的数目范围在范围
[0, 104]
内 -105 <= Node.val <= 105
pos
的值为-1
或者链表中的一个有效索引
进阶: 你是否可以使用 O(1)
空间解决此题?
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 快慢指针
if not head:
return None
slow,fast = head,head
while True:
if not fast or not fast.next:
return None
slow = slow.next
fast = fast.next.next
if slow == fast:
break
a = head
while a != slow:
a = a.next
slow = slow.next
return a
# # 哈希表
# if not head:
# return None
# visited = []
# p = head
# while p:
# if p in visited:
# return p
# else:
# visited.append(p)
# p = p.next
# return None
我这里没注释掉用哈希表的做法,用哈希表空间复杂度就上去了。这里直接快慢指针做。
这段代码是用来检测链表中是否有环,并找出环的入口点。这里使用了快慢指针的方法。下面是对代码的详细解释:
- 初始化两个指针
slow
和fast
,都指向链表的头节点head
。 - 使用一个循环,条件为
while True
,不断进行快慢指针的移动,直到它们相遇。如果快指针fast
或者fast.next
为空,说明链表中没有环,直接返回None
。 - 当快慢指针相遇时,将慢指针重新指向链表的头节点
head
,然后慢指针和快指针同时以相同的速度向前移动,每次移动一步。当慢指针和快指针再次相遇时,就是环的入口点。 - 返回相遇点,即为环的入口点。
这个算法的原理是,当快慢指针相遇时,慢指针走了 a + b
步,快指针走了 a + b + c + b
步(其中,a是链表头到环的入口的距离,b是环的入口到相遇点的距离,c是环的剩余部分的长度)。由于快指针的速度是慢指针的两倍,所以快指针走的距离也是慢指针的两倍。因此,a + b + c + b = 2 * (a + b)
,化简得到 a = c
,这就是为什么在慢指针和快指针相遇后,再用一个指针从头节点开始,与慢指针相遇的节点就是环的入口点。
这种解法的时间复杂度是O(N),其中N是链表中的节点数。空间复杂度是O(1),因为只使用了常数级别的额外空间。
287. 寻找重复数 - 力扣(LeetCode)
给定一个包含 n + 1
个整数的数组 nums
,其数字都在 [1, n]
范围内(包括 1
和 n
),可知至少存在一个重复的整数。
假设 nums
只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums
且只用常量级 O(1)
的额外空间。
示例 1:
输入: nums = [1,3,4,2,2]
输出: 2
示例 2:
输入: nums = [3,1,3,4,2]
输出: 3
提示:
1 <= n <= 105
nums.length == n + 1
1 <= nums[i] <= n
nums
中 只有一个整数 出现 两次或多次 ,其余整数均只出现 一次
进阶:
- 如何证明
nums
中至少存在一个重复的数字? - 你可以设计一个线性级时间复杂度
O(n)
的解决方案吗?
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
# 双指针
fast = slow = 0
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
fast = 0
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return fast
# # 原地哈希,注意i的增加时机
# n, i = len(nums), 0
# while i < n:
# t, idx = nums[i], nums[i] - 1
# if nums[idx] == t:
# if idx != i:
# return t
# i += 1
# else:
# nums[i], nums[idx] = nums[idx], nums[i]
# return -1
使用了快慢指针的方法来解决这个问题。下面是对代码的详细解释:
- 初始化两个指针
fast
和slow
,都指向数组的第一个元素。 - 使用一个循环,
slow
每次移动一步,fast
每次移动两步,直到它们相遇。如果数组中有重复数字,fast
和slow
最终会相遇。 - 当
fast
和slow
相遇时,将fast
重新指向数组的第一个元素,然后fast
和slow
每次都移动一步,直到它们再次相遇。相遇的点就是重复的数字。
这个算法的原理是,如果数组中存在重复数字,那么数组中的元素可以看作是一个链表,每个元素的值作为下一个元素的索引。由于有重复数字,所以在这个链表中一定存在一个环。快慢指针相遇的点就是环的入口点,即重复的数字。
这种解法的时间复杂度是O(N),其中N是数组的长度。由于它使用了常数级别的额外空间,所以空间复杂度是O(1)。
202. 快乐数 - 力扣(LeetCode)
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n
是 快乐数 就返回 true
;不是,则返回 false
。
示例 1:
输入: n = 19
输出: true
解释: 12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例 2:
输入: n = 2
输出: false
提示:
1 <= n <= 231 - 1
class Solution:
def isHappy(self, n: int) -> bool:
def convert(n):
res = 0
while n != 0:
n, m = divmod(n, 10)
res += m ** 2
return res
while n != 1:
n = convert(n)
if n==4:
return False
return True
这个解法使用了一个内部函数 convert(n)
,它接受一个整数 n
,然后将每个数字的平方和计算出来。然后在主函数中,使用一个循环,不断调用 convert(n)
,直到 n
变为1(是快乐数)或者 n
变为4(不是快乐数)。如果 n
变为4,就说明陷入了循环,返回 False
,否则返回 True
。
这个解法的原理是,如果一个数是快乐数,那么不断进行平方和操作最终会得到1。如果一个数不是快乐数,那么最终会陷入一个循环,而在这个循环中,所有数字的平方和加起来等于4。所以,当 n
变为4时,就可以判断它不是快乐数,返回 False
。
这种解法的时间复杂度取决于在每次迭代中计算平方和的次数,但是由于数字的位数是有限的,所以它的时间复杂度是有上限的,是O(1)级别的。空间复杂度是O(1),因为只使用了常数级别的额外空间。
在快乐数问题中,我们不断地将一个数的各个位上的数字的平方和计算出来,直到得到1(是快乐数)或者陷入循环(不是快乐数)。如果一个数不是快乐数,它会陷入一个循环中,而这个循环的性质与环形链表类似。在环形链表问题中,使用快慢指针,如果链表中有环,快慢指针最终会相遇。在快乐数问题中,如果一个数不是快乐数,那么它的平方和序列会陷入一个循环,而循环的性质也可以使用快慢指针来判断。