算法 Day3-队列 / deque + 链表

复习

给定整数数组:

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

dequedouble-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。
相关推荐
名字还没想好☜1 小时前
Python 的 __call__ 实战:让实例像函数一样被调用,做带状态计数器、缓存器与可配置策略
开发语言·后端·python·缓存·编程语言
cpolar技术支持1 小时前
AI Agent 跑半小时就忘目标?用检查点与任务账本做可恢复长任务,cpolar 分享只读时间线
python·sqlite·cpolar·ai agent·任务恢复
张小姐的猫1 小时前
【AI大模型接入SDK】 —— SQLite上手
linux·开发语言·c++·人工智能·python·log4j
529宝宝起名网1 小时前
用 Python 实现基于字源特征的名字文化评分算法:从六书部首到文化寓意的多维度评分模型
python
努力学习的代码小白1 小时前
mem0源码学习03
python
码流子1 小时前
2026 图像数据标注工具横评:LabelImg / CVAT / Label Studio / X-AnyLabeling / 国产Web平台,到底怎么选?
大数据·人工智能·算法
hansang_IR1 小时前
【题解】[JSOI2016] 最佳团体(分数规划)
c++·算法
zzj_2626102 小时前
Python数组定义及操作
python