第七章 图 (Graph)
图是一种复杂的数据结构,广泛应用于计算机科学各个领域,如社交网络、通信网络、导航系统等。理解图的基本概念和相关算法,可以帮助我们解决许多现实世界的问题。
7.1 图的基本概念和术语
- 图(Graph):由顶点(Vertex)和边(Edge)组成的集合。其中,顶点表示数据元素,边表示顶点之间的关系。
- 有向图(Directed Graph):边有方向性的图,表示从一个顶点指向另一个顶点的关系。
- 无向图(Undirected Graph):边无方向性的图,表示两个顶点之间的双向关系。
- 加权图(Weighted Graph):边带有权重的图,权重通常表示连接两顶点的代价或距离。
- 路径(Path):从一个顶点出发经过多个顶点到达另一个顶点的序列。
- 环(Cycle):路径的起始顶点和终止顶点相同。
- 连通图(Connected Graph):任意两个顶点之间都有路径相连的图。
- 稠密图与稀疏图:稠密图的边数接近最大可能数;稀疏图的边数远小于可以存在的最大数。
7.2 图的表示方法
-
邻接矩阵(Adjacency Matrix):使用一个二维数组表示图的连接关系,其中
matrix[i][j]
表示顶点 i 和顶点 j 之间的边。- 易于实现和理解,但对于稀疏图占用较多空间。
-
邻接表(Adjacency List):使用数组加链表(或向量)来存储每个顶点的邻接顶点。
- 空间效率高,尤其适合稀疏图。
7.3 深度优先搜索 (DFS)
深度优先搜索是一种遍历图的方法,尽可能地深入访问节点。
- 思想:优先访问未访问过的邻接顶点,直至无法前进再回溯。
- 应用:
- 检测环
- 拓扑排序
void DFS(Graph *graph, int vertex, bool visited[]) {
visited[vertex] = true;
printf("Visited %d\n", vertex);
for (int v: graph->adj[vertex]) {
if (!visited[v]) {
DFS(graph, v, visited);
}
}
}
7.4 广度优先搜索 (BFS)
广度优先搜索遍历当前顶点的所有邻接顶点后,再逐层深入。
- 思想:使用队列存储中间节点,从而按层次遍历。
- 应用:
- 最短路径搜索(无权图)
- 分层图处理
void BFS(Graph *graph, int start) {
bool *visited = (bool *)malloc(graph->numVertices * sizeof(bool));
memset(visited, false, graph->numVertices);
Queue *queue = createQueue();
enqueue(queue, start);
visited[start] = true;
while (!isEmpty(queue)) {
int vertex = dequeue(queue);
printf("Visited %d\n", vertex);
for (int v: graph->adj[vertex]) {
if (!visited[v]) {
visited[v] = true;
enqueue(queue, v);
}
}
}
}
7.5 最短路径算法
-
Dijkstra算法:求解非负权重单源最短路径。
- 使用优先队列实现可以提高效率。
-
Floyd-Warshall算法:适用于任意权重图求解多源最短路径。
- 借助动态规划更新距离矩阵。
7.6 最小生成树
最小生成树是一种无向图中包含所有顶点的最小权重树。
-
Prim算法:从一个节点开始逐步扩展生成树。
- 适合稠密图。
-
Kruskal算法:按边权重递增顺序选择边构成生成树。
- 适合稀疏图,采用并查集实现有助于提高效率。
通过本章的学习,您将能够掌握图的基本概念、表示方法以及常用算法的实现和应用,为解决各种图相关的实际问题奠定坚实基础。
第八章 常见算法
在本章节中,我们将介绍一些常用的算法,包括排序算法、搜索算法,以及递归与回溯方法。这些算法不仅在计算机科学中具有广泛的应用,也是编程面试中常见的考核知识点。
8.1 排序算法
排序算法用于将一组无序元素排列为有序序列。在不同场景下选择合适的排序算法可以显著提高程序的效率。
8.1.1 冒泡排序 (Bubble Sort)
冒泡排序是一种简单但效率较低的排序算法。它通过多次遍历需要排序的列表,逐步将较小的元素移动到前面。
- 特点:
- 时间复杂度为 (O(n^2))
- 对小规模数据较易实现
- 在几乎有序的数据上表现较好
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
8.1.2 选择排序 (Selection Sort)
选择排序通过不断地选择剩余元素中的最小(或最大)元素并将其放置到正确的位置来进行排序。
- 特点:
- 时间复杂度为 (O(n^2))
- 相对较少的交换操作
- 不稳定
void selectionSort(int arr[], int n) {
for (int i = 0; i < n-1; i++) {
int min_idx = i;
for (int j = i+1; j < n; j++) {
if (arr[j] < arr[min_idx])
min_idx = j;
}
int temp = arr[min_idx];
arr[min_idx] = arr[i];
arr[i] = temp;
}
}
8.1.3 插入排序 (Insertion Sort)
插入排序通过构建有序序列,对未排序的数据,在已排序序列中从后向前扫描,找到相应位置插入。
- 特点:
- 时间复杂度为 (O(n^2))(但在数据接近排序的情况下效率高)
- 稳定
- 适用于小数据集或输入数据接近排序时
void insertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
8.1.4 归并排序 (Merge Sort)
归并排序是一种分治算法,通过递归将数组分为两个子数组,并将排序后的子数组合并。
- 特点:
- 时间复杂度为 (O(n \log n))
- 稳定
- 适合排序大型数据集
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int L[n1], R[n2];
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, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++