之前把代码随想录的所有题目都过了一遍(图论除外,只过了部分题目),但题量未全部覆盖hot100。本文在此补全(30题左右),依旧按照代码随想录中划分的数据结构来分组
- 代码随想录的题目总结在 代码随想录 - 专栏 - CSDN
- Hot 100 题单:LeetCode 热题 100 - 学习计划
(一)数组
169. 多数元素
找一个数组里出现次数过半的多数元素(题目保证这个数一定存在)。不给额外空间,时间复杂度要求O(n)
**摩尔投票法:**因为多数元素的数量比其他所有元素加起来还要多,所以如果是不同阵营的人一换一同归于尽,最后活下来的一定是那个多数元素
python
class Solution:
def majorityElement(self, nums: List[int]) -> int:
votes = 0 # 擂台上数字的生命值
x = 0 # 上擂台的数,初始为0
for num in nums:
if votes == 0: # 擂台上没人
x = num # 数字直接上擂台
if num == x: # 遇到同伙,生命值+1
votes += 1
else: # 遇到敌人,极限一换一,生命值-1
votes -= 1
return x # 最后在擂台上的数字就是多数元素
- 时间复杂度:O(n)
- 空间复杂度:O(1)
136. 只出现一次的数字
非空整数数组,找出那个只出现一次的数字,其他所有元素都是出现两次。不给额外空间,时间复杂度要求O(n)
位运算里的异或运算:3个重要性质
- 两个相同数字异或,结果是0( "消消乐" 机制)
- 任何数字和0进行异或,结果还是它自己
- 满足交换律,把一堆数字打乱顺序随便异或,结果仍不变
python
class Solution:
def singleNumber(self, nums: List[int]) -> int:
x = 0
for num in nums:
x = x ^ num # 异或
return x # 所有成对的数字在异或过程中都抵消了,最后留下来的就是那个只出现一次的数字
- 时间复杂度:O(n)
- 空间复杂度:O(1)
189. 轮转数组

python
class Solution:
def rotate(self, nums: list[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
def reverse(i, j):
while i < j:
nums[i], nums[j] = nums[j], nums[i]
i += 1
j -= 1
n = len(nums)
k = k % n # 轮转 k 次等同于轮转 k % n 次
reverse(0, n-1) # 反转整个数组
reverse(0, k-1) # 反转前 k 个
reverse(k, n-1) # 反转后 n-k 个
- 时间复杂度:O(n)
- 空间复杂度:O(1)
31. 下一个排列
目标:找到整数数组按字典序排列的下一个更大的排列
- 字典序 :类似于字典中单词的排序。比如
[1,2,3]->[1,3,2]->[2,1,3]->[2,3,1]->[3,1,2]->[3,2,1] - 如果已经是最大排列(如
[3,2,1]) :则返回最小排列(即升序排列[1,2,3])

一个例子:

python
class Solution:
def nextPermutation(self, nums: list[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
if n <= 1:
return
# 1. 从右向左找第一个升序对 (i, i+1),即 nums[i] < nums[i+1]
i = n - 2
while i >= 0 and nums[i] >= nums[i+1]:
i -= 1
# 如果找到了这个 i,说明不是完全降序,可以执行交换
if i >= 0:
# 2. 在 i 的右侧从右向左找第一个大于 nums[i] 的数
j = n - 1
while j > i and nums[j] <= nums[i]:
j -= 1
# 3. 交换
nums[i], nums[j] = nums[j], nums[i]
# 4. 反转 i+1 到末尾的区间,使其变为升序
# 注意:如果没有找到 i (即整个数组降序),i 为 -1,此时反转整个数组
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
238. 除了自身以外数组的乘积
题意关键点:
- 不能使用除法
- 前缀积 * 后缀积 在32位整数范围内(暗示使用 前缀积*后缀积 解法)

一个元素前面什么都没有的时候,默认的前缀和是0、前缀积是1
python
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
pre_list = [1] * len(nums)
pre_prod = 1
for i in range(len(nums)):
pre_list[i] = pre_prod
pre_prod *= nums[i]
suf_list = [1] * len(nums)
suf_prod = 1
for i in range(len(nums)-1, -1, -1):
suf_list[i] = suf_prod
suf_prod *= nums[i]
ans = []
for i in range(len(nums)):
ans.append(pre_list[i] * suf_list[i])
return ans
73. 矩阵置零
【大厂程序员带你刷力扣】【LeetCode 73】矩阵置零_哔哩哔哩_bilibili
时间复杂度:O(m×n);空间复杂度:O(1),因为题目说了要求原地
巧妙地利用矩阵的第一行和第一列来充当"标记数组",记录哪些行和列需要被置为 0。第一个格子既代表了第一行也代表了第一列,所以要额外创建一个变量来保证左上角的位置只被使用一次
python
class Solution:
def setZeroes(self, matrix: list[list[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
rows = len(matrix)
cols = len(matrix[0])
zeroRow = 1 # 定义一个额外的变量,确定矩阵的第一行是否需要置为0。初始化为1,表示不需要置为0
# 遍历整个矩阵,修改标记
for i in range(rows):
for j in range(cols):
if matrix[i][j] == 0:
matrix[0][j] = 0 # 第0行的标记
if i > 0:
matrix[i][0] = 0
else:
zeroRow = 0
# 根据标记数组,修改矩阵中的元素(除了第0行和第0列)
for i in range(1, rows):
for j in range(1, cols):
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0
# 修改第0列对应的元素
if matrix[0][0] == 0:
for i in range(1, rows):
matrix[i][0] = 0
# 修改第0行对应的元素
if zeroRow == 0:
for j in range(cols):
matrix[0][j] = 0
48. 旋转图像
【大厂程序员带你刷力扣】【LeetCode 48】旋转图像_哔哩哔哩_bilibili
python
class Solution:
def rotate(self, matrix: list[list[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
left, right = 0, len(matrix)-1
while left < right:
# 从四个角开始旋转,然后依次旋转基于四个角平移1个位置、2个位置...的元素
for i in range(right-left): # 对于n×n的矩阵,旋转最外层的时候,需要旋转n-1次
top, bottom = left, right
topleft = matrix[top][left+i] # 定义一个变量保存当前矩阵左上角的值
# 旋转四个角
matrix[top][left+i] = matrix[bottom-i][left]
matrix[bottom-i][left] = matrix[bottom][right-i]
matrix[bottom][right-i] = matrix[top+i][right]
matrix[top+i][right] = topleft
left += 1
right -= 1
240. 搜索二维矩阵 II
【力扣hot100】【LeetCode 240】搜索二维矩阵II|整行整列排除_哔哩哔哩_bilibili
python
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 第0行、最后一列的位置
startRow, startCol = 0, len(matrix[0])-1
# 从右上角开始寻找目标值
while startCol >= 0 and startRow < len(matrix):
if matrix[startRow][startCol] == target:
return True
elif matrix[startRow][startCol] < target:
startRow += 1
else:
startCol -= 1
return False
- 时间复杂度 O(m+n)
(二)链表
234. 回文链表
借着本题顺便复习一下【206. 反转链表】
题目:判断一个单链表是不是回文链表,回文即对称
- 不用额外数组,O(1) 空间复杂度
思路:找到链表的中间位置,然后把后半部分链表进行反转,反转之后使用两个指针,一个从头开始,一个从中点开始,齐头并进进行对比
- **快慢指针找中点:**只要快指针不到头,且快指针的下一个节点也不为空,就让慢指针走一步、快指针走两步。循环结束时,慢指针指的地方就是要找的中间位置
- **原地反转单链表:**需要三个变量,当前节点、前一个节点、下一个节点。遍历时,先把当前节点的下一个节点保存下来,然后让当前节点掉头指向前一个节点,接着把当前节点和前一个节点都往后挪一位
- 只要反转后的链表还没走到头,就不断比较前半段和后半段当前节点的值
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
mid = self.find_mid(head)
mid = self.reverse(mid)
cur1, cur2 = head, mid
while cur1 and cur2:
if cur1.val != cur2.val:
return False
cur1 = cur1.next
cur2 = cur2.next
return True
def reverse(self, head): # 反转链表
cur = head
pre = None
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
def find_mid(self, head): # 找单链表的中点
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
- 时间复杂度:O(n)
- 空间复杂度:O(1)
2. 两数相加

- 显然,不能直接转成整数计算(因为链表可能很长,会溢出)
思路: 模拟小学数学的竖式加法,从低位到高位逐位相加,处理进位
- 同时遍历两个链表,取出当前位的值(如果链表已结束,则值为0)
- 计算
sum = val1 + val2 + carry(carry是上一位的进位) - 当前位结果 =
sum % 10,新进位 =sum // 10 - 创建新节点,继续处理下一位
- 遍历结束后,如果还有进位,需要额外添加一个节点
python
l1: 2 → 4 → 3 (数字 342)
l2: 5 → 6 → 4 (数字 465)
进位
第1步: 2 + 5 + 0 = 7 → 结果:7, 进位0
第2步: 4 + 6 + 0 = 10 → 结果:0, 进位1
第3步: 3 + 4 + 1 = 8 → 结果:8, 进位0
结果链表: 7 → 0 → 8 (数字 807)
代码:
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
# 创建虚拟头节点
dummy_head = ListNode(val=0)
cur = dummy_head
next_cnt = 0 # 进位
while l1 or l2 or next_cnt:
# 获取当前位的值,如果节点不存在则取0
val1 = l1.val if l1 else 0
val2 = l2.val if l2 else 0
sum_res = val1 + val2 + next_cnt
# 更新进位
next_cnt = sum_res // 10
cur.next = ListNode(val=sum_res % 10)
# 移动指针
cur = cur.next
if l1:
l1 = l1.next
if l2:
l2 = l2.next
return dummy_head.next
- 时间复杂度:O(max(m, n)),m和n分别是两个链表的长度
- 空间复杂度:O(max(m, n)),结果链表的长度
如果是ACM模式,需要写个测试用例来验证代码是否正确,需要掌握数组->链表的写法,如下:
python
def create_listnode(arr):
"""把数组转成链表"""
dummy = ListNode(0)
cur = dummy
for val in arr:
cur.next = ListNode(val)
cur = cur.next
return dummy.next
def listnode_to_arr(head):
"""把链表转成数组,方便看结果"""
arr = []
while head:
arr.append(head.val)
head = head.next
return arr
- 测试代码:
python
if __name__ == "__main__":
# 测试用例: 342 + 465 = 807
l1 = create_listnode([2, 4, 3])
l2 = create_listnode([5, 6, 4])
result = Solution().addTwoNumbers(l1, l2)
print(listnode_to_arr(result)) # 输出: [7,0,8]
148. 排序链表
做这题之前发现前置的两个题目都做过了:
直接缝合!
**step1:**如下图,876题返回链表的中间节点,记为head2,并断开head2前一个节点与head2的连接

**step2:**分治。递归调用本题的函数,分别排序head和head2(各一半),返回各自升序完的头节点
**step3:**排序后,得到了两个有序链表。再结合21题,即可得到整体排序后的链表,返回头节点
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 如果链表为空或者只有一个节点,无需排序
if head is None or head.next is None:
return head
# 调用middleNode,找到中间节点head2
head2 = self.middleNode(head)
# 递归调用sortList,分别对两段链表升序排序
head = self.sortList(head)
head2 = self.sortList(head2)
# 再调用mergeTwoLists,合并两个有序链表,保证合并后整体升序
return self.mergeTwoLists(head, head2)
# 876. 链表的中间结点 (新增两行即可,即有注释的那两行)
def middleNode(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = head
fast = head
while fast and fast.next:
pre = slow # 记录slow的前一个节点
slow = slow.next
fast = fast.next.next
pre.next = None # 断开slow的前一个节点和slow的连接
return slow
# 21. 合并两个有序链表 (原封不动搬过来)
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
if not list1:
return list2
if not list2:
return list1
dummy_head = ListNode(val=0)
cur = dummy_head
while list1 and list2:
if list1.val < list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
if list1:
cur.next = list1
if list2:
cur.next = list2
return dummy_head.next
138. 随机链表的复制
深拷贝带随机指针的链表 ,难点在于:在复制节点时,random 指针可能指向链表中任意一个节点,而这个节点可能还没被创建出来。
哈希表思路:
-
第一遍遍历原链表,创建所有新节点,并用哈希表记录
原节点 -> 新节点的映射关系。 -
第二遍遍历原链表,根据哈希表设置新节点的
next和random指针。
python
"""
# Definition for a Node.
class Node:
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
self.val = int(x)
self.next = next
self.random = random
"""
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
if not head:
return None
# 先遍历一遍,建立映射:原节点 -> 新节点
node_map = {}
cur = head
while cur:
node_map[cur] = Node(cur.val) # key为当前节点值,value为新建一个节点,且用原节点指向它
cur = cur.next
# 给新节点赋值两个指针
cur = head
while cur:
# 如果原节点的 next 不为空,新节点的 next 指向映射中的新节点
if cur.next:
node_map[cur].next = node_map[cur.next]
if cur.random:
node_map[cur].random = node_map[cur.random]
cur = cur.next
return node_map[head]
- 时间复杂度:O(n),遍历两次链表
- 空间复杂度:O(n),哈希表存储 n 个节点映射
23. 合并 K 个升序链表
逐个合并
合并两个有序链表
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, list1: ListNode | None, list2: ListNode | None) -> ListNode | None:
dummy = ListNode(0)
cur = dummy
while list1 and list2:
if list1.val < list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
cur.next = list1 or list2
return dummy.next
假设一共有K个链表,总共N个节点,每次合并时,前面已经合并好的链表会越来越长,被反复遍历,时间复杂度 O(NK)
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: list[ListNode | None]) -> ListNode | None:
# 合并两个有序链表的辅助函数
def mergeTwoLists(list1: ListNode | None, list2: ListNode | None) -> ListNode | None:
dummy = ListNode(0) # 哨兵节点,简化头节点处理
cur = dummy
while list1 and list2: # 逐一比较两个链表的节点
if list1.val < list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
cur.next = list1 or list2 # 将剩余部分直接接上
return dummy.next
if not lists:
return None
result = lists[0] # 从第一个链表开始
for i in range(1, len(lists)): # 依次合并后续每个链表
result = mergeTwoLists(result, lists[i])
return result
最小堆(优先队列)
与其每次在 K个链表里一个个比较谁最小,不如用一个大小为K的最小堆来帮我们快速找到当前最小的节点
- 先把每个链表的头节点全部放进(大小为K的)最小堆
- 然后每次从堆里弹出最小的节点,接到结果链表的末尾
- 如果这个节点后面还有下一个节点,就把下一个节点放进堆里
- 重复这个过程,直到堆为空
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: list[ListNode | None]) -> ListNode | None:
dummy = ListNode(0)
cur = dummy
heap = [] # 最小堆
for i, node in enumerate(lists): # 将每个链表的头节点入堆
if node:
heapq.heappush(heap, (node.val, i, node))
while heap:
val, i, node = heapq.heappop(heap) # 弹出当前最小节点
cur.next = node # 接到结果链表末尾
cur = cur.next
if node.next: # 如果弹出节点有后续节点,入堆
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
堆的大小始终不超过K,每次插入和弹出都是O(logK)。总共N个节点,每个进出堆一次,所以总时间复杂度是O(NlogK)
两两合并(分治法)
类似归并排序,每一轮把K个链表两两配对,每对合并成一个有序链表,合并后链表数量减半,然后进入下一轮,继续两两配对合并,直到只剩一个链表

python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: list[ListNode | None]) -> ListNode | None:
# 合并两个有序链表的辅助函数
def mergeTwoLists(list1: ListNode | None, list2: ListNode | None) -> ListNode | None:
dummy = ListNode(0) # 哨兵节点,简化头节点处理
cur = dummy
while list1 and list2: # 逐一比较两个链表的节点
if list1.val < list2.val:
cur.next = list1
list1 = list1.next
else:
cur.next = list2
list2 = list2.next
cur = cur.next
cur.next = list1 or list2 # 将剩余部分直接接上
return dummy.next
if not lists:
return None
# 两两配对合并
while len(lists) > 1:
merged = []
for i in range(0, len(lists), 2):
l1 = lists[i]
l2 = lists[i+1] if i+1 < len(lists) else None
merged.append(mergeTwoLists(l1, l2))
lists = merged
return lists[0]
- 整个过程只需要 logK 轮,每一轮所有节点被遍历一次,代价是O(N)。时间复杂度O(NlogK)
- 不需要额外堆结构,实现简单
146. LRU 缓存
LRU Cache | LeetCode 146_哔哩哔哩_bilibili

- get:通过 key 获取 value,如果 key 不存在就返回 -1
- put:插入或更新一个键值对,但缓存有容量限制,当缓存满了的时候,要淘汰最久没有被使用过的那个键值对
- 不管是 get 还是 put,都算使用了这个 key
- 题目要求 get 和 put 都是 O(1) 时间复杂度。每一步操作都是在链表上做常数次指针修改 + 哈希表的 O(1) 查找

初始状态只有 head 和 tail 两个哨兵,哈希表为空
定义一个 Node类,每个节点存 key/value 和前后指针
LRU初始化时创建哈希表和带哨兵的双向链表
两个辅助方法:remove把节点从链表中摘掉、add_to_head把节点插入头部
get方法 先查哈希表,找到就移到头部并返回值;put方法如果key已经存在,就先删掉旧的节点,然后创建新节点插入头部,如果超出容量就淘汰尾部节点。注意节点里存了key,这是因为淘汰尾部节点时,需要知道它的key,才能从哈希表里删掉对应的记录
python
class Node:
def __init__(self, key=0, val=0):
# 每个节点存key、value和前后指针
self.key = key
self.val = val
self.pre = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.cache = {} # 创建哈希表
# 创建带哨兵的双向链表
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.pre = self.head
def _remove(self, node): # 把节点从链表中摘掉
node.pre.next = node.next
node.next.pre = node.pre
def _add_to_head(self, node): # 把节点插入头部
node.next = self.head.next
node.pre = self.head
self.head.next.pre = node
self.head.next = node
def get(self, key: int) -> int: # 先查哈希表,找到就移到头部并返回值
if key not in self.cache:
return -1
node = self.cache[key]
self._remove(node)
self._add_to_head(node)
return node.val
def put(self, key: int, value: int) -> None:
# 如果key已存在,就先删掉旧的节点
if key in self.cache:
self._remove(self.cache[key])
del self.cache[key]
# 然后创建新节点插入头部
node = Node(key, value)
self.cache[key] = node
self._add_to_head(node)
# 如果超出容量就淘汰尾部节点
if len(self.cache) > self.cap:
lru = self.tail.pre
self._remove(lru)
del self.cache[lru.key] # 注意节点里存了key,因为淘汰尾部节点时,需要知道它的key才能从哈希表里删掉对应的记录
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)
- 整体时间复杂度 get 和 put 都是 O(1)【题目要求】
- 空间复杂度是 O(capacity),即缓存容量的大小
(三)哈希表
560. 和为 K 的子数组
数组元素可能存在负数。如果全是正数,可以用滑动窗口(双指针)。因为存在负数,前缀和不是单调递增的,滑动窗口失效。
最优解法 :前缀和 + 哈希表
-
原理:连续子数组
[i+1, j]的和为k,等价于preSum[j] - preSum[i] = k,即preSum[i] = preSum[j] - k -
遍历数组,计算当前位置的前缀和
preSum。如果在哈希表中存在preSum - k,说明存在以当前元素结尾、和为k的子数组,其数量等于哈希表中preSum - k出现的次数 -
需要初始化哈希表
{0: 1},表示前缀和为 0 的情况出现 1 次(即从数组开头到当前元素的子数组和恰好为k)
python
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
# 哈希表记录前缀和出现的次数
# key: 前缀和,value: 出现次数
hash_map = {0: 1} # 初始化
cur_sum = 0
cnt = 0
for num in nums:
cur_sum += num
target = cur_sum - k
if target in hash_map:
cnt += hash_map[target]
hash_map[cur_sum] = hash_map.get(cur_sum, 0) + 1
return cnt
- 时间复杂度: O(N),只需遍历一次数组。
- 空间复杂度: O(N),哈希表最坏情况下需要存储 N 个不同的前缀和。
41. 缺失的第一个正数
原地哈希

对于长度为n的数组,其缺失的第一个正数的范围是1~n+1
核心操作:数字 x 应该放在下标 x-1 的位置
- 对于3这个元素,需要交换到3-1=2的位置,即跟元素1互换位置,数组变成1, 4, 3, -1。交换完该位置元素为1,在1-1=0的位置。继续下一个元素
- 对于4这个元素,需要交换到4-1=3的位置,即跟元素-1互换位置,数组变成1, -1, 3, 4。交换完该位置元素为-1,不在范围内。继续下一个元素
- 下一个位置元素3,已经在3-1=2的位置了,不需要交换;同理4也已经在4-1=3的位置了。遍历结束
- 只有 i=1 位置的元素不满足 i+1 = numsi,所以缺失的元素就是2
python
class Solution:
def firstMissingPositive(self, nums: list[int]) -> int:
n = len(nums)
# 遍历数组的每一个元素,把它交换到正确的位置
for i in range(n):
# 只要当前元素在 1~n 的范围内,但它不在正确的位置上,就对它进行交换
while 1 <= nums[i] <= n:
idx = nums[i] - 1
if nums[i] == nums[idx]:
break
nums[i], nums[idx] = nums[idx], nums[i]
# 再遍历一遍数组
for i in range(n):
if nums[i] != i+1:
return i+1
return n+1 # 所有的 1~n 都在,缺失的就是 n+1
(四)字符串
394. 字符串解码
kstr 表示将 str 重复 k 次,括号可以嵌套
字符串解码 | LeetCode 394_哔哩哔哩_bilibili
栈
由内到外的解码顺序,其实就是后进先出(栈)
- 从左到右扫描,遇到数字就逐位累积,比如先看到1再看到2,就累积成12
- 遇到左括号,把当前字符串和数字压入栈,然后重置
- 遇到右括号,弹出栈顶,把当前字符串重复对应次数,拼到之前的字符串后面
- 遇到普通字母,就直接追加到当前字符串

python
class Solution:
def decodeString(self, s: str) -> str:
stack = []
cur_str = ""
cur_num = 0
for ch in s:
if ch.isdigit():
cur_num = cur_num * 10 + int(ch)
elif ch == '[':
stack.append((cur_str, cur_num))
cur_str = ""
cur_num = 0
elif ch == ']':
pre_str, num = stack.pop()
cur_str = pre_str + cur_str * num
else:
cur_str += ch
return cur_str
- 时空间复杂度都是 O(输出长度)
栈数据结构天然能够处理嵌套,左括号保存上下文,右括号恢复上下文,最内层的括号最先被解码
递归
每层递归处理一对括号,调用栈自动保存上下文,和显式栈本质等价
python
class Solution:
def decodeString(self, s: str) -> str:
i = 0
def decode():
nonlocal i # 全局指针
res = ""
while i < len(s) and s[i] != ']': # 从左到右扫描
if s[i].isdigit(): # 遇到数字,解析出完整的重复次数
num = 0
while i < len(s) and s[i].isdigit():
num = num * 10 + int(s[i])
i += 1
i += 1 # skip '['
res += num * decode() # 递归处理括号内的内容,返回后把结果重复拼接
i += 1 # skip ']'
else: # 普通字母直接追加
res += s[i]
i += 1
return res
return decode()
(五)栈与队列
215. 数组中的第K个最大元素
优先级队列(小顶堆)
看到这题第一反应便是联想到【347. 前 K 个高频元素】,用优先级队列(小顶堆)
- import heapq
- pri_que = \[\] # 优先级队列
- heapq.heappush(pri_que, val) # 入堆
- heapq.heappop(pri_que)
python
# 347题 前 K个高频元素
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 统计元素出现的次数
dic = {}
for i in nums:
if i not in dic:
dic[i] = 1
else:
dic[i] += 1
# 优先级队列
pri_que = []
# key为元素
for key, val in dic.items():
heapq.heappush(pri_que, (val, key)) # val是元素出现次数
if len(pri_que) > k:
heapq.heappop(pri_que) # pri_que中存的是前k个高频的 [val, key] 对
res = []
while k:
res.append(heapq.heappop(pri_que)[1])
k -= 1
return res
Python 通过 heapq 模块提供堆队列(优先队列)的实现,heap0 始终是最小元素(小顶堆)
以[3,2,1,5,6,4], k = 2 为例:
python
步骤0: []
步骤1: [3]
步骤2: [2, 3]
步骤3: [1,3,2] → 弹出1 → [2,3]
步骤4: [2,3,5] → 弹出2 → [3,5]
步骤5: [3,5,6] → 弹出3 → [5,6]
步骤6: [4,6,5] → 弹出4 → [5,6]
最终: [5,6] → 堆顶是5
- 堆中始终保存着当前已处理元素中最大的k个元素
- 堆顶是这k个元素中最小的那个 ,也就是当前第k大的元素
- 遍历完所有元素后,堆顶就是整个数组第k大的元素
python
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
pri_que = []
for i in nums:
heapq.heappush(pri_que, i)
if len(pri_que) > k:
heapq.heappop(pri_que)
return pri_que[0]
- 时间复杂度:O(nlogk),n为遍历数组,k为堆只维护k个元素
- 空间复杂度:O(k),堆最多存储k个元素
快速选择法
数组中的第k个最大元素 | LeetCode 215_哔哩哔哩_bilibili
小顶堆的做法时间复杂度不符合题目要求。这里介绍另一种做法:借鉴快速排序的分区思想。每轮随机选 pivot,分三区
python
左区: > pivot
中区: = pivot
右区: < pivot
-
如果
k <= |左|(左区元素个数(都 > pivot)):第 K 大一定在左区,递归左区 -
如果
k <= |左| + |中|:第 K 大落在中区 ,而中区所有值都等于 pivot,所以 pivot 就是答案,直接返回 -
否则:第 K 大在右区 ,递归右区,且 k 要减去
|左| + |中|
python
import random
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# 随机选一个 pivot,最坏情况 O(n^2) 几乎不会出现
pivot = random.choice(nums)
# 把数组分成三组
left = [x for x in nums if x > pivot]
mid = [x for x in nums if x == pivot]
right = [x for x in nums if x < pivot]
# 如果k落在左边部分,就递归去左边找
if k <= len(left):
return self.findKthLargest(left, k)
elif k <= len(left) + len(mid):
return pivot
else:
return self.findKthLargest(right, k - len(left) - len(mid))
- 时间复杂度是 O(n),符合题目强制的要求
32. 最长有效括号
最长有效括号 | LeetCode 32_哔哩哔哩_bilibili
给你一个只含左括号和右括号的字符串,找出其中最长的有效括号子串,返回它的长度。注意必须是连续子串,不能跳着选
栈
栈里存的不是括号本身,是索引(因为我们要算的是长度,光知道括号匹配了还不够,必须知道有效子串从哪里开始)
- 栈底始终放一个哨兵,初始值为-1(有效子串的左边界还没确定),这个哨兵代表有效子串左边界的前一个位置。用当前索引减去它,就能直接算出有效长度
- 从左到右扫描,遇到左括号( 就把索引压栈 ;遇到右括号 )先弹出栈顶,如果弹出后栈空了,说明这个右括号没有匹配,它就成为新的分界点,把索引压入栈底当新哨兵;如果栈不为空,就用当前索引减去新的栈顶,得到以当前右括号结尾的有效子串长度,更新最大值
python
class Solution:
def longestValidParentheses(self, s: str) -> int:
stack = [-1]
max_len = 0
for i, ch in enumerate(s):
if ch == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i)
else:
max_len = max(max_len, i-stack[-1])
return max_len
- 时间:O(n)
- 空间:O(n)
左右计数器法(更省空间)
只用两个计数器 left 和 right,从左到右扫一遍,遇到左括号、left+1,遇到右括号、right+1
- 如果两者相等,说明从上次清零以来,左右括号一一配对,这就是一段有效括号。更新最大长度
- 一旦右括号多了,立刻清零、重新开始
但从左往右扫,只能检测到右括号过多的情况,如果左括号一直多于右括号,两者永远不会相等,有效括号就被漏掉了
所以还要从右往左再扫一遍,这次反过来,左括号多了就清零
两遍扫描互补,覆盖所有情况
python
class Solution:
def longestValidParentheses(self, s: str) -> int:
max_len = 0
left = right = 0
for ch in s:
if ch == '(':
left += 1
else:
right += 1
if left == right:
max_len = max(max_len, 2*right)
elif right > left:
left = right = 0
left = right = 0
for ch in reversed(s):
if ch == '(':
left += 1
else:
right += 1
if left == right:
max_len = max(max_len, 2*left)
elif left > right:
left = right = 0
return max_len
- 时间:O(n)
- 空间:O(1)
155. 最小栈
设计一个栈,除了常规的 push、pop、top,还要支持 getMin 操作,且都在 O(1) 时间内完成
最小栈 | LeetCode 155_哔哩哔哩_bilibili
辅助栈
引入一个辅助栈,主栈正常存数据,辅助栈同步记录每一步的最小值
push的时候辅助栈压入当前值和辅助栈顶的较小者,pop的时候两个栈同步弹出,getMin的时候直接读辅助栈的栈顶即可

python
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, value: int) -> None:
self.stack.append(value)
cur_min = value
if self.min_stack:
cur_min = min(value, self.min_stack[-1])
self.min_stack.append(cur_min)
def pop(self) -> None:
self.stack.pop()
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(value)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()
- 时间:O(1)
- 空间:O(n)
差值法(更省空间)
栈里不存原始值,而是存当前值和最小值的差,再用一个变量单独跟踪最小值。这样只需要一个栈+一个变量就够了
python
class MinStack:
def __init__(self):
self.stack = [] # 存的不是原始数值,而是差值(val - min_val)
self.min_val = 0 # 记录当前栈中的最小值
def push(self, value: int) -> None:
if not self.stack:
self.stack.append(0) # 说明是第一个元素。将 0 入栈(因为当前值就是最小值,差值为0)
self.min_val = value
else:
diff = value - self.min_val
self.stack.append(diff)
if diff < 0:
self.min_val = value # 如果 diff < 0,说明新加入的 val 比当前最小值还要小。因此,更新 self.min_val = val
def pop(self) -> None:
diff = self.stack.pop()
if diff < 0: # 说明刚刚弹出的这个元素在当时是最小值。那么现在它被弹出了,我们需要恢复之前的最小值
self.min_val -= diff
def top(self) -> int: # 获取栈顶元素
diff = self.stack[-1]
if diff < 0: # 说明当前栈顶元素就是最小值
return self.min_val
return self.min_val + diff
def getMin(self) -> int:
return self.min_val
# Your MinStack object will be instantiated and called as such:
# obj = MinStack()
# obj.push(value)
# obj.pop()
# param_3 = obj.top()
# param_4 = obj.getMin()
- 时间复杂度:所有操作都是 O(1)
- 空间复杂度:O(N)。只使用了一个栈,比双栈法省了一半的空间
295. 数据流的中位数
【大厂程序员带你刷力扣】【LeetCode 295】数据流的中位数|大根堆|小根堆_哔哩哔哩_bilibili
【大根堆】 用来保存值较小的部分,**【小根堆】**用来维护值较大的部分

把数据流分成两部分,左右元素个数相差不超过1,左边的max要 < 右边的min,如果不满足,需要把大根堆中的max 弹出来,压到小根堆中
插入元素的时间复杂度可以降到 O(logn)
- 如果两个堆中的元素相等,在获取中位数的时候可以取左边部分的max和右边部分的min(这两个操作的时间复杂度都是 O(1)),返回两者的平均值
- 如果大根堆元素个数比小根堆多一个,直接返回大根堆中的最大值
- 如果小根堆元素个数比大根堆多一个,直接返回小根堆中的最小值
整个插入操作过程:

python
class MedianFinder:
def __init__(self):
self.m_small = [] # 大根堆 (用负数存储实现)
self.m_large = [] # 小根堆
def addNum(self, num: int) -> None:
# 元素先插到大根堆
heapq.heappush(self.m_small, -num)
# 保证大根堆的最大值 小于 小根堆的最小值
if self.m_small and self.m_large and -self.m_small[0] > self.m_large[0]:
# 如果不满足,需要把大根堆中的max 弹出来,压到小根堆中
heapq.heappush(self.m_large, -heapq.heappop(self.m_small))
# 保持两个堆之间元素个数相差不超过 1
if len(self.m_small) > len(self.m_large) + 1:
heapq.heappush(self.m_large, -heapq.heappop(self.m_small))
if len(self.m_large) > len(self.m_small) + 1:
heapq.heappush(self.m_small, -heapq.heappop(self.m_large))
def findMedian(self) -> float:
if len(self.m_large) > len(self.m_small):
return self.m_large[0]
if len(self.m_small) > len(self.m_large):
return -self.m_small[0]
return (-self.m_small[0] + self.m_large[0]) / 2
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()
在 Python 中,大根堆之所以要用负数存储实现,根本原因在于:Python 内置的
heapq模块只提供了小根堆,没有提供大根堆。为了复用
heapq的底层 C 语言实现(保证 O(logN) 的高效性能),开发者们发明了这种"取负数"的巧妙技巧。
(六)二叉树
543. 二叉树的直径
这题的核心就是给定任意一个根节点,经过这个根节点的最长路径的长度是左子树的最大深度加右子树的最大深度。然后全局的最长路径不一定经过总树的root,但是总会经过某一个子树的根节点,所以直接后序遍历所有根节点,计算经过每个root的最长路径,取max
python
不是所有路径都经过根。一个例子:
1
/ \
2 3
/ \
4 5
/ \
6 7
最长路径是 6→4→2→5→7,长度 4,完全在左子树内部,不经过根节点 1
但在遍历到节点 2 时,左深度2(4→6)+ 右深度2(5→7)= 4,仍然被捕捉到了
全局最长路径必定是某一个节点左右最大深度相加
python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
self.ans = 0 # 记录所有节点中的最大直径
def dfs(node):
if not node:
return 0
left_depth = dfs(node.left) # 左子树最大深度
right_depth = dfs(node.right)
# 经过当前节点的最长路径 = 左子树深度 + 右子树深度
self.ans = max(self.ans, left_depth + right_depth)
return max(left_depth, right_depth) + 1 # 返回当前节点的最大深度(给父节点用)
dfs(root)
return self.ans
- 时间:O(n),每个节点访问一次
- 空间:O(h),递归栈深度(h 为树高)
114. 二叉树展开为链表
原地展开(最经典解法)
对于当前节点,如果有左子树,找到左子树的最右节点,将右子树接到这个最右节点的右边,然后把左子树移到右边。以示例1为例:
python
1
/ \
2 5
/ \ \
3 4 6
第一步处理节点 1:
- 左子树最右节点是 4
- 将 5 接到 4 右边
- 左子树 2 移到右边
1
\
2
/ \
3 4
\
5
\
6
步骤2: curr = 2
2有左子树吗?有(3)
左子树最右节点 = 3
3.right = 4 (注意:4.right已经是5了,所以实际是3.right=4,间接连接了5,6)
2.right = 3
2.left = None
1
\
2
\
3
\
4
\
5
\
6
代码:
python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
cur = root
while cur:
# 对于当前节点,如果有左子树
if cur.left:
node = cur.left
# 找到左子树的最右节点
while node.right:
node = node.right
# 将右子树接到这个最右节点的右边
node.right = cur.right
# 然后把左子树移到右边
cur.right = cur.left
cur.left = None
# 继续处理下一个节点
cur = cur.right
- **时间复杂度:**O(n)
- **空间复杂度:**O(1)
逆先序遍历

按照「右-左-根」的逆先序遍历,不断将当前节点的右指针指向上一个访问的节点。以示例1为例:
python
1
/ \
2 5
/ \ \
3 4 6
self.prev = None
def dfs(node):
if not node: return
dfs(node.right)
dfs(node.left)
node.right = self.prev
node.left = None
self.prev = node
====================================================
调用 dfs(6)
执行 node.right = prev(prev=None) → 6.right = None
node.left = None
prev = 6
此时链表: 6 → None
返回 dfs(5)
====================================================
dfs(5.left) → dfs(None) 完成
执行 node.right = prev(prev=6) → 5.right = 6
node.left = None
prev = 5
此时链表: 5 → 6 → None
返回 dfs(1)
代码:
python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
head = None
def flatten(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
if root is None:
return
# 前序遍历 (中左右) 的逆序: 右左中
self.flatten(root.right)
self.flatten(root.left)
# 中
root.left = None
root.right = self.head
self.head = root
- 空间复杂度:O(h) 递归栈
437. 路径总和 III
题目规定"路径不需要从根节点开始,也不需要在叶子节点结束",这意味着树中的任何一个节点都可以作为路径的起点。因此,我们需要遍历每一个节点,分别计算以它们为起点的合法路径数量


python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
def dfs(root, targetSum): # 从当前节点(root)开始
if not root:
return 0
if root.val == targetSum:
ans = 1
else:
ans = 0
return ans + dfs(root.left, targetSum - root.val) + dfs(root.right, targetSum - root.val)
if not root:
return 0
return dfs(root, targetSum) + self.pathSum(root.left, targetSum) + self.pathSum(root.right, targetSum)
(七)回溯
79. 单词搜索
**递归参数:**当前元素在矩阵 board 中的行列索引 i 和 j ,当前目标字符在 word 中的索引 k
终止条件:
- 返回 false :(1) 行或列索引越界 或 (2) 当前矩阵元素与目标字符不同 或 (3) 当前矩阵元素已访问过 ( (3) 可合并至 (2) ,因为已访问过的元素会被修改为空字符)
- 返回 true :k = len(word),即字符串 word 已全部匹配
递推工作:
- 标记当前矩阵元素:将 boardij 修改为 空字符,代表此元素已访问过,防止之后搜索时重复访问
- 搜索下一单元格:朝当前元素的 上、下、左、右 四个方向开启下层递归,使用 或 连接 (代表只需找到一条可行路径就直接返回,不再做后续 DFS ),并记录结果至 res
- 还原当前矩阵元素:将 boardij 元素还原至初始值,即 wordk
**返回值:**返回布尔量 res ,代表是否搜索到目标字符串
python
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
# 单词可能从棋盘的任意位置开始,因此主函数需要遍历整个棋盘作为起点
for i in range(len(board)):
for j in range(len(board[0])):
if self.backtracking(board, i, j, word, 0):
return True
return False
def backtracking(self, board, i, j, word, k):
# 终止条件
if k == len(word): # 已经匹配完整个单词
return True
if i >= len(board) or i < 0 or j >= len(board[0]) or j < 0: # 越界判断
return False
if board[i][j] != word[k]: # 字符不匹配
return False
board[i][j] = '' # 标记当前格子为已访问
# 向上下左右四个方向搜索
# 递归到下一个格子时,应该匹配 word 的下一个字符,即 k+1
res = self.backtracking(board, i+1, j, word, k+1) or self.backtracking(board, i-1, j, word, k+1) or self.backtracking(board, i, j+1, word, k+1) or self.backtracking(board, i, j-1, word, k+1)
board[i][j] = word[k] # 回溯,恢复当前格子
return res
时间复杂度:O(m × n × 3^L)
- 棋盘上有
m × n个起点 3^L:每条路径的搜索分支(第一步 4 个方向,之后每步 3 个方向),其中L为单词长度
空间复杂度:O(L)
- 主要是递归调用栈的空间开销,递归深度 = 单词长度L
(八)贪心
无
(九)动态规划
5. 最长回文子串
看到这题第一反应便是联想到【647. 回文子串】
双指针(中心扩展法)
python
class Solution:
def longestPalindrome(self, s: str) -> str:
res = ""
for i in range(len(s)):
one = self.extend(i, i, s)
two = self.extend(i, i+1, s)
if len(one) > len(res):
res = one
if len(two) > len(res):
res = two
return res
def extend(self, i, j, s):
while i >= 0 and j < len(s) and s[i] == s[j]:
i -= 1
j += 1
return s[i+1: j] # 跳出while循环的时候说明 s[i] != s[j]。故返回的是 [i+1, j-1]
动态规划
-
647题只关心个数 (每找到一个
dp[i][j] = True,res += 1) -
5题关心最长的那个 (找到
dp[i][j] = True时,记录(j - i + 1)和起始位置)
python
class Solution:
def longestPalindrome(self, s: str) -> str:
dp = [[False] * len(s) for _ in range(len(s))]
start = 0
max_len = 1
for i in range(len(s)-1, -1, -1):
for j in range(i, len(s)):
if s[i] == s[j]:
if j - i <= 1:
dp[i][j] = True
elif dp[i+1][j-1] == True:
dp[i][j] = True
if dp[i][j] and j-i+1 > max_len:
max_len = j-i+1
start = i
return s[start: start+max_len]
64. 最小路径和
python
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)] # m×n
# 初始化
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j] + grid[i][j], dp[i][j-1] + grid[i][j])
return dp[m-1][n-1]
152. 乘积最大子数组
不能用简单的贪心或只记录最大值来做 。
原因是乘法中"负负得正" 。例如 [-2, 5, -3],当前遍历到 5 时,最大值是 5,最小值是 -10。当遇到 -3 时,-10 * -3 = 30,反而变成了最大值。
因此,我们在遍历时,需要同时维护当前以 nums[i] 结尾的"最大乘积"和"最小乘积"。
思路: 遍历数组,对于每个位置 i,我们需要计算以 nums[i] 结尾的连续子数组的最大乘积 curr_max 和最小乘积 curr_min。
状态转移方程:
-
curr_max = max(nums[i], nums[i] * prev_max, nums[i] * prev_min) -
curr_min = min(nums[i], nums[i] * prev_max, nums[i] * prev_min)
我们需要用一个全局变量 ans 来记录遍历过程中出现过的最大 curr_max。
python
class Solution:
def maxProduct(self, nums: list[int]) -> int:
if not nums:
return 0
cur_max = cur_min = ans = nums[0] # 初始化:最大乘积、最小乘积、最终答案都初始化为第一个元素
for i in range(1, len(nums)):
x = nums[i]
# 需要临时变量,因为计算 curr_min 时需要用到原来的 curr_max
temp_max = max(x, cur_max * x, cur_min * x)
temp_min = min(x, cur_max * x, cur_min * x)
cur_max = temp_max
cur_min = temp_min
ans = max(cur_max, ans) # 更新全局最大值
return ans
复杂度:
-
时间复杂度: O(N),只需遍历一次数组。
-
空间复杂度: O(1),只使用了常数个变量。
118. 杨辉三角
定义:整个三角形最左侧和最右侧都是1,中间每个数都是左上方+右上方
边界条件赋值,中间条件动态规划
python
class Solution:
def generate(self, numRows: int) -> list[list[int]]:
ret = []
for i in range(numRows):
row = []
for j in range(0, i+1):
if j==0 or j==i:
row.append(1)
else:
row.append(ret[i-1][j] + ret[i-1][j-1])
ret.append(row)
return ret
(十)图论
208. 实现 Trie (前缀树)
【大厂程序员带你刷力扣】【Leetcode 208】实现Tire(前缀树)_哔哩哔哩_bilibili(C++实现)
对于每个节点,都可能有 a-z 26个子孩子节点
python
class TrieNode:
def __init__(self):
self.children = {}
self.isEnd = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
cur = self.root
for char in word:
if char not in cur.children:
cur.children[char] = TrieNode()
cur = cur.children[char]
cur.isEnd = True
def search(self, word: str) -> bool:
cur = self.root
for char in word:
if char not in cur.children:
return False
cur = cur.children[char]
return cur.isEnd
def startsWith(self, prefix: str) -> bool:
cur = self.root
for char in prefix:
if char not in cur.children:
return False
cur = cur.children[char]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
-
search: 不仅要看路径是否存在(for循环没有中途return False),最终返回时必须判断当前节点是否是单词结尾 (return cur.isEnd) -
startsWith: 只要路径存在就可以直接return True,不需要关心最后一个节点是否是isEnd