为什么学习数据结构与算法?
在开始之前,先理解为什么它如此重要:

- 写出高效的代码:选择合适的数据结构和算法,可以让你的程序运行得更快、占用更少的内存,在处理大规模数据时,这往往是成败的关键。
- 通过技术面试:几乎所有顶尖科技公司的面试都会考察这部分内容,它不仅考察知识,更考察你的逻辑思维和解决问题的能力。
- 提升编程内功:学习 DSA 能让你深刻理解代码背后的工作原理,写出更优雅、更健壮、更易于维护的代码。
第一部分:数据结构
数据结构是计算机中存储、组织数据的方式,不同的数据结构有不同的优缺点,适用于不同的场景。
基础数据结构
1 数组
- 描述:在内存中连续存储的一组元素,每个元素可以通过索引(下标)来访问。
- Python 实现:Python 的
list就是动态数组的实现。 - 优点:
访问速度快:通过索引访问元素的时间复杂度是 O(1)。
- 缺点:
- 插入和删除慢:在数组中间或开头插入/删除元素,需要移动大量元素,时间复杂度为 O(n)。
- 大小固定(静态数组)或需要扩容(动态数组)。
- Python 示例:
arr = [10, 20, 30, 40, 50] print(arr[0]) # 访问第一个元素,输出 10 arr.append(60) # 在末尾添加元素,时间复杂度 O(1) (均摊) arr.insert(1, 15) # 在索引1处插入元素,时间复杂度 O(n)
2 链表
-
描述:由一系列节点组成,每个节点包含数据和指向下一个节点的指针,内存中的存储不连续。
-
Python 实现:Python 没有内置的链表,但我们可以轻松实现。
(图片来源网络,侵删) -
优点:
- 插入和删除快:只需修改指针即可,时间复杂度为 O(1)(如果已知节点位置)。
- 大小灵活,无需预先分配。
-
缺点:
- 访问慢:不能通过索引直接访问,必须从头节点开始遍历,时间复杂度为 O(n)。
- 额外空间开销:每个节点都需要存储指针。
-
Python 示例:
class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next # 创建链表 1 -> 2 -> 3 head = ListNode(1) head.next = ListNode(2) head.next.next = ListNode(3) # 遍历链表 current = head while current: print(current.val, end=" -> ") current = current.next # 输出: 1 -> 2 -> 3 ->
3 栈
-
描述:一种“后进先出”(LIFO, Last-In-First-Out)的数据结构,就像一摞盘子。
-
Python 实现:可以使用
list的append()和pop()方法来模拟栈。 -
核心操作:
push: 入栈,添加元素到栈顶。pop: 出栈,移除并返回栈顶元素。
-
应用场景:函数调用、表达式求值、括号匹配、浏览器历史记录等。
-
Python 示例:
stack = [] stack.append('A') # push A stack.append('B') # push B stack.append('C') # push C print(stack) # 输出: ['A', 'B', 'C'] top_element = stack.pop() # pop C print(top_element) # 输出: C print(stack) # 输出: ['A', 'B']
4 队列
-
描述:一种“先进先出”(FIFO, First-In-First-Out)的数据结构,就像排队买票。
-
Python 实现:
- 不推荐:使用
list的append()和pop(0)。pop(0)的时间复杂度是 O(n),因为需要移动所有元素。 - 推荐:使用
collections.deque,它为两端的操作提供了 O(1) 的时间复杂度。
- 不推荐:使用
-
核心操作:
enqueue: 入队,添加元素到队尾。dequeue: 出队,移除并返回队首元素。
-
应用场景:任务调度、广度优先搜索、消息缓冲等。
-
Python 示例:
from collections import deque queue = deque() queue.append('A') # enqueue A queue.append('B') # enqueue B queue.append('C') # enqueue C print(queue) # 输出: deque(['A', 'B', 'C']) front_element = queue.popleft() # dequeue A print(front_element) # 输出: A print(queue) # 输出: deque(['B', 'C'])
高级数据结构
1 哈希表 / 字典
-
描述:通过“键-值”对来存储数据,它使用哈希函数将键映射到一个存储桶(索引),从而实现快速查找。
-
Python 实现:Python 的
dict就是哈希表的实现。 -
优点:
查找、插入、删除的平均时间复杂度都是 O(1),非常高效。
-
缺点:
- 无序性(在 Python 3.7+ 中,字典按插入顺序保留,但底层仍是无序的哈希结构)。
- 哈希冲突可能影响性能。
-
应用场景:缓存、数据库索引、计数器、实现集合等。
-
Python 示例:
student_grades = {"Alice": 95, "Bob": 88, "Charlie": 76} # 查找 print(student_grades["Alice"]) # 输出: 95 # 插入 student_grades["David"] = 82 print(student_grades) # 输出: {'Alice': 95, 'Bob': 88, 'Charlie': 76, 'David': 82} # 删除 del student_grades["Bob"] print(student_grades) # 输出: {'Alice': 95, 'Charlie': 76, 'David': 82}
2 树
-
描述:一种分层数据结构,由节点和边组成,最顶层的节点称为根节点。
-
常见类型:
- 二叉树:每个节点最多有两个子节点(左子节点和右子节点)。
- 二叉搜索树:一种特殊的二叉树,对于任意节点,其左子树的所有节点值都小于该节点值,其右子树的所有节点值都大于该节点值,这使得查找非常高效。
-
核心操作:前序、中序、后序、层序遍历。
-
应用场景:文件系统、数据库索引、HTML DOM 结构等。
-
Python 示例 (二叉搜索树):
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right # 插入操作示例 def insert(root, val): if not root: return TreeNode(val) if val < root.val: root.left = insert(root.left, val) else: root.right = insert(root.right, val) return root # 中序遍历 (会得到有序序列) def inorder_traversal(root): if not root: return [] return inorder_traversal(root.left) + [root.val] + inorder_traversal(root.right) bst = None for num in [8, 3, 10, 1, 6, 14, 4, 7, 13]: bst = insert(bst, num) print(inorder_traversal(bst)) # 输出: [1, 3, 4, 6, 7, 8, 10, 13, 14]
3 堆
-
描述:一种特殊的完全二叉树,分为最大堆(父节点值大于子节点)和最小堆(父节点值小于子节点)。
-
Python 实现:使用
heapq模块,它实现的是最小堆。 -
核心操作:
heapq.heappush: 向堆中插入元素。heapq.heappop: 从堆中弹出最小元素。
-
应用场景:优先队列、寻找第 K 大/小的元素、堆排序。
-
Python 示例:
import heapq # 将列表转换为堆 (最小堆) numbers = [4, 1, 7, 3, 8, 5] heapq.heapify(numbers) print(numbers) # 输出: [1, 3, 5, 4, 8, 7] # 弹出最小元素 smallest = heapq.heappop(numbers) print(smallest) # 输出: 1 print(numbers) # 输出: [3, 4, 5, 7, 8] # 插入元素 heapq.heappush(numbers, 2) print(numbers) # 输出: [2, 3, 5, 7, 8, 4]
4 图
-
描述:由顶点和边组成的数据结构,用于表示多对多的关系。
-
Python 实现:通常使用邻接表(字典的字典)或邻接矩阵(二维数组)来表示。
-
核心算法:
- 深度优先搜索:沿着一条路径走到头,再回溯。
- 广度优先搜索:逐层访问所有顶点。
-
应用场景:社交网络、地图导航、推荐系统、网络拓扑。
-
Python 示例 (邻接表表示 + BFS/DFS):
# 用字典表示图的邻接表 graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E'] } # BFS 实现 def bfs(start_node): visited = set() queue = [start_node] visited.add(start_node) traversal_order = [] while queue: node = queue.pop(0) traversal_order.append(node) for neighbor in graph[node]: if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) return traversal_order print("BFS:", bfs('A')) # 输出: BFS: ['A', 'B', 'C', 'D', 'E', 'F'] # DFS 实现 (递归) def dfs_recursive(node, visited, traversal_order): visited.add(node) traversal_order.append(node) for neighbor in graph[node]: if neighbor not in visited: dfs_recursive(neighbor, visited, traversal_order) visited_set = set() dfs_order = [] dfs_recursive('A', visited_set, dfs_order) print("DFS (Recursive):", dfs_order) # 输出: DFS (Recursive): ['A', 'B', 'D', 'E', 'F', 'C']
第二部分:算法
算法是解决特定问题的一系列明确指令。
算法复杂度分析
- 时间复杂度:衡量算法执行时间随输入规模增长的趋势,使用大 O 表示法(如 O(1), O(log n), O(n), O(n log n), O(n²))。
- 空间复杂度:衡量算法所需额外内存空间随输入规模增长的趋势。
排序算法
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | Python 实现 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | sorted() (Timsort) 内部使用 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | - |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | - |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 | sorted() (Timsort) 内部使用 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 | sorted() (Timsort) 内部使用 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 | heapq 模块 |
注意:Python 内置的 list.sort() 和 sorted() 函数使用的是 Timsort 算法,这是一种结合了归并排序和插入排序的高效混合排序算法,时间复杂度为 O(n log n)。
搜索算法
| 算法 | 时间复杂度 | 描述 |
|---|---|---|
| 线性搜索 | O(n) | 遍历列表,逐个比较,适用于任何列表。 |
| 二分搜索 | O(log n) | 前提:列表必须是有序的,每次将搜索范围减半。 |
Python 示例 (二分搜索):
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1 # 未找到
sorted_list = [1, 3, 5, 7, 9, 11, 13, 15]
print(binary_search(sorted_list, 7)) # 输出: 3
print(binary_search(sorted_list, 8)) # 输出: -1
递归与分治
- 递归:一个函数直接或间接地调用自身,必须有基本情况(停止条件)和递归情况。
- 分治:将一个大问题分解成若干个相同或相似的子问题,递归解决子问题,最后将子问题的解合并成大问题的解。
- 三步曲:
- 分解:将问题分解成子问题。
- 解决:递归解决子问题,如果子问题足够小,直接解决。
- 合并:将子问题的解合并。
- 三步曲:
- 经典例子:归并排序、快速排序、二分搜索、斐波那契数列。
动态规划
- 核心思想:通过存储已经计算过的子问题的解(避免重复计算),从而解决复杂问题。
- 适用条件:
- 最优子结构:问题的最优解包含其子问题的最优解。
- 重叠子问题:子问题会被反复计算多次。
- 实现方式:
- 自顶向下(记忆化搜索):从大问题开始,递归求解,并用一个数组/哈希表记录结果。
- 自底向上:从最小的子问题开始,迭代求解,并逐步构建大问题的解。
- 经典例子:斐波那契数列、背包问题、最长公共子序列。
Python 示例 (斐波那契数列 - 记忆化搜索):
# 使用装饰器实现记忆化
from functools import lru_cache
@lru_cache(maxsize=None) # 缓存所有结果
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(10)) # 输出: 55
学习路径与资源推荐
学习路径建议
- 打好基础:彻底理解数组、链表、栈、队列这四种基础结构,并能用 Python 手动实现。
- 掌握核心思想:深入学习哈希表、树(特别是二叉树)、堆和图,理解它们的特性和应用场景。
- 算法入门:从排序和搜索开始,理解时间复杂度,然后学习递归和分治思想。
- 攻克难点:学习动态规划和贪心算法,这是算法学习的难点,需要多做题来理解。
- 刷题实践:在 LeetCode、HackerRank 等平台上刷题,从“简单”题开始,逐步挑战“中等”和“困难”题。
推荐资源
- 书籍:
- 《算法图解》:图文并茂,非常适合初学者。
- 《算法(第4版)》:经典教材,使用 Java,但思想是通用的。
- 《数据结构与算法分析:Python语言描述》:非常契合本主题。
- 在线课程:
- Coursera - "Algorithms, Part I & II" (普林斯顿大学):Robert Sedgewick 和 Kevin Wayne 的经典课程,虽然用 Java,但内容质量极高。
- 极客时间 -《数据结构与算法之美》:国内优秀课程,用通俗易懂的语言讲解。
- 刷题网站:
- LeetCode:最主流的刷题平台,题目丰富,有社区讨论。
- HackerRank:提供不同难度和领域的题目。
- 牛客网:国内流行的面试刷题平台。
学习数据结构与算法是一个循序渐进、不断实践的过程,不要害怕困难,从理解概念开始,多动手写代码,多思考不同数据结构和算法的优劣,当你能用 Python 优雅地实现它们,并自如地运用到解决实际问题中时,你的编程内功将会有质的飞跃,祝你学习顺利!
