第一章:算法基础——时间复杂度 & 空间复杂度
一、为什么要学复杂度
同样实现一个功能,写法不同运行速度、占用内存天差地别。
复杂度就是用来评估算法好坏的标准,不用跑代码,一眼看出谁快谁慢、谁省内存。
二、大O表示法
评判算法统一标准:大 O 符号表示法
只关注最高阶项,忽略常数、低次项。
常见复杂度从快到慢排序:
O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)O(1) < O(\log n) < O(n) < O(n\log n) < O(n^2) < O(n^3) < O(2^n)O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)
三、时间复杂度(执行时间)
1. O(1) 常数级
代码执行次数固定,和数据量 n 无关
def get_sum(a, b):
return a + b
无论传入什么,永远只执行1次,O(1)
2. O(n) 线性级
循环 n 次,数据越大执行次数越多
def print_n(n):
for i in range(n):
print(i)
循环跑 n 次,O(n)
3. O(n2n^2n2) 平方级
嵌套循环,一层n,一层n
def double_loop(n):
for i in range(n):
for j in range(n):
print(i, j)
n×nn \times nn×n 次,O(n2n^2n2)
4. O(logn\log nlogn) 对数级
每次数据缩小一半,典型:二分查找
数据翻倍,执行次数只加一点点,效率极高。
5. O(nlognn\log nnlogn) 线性对数级
代表算法:快速排序、归并排序
排序里最优的稳定复杂度。
四、空间复杂度(占用内存)
衡量算法额外开辟多少内存空间
- O(1):只用几个变量,不开新数组
- O(n):额外创建长度为n的列表
- O(n2n^2n2):开辟二维数组
示例:
# 空间O(1)
def func1(n):
s = 0
for i in range(n):
s += i
return s
# 空间O(n)
def func2(n):
lst = [i for i in range(n)]
return lst
五、三种情况分析
- 最好情况:运气最好,执行最少次数
- 最坏情况:运气最差,执行最多次(面试常看这个)
- 平均情况:所有情况平均耗时
第二章:单向链表
一、什么是链表
1. 数组/列表 的缺点
Python列表底层是顺序存储:
- 中间插入、删除元素,后面元素要整体搬家
- 数据量越大,效率越低
2. 链表特点
链表由一个个节点串联而成,每个节点包含两部分:
- 数据域:存当前节点数据
- 指针域:存下一个节点的地址
逻辑结构:
头节点 → 节点1 → 节点2 → 节点3 → 尾节点(指向None)
优点:
- 插入、删除只改指针,不用批量移动元素
- 内存不要求连续,灵活分配
缺点:
- 不能像列表那样按下标随机访问
- 只能从头节点开始逐个往后遍历
二、节点类设计
每个节点是一个独立对象
class Node:
def __init__(self, data):
# 数据域
self.data = data
# 指针域,默认指向空
self.next = None
三、单向链表整体结构
class SingleLinkedList:
def __init__(self):
# 初始化头节点为空
self.head = None
四、链表核心功能实现
1. 尾部添加节点
class SingleLinkedList:
def __init__(self):
self.head = None
# 尾部添加元素
def append(self, data):
# 创建新节点
new_node = Node(data)
# 如果链表为空,头节点指向新节点
if self.head is None:
self.head = new_node
return
# 不为空,遍历到尾节点
cur = self.head
while cur.next is not None:
cur = cur.next
# 尾节点指向新节点
cur.next = new_node
2. 遍历打印链表
# 遍历所有节点
def travel(self):
cur = self.head
while cur is not None:
print(cur.data, end=" → ")
cur = cur.next
print("None")
3. 头部插入节点
# 头部插入
def add_first(self, data):
new_node = Node(data)
# 新节点next指向原头节点
new_node.next = self.head
# 头节点换成新节点
self.head = new_node
4. 指定位置插入节点
# 指定下标插入
def insert(self, index, data):
if index < 0:
return
new_node = Node(data)
# 插在头部
if index == 0:
self.add_first(data)
return
cur = self.head
# 找到插入位置的前一个节点
for i in range(index - 1):
if cur.next is None:
break
cur = cur.next
# 调整指针
new_node.next = cur.next
cur.next = new_node
5. 删除指定数据节点
# 删除指定元素
def remove(self, data):
cur = self.head
pre = None
# 找要删除的节点
while cur is not None:
if cur.data == data:
# 如果是头节点
if pre is None:
self.head = cur.next
else:
pre.next = cur.next
return
pre = cur
cur = cur.next
6. 查找元素是否存在
# 查找元素
def search(self, data):
cur = self.head
while cur is not None:
if cur.data == data:
return True
cur = cur.next
return False
五、完整代码
# 节点类
class Node:
def __init__(self, data):
self.data = data
self.next = None
# 单向链表类
class SingleLinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if self.head is None:
self.head = new_node
return
cur = self.head
while cur.next is not None:
cur = cur.next
cur.next = new_node
def travel(self):
cur = self.head
while cur is not None:
print(cur.data, end=" → ")
cur = cur.next
print("None")
def add_first(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def insert(self, index, data):
if index < 0:
return
new_node = Node(data)
if index == 0:
self.add_first(data)
return
cur = self.head
for i in range(index - 1):
if cur.next is None:
break
cur = cur.next
new_node.next = cur.next
cur.next = new_node
def remove(self, data):
cur = self.head
pre = None
while cur is not None:
if cur.data == data:
if pre is None:
self.head = cur.next
else:
pre.next = cur.next
return
pre = cur
cur = cur.next
def search(self, data):
cur = self.head
while cur is not None:
if cur.data == data:
return True
cur = cur.next
return False
# 测试
if __name__ == "__main__":
sll = SingleLinkedList()
sll.append(10)
sll.append(20)
sll.append(30)
print("尾部添加后:")
sll.travel()
sll.add_first(5)
print("头部插入后:")
sll.travel()
sll.insert(2, 15)
print("下标2插入15后:")
sll.travel()
sll.remove(20)
print("删除20后:")
sll.travel()
print("查找15:", sll.search(15))
print("查找100:", sll.search(100))
六、单向链表复杂度总结
- 尾部添加:O(n)
- 头部插入:O(1)
- 中间插入/删除:O(n)
- 查找遍历:O(n)
第三章:栈
一、栈是什么
1. 核心特点
后进先出 LIFO
最后放进去的元素,最先拿出来。
生活例子:
- 叠盘子:最后放的盘子先拿走
- 浏览器后退、编辑器撤销 Ctrl+Z 底层都是栈
2. 栈结构术语
- 入栈 push:往栈里存数据
- 出栈 pop:从栈顶取出数据
- 栈顶 top:当前最后一个元素
- 栈底 bottom:最先放入的元素
二、Python 实现栈两种方式
- 用列表模拟栈
- 手写栈类
三、方式一:列表直接模拟栈
# 列表尾部当栈顶
stack = []
# 入栈 append
stack.append(10)
stack.append(20)
stack.append(30)
print("入栈后:", stack)
# 出栈 pop
print("出栈:", stack.pop())
print("出栈:", stack.pop())
# 查看栈顶元素
print("栈顶:", stack[-1])
特点:
- 尾部 append、pop 都是 O(1) 效率极高
- 简单开发直接用列表当栈
四、方式二:手写封装栈类
完整代码
class Stack:
def __init__(self):
# 私有容器
self.__items = []
# 入栈
def push(self, item):
self.__items.append(item)
# 出栈
def pop(self):
if self.is_empty():
return None
return self.__items.pop()
# 查看栈顶
def peek(self):
if self.is_empty():
return None
return self.__items[-1]
# 判断是否为空
def is_empty(self):
return len(self.__items) == 0
# 获取栈大小
def size(self):
return len(self.__items)
# 清空栈
def clear(self):
self.__items = []
测试
if __name__ == "__main__":
s = Stack()
s.push(1)
s.push(2)
s.push(3)
print("栈大小:", s.size())
print("栈顶元素:", s.peek())
print("出栈:", s.pop())
print("是否为空:", s.is_empty())
五、栈经典实战:括号匹配问题
题目要求
输入字符串包含 () [] {},判断括号是否合法匹配
规则:
- 左括号必须有对应右括号
- 必须按嵌套顺序闭合
- 不能交叉
算法思路
- 遍历字符串每个字符
- 遇到左括号:入栈
- 遇到右括号:
- 栈空 → 直接不匹配
- 栈顶左括号和当前右括号不配对 → 不匹配
- 配对成功 → 出栈
- 遍历结束,栈必须为空才算完全匹配
完整代码
def is_valid_bracket(s):
stack = Stack()
# 建立左右括号映射
bracket_map = {")":"(", "]":"[", "}":"{"}
for char in s:
# 左括号入栈
if char in bracket_map.values():
stack.push(char)
# 右括号匹配
elif char in bracket_map.keys():
# 栈空无左括号匹配
if stack.is_empty():
return False
# 取出栈顶左括号
top = stack.pop()
if top != bracket_map[char]:
return False
# 最后栈必须为空
return stack.is_empty()
# 测试
print(is_valid_bracket("()[]{}")) # True
print(is_valid_bracket("([)]")) # False
print(is_valid_bracket("{[]}")) # True
print(is_valid_bracket("((()))")) # True
六、栈复杂度与总结
- 入栈、出栈、取栈顶:O(1)
- 括号匹配遍历一次字符串:O(n)
- 栈核心思想:后进先出
- 适用场景:括号匹配、表达式求值、函数递归调用栈、撤销后退
第四章:队列
一、队列核心概念
1. 特点
先进先出 FIFO
先进入队列的元素,先被取出。
生活例子:
- 排队买票、排队打饭
- 消息异步队列、任务排队、请求限流
2. 队列术语
- 入队 enqueue:从队尾添加元素
- 出队 dequeue:从队头取出元素
- 队头 front:最先进入的元素
- 队尾 rearou:最后进入的元素
二、列表模拟普通队列
用列表头部出队 pop(0)
queue = []
# 入队 队尾添加
queue.append(10)
queue.append(20)
queue.append(30)
# 出队 队头弹出
print(queue.pop(0))
print(queue.pop(0))
缺点:
pop(0) 会让后面所有元素前移,时间复杂度 O(n),数据量大很慢。
三、collections.deque 双端队列
Python 内置高效双端队列,头尾增删都是 O(1)
常用用法
from collections import deque
# 创建空队列
q = deque()
# 队尾入队
q.append(10)
q.append(20)
q.append(30)
# 队头出队
print(q.popleft()) # 10
print(q.popleft()) # 20
# 队头插入
q.appendleft(5)
# 队尾弹出
print(q.pop())
deque 常用方法
append()队尾加appendleft()队头加popleft()队头出(高效)pop()队尾出clear()清空len(q)获取长度
四、手写实现普通队列
封装队列类,遵循先进先出
class Queue:
def __init__(self):
self.__items = []
# 入队:队尾添加
def enqueue(self, item):
self.__items.append(item)
# 出队:队头取出
def dequeue(self):
if self.is_empty():
return None
return self.__items.pop(0)
# 判断是否为空
def is_empty(self):
return len(self.__items) == 0
# 获取队列大小
def size(self):
return len(self.__items)
# 查看队头元素
def front(self):
if self.is_empty():
return None
return self.__items[0]
测试代码
if __name__ == "__main__":
q = Queue()
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print("队列大小:", q.size())
print("队头元素:", q.front())
print("出队:", q.dequeue())
print("出队:", q.dequeue())
print("是否为空:", q.is_empty())
五、手写双端队列
支持头尾都可以入队、出队
class DeQueue:
def __init__(self):
self.__items = []
# 队尾添加
def add_rear(self, item):
self.__items.append(item)
# 队头添加
def add_front(self, item):
self.__items.insert(0, item)
# 队头删除
def remove_front(self):
if self.is_empty():
return None
return self.__items.pop(0)
# 队尾删除
def remove_rear(self):
if self.is_empty():
return None
return self.__items.pop()
def is_empty(self):
return len(self.__items) == 0
def size(self):
return len(self.__items)
六、队列应用场景
- 任务排队、消息队列
- 广度优先搜索 BFS(树、图遍历)
- 多线程任务调度
- 窗口滑动、限流排队
七、复杂度总结
- 列表模拟队列
pop(0):O(n) 低效 - 内置
deque头尾增删:O(1) 高效 - 普通队列:先进先出;栈:后进先出
第五章:哈希表
一、哈希表是什么
哈希表(Hash Table)是键值对映射的数据结构,
核心优势:查询、插入、删除 平均时间复杂度 O(1)
Python 里的 dict 字典,底层就是哈希表。
二、核心原理
1. 哈希函数
把任意长度的 key,通过算法换算成固定数组下标
公式:
数组下标 = hash(关键字) % 数组长度
2. 哈希冲突
不同的 key,算出了同一个数组下标,这就是哈希冲突。
3. 解决哈希冲突常用两种方案
- 链地址法(拉链法):数组每个位置挂一个链表,冲突就往链表尾部挂
- 开放地址法:冲突了就往后找空位
我们这节用面试最常考:链地址法手写哈希表。
三、哈希表结构设计
- 底层是一个固定长度数组
- 数组每个元素是单向链表
- key 经过哈希函数 → 算出下标 → 对应链表插入/查找/删除
结构示意:
数组下标0 → 链表节点
数组下标1 → 链表节点→节点
数组下标2 → 空
...
四、先复用之前的节点类
class Node:
def __init__(self, key, value):
self.key = key
self.value = value
self.next = None
五、手写哈希表(链地址法完整实现)
具备功能:
- 新增/修改键值对 put
- 根据key取值 get
- 根据key删除 remove
- 打印全部元素
class HashTable:
def __init__(self, capacity=10):
# 底层数组容量
self.capacity = capacity
# 初始化数组,每个位置初始为None
self.table = [None] * self.capacity
# 哈希函数:计算下标
def _hash(self, key):
return hash(key) % self.capacity
# 添加/修改键值对
def put(self, key, value):
idx = self._hash(key)
new_node = Node(key, value)
# 当前下标位置为空,直接放头节点
if self.table[idx] is None:
self.table[idx] = new_node
return
# 不为空,遍历链表
cur = self.table[idx]
# 如果key已存在,覆盖value
while cur:
if cur.key == key:
cur.value = value
return
if cur.next is None:
break
cur = cur.next
# 尾部挂载新节点
cur.next = new_node
# 根据key获取value
def get(self, key):
idx = self._hash(key)
cur = self.table[idx]
while cur:
if cur.key == key:
return cur.value
cur = cur.next
# 找不到返回None
return None
# 删除指定key
def remove(self, key):
idx = self._hash(key)
cur = self.table[idx]
pre = None
while cur:
if cur.key == key:
# 是头节点
if pre is None:
self.table[idx] = cur.next
else:
pre.next = cur.next
return True
pre = cur
cur = cur.next
return False
# 打印哈希表所有数据
def show(self):
for i in range(self.capacity):
print(f"下标{i}: ", end="")
cur = self.table[i]
while cur:
print(f"({cur.key}:{cur.value})", end=" → ")
cur = cur.next
print("None")
六、测试手写哈希表
if __name__ == "__main__":
ht = HashTable(10)
# 添加键值对
ht.put("name", "张三")
ht.put("age", 20)
ht.put("city", "北京")
ht.put("score", 95)
# 打印整个哈希表
ht.show()
# 查询
print("姓名:", ht.get("name"))
print("年龄:", ht.get("age"))
# 修改
ht.put("age", 21)
print("修改后年龄:", ht.get("age"))
# 删除
ht.remove("city")
print("删除city后:")
ht.show()
七、哈希表复杂度
- 理想无冲突:插入、查找、删除 O(1)
- 大量冲突退化成链表:最坏 O(n)
- Python dict 底层会自动扩容,把冲突降到很低
八、哈希表适用场景
- 键值对快速存储查找
- 数据去重、统计频次
- 缓存设计、索引映射
- 算法题:两数之和、字母异位词
第六章:三大基础排序——冒泡、选择、插入排序
一、排序算法说明
三大简单排序:
- 冒泡排序
- 选择排序
- 插入排序
平均复杂度都是:O(n²),适合小规模数据。
一、冒泡排序
原理
相邻两个元素两两比较,大的往后冒泡,每一轮把当前最大值沉到末尾。
- 每一轮确定一个末尾有序元素
- 共需 n-1 轮
完整代码
def bubble_sort(arr):
# 拷贝一份,不修改原数组
nums = arr.copy()
n = len(nums)
# 外层:共比较 n-1 轮
for i in range(n - 1):
# 内层:每轮比较到无序区域末尾
for j in range(n - 1 - i):
if nums[j] > nums[j + 1]:
# 交换
nums[j], nums[j + 1] = nums[j + 1], nums[j]
return nums
# 测试
if __name__ == "__main__":
test = [5, 3, 8, 4, 2]
res = bubble_sort(test)
print("冒泡排序结果:", res)
优化版(已有序提前退出)
def bubble_sort_optimize(arr):
nums = arr.copy()
n = len(nums)
for i in range(n - 1):
flag = False # 标记本轮是否发生交换
for j in range(n - 1 - i):
if nums[j] > nums[j + 1]:
nums[j], nums[j + 1] = nums[j + 1], nums[j]
flag = True
# 没交换说明已经有序,直接退出
if not flag:
break
return nums
复杂度
- 最好:O(n)(优化后已有序)
- 最坏/平均:O(n²)
- 空间:O(1) 原地排序
二、选择排序
原理
每一轮找到最小值下标,和当前无序区间第一个位置交换。
- 不频繁交换,只记下标,一轮只交换一次
完整代码
def select_sort(arr):
nums = arr.copy()
n = len(nums)
for i in range(n - 1):
# 假设当前i为最小值下标
min_idx = i
# 找后续更小值的下标
for j in range(i + 1, n):
if nums[j] < nums[min_idx]:
min_idx = j
# 最小值和i位置交换
nums[i], nums[min_idx] = nums[min_idx], nums[i]
return nums
# 测试
test = [5, 3, 8, 4, 2]
print("选择排序结果:", select_sort(test))
复杂度
- 最好/最坏/平均:O(n²)
- 空间:O(1)
- 特点:交换次数比冒泡少,实际效率略高于冒泡
三、插入排序
原理
把数组分成有序区 + 无序区
逐个把无序区第一个元素,插入到前面有序区的合适位置。
像打牌理牌,一张一张插到正确位置。
完整代码
def insert_sort(arr):
nums = arr.copy()
n = len(nums)
# 从第二个元素开始往前插
for i in range(1, n):
# 当前要插入的数
temp = nums[i]
j = i - 1
# 往前挪,比temp大的后移
while j >= 0 and nums[j] > temp:
nums[j + 1] = nums[j]
j -= 1
# 放到正确位置
nums[j + 1] = temp
return nums
# 测试
test = [5, 3, 8, 4, 2]
print("插入排序结果:", insert_sort(test))
复杂度
- 最好:O(n)(已有序)
- 最坏/平均:O(n²)
- 空间:O(1)
- 实际效率:插入 > 选择 > 冒泡
四、三大基础排序总结
| 排序 | 平均复杂度 | 最好复杂度 | 原地排序 | 实际效率 |
|---|---|---|---|---|
| 冒泡 | O(n²) | O(n) | 是 | 最差 |
| 选择 | O(n²) | O(n²) | 是 | 中等 |
| 插入 | O(n²) | O(n) | 是 | 最好 |
第七章:进阶排序——快速排序、归并排序
一、进阶排序概述
前面冒泡、选择、插入都是 O(n²) 简单排序;
快速排序、归并排序属于 O(nlogn) 高效排序,面试必考、工程常用,都基于 分治+递归 思想。
一、快速排序
1. 核心原理
- 选一个基准值 pivot
- 把数组划分成:小于pivot | pivot | 大于pivot
- 左右两边递归再做同样划分
分治思想:挖坑 + 分区域 + 递归
2. 基础版快速排序
def quick_sort(arr):
# 递归终止条件:数组长度小于等于1直接返回
if len(arr) <= 1:
return arr
# 选第一个当基准值
pivot = arr[0]
# 划分三区
left = [x for x in arr[1:] if x < pivot]
mid = [pivot]
right = [x for x in arr[1:] if x >= pivot]
# 递归拼接
return quick_sort(left) + mid + quick_sort(right)
# 测试
if __name__ == "__main__":
test = [6, 3, 8, 5, 2, 9, 1]
res = quick_sort(test)
print("快速排序结果:", res)
3. 原地交换版(不额外开数组)
def quick_sort_in_place(arr, low, high):
def partition(l, r):
pivot = arr[l]
while l < r:
# 从右找比pivot小的
while l < r and arr[r] >= pivot:
r -= 1
arr[l] = arr[r]
# 从左找比pivot大的
while l < r and arr[l] <= pivot:
l += 1
arr[r] = arr[l]
# 把基准值放到中间
arr[l] = pivot
return l
if low < high:
# 划分基准下标
pi = partition(low, high)
# 递归排左边
quick_sort_in_place(arr, low, pi - 1)
# 递归排右边
quick_sort_in_place(arr, pi + 1, high)
# 测试
test = [6, 3, 8, 5, 2, 9, 1]
quick_sort_in_place(test, 0, len(test)-1)
print("原地快排结果:", test)
4. 复杂度
- 平均:O(nlogn)
- 最坏:O(n²)(已有序且选最左当基准)
- 空间:递归栈空间 O(logn)
- 特点:实际工程最快,默认内置排序底层就是改良快排
二、归并排序
1. 核心原理
分治两步:
- 分:把数组不断对半拆分,直到拆成单个元素
- 治:两两有序合并,最后合成一个完整有序数组
2. 完整代码实现
def merge_sort(arr):
# 递归终止
if len(arr) <= 1:
return arr
# 中间拆分
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# 合并两个有序数组
return merge(left, right)
# 合并两个有序数组
def merge(left, right):
res = []
i = j = 0
# 逐个比较小的先放入
while i < len(left) and j < len(right):
if left[i] < right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
# 补上剩余元素
res.extend(left[i:])
res.extend(right[j:])
return res
# 测试
test = [6, 3, 8, 5, 2, 9, 1]
res = merge_sort(test)
print("归并排序结果:", res)
3. 复杂度
- 最好/最坏/平均:O(nlogn) 稳定
- 空间:O(n) 需要额外数组存合并结果
- 特点:稳定排序,适合外排序、大数据分片排序
三、高效排序对比总结
| 排序 | 时间复杂度 | 稳定性 | 原地排序 | 特点 |
|---|---|---|---|---|
| 快速排序 | O(nlogn) | 不稳定 | 是 | 实际最快,工程首选 |
| 归并排序 | O(nlogn) | 稳定 | 否 | 稳定适合大数据、外排 |
第八章:查找算法——顺序查找、二分查找
一、查找算法介绍
查找:从一组数据中找到目标元素的位置或判断是否存在。
常用两种:
- 顺序查找(线性查找)
- 二分查找(折半查找,重点、面试必考)
一、顺序查找
原理
从头到尾逐个遍历,逐一对比,找到就返回下标,遍历完没找到返回-1。
- 数据无需有序
- 算法简单,效率低
代码实现
def linear_search(arr, target):
for idx, val in enumerate(arr):
if val == target:
return idx
return -1
# 测试
if __name__ == "__main__":
nums = [5, 2, 9, 1, 7]
print(linear_search(nums, 9))
print(linear_search(nums, 3))
复杂度
- 时间:平均/最坏 O(n)
- 空间:O(1)
二、二分查找(普通版)
前提
数组必须有序(升序)
原理
- 设定左边界 left、右边界 right
- 取中间下标 mid
- 中间值等于目标:直接返回mid
- 中间值 < 目标:去右半边找
- 中间值 > 目标:去左半边找
- 不断折半,范围缩小一半
循环版实现(最常用)
def binary_search(arr, target):
left = 0
right = len(arr) - 1
while left <= right:
# 中间下标
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
# 目标在右边
left = mid + 1
else:
# 目标在左边
right = mid - 1
# 没找到
return -1
# 测试 必须有序数组
nums = [1, 2, 5, 7, 9, 12, 15]
print(binary_search(nums, 7))
print(binary_search(nums, 3))
递归版二分查找
def binary_search_recur(arr, left, right, target):
if left > right:
return -1
mid = (left + right) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recur(arr, mid + 1, right, target)
else:
return binary_search_recur(arr, left, mid - 1, target)
# 测试
nums = [1, 2, 5, 7, 9, 12, 15]
print(binary_search_recur(nums, 0, len(nums)-1, 9))
复杂度
- 时间:O(logn) 极快,数据量越大优势越明显
- 空间:循环版O(1),递归版O(logn)
三、二分查找扩展:找左边界、右边界
适用场景:有重复元素,找第一次出现、最后一次出现的位置
1. 二分找左边界
def binary_left_bound(arr, target):
left = 0
right = len(arr) - 1
res = -1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
res = mid
right = mid - 1 # 继续往左找
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return res
# 测试
nums = [1,2,2,2,3,4,5]
print(binary_left_bound(nums, 2))
2. 二分找右边界
def binary_right_bound(arr, target):
left = 0
right = len(arr) - 1
res = -1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
res = mid
left = mid + 1 # 继续往右找
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return res
nums = [1,2,2,2,3,4,5]
print(binary_right_bound(nums, 2))
四、两种查找对比
| 查找方式 | 是否需要有序 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 顺序查找 | 不需要 | O(n) | 小规模、无序数据 |
| 二分查找 | 必须有序 | O(logn) | 大规模、静态有序数据 |
第九章:二叉树
一、树基础概念
1. 什么是树
树是层次化非线性数据结构,区别于链表、栈、队列的线性结构。
术语:
- 根节点:最顶层,没有父节点
- 父节点、子节点、兄弟节点
- 叶子节点:没有子节点
- 高度:从当前节点到最远叶子的边数
- 深度:从根到当前节点的边数
2. 二叉树定义
每个节点最多只有2个子节点:左孩子、右孩子
满二叉树、完全二叉树、普通二叉树。
二、二叉树节点类
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None # 左子树
self.right = None # 右子树
三、手动构建一棵二叉树
# 构建树:
# 1
# / \
# 2 3
# / \
# 4 5
def build_tree():
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)
return root
四、二叉树三种遍历(递归版 最简单)
遍历规则:
- 前序:根 → 左 → 右
- 中序:左 → 根 → 右
- 后序:左 → 右 → 根
1. 前序遍历
def pre_order(node):
if not node:
return
print(node.val, end=" ")
pre_order(node.left)
pre_order(node.right)
2. 中序遍历
def in_order(node):
if not node:
return
in_order(node.left)
print(node.val, end=" ")
in_order(node.right)
3. 后序遍历
def post_order(node):
if not node:
return
post_order(node.left)
post_order(node.right)
print(node.val, end=" ")
测试运行
if __name__ == "__main__":
root = build_tree()
print("前序遍历:")
pre_order(root)
print("\n中序遍历:")
in_order(root)
print("\n后序遍历:")
post_order(root)
输出:
前序遍历:
1 2 4 5 3
中序遍历:
4 2 5 1 3
后序遍历:
4 5 2 3 1
五、层序遍历(BFS 广度优先)
按从上到下、从左到右一层一层遍历,用队列实现
from collections import deque
def level_order(root):
if not root:
return
q = deque()
q.append(root)
while q:
node = q.popleft()
print(node.val, end=" ")
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
六、迭代版遍历(不用递归,栈模拟)
前序迭代
def pre_order_iter(root):
if not root:
return
stack = [root]
while stack:
node = stack.pop()
print(node.val, end=" ")
# 栈后进先出,先压右再压左
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
中序迭代
def in_order_iter(root):
stack = []
cur = root
while cur or stack:
while cur:
stack.append(cur)
cur = cur.left
cur = stack.pop()
print(cur.val, end=" ")
cur = cur.right
七、二叉树常见基础小题
- 求二叉树节点个数
- 求二叉树叶子节点个数
- 求二叉树最大深度
- 翻转二叉树
示例:求树的节点总数
def count_node(node):
if not node:
return 0
return 1 + count_node(node.left) + count_node(node.right)
示例:求二叉树深度
def tree_depth(node):
if not node:
return 0
left_dep = tree_depth(node.left)
right_dep = tree_depth(node.right)
return max(left_dep, right_dep) + 1
第十章:二叉搜索树 BST
一、二叉搜索树 BST 定义
二叉搜索树是特殊的二叉树,满足严格规则:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 左右子树也都是二叉搜索树
核心特性:
- 中序遍历 = 升序有序数组
- 查找、插入、删除 平均 O(logn),最坏 O(n)
二、复用树节点
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
三、二叉搜索树类封装
基础框架
class BST:
def __init__(self):
self.root = None
四、BST 插入操作
思路
- 树空:新节点作为根
- 不为空:和当前节点比较
- 小于当前值 → 往左子树走
- 大于当前值 → 往右子树走
- 直到遇到空位置,挂载新节点
插入代码(递归版)
class BST:
def __init__(self):
self.root = None
# 插入入口
def insert(self, val):
self.root = self._insert(self.root, val)
# 递归内部实现
def _insert(self, node, val):
# 找到空位置,新建节点
if not node:
return TreeNode(val)
if val < node.val:
node.left = self._insert(node.left, val)
elif val > node.val:
node.right = self._insert(node.right, val)
# 相等值 直接忽略(BST一般不存重复值)
return node
五、BST 查找操作
思路
从根开始比较:
- 相等 → 找到
- 更小 → 左子树
- 更大 → 右子树
- 走到空 → 不存在
# 查找
def search(self, val):
return self._search(self.root, val)
def _search(self, node, val):
if not node:
return False
if val == node.val:
return True
elif val < node.val:
return self._search(node.left, val)
else:
return self._search(node.right, val)
六、中序遍历(验证升序)
BST 中序遍历一定是从小到大有序
# 中序遍历
def in_order(self):
res = []
self._in_order(self.root, res)
return res
def _in_order(self, node, res):
if not node:
return
self._in_order(node.left, res)
res.append(node.val)
self._in_order(node.right, res)
七、BST 删除操作
删除分三种情况:
- 叶子节点:直接删,置为 None
- 只有一个子节点:用子节点顶替自己
- 有左右两个子节点:
- 找右子树最小值 或者 左子树最大值 替换当前节点
- 再删掉那个替补节点
完整删除代码
# 删除入口
def remove(self, val):
self.root = self._remove(self.root, val)
def _remove(self, node, val):
if not node:
return None
# 先找到要删除的节点
if val < node.val:
node.left = self._remove(node.left, val)
elif val > node.val:
node.right = self._remove(node.right, val)
else:
# 情况1:叶子节点 或 只有一个孩子
if not node.left:
return node.right
if not node.right:
return node.left
# 情况2:左右都有孩子,找右子树最小节点
min_right = self._get_min(node.right)
node.val = min_right.val
# 删除右子树最小值节点
node.right = self._remove(node.right, min_right.val)
return node
# 获取以node为根的最小节点
def _get_min(self, node):
cur = node
while cur.left:
cur = cur.left
return cur
八、完整测试代码
if __name__ == "__main__":
bst = BST()
# 插入数据
nums = [5,3,7,2,4,6,8]
for n in nums:
bst.insert(n)
# 中序遍历 升序输出
print("BST中序遍历:", bst.in_order())
# 查找
print("查找4:", bst.search(4))
print("查找9:", bst.search(9))
# 删除
bst.remove(7)
print("删除7后:", bst.in_order())
输出:
BST中序遍历: [2, 3, 4, 5, 6, 7, 8]
查找4: True
查找9: False
删除7后: [2, 3, 4, 5, 6, 8]
九、BST 复杂度与特点
- 平均查找、插入、删除:O(logn)
- 最坏退化成单链链表:O(n)
- 中序遍历天然升序,适合排序、有序查找
- 底层 TreeSet、TreeMap 原理就是改良版 BST(红黑树)
第十一章:堆 & 优先队列、大顶堆/小顶堆、heapq、TopK经典题
一、堆是什么
1. 堆的本质
堆是完全二叉树,同时满足堆序性质:
- 小顶堆:父节点值 ≤ 左右孩子节点值;堆顶是最小值
- 大顶堆:父节点值 ≥ 左右孩子节点值;堆顶是最大值
2. 存储方式
堆不用链式节点,直接用数组/列表顺序存储
完全二叉树数组下标规律:
- 下标
i的左孩子:2*i + 1 - 下标
i的右孩子:2*i + 2 - 下标
i的父节点:(i - 1) // 2
二、Python 内置 heapq 模块
Python 只自带小顶堆,没有原生大顶堆。
1. heapq 常用API
import heapq
heapq.heappush(heap, item)推入堆heapq.heappop(heap)弹出堆顶最小元素heapq.heapify(list)列表原地建堆 O(n)heapq.nlargest(k, iterable)取前k大heapq.nsmallest(k, iterable)取前k小
2. 基础使用:小顶堆
import heapq
heap = []
# 入堆
heapq.heappush(heap, 5)
heapq.heappush(heap, 2)
heapq.heappush(heap, 8)
heapq.heappush(heap, 1)
# 依次弹出,从小到大
while heap:
print(heapq.heappop(heap), end=" ")
3. 列表原地建堆 heapify
import heapq
nums = [3,1,4,2,5]
heapq.heapify(nums) # 原地变成小顶堆
print(nums)
4. 模拟大顶堆
思路:元素取负数,用小顶堆模拟大顶堆
import heapq
max_heap = []
nums = [3,1,4,2,5]
for n in nums:
heapq.heappush(max_heap, -n)
# 弹出时再取反
while max_heap:
print(-heapq.heappop(max_heap), end=" ")
三、手写小顶堆
封装堆:初始化、上浮、下沉、入堆、出堆
class MinHeap:
def __init__(self):
self.heap = []
# 上浮:新元素往上换到合适位置
def _sift_up(self, idx):
while idx > 0:
parent_idx = (idx - 1) // 2
if self.heap[idx] >= self.heap[parent_idx]:
break
# 交换父子
self.heap[idx], self.heap[parent_idx] = self.heap[parent_idx], self.heap[idx]
idx = parent_idx
# 下沉:删除堆顶后,最后元素往下换
def _sift_down(self, idx):
n = len(self.heap)
while True:
left = 2 * idx + 1
right = 2 * idx + 2
smallest = idx
if left < n and self.heap[left] < self.heap[smallest]:
smallest = left
if right < n and self.heap[right] < self.heap[smallest]:
smallest = right
if smallest == idx:
break
self.heap[idx], self.heap[smallest] = self.heap[smallest], self.heap[idx]
idx = smallest
# 入堆
def push(self, val):
self.heap.append(val)
self._sift_up(len(self.heap)-1)
# 出堆
def pop(self):
if not self.heap:
return None
res = self.heap[0]
# 最后一个元素放到堆顶
self.heap[0] = self.heap[-1]
self.heap.pop()
self._sift_down(0)
return res
# 判空
def is_empty(self):
return len(self.heap) == 0
测试手写小顶堆
if __name__ == "__main__":
h = MinHeap()
for num in [5,3,8,1,2]:
h.push(num)
while not h.is_empty():
print(h.pop(), end=" ")
四、经典算法题:TopK 问题
题目
从一堆数据中,找出前 K 大的数
最优思路
维护一个大小为K的小顶堆:
- 前K个元素直接入堆
- 后面每个元素比堆顶大,就弹出堆顶、加入当前元素
- 最后堆里就是前K大
import heapq
def top_k(nums, k):
heap = []
for n in nums:
if len(heap) < k:
heapq.heappush(heap, n)
else:
if n > heap[0]:
heapq.heappop(heap)
heapq.heappush(heap, n)
# 排序返回结果
return sorted(heap, reverse=True)
# 测试
arr = [9,3,7,1,8,2,6,4,5]
print(top_k(arr, 3))
五、堆排序原理
- 把数组建成堆
- 不断弹出堆顶,放到数组末尾
时间复杂度:O(nlogn)
六、堆总结
- 小顶堆堆顶最小,大顶堆堆顶最大
- Python heapq 只有小顶堆,负数模拟大顶堆
- 核心操作:上浮、下沉
- 经典应用:TopK、堆排序、优先队列、任务调度
第十二章:并查集(Union-Find)原理 + 手写实现 + 经典例题
一、并查集是什么
并查集是一种处理不相交集合合并与查询的数据结构。
核心两个操作:
- 查 Find:查找某个元素的根节点,判断两个元素是否在同一集合
- 并 Union:把两个集合合并成一个
适用场景:
- 朋友圈关系、亲戚连通
- 岛屿数量、连通分量
- 最小生成树 Kruskal 算法
- 网络节点连通性
二、并查集核心思想
- 初始每个元素自己是一个集合,父节点指向自己
- 合并:把一个集合的根,挂到另一个集合根下面
- 查询:一路往上找根节点
- 优化:路径压缩 + 按秩合并,几乎接近 O(1)
三、基础版并查集(无优化)
class UnionFind:
def __init__(self, size):
# 父节点数组,初始自己指向自己
self.parent = list(range(size))
# 查找根节点
def find(self, x):
# 自己就是根
if self.parent[x] == x:
return x
# 递归往上找父节点
return self.find(self.parent[x])
# 合并两个集合
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
# 根不同就合并
if x_root != y_root:
self.parent[y_root] = x_root
# 判断是否连通
def is_connected(self, x, y):
return self.find(x) == self.find(y)
测试基础版
if __name__ == "__main__":
uf = UnionFind(5)
uf.union(0,1)
uf.union(1,2)
print(uf.is_connected(0,2)) # True
print(uf.is_connected(0,3)) # False
四、优化版并查集(路径压缩 + 按秩合并)
效率拉满,面试标准写法
- 路径压缩:查找时把沿途所有节点直接挂到根,下次查找超快
- 按秩合并:树矮的挂到树高的下面,避免树变深
class UnionFindPro:
def __init__(self, size):
self.parent = list(range(size))
# 秩:记录树的高度
self.rank = [1] * size
# 路径压缩查找
def find(self, x):
if self.parent[x] != x:
# 递归找根,顺便把父节点直接设为根
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
# 按秩合并
def union(self, x, y):
x_root = self.find(x)
y_root = self.find(y)
if x_root == y_root:
return
# 矮树挂到高树下面
if self.rank[x_root] < self.rank[y_root]:
self.parent[x_root] = y_root
else:
self.parent[y_root] = x_root
# 高度相等时,合并后根高度+1
if self.rank[x_root] == self.rank[y_root]:
self.rank[x_root] += 1
def is_connected(self, x, y):
return self.find(x) == self.find(y)
五、经典实战:岛屿数量(并查集解法)
题目
给定二维网格,1是陆地,0是水域,求连通的岛屿数量
上下左右相连的1算同一个岛屿。
解题思路
- 遍历每个格子,是陆地就初始化自己为一个集合
- 往右、往下看相邻陆地,合并集合
- 最后统计根节点总数就是岛屿数量
完整代码
def num_islands(grid):
if not grid or not grid[0]:
return 0
m = len(grid)
n = len(grid[0])
# 把二维坐标转一维下标:i * n + j
def get_idx(i,j):
return i * n + j
uf = UnionFindPro(m * n)
count = 0
# 方向:右、下
dirs = [(0,1), (1,0)]
for i in range(m):
for j in range(n):
if grid[i][j] == "1":
count += 1
# 遍历相邻
for dx, dy in dirs:
x = i + dx
y = j + dy
if 0 <= x < m and 0 <= y < n and grid[x][y] == "1":
# 不在同一集合就合并,数量减1
if not uf.is_connected(get_idx(i,j), get_idx(x,y)):
uf.union(get_idx(i,j), get_idx(x,y))
count -= 1
return count
# 测试
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
print(num_islands(grid)) # 输出3
六、并查集复杂度总结
- 优化后(路径压缩+按秩合并):近似 O(1)
- 适合处理大规模连通性问题
- 代码模板固定,刷题直接套
第十三章:图
一、图的基本概念
1. 图组成
- 顶点(Vertex):节点
- 边(Edge):顶点之间的连线
2. 图分类
- 无向图、有向图
- 无权图、带权图
- 连通图、非连通图
- 环、有向无环图DAG
3. 两种存储方式
- 邻接矩阵:二维数组,适合顶点少
- 邻接表:字典/列表嵌套,适合顶点多、稀疏图(开发最常用)
二、邻接表存储图
用字典存:key顶点,value相邻顶点列表
1. 无向图构建
# 邻接表表示无向图
graph = {
"A": ["B", "C"],
"B": ["A", "D", "E"],
"C": ["A", "F"],
"D": ["B"],
"E": ["B", "F"],
"F": ["C", "E"]
}
2. 封装图类(增顶点、增边)
class Graph:
def __init__(self):
# 邻接表
self.adj = {}
# 添加顶点
def add_vertex(self, v):
if v not in self.adj:
self.adj[v] = []
# 添加无向边
def add_edge(self, v1, v2):
self.add_vertex(v1)
self.add_vertex(v2)
# 互相加入邻接列表
self.adj[v1].append(v2)
self.adj[v2].append(v1)
# 打印邻接表
def show(self):
for k, v in self.adj.items():
print(f"{k}: {v}")
测试构建图
if __name__ == "__main__":
g = Graph()
g.add_edge("A","B")
g.add_edge("A","C")
g.add_edge("B","D")
g.add_edge("B","E")
g.add_edge("C","F")
g.add_edge("E","F")
g.show()
三、图深度优先遍历 DFS
思路
- 用集合记录已访问顶点
- 访问当前顶点,标记已访问
- 递归遍历所有未访问邻接点
递归版 DFS
class Graph:
# ... 接上上面代码 ...
def dfs(self, start):
visited = set()
self._dfs(start, visited)
def _dfs(self, v, visited):
# 访问当前节点
print(v, end=" ")
visited.add(v)
# 遍历邻接点
for neighbor in self.adj[v]:
if neighbor not in visited:
self._dfs(neighbor, visited)
迭代版 DFS(栈实现)
def dfs_iter(self, start):
visited = set()
stack = [start]
while stack:
v = stack.pop()
if v in visited:
continue
print(v, end=" ")
visited.add(v)
# 逆序入栈,保证遍历顺序和递归一致
for neighbor in reversed(self.adj[v]):
if neighbor not in visited:
stack.append(neighbor)
四、图广度优先遍历 BFS
思路
- 借助队列
- 先访问当前层所有节点,再访问下一层
- 最短路径、最少步数 首选BFS
BFS 完整实现
from collections import deque
class Graph:
# ... 省略前面代码 ...
def bfs(self, start):
visited = set()
q = deque()
q.append(start)
visited.add(start)
while q:
v = q.popleft()
print(v, end=" ")
# 邻接点入队
for neighbor in self.adj[v]:
if neighbor not in visited:
visited.add(neighbor)
q.append(neighbor)
五、完整测试 DFS / BFS
if __name__ == "__main__":
g = Graph()
g.add_edge("A","B")
g.add_edge("A","C")
g.add_edge("B","D")
g.add_edge("B","E")
g.add_edge("C","F")
g.add_edge("E","F")
print("深度优先DFS:")
g.dfs("A")
print("\n广度优先BFS:")
g.bfs("A")
六、有向图简单改造
只需要改加边函数,不用双向添加:
# 有向边 v1 -> v2
def add_direct_edge(self, v1, v2):
self.add_vertex(v1)
self.add_vertex(v2)
self.adj[v1].append(v2)
七、图算法应用场景
- DFS:连通分量、拓扑排序、岛屿问题、路径搜索
- BFS:最短路径、最少换乘、迷宫最短步数
- 进阶:Dijkstra最短路径、Prim/Kruskal最小生成树
第十四章:贪心算法
一、贪心算法核心原理
1. 什么是贪心
每一步只做当前局部最优选择,期望最终得到全局最优。
- 不往后看、不回头、不考虑未来,只管当下最优
- 不是所有问题都能用贪心,必须满足两个条件:
- 贪心选择性质:局部最优能导出全局最优
- 最优子结构:子问题最优 包含在全局最优里
2. 适用题型特征
- 区间调度、活动安排
- 零钱兑换(特定面额)
- 跳跃游戏
- 哈夫曼编码
- 分发饼干、贪心区间覆盖
二、例题1:分发饼干(入门经典)
题目
孩子有胃口 g,饼干尺寸 s;
每个孩子最多一块饼干,饼干尺寸 >= 胃口才能满足。
求最多能满足几个孩子。
思路
- 孩子胃口、饼干都从小到大排序
- 小饼干优先喂胃口小的孩子
- 双指针逐个匹配
代码
def find_content_children(g, s):
g.sort()
s.sort()
i = j = count = 0
while i < len(g) and j < len(s):
if s[j] >= g[i]:
# 能满足
count += 1
i += 1
j += 1
else:
# 饼干太小,换下一块
j += 1
return count
# 测试
print(find_content_children([1,2,3], [1,1])) # 1
print(find_content_children([1,2], [1,2,3])) # 2
三、例题2:活动选择 / 区间调度
题目
给若干活动开始时间、结束时间,同一时间只能参加一个活动,求最多能参加几个活动。
贪心策略
优先选结束时间最早的,给后面留更多时间。
代码
def activity_selection(activities):
# 按结束时间升序排序
activities.sort(key=lambda x: x[1])
res = [activities[0]]
last_end = activities[0][1]
for act in activities[1:]:
start, end = act
if start >= last_end:
res.append(act)
last_end = end
return res
# 测试:(开始, 结束)
acts = [(1,3), (2,4), (3,5), (4,6)]
print(activity_selection(acts))
四、例题3:跳跃游戏
题目
数组每个元素代表当前位置最多能跳几步,判断能不能跳到最后下标。
贪心思路
实时维护能跳到的最远位置,遍历不断更新。
def can_jump(nums):
max_reach = 0
n = len(nums)
for i in range(n):
if i > max_reach:
return False
max_reach = max(max_reach, i + nums[i])
if max_reach >= n - 1:
return True
return True
# 测试
print(can_jump([2,3,1,1,4])) # True
print(can_jump([3,2,1,0,4])) # False
五、例题4:零钱兑换(贪心可行版)
注意:只有纸币面额是规范整除体系时贪心才正确
题目
面额 [1,5,10,20,50,100],凑总金额,求最少纸币张数
贪心策略
从大面额开始优先选
def coin_change_greedy(amount):
coins = [100,50,20,10,5,1]
res = []
for c in coins:
while amount >= c:
res.append(c)
amount -= c
return res
print(coin_change_greedy(136))
六、贪心算法总结
- 核心:局部最优 → 推全局最优
- 套路:先排序 + 依次贪心选择
- 常见模型:
- 排序+双指针
- 区间按结束时间排序
- 维护最远可达边界
- 局限性:不满足贪心性质的问题,只能用动态规划
第十五章:动态规划DP入门
一、动态规划核心思想
1. 什么是DP
动态规划 Dynamic Programming,把大问题拆解成子问题,
保存子问题的结果,避免重复计算,以空间换时间。
2. DP三要素
- 状态定义:dp[i] 代表什么含义
- 状态转移方程:大问题怎么由子问题推导出来
- 初始边界条件:最开始’s的基础值
3. 解题套路
- 拆分子问题
- 画dp数组、找规律
- 写状态转移
- 初始化边界
- 循环计算
二、例题1:斐波那契数列(DP入门必学)
题目
1, 1, 2, 3, 5, 8 …
第n项 = 前两项之和
状态定义
dp[i]:第 i 个斐波那契数
转移方程
dp[i] = dp[i-1] + dp[i-2]
代码
def fib(n):
if n <= 2:
return 1
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 1
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print(fib(10))
空间优化(只用两个变量)
def fib_optimize(n):
if n <= 2:
return 1
a, b = 1, 1
for _ in range(3, n+1):
c = a + b
a = b
b = c
return b
print(fib_optimize(10))
三、例题2:爬楼梯
题目
一次可以爬 1 阶 或 2 阶,爬到 n 阶有多少种方式?
分析
- 到第n阶:只能从 n-1 走1步 或 n-2 走2步
- 本质就是斐波那契变形
状态定义
dp[i]:爬到第 i 阶的方法数
转移方程
dp[i] = dp[i-1] + dp[i-2]
代码
def climb_stairs(n):
if n <= 2:
return n
dp = [0]*(n+1)
dp[1] = 1
dp[2] = 2
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
print(climb_stairs(10))
四、例题3:最小路径和(二维DP)
题目
给定 m x n 网格,只能向右、向下走,从左上角到右下角,路径数字和最小是多少。
状态定义
dp[i][j]:走到第i行第j列的最小路径和
转移方程
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
代码
def min_path_sum(grid):
m = len(grid)
n = len(grid[0])
# 复制网格当dp数组
dp = [[0]*n for _ in range(m)]
dp[0][0] = grid[0][0]
# 初始化第一行 只能从左边来
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 初始化第一列 只能从上面来
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
# 遍历填充其他位置
for i in range(1, m):
for j in range(1, n):
dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])
return dp[m-1][n-1]
# 测试
grid = [
[1,3,1],
[1,5,1],
[4,2,1]
]
print(min_path_sum(grid)) # 7
五、例题4:打家劫舍(一维DP经典)
题目
数组每个元素是房间金额,不能偷相邻房间,求能偷的最大金额。
状态定义
dp[i]:前 i 个房间能偷的最大金额
转移方程
- 偷第i间:
dp[i-2] + nums[i] - 不偷第i间:
dp[i-1]
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
代码
def rob(nums):
if not nums:
return 0
n = len(nums)
if n == 1:
return nums[0]
dp = [0]*n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[-1]
print(rob([1,2,3,1]))
print(rob([2,7,9,3,1]))
六、动态规划入门总结
- DP核心:拆分子问题 + 记录子问题结果
- 三步走:状态定义 → 转移方程 → 初始化边界
- 常见题型:
- 一维DP:斐波那契、爬楼梯、打家劫舍
- 二维DP:最小路径和、背包、子序列问题
- 大部分DP都可以空间优化,从数组压缩成变量
第十六章:双指针算法
一、双指针算法总览
1. 核心作用
把暴力两层循环 O(n²) 优化成 O(n),
是数组、字符串、链表刷题最常用、必掌握技巧。
2. 三大分类
- 左右指针(相向指针):一头一尾往中间收缩
- 快慢指针(同向指针):一慢一快同方向走
- 滑动窗口(双指针进阶):左右指针框住区间,动态伸缩
二、类型一:左右指针(相向型)
适用场景:
有序数组两数之和、反转数组、回文串判断、二分类题目
1. 反转数组
def reverse_array(nums):
left = 0
right = len(nums) - 1
while left < right:
# 交换
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
return nums
# 测试
print(reverse_array([1,2,3,4,5]))
2. 判断回文字符串
def is_palindrome(s):
left = 0
right = len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
print(is_palindrome("abba"))
print(is_palindrome("abcde"))
3. 有序数组两数之和
def two_sum_sorted(nums, target):
left = 0
right = len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == target:
return [left + 1, right + 1]
elif total < target:
left += 1
else:
right -= 1
return []
# 测试
print(two_sum_sorted([2,7,11,15], 9))
三、类型二:快慢指针(同向型)
适用场景:
有序数组去重、链表找中点、链表判环、删除指定元素
1. 有序数组原地去重
def remove_duplicates(nums):
if not nums:
return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast]
# 不重复元素个数
return slow + 1
# 测试
arr = [1,1,2,2,3,4,4,4]
print(remove_duplicates(arr))
2. 链表快慢指针:找中间节点
class Node:
def __init__(self, val):
self.val = val
self.next = None
# 链表找中间节点
def find_middle_node(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow.val
3. 移除数组指定元素
def remove_element(nums, val):
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
arr = [3,2,2,3]
print(remove_element(arr, 3))
四、类型三:滑动窗口(双指针进阶)
核心思想:
用 left 和 right 框出一个窗口
- right 右扩:纳入新元素
- left 右缩:满足条件就缩小窗口
适用:子数组、子串、最长无重复子串、最小长度子数组
1. 长度最小的子数组
def min_subarray_len(nums, target):
left = 0
total = 0
res = float("inf")
for right in range(len(nums)):
total += nums[right]
# 满足条件,收缩左边界
while total >= target:
res = min(res, right - left + 1)
total -= nums[left]
left += 1
return res if res != float("inf") else 0
# 测试
print(min_subarray_len([2,3,1,2,4,3], 7))
2. 最长无重复字符子串
def length_of_longest_substring(s):
window = set()
left = 0
max_len = 0
for right in range(len(s)):
# 有重复就一直左移
while s[right] in window:
window.remove(s[left])
left += 1
window.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len
# 测试
print(length_of_longest_substring("abcabcbb"))
五、第十六章 双指针总结
- 左右指针:有序、两头往中间靠
- 快慢指针:同向走,慢指针留有效元素,快指针遍历
- 滑动窗口:区间动态伸缩,解决子数组、子串最值问题
- 所有双指针题型,时间复杂度都是 O(n),是刷题必备神器
&spm=1001.2101.3001.5002&articleId=161012355&d=1&t=3&u=0291afeab30545f3b2b29bd2585c27d4)
1655

被折叠的 条评论
为什么被折叠?



