第七章 图 (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++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}
}
8.1.5 快速排序 (Quick Sort)
快速排序是基于分治法的另一种高效排序算法,通过选择一个基准元素,把较小元素移到基准前面,较大元素移到基准后面。
- 特点:
- 平均时间复杂度为 (O(n \log n)),最差情况下 (O(n^2))
- 不稳定
- 适合大型、随机的数据集
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
return (i + 1);
}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
8.2 搜索算法
搜索算法用于在数据结构中找到特定元素或满足特定条件的元素。
8.2.1 线性搜索
线性搜索简单直接,在无序或有序的列表中从头遍历到尾部,直到找到目标元素。
- 特点:
- 时间复杂度为 (O(n))
- 适合小型、无序的数据集
int linearSearch(int arr[], int n, int x) {
for (int i = 0; i < n; i++)
if (arr[i] == x)
return i;
return -1;
}
8.2.2 二分搜索
二分搜索适用于有序数组,通过每次比较中间元素逐步缩小搜索范围。
- 特点:
- 时间复杂度为 (O(\log n))
- 需要排序好的数据集
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int mid = l + (r - l) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] < x)
l = mid + 1;
else
r = mid - 1;
}
return -1;
}
8.3 递归与回溯
递归是一种直接或间接调用自身的编程方式,在解决问题时可以将问题分解为相似的子问题。
8.3.1 递归基础
递归需要定义基准情形来终止递归调用,且每次递归调用问题应缩小以最终会达到基准情况。
- 应用例子:阶乘计算、斐波那契数列等。
int factorial(int n) {
if (n <= 1)
return 1;
else
return n * factorial(n - 1);
}
8.3.2 回溯算法及应用 (如 N 皇后问题)
回溯是一种尝试构建所有可能解决方案的暴力搜索方法,常用于组合问题、排列问题和求解约束问题。
- 应用例子:N皇后问题、迷宫求解、数独。
bool isSafe(int board[][N], int row, int col) {
for (int i = 0; i < col; i++)
if (board[row][i])
return false;
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--)
if (board[i][j])
return false;
for (int i = row, j = col; j >= 0 && i < N; i++, j--)
if (board[i][j])
return false;
return true;
}
bool solveNQUtil(int board[][N], int col) {
if (col >= N)
return true;
for (int i = 0; i < N; i++) {
if (isSafe(board, i, col)) {
board[i][col] = 1;
if (solveNQUtil(board, col + 1))
return true;
board[i][col] = 0; // 回溯
}
}
return false;
}
bool solveNQ() {
int board[N][N] = { { 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 },
{ 0, 0, 0, 0 } };
if (solveNQUtil(board, 0) == false) {
printf("Solution does not exist");
return false;
}
printSolution(board);
return true;
}
void printSolution(int board[N][N]) {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++)
printf(" %d ", board[i][j]);
printf("\n");
}
}
- 回溯算法特点:
- 为解决所有可能的解,最后需要回到未尝试的路径上
- 借助递归,将问题拆分为子问题,尝试探索每一个可能性
- 常应用于解决那些具有约束条件的组合优化问题
总结:在本节,通过对常用排序和搜索算法,以及递归与回溯方法的深入理解,您将能够有效地在实践中运用这些基本操作来处理复杂的数据结构问题。掌握这些算法是迈向更高编程技能的重要一步。接下来的内容将引导您进入项目实战和优化技术的领域,使所学的知识在实际中得到更好地应用和扩展。
通过把这些算法应用于项目中,开发者可以加深对它们的理解,同时提升解决实际问题的能力。 此外,在选择算法时应根据具体的需求和数据特性以优化应用的效率。
第九章 项目实战
项目实战这一章旨在通过具体的小型项目案例,帮助你将前面学习的数据结构和算法知识应用到实际问题中。在这些项目中,你将面对现实生活中的挑战,培养实际编码能力和问题解决能力。
9.1 实现一个简单的文本编辑器(结合链表)
在这个项目中,我们将创建一个简化版的文本编辑器,重点是文本的存储和基本编辑功能。由于文本编辑涉及频繁的插入和删除操作,双向链表作为底层数据结构非常适合。
-
核心功能:
- 文字插入:允许用户在文本的任意位置插入文字。
- 文字删除:允许用户删除特定位置的文字。
- 显示文本:展示编辑器当前的文本内容。
-
实现要点:
- 使用双向链表结构实现文本的存储,每个节点保存一个字符,这使得插入和删除操作可以在常量时间内完成。
- 支持快速的前后导航,实现光标在文本中的移动。
- 考虑用户输入和边界条件处理,例如光标的准确定位、文本行的切换以及空行的处理。
#include <stdio.h>
#include <stdlib.h>
// 定义双向链表节点
typedef struct Node {
char data;
struct Node* prev;
struct Node* next;
} Node;
// 创建新节点函数
Node* createNode(char data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (!newNode) {
printf("内存分配失败\n");
exit(1);
}
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
// 文本显示函数
void displayText(Node* head) {
Node* current = head;
while (current != NULL) {
printf("%c", current->data);
current = current->next;
}
printf("\n");
}
// 插入字符函数
Node* insertCharacter(Node* head, Node** current, char data) {
Node* newNode = createNode(data);
if (*current == NULL) { // 初始插入
head = newNode;
*current = head;
} else {
newNode->prev = *current;
newNode->next = (*current)->next;
if ((*current)->next != NULL) {
(*current)->next->prev = newNode;
}
(*current)->next = newNode;
*current = newNode; // 更新光标位置
}
return head;
}
// 删除当前字符函数
Node* deleteCharacter(Node* head, Node** current) {
if (*current == NULL) {
printf("没有可删除的字符\n");
return head;
}
Node* temp = *current;
if ((*current)->prev != NULL) {
(*current)->prev->next = (*current)->next;
} else {
head = (*current)->next; // 更新头指针
}
if ((*current)->next != NULL) {
(*current)->next->prev = (*current)->prev;
}
*current = (*current)->prev; // 移动光标到前一个字符
free(temp);
return head;
}
int main() {
Node* head = NULL; // 链表的头指针
Node* current = NULL; // 当前光标位置
char input[100];
char choice;
int i = 0;
printf("简单文本编辑器\n");
printf("i: 插入字符 d: 删除字符 p: 显示文本 q: 退出\n");
while (1) {
printf("请输入操作:");
scanf(" %c", &choice);
switch (choice) {
case 'i':
printf("输入要插入的字符:");
scanf(" %s", input);
while (input[i] != '\0') {
head = insertCharacter(head, ¤t, input[i]);
i++;
}
i = 0;
break;
case 'd':
head = deleteCharacter(head, ¤t);
break;
case 'p':
displayText(head);
break;
case 'q':
exit(0);
break;
default:
printf("无效输入\n");
}
}
return 0;
}
- 程序解释:
- 我们定义了一个基本的双向链表节点数据结构,其中
data
存储字符,prev
和next
分别指向前一个和后一个节点。 - 使用链表的
insertCharacter
和deleteCharacter
函数实现插入和删除操作。需要注意的是,在插入字符后,光标位置会被更新到新插入的字符节点上。 displayText
函数用于展示编辑器当前的文本内容。当程序用户选择展示文本时,它将从头到尾遍历链表并打印出其中的字符。
- 我们定义了一个基本的双向链表节点数据结构,其中
该简单文本编辑器项目展示了如何利用链表结构完成字符串的编辑操作,提高数据插入和删除效率,并帮助读者理解双向链表在文本编辑中的实际应用。随着功能和复杂度的增加,您可以考虑进一步扩展编辑器功能,如支持行保存、打开文件等功能。
9.2 实现一个简单的文本编辑器(结合链表)
在这个项目中,你将尝试创建一个简化版的文本编辑器,着重于文本的存储和基本编辑功能。由于需要频繁插入和删除操作,链表是一种理想的数据结构。
-
核心功能:
- 文字插入:用户能够在文本的任意位置插入文字。
- 文字删除:用户能够删除特定位置的文字。
- 显示文本:展示当前编辑器中的文本。
-
实现要点:
- 采用双向链表结构实现文本的存储,每个节点存储字符数据。
- 支持快速前后导航,插入和删除操作。
- 考虑用户输入和边界条件处理,例如光标操作和切换行。
#include <stdio.h>
#include <stdlib.h>
// 定义双向链表节点结构
typedef struct Node {
char data;
struct Node* prev;
struct Node* next;
} Node;
// 创建新节点
Node* createNode(char data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
// 在指定位置插入字符
void insertCharacter(Node** head, char data, int position) {
Node* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
Node* temp = *head;
int currentIndex = 0;
while (currentIndex < position && temp->next != NULL) {
temp = temp->next;
currentIndex++;
}
if (currentIndex == position) {
newNode->next = temp->next;
newNode->prev = temp;
if (temp->next != NULL) {
temp->next->prev = newNode;
}
temp->next = newNode;
} else {
printf("插入位置超出范围!\n");
}
}
// 删除指定位置的字符
void deleteCharacter(Node** head, int position) {
if (*head == NULL) {
printf("文本为空,无法删除!\n");
return;
}
Node* temp = *head;
int currentIndex = 0;
while (currentIndex < position && temp->next != NULL) {
temp = temp->next;
currentIndex++;
}
if (currentIndex == position) {
if (temp->prev != NULL) {
temp->prev->next = temp->next;
} else {
*head = temp->next;
}
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
free(temp);
} else {
printf("删除位置超出范围!\n");
}
}
// 显示文本链表中的所有字符
void displayText(Node* head) {
Node* temp = head;
while (temp != NULL) {
printf("%c", temp->data);
temp = temp->next;
}
printf("\n");
}
int main() {
Node* textEditor = NULL;
// 示例使用:插入字符
insertCharacter(&textEditor, 'H', 0);
insertCharacter(&textEditor, 'e', 1);
insertCharacter(&textEditor, 'l', 2);
insertCharacter(&textEditor, 'l', 3);
insertCharacter(&textEditor, 'o', 4);
// 显示文本内容
printf("当前文本内容:");
displayText(textEditor);
// 删除字符
deleteCharacter(&textEditor, 2); // 删除“l”
printf("删除后的文本内容:");
displayText(textEditor);
return 0;
}
- 详细说明:
- 双向链表:在节点插入和删除时,双向链表允许在任意位置高效操作,适用于文本编辑器这种需要频繁修改内容的应用。
- 字符存储:每个节点存储单个字符数据,以便于实现字符级操作。
- 边界处理:插入和删除操作中要注意边界处理,防止非法操作如插入和删除超出链表范围的节点。
9.3 实现一个简单的文本编辑器(结合链表)
在这个项目中,你将尝试创建一个简化版的文本编辑器,重点关注文本的存储和基本编辑功能。由于需要频繁的插入和删除操作,链表是一种理想的数据结构选择。
-
核心功能:
- 文字插入:用户能够在文本的任意位置插入文字。
- 文字删除:用户能够删除特定位置的文字。
- 显示文本:展示当前编辑器中的文本。
-
实现要点:
- 数据结构选择:采用双向链表结构进行文本的存储,每个节点存储一个字符的数据。双向链表允许从任一方向进行遍历,这对编辑器的快捷移动和编辑功能极为有利。每个节点包含字符值及指向下一个和前一个节点的指针。
- 导航和编辑:
- 快速导航:通过双向链表特性,支持快速的光标前后移动。
- 插入操作:当用户在特定位置插入字符时,在对应链表节点前后进行链接操作。
- 删除操作:删除操作仅需调整链表指针的链接,从而从链表中移除目标节点。
- 用户输入和异常处理:必须考虑用户输入边界条件,例如光标移动至行首或行末的情况,以及在空文本中进行编辑操作。
- 行管理与切换:为了管理多行文本,可将每一行视为一个链表的独立实例,使用链表的链表或额外的数据结构存储行信息以便快速切换。
-
示例代码:
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node {
char data;
struct Node *prev;
struct Node *next;
} Node;
// 在链表中插入一个新字符
void insert(Node **head, char data, int position) {
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = data;
if (*head == NULL) {
// If list is empty
newNode->next = newNode->prev = NULL;
*head = newNode;
return;
}
Node *temp = *head;
int idx = 0;
while (temp != NULL && idx < position) {
temp = temp->next;
idx++;
}
if (temp == NULL) { // Position is at end
newNode->next = NULL;
newNode->prev = *head;
while ((*head)->next != NULL) {
*head = (*head)->next;
}
(*head)->next = newNode;
} else { // Position is in middle or at beginning
newNode->next = temp;
newNode->prev = temp->prev;
if (temp->prev != NULL) {
temp->prev->next = newNode;
}
temp->prev = newNode;
if (position == 0) {
*head = newNode;
}
}
}
// 删除链表中指定位置的字符
void delete(Node **head, int position) {
if (*head == NULL) return;
Node *temp = *head;
int idx = 0;
while (temp != NULL && idx < position) {
temp = temp->next;
idx++;
}
if (temp == NULL) return; // Position is beyond list size
if (temp->prev != NULL) {
temp->prev->next = temp->next;
} else {
*head = temp->next; // If deleting head node
}
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
free(temp);
}
// 显示链表中的所有字符
void display(Node *head) {
Node *temp = head;
while (temp != NULL) {
printf("%c", temp->data);
temp = temp->next;
}
printf("\n");
}
int main() {
Node *head = NULL;
insert(&head, 'H', 0);
insert(&head, 'e', 1);
insert(&head, 'l', 2);
insert(&head, 'l', 3);
insert(&head, 'o', 4);
printf("Current text: ");
display(head);
delete(&head, 2);
printf("After deletion: ");
display(head);
return 0;
}
- 总结与未来扩展:当前的文本编辑器实现了基本的增删与显示功能。在接下来的扩展中,可以考虑支持更复杂的命令系统、撤销/重做功能以及文件的导入与导出操作,从而逐步完善为全功能文本编辑器。
9.4 实现一个简单的文本编辑器(结合链表)
在这个项目中,您将尝试创建一个简化版的文本编辑器,专注于文本的存储和基本编辑功能。由于文本编辑需要频繁的插入和删除操作,因此链表是一种理想的数据结构选择。
-
核心功能:
- 文字插入:用户能够在文本的任意位置插入文字。
- 文字删除:用户能够删除特定位置的文字。
- 显示文本:能够展示当前编辑器中的文本。
-
实现要点:
- 采用双向链表结构实现文本的存储,每个节点存储一个字符的数据。
- 支持快速前后导航,在文本中进行插入和删除操作。
- 充分考虑用户输入和边界条件的处理,例如光标操作和行的切换。
详细实现步骤:
-
数据结构设计:
- 定义一个节点结构体,其中包含字符数据、指向前一个节点和后一个节点的指针。
- 初始化一个链表来表示文本,其中每个字符对应链表中的一个节点。
typedef struct Node { char data; struct Node *prev; struct Node *next; } Node; Node *head = NULL; // 链表的头部 Node *cursor = NULL; // 当前光标位置
-
文字插入:
- 创建一个新节点,并将其插入到当前光标位置。
- 更新链表的指针以维持链表的完整性。
void insert_char(char c) { Node *new_node = (Node *)malloc(sizeof(Node)); new_node->data = c; if (cursor == NULL) { // 插入到链表的开头 head = new_node; new_node->prev = NULL; new_node->next = NULL; cursor = new_node; } else { // 插入到光标之后 new_node->next = cursor->next; new_node->prev = cursor; if (cursor->next != NULL) { cursor->next->prev = new_node; } cursor->next = new_node; cursor = new_node; } }
-
文字删除:
- 删除光标位置的字符,调整链表的指针。
- 处理边界情况,如删除头部或尾部的节点。
void delete_char() { if (cursor == NULL) { return; // 无需删除 } Node *to_delete = cursor; if (cursor->prev != NULL) { cursor->prev->next = cursor->next; } else { head = cursor->next; // 删除第一个节点 } if (cursor->next != NULL) { cursor->next->prev = cursor->prev; cursor = cursor->next; } else { cursor = cursor->prev; // 移动到前一个节点 } free(to_delete); }
-
显示文本:
- 遍历链表,从头到尾打印出所有字符。
void display_text() { Node *current = head; while (current != NULL) { printf("%c", current->data); current = current->next; } printf("\n"); }
这个项目示例通过双向链表实现了一些基础的文本编辑功能。你可以在此基础上扩展更多功能,例如撤销操作、多行支持、剪切/复制/粘贴等。通过使用链表,能够以较高效的方式处理文本的插入和删除操作,这对理解动态数据结构和指针的运用大有裨益。