第一章:C++常用算法概述
C++标准模板库(STL)提供了丰富的算法组件,广泛应用于数据处理、排序、查找和集合操作等场景。这些算法定义在
<algorithm>头文件中,结合迭代器使用,能够高效地操作容器中的元素。
常见分类
- 非修改性算法:遍历但不改变元素值,如
std::find、std::count - 修改性算法:对元素进行变换或复制,如
std::transform、std::copy - 排序相关算法:包括
std::sort、std::partial_sort等 - 二分查找与有序操作:适用于已排序容器,如
std::binary_search、std::merge
典型示例:排序与查找
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> data = {5, 2, 8, 1, 9};
// 排序
std::sort(data.begin(), data.end()); // 升序排列
// 二分查找前需确保有序
bool found = std::binary_search(data.begin(), data.end(), 8);
std::cout << "Element 8 found: " << found << std::endl;
return 0;
}
上述代码先调用
std::sort对容器排序,随后使用
std::binary_search判断元素是否存在,体现了算法组合使用的典型模式。
性能对比参考
| 算法 | 时间复杂度 | 适用场景 |
|---|
| std::sort | O(n log n) | 通用排序 |
| std::find | O(n) | 无序序列查找 |
| std::binary_search | O(log n) | 有序序列快速判断存在性 |
第二章:排序与查找算法实现
2.1 快速排序的原理与递归实现
快速排序是一种基于分治策略的高效排序算法,其核心思想是通过一趟排序将待排序数组分割成独立的两部分,其中一部分的所有元素均小于另一部分,然后递归地对这两部分继续排序。
算法基本步骤
- 选择一个基准元素(pivot),通常取首元素或随机选取;
- 将数组重新排列,使得比基准小的元素位于左侧,大的位于右侧;
- 递归地对左右两个子数组进行快速排序。
递归实现代码
func quickSort(arr []int, low, high int) {
if low < high {
pi := partition(arr, low, high) // 获取分区索引
quickSort(arr, low, pi-1) // 排序左子数组
quickSort(arr, pi+1, high) // 排序右子数组
}
}
上述代码中,
low 和
high 表示当前处理区间的边界,
partition 函数负责完成基准元素的定位。递归调用在子区间上持续进行,直至区间长度为1或为空。
2.2 归并排序与非比较排序的应用对比
归并排序作为典型的基于比较的排序算法,时间复杂度稳定在 O(n log n),适用于数据规模较大且对稳定性有要求的场景。其核心思想是分治法,将数组不断二分至单个元素,再合并有序子序列。
归并排序代码实现
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
let result = [];
while (left.length && right.length) {
if (left[0] <= right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
return result.concat(left).concat(right);
}
该实现通过递归拆分数组,
merge 函数负责合并两个有序数组,保证了排序的稳定性。
非比较排序的典型应用
计数排序、基数排序等非比较排序在特定条件下可达到 O(n) 时间复杂度,适用于整数或有限范围内的键值排序。
- 归并排序:通用性强,适合任意可比较数据
- 计数排序:适用于小范围整数,空间换时间
- 基数排序:用于多关键字排序,如日期、字符串
2.3 二分查找的边界处理与STL封装
在实际应用中,二分查找的边界条件极易出错,尤其是当目标值重复或不存在时。正确处理左闭右开区间是关键。
常见边界陷阱
- 循环条件误用
left <= right 导致越界 - 更新
mid 时未防止整数溢出:mid = left + (right - left) / 2 - 相等时未决定是否继续向左/右收缩
STL中的封装实现
auto it = std::lower_bound(vec.begin(), vec.end(), target);
if (it != vec.end() && *it == target) {
// 找到目标
}
std::lower_bound 返回首个不小于目标的位置,
upper_bound 返回首个大于位置,二者结合可精准定位区间,避免手动实现的边界错误。
2.4 堆排序与优先队列的底层构建
堆是一种完全二叉树结构,通过数组实现时可高效利用内存。最大堆的父节点值始终不小于子节点,最小堆则相反,这一性质是堆排序和优先队列的核心基础。
堆的数组表示与索引关系
对于索引
i 处的节点:
- 父节点索引为
(i-1)/2 - 左子节点为
2*i+1 - 右子节点为
2*i+2
最大堆调整操作(Max-Heapify)
void max_heapify(int arr[], int n, int i) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < n && arr[left] > arr[largest])
largest = left;
if (right < n && arr[right] > arr[largest])
largest = right;
if (largest != i) {
swap(&arr[i], &arr[largest]);
max_heapify(arr, n, largest);
}
}
该函数确保以
i 为根的子树满足最大堆性质,时间复杂度为
O(log n)。
堆排序与优先队列应用
堆排序通过构建初始堆并逐个提取根节点实现
O(n log n) 排序;优先队列利用堆动态维护最高优先级元素,插入和删除操作均为
O(log n)。
2.5 实战:高效排序在大数据去重中的应用
在处理海量数据时,去重是常见需求。通过先排序后合并的策略,可显著提升效率。排序后相同值相邻,便于线性扫描去重。
核心思路
利用高效排序算法(如快速排序或归并排序)预处理数据,使重复元素聚集,再单次遍历完成去重。
代码实现
// 假设输入为整型切片
func deduplicate(sortedData []int) []int {
if len(sortedData) == 0 {
return sortedData
}
result := []int{sortedData[0]}
for i := 1; i < len(sortedData); i++ {
if sortedData[i] != sortedData[i-1] {
result = append(result, sortedData[i])
}
}
return result
}
该函数在已排序数据上运行,时间复杂度为 O(n),依赖排序阶段的 O(n log n)。
性能对比
| 方法 | 时间复杂度 | 空间复杂度 |
|---|
| 哈希表去重 | O(n) | O(n) |
| 排序+扫描 | O(n log n) | O(1) |
第三章:动态规划经典问题解析
3.1 背包问题的多种状态转移方程设计
在动态规划中,背包问题的状态转移方程设计直接影响求解效率与适用场景。通过定义不同的状态维度,可灵活应对各类变体。
基本0-1背包状态转移
最经典的形式是:设
dp[i][w] 表示前
i 个物品在容量为
w 时的最大价值。
for (int i = 1; i <= n; i++) {
for (int w = W; w >= weight[i]; w--) {
dp[w] = max(dp[w], dp[w - weight[i]] + value[i]);
}
}
该代码采用滚动数组优化空间,逆序遍历避免重复选取。
完全背包的优化策略
允许物品无限次选取时,正序遍历即可实现状态累积:
- 状态定义不变,但内层循环从
weight[i] 到 W - 每次更新都可能再次使用同一物品
不同约束条件下,合理设计状态转移路径是提升算法性能的关键。
3.2 最长公共子序列与字符串匹配优化
在处理文本相似度和差异分析时,最长公共子序列(LCS)是核心算法之一。它能在不改变字符顺序的前提下,找出两个字符串中最长的共享子序列。
动态规划求解 LCS
func longestCommonSubsequence(text1, text2 string) int {
m, n := len(text1), len(text2)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
if text1[i-1] == text2[j-1] {
dp[i][j] = dp[i-1][j-1] + 1
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
}
}
}
return dp[m][n]
}
该函数使用二维 DP 表记录匹配状态。dp[i][j] 表示 text1 前 i 个字符与 text2 前 j 个字符的 LCS 长度。时间复杂度为 O(mn),空间可优化至 O(min(m,n))。
应用场景对比
| 场景 | LCS 优势 |
|---|
| 代码比对 | 精准定位增删行 |
| 生物信息学 | 序列同源性分析 |
3.3 实战:动态规划在路径规划中的性能提升
在复杂环境下的路径规划中,传统搜索算法如Dijkstra或A*在大规模网格中计算开销显著。动态规划(DP)通过状态转移与记忆化搜索,有效减少重复计算。
核心算法实现
def dp_shortest_path(grid):
rows, cols = len(grid), len(grid[0])
dp = [[float('inf')] * cols for _ in range(rows)]
dp[0][0] = grid[0][0]
# 初始化第一行和第一列
for i in range(1, rows):
dp[i][0] = dp[i-1][0] + grid[i][0]
for j in range(1, cols):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 状态转移:从上或左选择最小路径
for i in range(1, rows):
for j in range(1, cols):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[rows-1][cols-1]
上述代码通过构建二维DP表,逐行逐列更新最短路径值。时间复杂度由指数级优化至O(m×n),显著提升大规模地图的响应速度。
性能对比
| 算法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|
| A* | O(b^d) | O(b^d) | 稀疏图、启发式强 |
| DP | O(mn) | O(mn) | 网格路径、成本固定 |
第四章:图论与搜索算法实践
4.1 深度优先搜索与连通分量检测
深度优先搜索(DFS)是图遍历的基础算法,广泛应用于连通分量的识别。通过递归或栈结构深入探索每个顶点的邻接节点,标记已访问状态以避免重复。
核心算法实现
func dfs(graph map[int][]int, visited map[int]bool, node int) {
visited[node] = true
for _, neighbor := range graph[node] {
if !visited[neighbor] {
dfs(graph, visited, neighbor)
}
}
}
该函数递归访问当前节点的所有未访问邻居。graph 使用邻接表存储图结构,visited 记录访问状态,防止无限循环。
连通分量检测流程
- 初始化所有顶点为未访问状态
- 遍历每个顶点,若未访问,则启动一次 DFS
- 每次 DFS 调用即发现一个连通分量
4.2 广度优先搜索与最短路径求解
算法核心思想
广度优先搜索(BFS)按层级遍历图中节点,从起始点出发逐层扩展,确保首次到达目标节点时路径最短。适用于无权图的最短路径求解。
代码实现示例
from collections import deque
def bfs_shortest_path(graph, start, end):
queue = deque([(start, [start])]) # 队列存储 (当前节点, 路径)
visited = set()
while queue:
node, path = queue.popleft()
if node == end:
return path # 找到最短路径
if node not in visited:
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
queue.append((neighbor, path + [neighbor]))
return None # 未找到路径
该函数使用双端队列维护待访问节点,
visited 集合避免重复访问,
path 记录当前路径。每轮从队首取出节点并扩展其邻接点,保证首次抵达终点时路径最短。
时间复杂度分析
- 时间复杂度:O(V + E),其中 V 为顶点数,E 为边数
- 空间复杂度:O(V),用于存储队列和访问标记
4.3 Dijkstra算法的手动实现与堆优化
基础Dijkstra算法实现
Dijkstra算法用于求解单源最短路径问题,适用于带权有向图或无向图。其核心思想是贪心策略:每次从未访问的节点中选择距离起点最近的节点进行扩展。
import heapq
def dijkstra_basic(graph, start):
dist = {node: float('inf') for node in graph}
dist[start] = 0
visited = set()
while True:
u = None
for node in graph:
if node not in visited and (u is None or dist[node] < dist[u]):
u = node
if u is None or dist[u] == float('inf'):
break
visited.add(u)
for v, weight in graph[u]:
if dist[u] + weight < dist[v]:
dist[v] = dist[u] + weight
return dist
该版本使用数组查找最小距离节点,时间复杂度为 O(V²),适合稠密图。
堆优化版本
通过最小堆维护当前最短距离,将节点选择操作优化至 O(log V)。
def dijkstra_heap(graph, start):
dist = {node: float('inf') for node in graph}
dist[start] = 0
heap = [(0, start)]
while heap:
d, u = heapq.heappop(heap)
if d > dist[u]:
continue
for v, weight in graph[u]:
new_dist = dist[u] + weight
if new_dist < dist[v]:
dist[v] = new_dist
heapq.heappush(heap, (new_dist, v))
return dist
堆优化后时间复杂度降为 O((V + E) log V),显著提升稀疏图性能。
4.4 实战:拓扑排序在任务调度系统中的应用
在分布式任务调度系统中,任务间常存在依赖关系,需确保前置任务完成后再执行后续任务。拓扑排序通过处理有向无环图(DAG)提供了一种有效的调度策略。
依赖建模与图结构
将每个任务视为图中的节点,依赖关系作为有向边。若任务B依赖任务A,则存在边 A → B。
拓扑排序实现
使用Kahn算法进行排序:
func topologicalSort(graph map[string][]string, inDegree map[string]int) []string {
var result []string
queue := []string{}
// 初始化入度为0的节点
for node := range inDegree {
if inDegree[node] == 0 {
queue = append(queue, node)
}
}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
result = append(result, current)
// 更新邻接节点的入度
for _, neighbor := range graph[current] {
inDegree[neighbor]--
if inDegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
return result
}
代码中,
graph表示邻接表,
inDegree记录各节点入度。算法从入度为0的节点开始,逐步释放依赖,生成合法执行序列。
第五章:算法进阶与综合能力提升
动态规划优化实战
在处理背包类问题时,状态压缩可显著降低空间复杂度。例如,在0-1背包中,通过逆序遍历重量维度,可将二维DP数组优化为一维:
// 空间优化后的0-1背包
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int w = W; w >= weight[i]; w--) {
dp[w] = max(dp[w], dp[w - weight[i]] + value[i]);
}
}
图论中的多源最短路径
Floyd-Warshall算法适用于小规模稠密图的全源最短路径计算,其核心是三重循环松弛操作:
- 初始化距离矩阵,对角线为0,不可达设为无穷大
- 枚举中转点k,更新所有点对(i,j)的最短距离
- 时间复杂度O(V³),适合V ≤ 200的场景
实际工程中的算法选择策略
| 场景 | 推荐算法 | 时间复杂度 |
|---|
| 实时查询路径 | Dijkstra + 堆优化 | O((V+E)logV) |
| 频繁更新边权 | SPFA(稀疏图) | O(E) ~ O(VE) |
Graph Representation:
A --5--> B
| |
3 2
| |
v v
C --1--> D