算法和数据结构
算法
排序算法
基础排序算法
冒泡排序 (Bubble Sort)
- 算法思想:重复遍历列表,比较相邻元素,如果顺序错误就交换。每一轮遍历都会将最大的元素“冒泡”到末尾。
- 复杂度:$O(n^2)$
- 稳定性:稳定
- 适用场景:数据量极小,教学演示。
选择排序 (Selection Sort)
- 算法思想:每次从未排序部分找到最小元素,放到已排序部分的末尾。
- 复杂度:$O(n^2)$
- 稳定性:不稳定
- 适用场景:数据量小,内存写入成本高(交换次数少)。
插入排序 (Insertion Sort)
- 算法思想:将未排序元素插入到已排序序列的正确位置。类似打扑克牌理牌。
- 复杂度:$O(n^2)$
- 稳定性:稳定
- 适用场景:数据量小,或数据基本有序。
快速排序 (Quick Sort)
- 算法思想:分治法。选择一个基准(pivot),将数组分为两部分,左边小于基准,右边大于基准,然后递归排序。
- 复杂度:平均 $O(n \log n)$,最坏 $O(n^2)$。
- 空间复杂度:$O(\log n)$ (递归栈)。
- 稳定性:不稳定。
- 适用场景:通用排序,数组排序首选。
- C++实现:
void quickSort(vector<int>& arr, int low, int high) {
// 递归结束条件:如果区间只有一个元素或没有元素,就不排了
if (low < high) {
// --- 分区操作 (Partition) 开始 ---
int pivot = arr[high]; // 选取最后一个元素作为“基准”
// i 指向“比 pivot 小的区域”的最后一个元素
// 初始时该区域为空,所以 i 在 low 的左边
int i = low - 1;
// j 从头遍历到倒数第二个元素
for (int j = low; j < high; j++) {
// 如果当前元素比基准小
if (arr[j] < pivot) {
i++; // 扩充“小数区域”的边界
swap(arr[i], arr[j]); // 把这个小数交换到“小数区域”里去
}
}
// 循环结束后,i 左边(包括 i)都是小数,i 的右边都是大数
// 现在把基准值 pivot (arr[high]) 放到中间(i+1 的位置)
swap(arr[i + 1], arr[high]);
// 记录基准值现在的下标,它已经排好序了,以后不用动它
int pi = i + 1;
// --- 分区操作结束 ---
// 递归排序基准值左边的子数组
quickSort(arr, low, pi - 1);
// 递归排序基准值右边的子数组
quickSort(arr, pi + 1, high);
}
}归并排序 (Merge Sort)
- 算法思想:分治法。将数组分成两半,分别排序,然后合并两个有序数组。
- 复杂度:$O(n \log n)$。
- 空间复杂度:$O(n)$。
- 稳定性:稳定。
- 适用场景:链表排序,需要稳定排序的大规模数据,外部排序。
- C++实现:
// l: 左边界, m: 中间点, r: 右边界
void merge(vector<int>& arr, int l, int m, int r) {
// --- 第一步:计算长度并备份数据 ---
int n1 = m - l + 1; // 左半边的长度(注意 +1,因为 m 包含在左边)
int n2 = r - m; // 右半边的长度
// 创建临时数组(归并排序需要额外的内存空间,这是它的缺点)
vector<int> L(n1), R(n2);
// 把数据从原数组拷贝到临时数组 L 和 R
for (int i = 0; i < n1; i++) L[i] = arr[l + i];
for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];
// --- 第二步:双指针合并 ---
int i = 0; // L 数组的指针
int j = 0; // R 数组的指针
int k = l; // 原数组 arr 的指针(注意从 l 开始,不是 0)
// 当两个数组都没走完时
while (i < n1 && j < n2) {
// 谁小选谁,放回 arr 中
// 注意:<= 保证了算法的“稳定性”(相同元素位置不变)
if (L[i] <= R[j]) {
arr[k++] = L[i++];
} else {
arr[k++] = R[j++];
}
}
// --- 第三步:处理剩余元素 ---
// 有可能左边剩下了,也有可能右边剩下了,把剩下的全部抄过去
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}
void mergeSort(vector<int>& arr, int l, int r) {
if (l < r) {
// 1. 找中间点(防止 (l+r) 溢出的写法)
int m = l + (r - l) / 2;
// 2. 递归拆分左半边 [l ... m]
mergeSort(arr, l, m);
// 3. 递归拆分右半边 [m+1 ... r]
mergeSort(arr, m + 1, r);
// 4. 【重点】左右两边都排好了,现在把它们“缝合”起来
merge(arr, l, m, r);
}
}堆排序 (Heap Sort)
- 算法思想:利用堆这种数据结构。先建最大堆,然后将堆顶(最大值)与末尾交换,缩小堆范围并调整堆,重复直到有序。
- 复杂度:$O(n \log n)$。
- 空间复杂度:$O(1)$。
- 稳定性:不稳定。
- 适用场景:内存受限,需要最坏情况 $O(n \log n)$。
- C++实现:
void heapify(vector<int>& arr, int n, int i) {
int largest = i;
int l = 2 * i + 1;
int r = 2 * i + 2;
if (l < n && arr[l] > arr[largest]) largest = l;
if (r < n && arr[r] > arr[largest]) largest = r;
if (largest != i) {
swap(arr[i], arr[largest]);
heapify(arr, n, largest);
}
}
void heapSort(vector<int>& arr) {
int n = arr.size();
for (int i = n / 2 - 1; i >= 0; i--) heapify(arr, n, i);
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}桶排序 (Bucket Sort)
- 算法思想:将数据分到有限数量的桶里,每个桶再分别排序(通常用插入排序)。
- 复杂度:平均 $O(n+k)$,最坏 $O(n^2)$。
- 适用场景:数据均匀分布在某个范围内。
计数排序 (Counting Sort)
- 算法思想:统计每个元素出现的次数,根据统计结果直接填充目标数组。
- 复杂度:$O(n+k)$。
- 适用场景:整数排序,范围小。
基数排序 (Radix Sort)
- 算法思想:按照低位先排序,然后收集;再按照高位排序,再收集;依次类推,直到最高位。
- 复杂度:$O(nk)$,k为最大位数。
- 适用场景:整数,字符串。
四分位距法
四分位距(Interquartile Range, IQR)是统计学中衡量数据离散程度的一种方法。它表示数据集中间50%的范围,即第三四分位数($Q_{3}$)与第一四分位数($Q_{1}$)之间的差值。
计算方法
- 确定中位数 ($Q_{2}$):将数据按升序排列,找到中间值。
- 确定第一四分位数 ($Q_{1}$):中位数以下的数据的中间值。
- 确定第三四分位数 ($Q_{3}$):中位数以上的数据的中间值。
- 计算IQR:$IQR = Q_{3} - Q_{1}$
应用
IQR常用于识别异常值(outliers)。通常,如果一个数据点低于 $Q1 - 1.5 * IQR$ 或高于 $Q3 + 1.5 * IQR$,则被认为是异常值。
import numpy as np
# 示例数据
data = np.array([1, 2, 5, 6, 7, 9, 12, 15, 18, 19, 27])
# 计算 Q1, Q3
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
# 计算 IQR
iqr = q3 - q1
print(f"数据集: {data}")
print(f"第一四分位数 (Q1): {q1}")
print(f"第三四分位数 (Q3): {q3}")
print(f"四分位距 (IQR): {iqr}")
# 识别异常值
lower_bound = q1 - 1.5 * iqr
upper_bound = q3 + 1.5 * iqr
outliers = data[(data < lower_bound) | (data > upper_bound)]
print(f"异常值界限: (下界: {lower_bound}, 上界: {upper_bound})")
print(f"异常值: {outliers}")匈牙利算法 (Hungarian Algorithm)
核心思想
匈牙利算法主要用于解决二分图的最大匹配问题,核心在于寻找增广路径。
- 二分图 (Bipartite Graph):图的顶点集可以分为两个互不相交的子集 $U$ 和 $V$,图中每一条边 $(u, v)$ 都连接 $U$ 中的一个顶点和 $V$ 中的一个顶点。
- 匹配 (Matching):图的一个边集,其中任意两条边都没有公共顶点。
- 最大匹配 (Maximum Matching):包含边数最多的匹配。
- 完美匹配 (Perfect Matching):所有顶点都在匹配中的匹配。
关键概念
- 交替路 (Alternating Path):从一个未匹配点出发,依次经过“非匹配边、匹配边、非匹配边…”的路径。
- 增广路 (Augmenting Path):一条起点和终点都是未匹配点的交替路。
- 性质:增广路上的边数一定是奇数。非匹配边比匹配边多一条。
- 操作:把增广路上的“匹配边”和“非匹配边”取反(互换),匹配数就会 +1。
算法步骤 (DFS版本)
- 初始化所有边为非匹配边。
- 遍历左侧集合($U$)的每一个顶点 $u$:
- 如果 $u$ 还没有匹配:
- 尝试寻找一条从 $u$ 出发的增广路径(通常用 DFS 或 BFS)。
- 如果找到了,就对路径取反,匹配数 +1。
- 清空
visited数组(用于 DFS 防止死循环)。
- 如果 $u$ 还没有匹配:
- 遍历结束,当前的匹配数即为最大匹配数。
复杂度
- 时间复杂度:邻接矩阵 $O(V \cdot E)$ 或 $O(V^3)$。
- 空间复杂度:$O(V^2)$ 或 $O(E)$。
C++实现
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int MAXN = 505; // 最大顶点数
vector<int> adj[MAXN]; // 邻接表存图
int match[MAXN]; // match[v] = u 表示右边的点 v 匹配了左边的点 u
bool vis[MAXN]; // visited 数组,记录右边的点是否在本次 DFS 中被访问过
// 向图中添加边 u -> v (u 在左边,v 在右边)
void addEdge(int u, int v) {
adj[u].push_back(v);
}
// DFS 寻找增广路径
bool dfs(int u) {
for (int v : adj[u]) {
if (!vis[v]) {
vis[v] = true; // 标记右边的点 v 已访问
// 如果 v 还没有匹配,或者 v 已经匹配了但它的匹配对象 match[v] 可以找到另一个匹配
if (match[v] == -1 || dfs(match[v])) {
match[v] = u; // 尝试把 v 匹配给 u
return true;
}
}
}
return false;
}
int hungarian(int N) {
int result = 0;
memset(match, -1, sizeof(match)); // 初始化所有点未匹配
// 遍历左边的所有点 N
for (int i = 1; i <= N; i++) {
memset(vis, 0, sizeof(vis)); // 每次寻找增广路前都要清空 visited
if (dfs(i)) {
result++;
}
}
return result;
}
int main() {
int n, m, e; // n: 左边点数, m: 右边点数, e: 边数
cin >> n >> m >> e;
for (int i = 0; i < e; i++) {
int u, v;
cin >> u >> v;
// 假设输入是 1-based index,如果题目要求,这里可能要做调整
addEdge(u, v);
}
cout << hungarian(n) << endl;
return 0;
}数据结构
优先队列
优先队列(Priority Queue)是一种特殊的队列,其中每个元素都有一个关联的“优先级”。与普通队列“先进先出”(FIFO)的规则不同,优先队列中的元素是按照它们的优先级顺序出队的。通常,优先级最高的元素最先出队。
优先队列常用于需要高效处理“最重要”任务的场景,例如:
- Dijkstra算法:在图中找到最短路径。
- 任务调度:操作系统根据任务的优先级来决定先执行哪个任务。
最小堆
说明
- 结构属性:最小堆通常是一种特殊的“树形”结构,是一棵完全二叉树,这意味着除了最后一层,所有层都是满的,并且最后一层节点都尽可能地靠左排列,这样用列表实现时,天然就满足了,因为我们总是把新元素放到队尾。
- 堆属性:对于最小堆来说,任何一个节点(父节点)得值,都小于或等于其所有子节点的值,这个属性结果就是堆顶永远是最小元素。
添加新元素
- 一个新元素被添加到列表的末尾。
- 比较这个新元素和它的父节点。
- 如果新元素比父节点小,这违反了最小堆的属性。于是,它们两个交换位置。
- 交换后,新元素跑到了父节点的位置。它继续和它的新父节点比较,重复这个过程,直到它不再比父节点小,或者它已经到达了堆顶(索引0)。这个过程就像一个“气泡”从水底不断上浮。
移除堆顶
- 移除堆顶元素(最小值)。
- 为了填补这个空缺并保持树的完整性,我们把列表的最后一个元素移动到堆顶。
- 这时,堆顶的这个新元素很可能非常大,破坏了堆属性。
- 从堆顶开始,将这个元素与它的子节点比较。
- 它会找到两个子节点中较小的那一个进行比较。
- 如果当前节点比它较小的子节点大,就和这个较小的子节点交换。
- 交换后,它继续向下移动,重复这个过程,直到它不再比它的子节点大,或者它成为了一个叶子节点(没有子节点)。这个过程就像一块“石头”在水中不断下沉。
示例
使用 heapq 模块
Python 的 heapq 模块提供了一个最小堆(Min Heap)的实现,这可以非常方便地用来构建优先队列。在最小堆中,优先级最小的元素总是在堆顶(即列表的第一个元素)。
下面是一个使用 heapq 实现的优先队列的例子:
import heapq
class PriorityQueue:
def __init__(self):
self._queue = []
self._index = 0
def push(self, item, priority):
# heapq是最小堆,所以我们存入一个元组 (priority, index, item)
# index 是为了在优先级相同时,保持插入顺序
heapq.heappush(self._queue, (priority, self._index, item))
self._index += 1
def pop(self):
# heappop总是返回优先级最小的元素
return heapq.heappop(self._queue)[-1]
def is_empty(self):
return not self._queue
# 示例
pq = PriorityQueue()
pq.push("任务A", 3)
pq.push("任务B", 1)
pq.push("任务C", 2)
print("按优先级顺序处理任务:")
while not pq.is_empty():
print(pq.pop()) # 输出: 任务B, 任务C, 任务A红黑树
核心思想
- 红黑树是自平衡二叉搜索树,借助颜色约束与旋转保持近似平衡,使操作稳定为对数时间。
性质
- 每个节点为红或黑。
- 根为黑。
- 所有叶子(NIL哨兵)为黑。
- 红节点的两个子节点均为黑。
- 任意节点到其所有叶子路径上的黑节点数相同(黑高)。
复杂度与高度
- 查找、插入、删除均为
O(log n)。 - 树高中最多约为
2 · log2(n+1),比普通 BST 更稳定,略逊于 AVL 但旋转更少。
基本操作
- 左旋:将某节点的右子提升为父,原节点变为其左子,保持中序序列不变。
- 右旋:与左旋相反。
- 插入修复(新节点默认红):
- 父黑:无需修复。
- 父红、叔红:父与叔改黑,祖父改红,向上继续处理祖父。
- 父红、叔黑、内侧:先对父做一次旋转转成外侧,再执行外侧情况。
- 父红、叔黑、外侧:以祖父为轴旋转,父改黑、祖父改红,结束。
- 删除修复(删除黑节点导致黑高减少时):
- 兄弟红:兄弟改黑、父改红,围绕父旋转,转化为兄弟黑的情形。
- 兄弟黑、兄弟的两个子皆黑:兄弟改红,问题向父上推。
- 兄弟黑、兄弟的远侄子红:围绕父旋转,兄弟取父色、父改黑、远侄子改黑,结束。
- 兄弟黑、近侄子红、远侄子黑:先围绕兄弟旋转使远侄子变红,再按上一条处理。
与 AVL 对比
- AVL 更紧平衡、查找略快;红黑树插删旋转更少,工程更常用(如关联容器)。
典型应用
- 有序映射与集合(如
TreeMap/TreeSet、std::map)。 - 调度器、内存管理、区间集合等需要稳定对数时间的场景。
术语
- 黑高:从某节点到其任一叶子的黑节点数量。
- 叔叔:父的另一子。
- 外侧/内侧:相对祖父而言,父-子是否在同一方向。
并查集
核心思想
- 维护若干不相交的集合(等价类),支持合并与查询是否同集合,基于树结构与父指针实现。
操作
- 初始化:每个元素自成一个集合,父为自身。
- 查找(find,路径压缩):沿父指针找到代表元素,并在返回途中压缩路径。
- 合并(union,按大小/按秩):将两个集合的代表合并到更“重”的集合下,降低高度。
- 查询同集(same):比较两个元素的代表是否相同。
- 增强:集合大小查询、当前连通分量计数。
复杂度
- 摊还近似常数,严格上界
O(α(n)),其中α为阿克曼函数的反函数,增长极慢。
常见变体
- 按秩 vs 按大小。
- 带权并查集(维护相对差值/颜色等)。
- 可回滚并查集(离线、分治场景)。
- 动态连通性与区间合并。
典型应用
- 图的连通性判定、Kruskal 最小生成树。
- 等价类归并、岛屿计数、好友圈/群组聚合。
C++实现
class DisjointSet {
public:
DisjointSet(int n): n_(n), components(n) {
parent = new int[n];
sz = new int[n];
for (int i = 0; i < n; ++i) { parent[i] = i; sz[i] = 1; }
}
~DisjointSet() {
delete[] parent;
delete[] sz;
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
bool unite(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (sz[ra] < sz[rb]) { int t = ra; ra = rb; rb = t; }
parent[rb] = ra;
sz[ra] += sz[rb];
--components;
return true;
}
bool same(int a, int b) { return find(a) == find(b); }
int set_size(int x) { return sz[find(x)]; }
int count() const { return components; }
private:
int n_;
int* parent;
int* sz;
int components;
};KD-Tree
核心思想
KD-Tree(K-Dimensional Tree)是一种用于组织 $k$ 维空间中点集的二叉搜索树。它将 $k$ 维空间递归地划分成超矩形区域。
构建过程
- 选择维度:每一层选择一个维度进行划分。通常轮流选择(如 x -> y -> z -> x…)或选择方差最大的维度。
- 选择分裂点:在选定维度上,选择中位数作为分裂点(Pivot),将数据分为两部分。
- 递归构建:小于中位数的点放入左子树,大于中位数的点放入右子树,递归直到节点为空或只剩一个点。
关键操作
最近邻搜索 (Nearest Neighbor Search)
- 下探:从根节点开始,根据当前维度的值向左或向右递归,直到叶子节点。将当前叶子节点作为“当前最近点”。
- 回溯:向上回溯。
- 计算当前节点与目标点的距离,如果更近则更新“当前最近点”。
- 剪枝判断:检查以目标点为球心、以“当前最小距离”为半径的超球体,是否与当前节点的分割超平面相交。
- 如果相交(即目标点到分割平面的距离 < 当前最小距离),则需要进入另一侧子树查找。
- 如果不相交,则无需进入另一侧子树。
复杂度
- 构建时间:$O(N \log N)$。
- 空间复杂度:$O(N)$。
- 搜索时间:
- 平均:$O(\log N)$。
- 最坏:$O(N)$(维数灾难,当 $k$ 很大时效率退化)。
适用场景
- 多维空间的关键搜索(如 KNN 算法)。
- 范围查询。
- 图像检索、点云处理。
C++实现
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
struct Point {
vector<int> coords;
int id;
};
struct Node {
Point point;
Node *left = nullptr;
Node *right = nullptr;
int axis;
Node(Point p, int ax) : point(p), axis(ax) {}
};
// 计算两点间的欧几里得距离平方
double distSq(const Point& a, const Point& b) {
double sum = 0;
for (size_t i = 0; i < a.coords.size(); ++i) {
double diff = a.coords[i] - b.coords[i];
sum += diff * diff;
}
return sum;
}
// 构建 KD-Tree
Node* buildKDTree(vector<Point>& points, int depth) {
if (points.empty()) return nullptr;
int k = points[0].coords.size();
int axis = depth % k;
// 根据当前轴排序
sort(points.begin(), points.end(), [axis](const Point& a, const Point& b) {
return a.coords[axis] < b.coords[axis];
});
int mid = points.size() / 2;
Node* node = new Node(points[mid], axis);
vector<Point> leftPoints(points.begin(), points.begin() + mid);
vector<Point> rightPoints(points.begin() + mid + 1, points.end());
node->left = buildKDTree(leftPoints, depth + 1);
node->right = buildKDTree(rightPoints, depth + 1);
return node;
}
// 最近邻搜索
void nearestNeighbor(Node* root, const Point& target, Node*& best, double& minDistSq) {
if (root == nullptr) return;
double d = distSq(root->point, target);
if (d < minDistSq) {
minDistSq = d;
best = root;
}
int axis = root->axis;
double diff = target.coords[axis] - root->point.coords[axis];
// 优先搜索可能更近的一侧
Node* near = (diff < 0) ? root->left : root->right;
Node* far = (diff < 0) ? root->right : root->left;
nearestNeighbor(near, target, best, minDistSq);
// 如果分割平面与当前最小球相交,则需要搜索另一侧
if (diff * diff < minDistSq) {
nearestNeighbor(far, target, best, minDistSq);
}
}
int main() {
vector<Point> points = {
{{2, 3}, 1}, {{5, 4}, 2}, {{9, 6}, 3}, {{4, 7}, 4}, {{8, 1}, 5}, {{7, 2}, 6}
};
Node* root = buildKDTree(points, 0);
Point target = {{9, 2}, 0};
Node* best = nullptr;
double minDistSq = 1e9;
nearestNeighbor(root, target, best, minDistSq);
cout << "Nearest point: (" << best->point.coords[0] << ", " << best->point.coords[1] << ")" << endl;
cout << "Distance squared: " << minDistSq << endl;
return 0;
}