一、学习数据结构与算法的框架思维
1、核心结论:
种种数据结构,皆为数组 (顺序存储)和链表 (链式存储)的变换。(索引访问与指针)
数据结构的关键点在于遍历和访问,具体一点就是:增删查改等基本操作。
种种算法,皆为穷举 。穷举的关键点在于无遗漏和无冗余
各种数据结构的遍历 + 访问仅两种形式:迭代(for / while)与 递归(函数自调:自己调用自己)
2、迭代与递归
2.1 迭代
python
迭代(iterative)------ 自己一步一步数台阶
脚下踩一根计数器 i=0→1→2...
每数一步,状态全在你自己口袋里(循环变量、栈、指针)
CPU 只关心"下一步去哪",不回头找导游
线性结构,适合 循环迭代,以数组为例:
python
def traverse(arr: List[int]):
for i in range(len(arr)):
# 迭代访问 arr[i]
2.1.1 数组求和
python
def sum_iter(a):
total = 0
for x in a: # 迭代遍历
total += x
return total
print(sum_iter([7, 3, 5])) # 15
2.1.2 链表遍历
python
# 基本的单链表节点
class ListNode:
def __init__(self, val):
self.val = val
self.next = None
#遍历方式1:迭代访问
def traverse(head: ListNode) -> None:
p = head
while p is not None:
# 迭代访问 p.val
p = p.next
2.1.3 图遍历
python
from typing import List, Dict, Set, Deque
from collections import deque
# ---------- 图定义 ----------
Graph = Dict[int, List[int]] # 邻接表:{节点: [邻居, ...]}
# ---------- 广度优先 BFS(迭代) ----------
def bfs(graph: Graph, root: int) -> None:
if root not in graph:
return
visited: Set[int] = set()
q: Deque[int] = deque([root])
visited.add(root)
while q: # 标准队列迭代
node = q.popleft()
print(node) # 访问节点
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
q.append(neighbor)
2.2 递归(套娃-自己调自己)
python
概念理解:
递归(recursive)------ 导游们接力,【每层】的【导游-函数】负责一层处理
你问导游 A:"到山顶多少阶?"
A 只记 "【我这一层的阶数】 + 【下一层导游 B 的答案】", 然后把问题原样扔给 导游B。
每层导游拍照存档自己的阶数,直到最后一阶不再接力。回程时,照片从后往前逐层收集,结果逐层返回。
每层只做"自己这一层 + 剩余结果",而剩余结果交给下一层复制粘贴的这同一段代码去完成,这就是递归。
2.2.1 数组求和-递归方式
python
def sum_rec(a, i=0): # 函数签名:a是数组,i是元素索引
if i == len(a): # 基准case:停止条件。到最后一阶直接返回,避免越界
return 0 # 避免越界
return a[i] + sum_rec(a, i + 1)
# 把当前元素 a[i] "拿在手里",每层的元素累计操作
#如果是return sum_rec(a, i + 1),则表示跳过当前元素,只算"剩余的部分"
运行结果:
sum_rec([7,3,5], 0)
= 7 + sum_rec([7,3,5], 1)
= 7 + (3 + sum_rec([7,3,5], 2))
= 7 + 3 + (5 + sum_rec([7,3,5], 3))
= 7 + 3 + 5 + 0 ← i==3 越界,返回 0
= 15
一些递归的概念
python
1、函数栈帧:
就是函数被调用时,在内存里临时建立的一小块"工作台",(如例子上的 每层的照片)
里面放着:
1)当前函数的局部变量
2)返回地址(调用完后回到哪条指令)
3)参数值
4)一些控制信息(保存的寄存器、上一帧指针等)
调用函数 → 压入一帧;返回 → 弹出这一帧。
所有帧按"后调用先弹出"的顺序串在系统调用栈上,这就是递归深度过深会爆 StackOverflow 的原因。
上面例子中:
函数栈帧里只记录"当前这一层"的局部变量名 i 和指向列表 a 的引用(指针),不会把整个列表或元素复制进帧。所以 a[i] 只是通过帧里的引用去堆上读取数据,本身不是栈帧的一部分。但其实就是可以代指栈帧
2.2.1 链表遍历
既可以迭代 又可以递归的,以链表为例:
python
# 基本的单链表节点
class ListNode:
def __init__(self, val):
self.val = val
self.next = None
#遍历方式2:递归访问
def traverse(head: ListNode) -> None:
if head is None: # 终止条件
return
print(head.val) # 访问当前节点
traverse(head.next) # 处理后续链表
2.2.3 二叉树遍历
非线性结构,适合递归,以二叉树为例:
与上面的链表相似
python
# 基本的二叉树节点
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def traverse(root: TreeNode) -> None:
if root is None: # 终止条件
return
print(root.val) # 访问当前节点
traverse(root.left) # 递归左子树
traverse(root.right) # 递归右子树
2.2.4 N叉数的遍历
与二叉树相似
python
# 基本的 N 叉树节点
from typing import List
class TreeNode:
val: int
children: List['TreeNode']
def traverse(root: TreeNode) -> None:
if root is None: # 基准:空树
return
print(root.val) # 先访问当前节点(前序)
for child in root.children: # 再依次递归每棵子树
traverse(child)
2.2.5 图的遍历
与N叉数相似,对于环,要单独使用
python
from typing import List, Dict, Set, Deque
from collections import deque
# ---------- 图定义 ----------
Graph = Dict[int, List[int]] # 邻接表:{节点: [邻居, ...]}
# ---------- 深度优先 DFS(递归) ----------
def dfs(graph: Graph, root: int, visited: Set[int]) -> None:
if root not in graph: # 停止条件:空图或孤立点
return
print(root) # 访问当前节点(前序)
visited.add(root)
for neighbor in graph[root]: # 依次递归每条边
if neighbor not in visited:
dfs(graph, neighbor, visited)
带环的图,需要使用 布尔数组 visited
做标记
python
from typing import Dict, List, Set, Deque
from collections import deque
Graph = Dict[int, List[int]]
# ---------- DFS(递归) ----------
def dfs(graph: Graph, root: int, visited: Set[int]) -> None:
if root not in graph:
return
print(root)
visited.add(root)
for nxt in graph[root]:
if nxt not in visited: # 环被这一步剪掉
dfs(graph, nxt, visited)
3、数据结构根本
3.1 数组
连续存储,可通过索引随机访问元素。内存空间需要一次性分配好。如果需要扩容,时间复杂度是O(N),如果是在数组中间插入/删除元素,时间复杂度也是O(N)。因为都需要移动其他元素保证连续存储。
3.2 链表
非连续存储,所以不能随机访问元素。需要指针指向下一元素。删除/插入元素,只需要操作某一元素的前后指针,所以时间复杂度是O(1)。因为需要存储前后指针,需要额外的存储空间。
4、数据结构基本类型
4.1、数组
4.2、链表
4.3、哈希表
通过散列函数把键映射到一个大数组里。
拉链法需要链表特性。
线性探查法需要数组连续访问特性。
4.4、队列
可以使用链表也可以使用数组实现。
用数组实现,就要处理扩容缩容的问题;
用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针
1
4.5、栈
可以使用链表也可以使用数组实现。
用数组实现,就要处理扩容缩容的问题;
用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针
可以使用链表也可以使用数组实现
用数组实现,就要处理扩容缩容的问题;
用链表实现,没有扩/缩容问题,但需要更多的内存空间存储节点指针
4.6、树
用数组实现的是:完全二叉树、二叉堆
用链表实现的是:二叉搜索树、红黑树、B数、AVL树
4.7、图
图的两种存储方式,邻接表就是链表,邻接矩阵就是二维数组。
5、算法的本质
计算机算法,最笨但最通用的是:穷举。然后可以变形成聪明的穷举
python
大部分开发岗位工作中都是基于现成的开发框架做事,不怎么会碰到底层数据结构和算法相关的问题,但另一个事实是,只要你想找技术相关的岗位,数据结构和算法的考察是绕不开的,因为这块知识点是公认的程序员基本功。
为了区分,不妨称算法工程师研究的算法为「数学算法」,称刷题面试的算法为「计算机算法」,我们的目标主要聚焦的是「计算机算法」。
找一份开发岗位的工作,所以你真的不需要有多少数学基础,只要学会用计算机思维解决问题就够了
5.1 排列组合问题
排列组合问题抽象成一棵树,要精确地使用代码遍历这棵树的所有节点,不能漏不能多,才能写出正确的代码
5.2 有序数组中,寻找一个元素
在有序数组中寻找一个元素,用一个 for 循环暴力穷举谁都会,但 二分搜索算法 就是更聪明的穷举方式,拥有更好的时间复杂度
5.3 动态规划
动态规划是无冗余地穷举所有解,然后找一个最值
5.4 贪心算法
贪心算法就是在题目中发现一些规律(专业点叫贪心选择性质),使得你不用完整穷举所有解就可以得出答案
5.5 计算连通分量
想判断图中的两个节点是否连通,用 DFS/BFS 暴力搜索(穷举)肯定可以做到,但 Union Find 算法硬是用数组模拟树结构,把连通性相关的操作复杂度给干到 O(1)
6、常见的算法技巧
链表和数组
1、单链表常考的技巧就是双指针
判断单链表是否成环,暴力解是用一个 HashSet
之类的数据结构来缓存走过的节点,遇到重复的就说明有环。
但用快慢指针可以避免使用额外的空间,这就是聪明地穷举
2、数组常用的技巧也是双指针
3、二分搜索技巧
可以归为两端向中心的双指针。
如果在数组中搜索元素,一个 for 循环花 O(N)时间穷举肯定能搞定,但是二分搜索告诉你,如果数组是有序的,它只要 O(logN)的复杂度,这就是一种更聪明的搜索方式。
4、滑动窗口算法
典型的快慢双指针。用嵌套 for 循环花 O(N^2) 的时间肯定可以穷举出所有子数组。但是滑动窗口算法表示,在某些场景下,它可以用一快一慢两个指针,只需 O(N) 的时间就可以找到答案,这就是更聪明地穷举方式。
5、前缀和 -技巧
频繁地让你计算子数组的和,每次用 for 循环去遍历肯定没问题,但前缀和技巧预计算一个 preSum
数组,就可以避免循环。
6、差分数组技巧
频繁地让你对子数组进行增减操作,也可以每次用 for 循环去操作,但差分数组技巧维护一个 diff
数组,也可以避免循环。
二叉树系列
二叉树模型几乎是所有高级算法的基础。叉树题目的递归解法可以分两类思路:
第一类是遍历一遍二叉树得出答案:回溯算法,
第二类是通过分解问题计算出答案:动态规划算法
1、遍历二叉树最大深度
这个逻辑就是用 traverse
函数遍历了一遍二叉树的所有节点,维护 depth
变量,在叶子节点的时候更新最大深度。
python
class Solution:
def __init__(self):
# 记录最大深度
self.res = 0
# 记录当前遍历节点的深度
self.depth = 0
def maxDepth(self, root: TreeNode) -> int:
self.traverse(root)
return self.res
def traverse(self, root: TreeNode) -> None:
if not root:
# 到达叶子节点
self.res = max(self.res, self.depth)
return
# 前序遍历位置
self.depth += 1
self.traverse(root.left)
self.traverse(root.right)
# 后序遍历位置
self.depth -= 1
2、全排列问题:
本质就是多叉树的遍历,所以说回溯算法本质就是遍历多叉树,只要能把问题抽象成树结构,就一定能用回溯算法解决。
python
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
# 记录所有全排列
res = []
# 记录当前正在穷举的排列
track = []
# track 中的元素会被标记为 true,避免重复使用
used = [False] * len(nums)
# 主函数,输入一组不重复的数字,返回它们的全排列
def backtrack(nums):
# 到达叶子节点,track 中的元素就是一个全排列
if len(track) == len(nums):
res.append(track[:])
return
for i in range(len(nums)):
# 排除不合法的选择
if used[i]:
# nums[i] 已经在 track 中,跳过
continue
# 做选择
track.append(nums[i])
used[i] = True
# 进入递归树的下一层
backtrack(nums)
# 取消选择
track.pop()
used[i] = False
backtrack(nums)
return res
3、二叉树最大深度 --分解问题
python
# 定义:输入根节点,返回这棵二叉树的最大深度
def maxDepth(root: TreeNode) -> int:
if root is None:
return 0
# 递归计算左右子树的最大深度
leftMax = maxDepth(root.left)
rightMax = maxDepth(root.right)
# 整棵树的最大深度就是左右子树的最大深度加一
res = max(leftMax, rightMax) + 1
return res