复习
给定整数数组:
python
nums = [2, 1, 5, 1, 3, 2]
k = 3
求长度为 k 的连续子数组最大和。
先判断:
python
用什么算法?
为什么?
时间复杂度和空间复杂度?
答案
这是:
python
固定长度滑动窗口
因为题目是:
python
连续子数组
固定长度 k
每次窗口右移时,只需要加一个新元素、减一个旧元素
代码:
python
def max_sum_k(nums, k ) :
window_sum = sum(nums[:k]
ans = window_sum
for right in range(k , len(nums)):
window_sum += nums[right]
window_sum -= nums[right - k ]
ans = max(ans , window_sum )
return ans
powershell
时间:
O(n)
空间:
O(1)
如果你第一反应是把所有长度为 k 的子数组分别 sum() 一遍,那就是:
O(nk)
窗口的意义就是:
不重复计算重叠区间。
Part A|队列 Queue / deque
deque 是 double-ended queue (双端队列)的缩写,读作 "deck"(音似「戴克」)。它支持在队列的两端高效地添加(append)和弹出(pop)元素,时间复杂度均为 O(1)。
1. 队列是什么?
python
队列的核心规则:
First In, First Out,先进先出。
想象排队买东西:
先来的人 → 先处理
后来的人 → 排在后面
和昨天的栈正好相反:
python
Stack:LIFO 后进先出
Queue:FIFO 先进先出
2. Python 里为什么不用 list 做普通队列?
你当然可以:
python
queue = []
queue.append(x)
queue.pop(0)
powershell
但是:
pop(0)
是:
O(n)
因为删除第一个元素以后,后面的元素都要移动。
所以 Python 里应该:
python
from collections import deque
queue = deque()
#入队:
queue.append(x)
#出队:
x = queue.popleft()
两者通常都是:
python
O(1)
3. deque 是什么?
python
deque:
Double-ended Queue,双端队列。
两边都可以:
添加
删除
Python:
python
from collections import deque
q = deque()
q.append(1) # 右边加入
q.appendleft(0) # 左边加入
q.pop() # 右边删除
q.popleft() # 左边删除
这些操作通常都是:
python
O(1)
4. 普通队列适合什么题?
典型信号:
powershell
按到达顺序处理
层级遍历
任务排队
消息处理
最近 N 个事件
BFS
尤其看到:
python
"从某个起点,一层一层向外扩散"
马上想到:
python
BFS + Queue
5. deque 还有一个高级用途:单调队列
昨天刚学了:
python
单调栈。
今天有一个近亲:
python
单调队列。
它非常擅长:
滑动窗口中的最大值 / 最小值。
比如窗口:
python
[1,3,-1,-3,5,3,6,7]
窗口大小:
k = 3
我们不断滑动窗口,需要快速知道:
当前窗口最大值是谁?
如果每个窗口都遍历:
python
O(k)
总复杂度:
python
O(nk)
单调队列可以做到:
python
O(n)
LeetCode 239. 滑动窗口最大值
python
from collections import deque
# 队列里只保留"有可能成为窗口最大值"的候选者,而且从队头到队尾是递减的。
# 队头永远是当前窗口最大值
# 新来的数比队尾大-》队尾没机会了,踢掉
# 窗口滑走时队头过期了-〉弹掉
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
q = deque()
ans = []
for i , x in enumerate(nums):
#删除窗口左边已经过期的元素
while q and q[0] <= i- k :
q.popleft()
# 当 i 是右指针时,窗口是 [left, i],长度为 k。left = i - k + 1
# dq 里存的是索引。队头 dq[0] 是"当前窗口最大值的索引"。
# 如果队头那个索引已经滑出窗口左边界了(比 left 还小),就把它踢掉。
# 删除对味比当前元素小的元素
while q and nums[q[-1]] <= x :
q.pop()
q.append(i) #当前下标入队
if i>= k-1: #窗口形成后记录最大值
ans.append(nums[q[0]])
return ans
单调队列不是:
"整个窗口里所有元素都按大小排序。"
不是。
它只保留:
仍有可能成为当前或未来窗口最大值的候选元素。
这也是它能高效的关键。
Part B|链表 Linked List
1. 链表是什么?
数组是:
连续存储
链表不是。
链表由一个个节点组成:
value \| next
例如:
1 → 3 → 7 → 9 → None
每个节点保存:
当前值
+
下一个节点的位置/引用
Python 常见定义:
python
class ListNode:
def __init__(self , val = 0 , next = None):
self.val = val
self.next = next
2. 数组和链表区别
数组:
随机访问:O(1)
中间插入删除:O(n)
链表:
随机访问:O(n)
已知节点位置后插入删除:O(1)
为什么链表找第 100 个节点是 O(n)?
因为没有:
base + index
这种直接定位方式。
必须:
python
head
↓
next
↓
next
↓
...
一个个走。
3. 链表真正难在哪里?
不是定义。
而是:
修改 next 的顺序。
链表题特别容易出现:
python
你刚把 next 改了
↓
原来的后半截链表找不到
所以链表题最重要的习惯:
先保存,再修改。
例如:
python
nxt = cur.next
cur.next = prev
先保存 nxt,然后再断开原链接。
4. 链表常见套路
看到:
python
反转链表
删除节点
倒数第 K 个
链表环
链表中点
合并链表
常见工具:
python
dummy node
快慢指针
prev / cur / next
5. Dummy Node 是什么?
链表题特别常见。
假设:
python
1 → 2 → 3
你要删除:
python
1
头节点本身发生变化。
这时候处理起来比较麻烦。
可以建立一个假的头:
python
dummy → 1 → 2 → 3
这样:
python
删除 1
和:
删除 2
逻辑一样:
找前驱节点然后修改 next。
所以 Dummy Node 的核心价值是:
统一头节点和普通节点的处理逻辑。
LeetCode 206. 反转链表
三个指针
维护:
python
prev
cur
nxt
初始:
python
None ← prev
cur
↓
1 → 2 → 3 → 4
核心操作
python
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
prev = None
cur = head
while cur :
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
return prev
保存下一个节点-》修改当前指针-〉移动两个指针

做题
933 Number of Recent Calls
python
from collections import deque
class RecentCounter:
def __init__(self):
self.q = deque() #存放所有请求的时间戳,从小到大排
def ping(self, t: int) -> int:
self.q.append(t) # 新请求进来,放在队尾
while self.q[0] < t-3000:
self.q.popleft() # 队头太老了,就踢掉
return len(self.q) #剩下的都是窗口内的,返回数量
# deque 存所有时间戳,每次来新的先加进去,再把超出 3000ms 的老请求从队头踢掉,剩下的数量就是答案。
# Your RecentCounter object will be instantiated and called as such:
# obj = RecentCounter()
# param_1 = obj.ping(t)
21 题(Merge Two Sorted Lists)
list1 不是一个"链表对象",它就是一个 ListNode 对象的引用(指针),指向这条链表的第一个节点。
list1 本身就是指向节点 1 的变量。
list1 只是一个指向头节点的指针。
list1 自己就是头,list1.val 是读头节点的值。
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: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
# dummy = ListNode() # 假头节点,不存有效数据,只是为了方便返回结果
# tail = dummy # tail 始终指向"新链表的尾巴",新节点就挂在 tail.next 上
dummy = ListNode() #在内存里新建了一个节点对象,然后让变量 dummy 指向它。
tail = dummy # 让变量 tail 也指向同一个对象。dummy 从头到尾没动过,但它身后的 .next 链已经把所有节点串好了。
while list1 and list2 :
if list1.val <= list2.val:
tail.next = list1 # dummy 和 tail 指向同一个对象,所以通过 tail 改 .next,dummy 也能看到这个变化。
list1 = list1.next
else:
tail.next = list2
list2 = list2.next
tail = tail.next
tail.next = list1 if list1 else list2
return dummy.next
141 环形链表
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast : # 为什么用 is 而不是 ==?
# == 比较的是值(两个节点 val 可能都是 3,但它们是不同的节点)
# is 比较的是内存地址(是不是同一个对象)
return True
return False
19题
python
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
# fast 比 slow 多走 n+1 步,这样 fast 到 None 时,slow 刚好在要删节点的前一个。
dummy = ListNode(0,head) # 假头,防止删第一个节点时没有前驱
fast = dummy
slow = dummy
for _ in range(n+1): # # 快指针先走 n+1 步
fast = fast.next
while fast: # 然后一起走
fast = fast.next
slow = slow.next
slow.next = slow.next.next # 跳过要删的节点
return dummy.next
小训练
题目
给定一系列操作:
python
push x
pop
front
模拟一个队列。
输入:
python
6
push 1
push 2
front
pop
front
pop
输出:
python
1
1
2
2
Python:
python
from collections import deque
n = int(input())
q = deque()
for _in range(n):
parts = input().split()
if parts[0] == "push":
q.append(int(parts[1]))
elif parts[0] = "pop":
if q:
print(q/popleft())
elif parts[0] == "front":
if q:
print(q[0])
python
输入拆分
操作模拟
边界判断
面试手撕训练|今天选:反转链表
这是链表面试最经典的一题。
① 直观方案
python
可以先把所有节点保存到数组再反向连接,但需要 O(n) 额外空间。
② 优化
python
实际上只需要修改每个节点的 next 指针,因此可以用三个指针 prev、cur 和 next 原地反转。
③ 为什么需要 next?
python
因为修改 cur.next 后,原来的后继节点会丢失,所以修改前必须先保存。
④ 更新顺序
python
保存 nxt
↓
cur.next = prev
↓
prev = cur
↓
cur = nxt
⑤ 复杂度
每个节点访问一次,时间 O(n),只使用常数个指针,额外空间 O(1)。
Day 3 Cheat Sheet
Queue / deque
识别信号:
python
先进先出
按顺序处理
任务排队
层序
BFS
最近一段时间的事件
Python:
python
from collections import deque
q = deque()
q.append(x)
x = q.popleft()
不要:
list.pop(0)
因为通常是:
O(n)
单调队列
识别:
python
滑动窗口最大值
滑动窗口最小值
连续区间 + 快速极值
核心原则:
python
队列只保留仍然有资格成为答案的候选元素。
最大值模板:
python
from collections import deque
q = deque()
for i, x in enumerate(nums):
while q and q[0] <= i - k:
q.popleft()
while q and nums[q[-1]] <= x:
q.pop()
q.append(i)
if i >= k - 1:
ans.append(nums[q[0]])
链表
识别:
python
反转
删除节点
倒数第 K 个
环
中点
链表合并
核心工具:
python
dummy
快慢指针
prev / cur / next
反转模板:
python
prev = None
cur = head
while cur:
nxt = cur.next
cur.next = prev
prev = cur
cur = nxt
今天最容易犯的错误
用 list.pop(0) 模拟队列,忽略它的 O(n) 移动成本。
单调队列只存值,最后发现无法判断元素是不是已经离开窗口。
认为单调队列要保存窗口全部元素;实际上只保留候选答案。
链表修改 next 前没保存后继节点,直接把后半截链表弄丢。
删除链表头节点时写一堆特殊判断,而不是使用 dummy node。
快慢指针题没有先明确"两个指针之间到底保持多大距离"。
环形链表知道快慢指针,却解释不了为什么快指针最终能追上慢指针。
今天真正要带走四句话:
python
先进先出 → Queue。
滑动窗口里频繁求最大/最小 → 单调 deque。
链表改指针前,先保存 next。
头节点可能变化 → 优先想 dummy node。