目录
- 一、树与图:数据结构新视野
- 二、树与图基础概念探秘
-
- [2.1 树的结构剖析](#2.1 树的结构剖析)
- [2.2 图的结构解析](#2.2 图的结构解析)
- 三、二叉树实战演练
-
- [3.1 二叉树的遍历](#3.1 二叉树的遍历)
-
- [3.1.1 递归实现](#3.1.1 递归实现)
- [3.1.2 实战:构建与验证](#3.1.2 实战:构建与验证)
- [3.2 二叉树的应用](#3.2 二叉树的应用)
-
- [3.2.1 实战:查找指定值](#3.2.1 实战:查找指定值)
- 四、图的实战探索
-
- [4.1 图的存储](#4.1 图的存储)
-
- [4.1.1 邻接表实现](#4.1.1 邻接表实现)
- [4.2 图的遍历](#4.2 图的遍历)
-
- [4.2.1 实战:DFS 遍历无向图](#4.2.1 实战:DFS 遍历无向图)
- [4.2.2 实战:BFS 查找最短路径](#4.2.2 实战:BFS 查找最短路径)
- 五、总结与展望
一、树与图:数据结构新视野
在 C 语言的世界里,数据结构是构建高效程序的基石。当我们处理复杂的关系和层次化的数据时,树与图这两种强大的数据结构就派上了用场。树,以其独特的层次结构,在文件系统、决策算法等场景中大放异彩,帮助我们组织和管理具有层级关系的数据。图,则更加灵活,它能够描述任意节点之间的复杂关系,在社交网络分析、路线规划等领域发挥着关键作用,让我们可以轻松处理各种网状结构的数据。接下来,让我们深入探索树与图的世界,了解它们的基本概念、实现方式以及在 C 语言中的实战应用。
二、树与图基础概念探秘
2.1 树的结构剖析
在数据结构的领域中,树是一种重要的非线性结构,其独特的层次关系,能够很好地模拟现实生活中的层级体系。以文件系统为例,根目录就如同树的根节点,是整个文件系统的起始点,所有其他目录和文件都从根目录衍生而来,根节点是整棵树的核心,没有父节点。
在树的结构里,除了根节点外,其他节点若连接着下层节点,便被称为子节点。这些子节点是构建树状结构的重要部分,它们通过与父节点的连接,形成了树的分支。如在一个公司的组织架构中,部门经理节点对于高层领导节点来说是子节点,同时对于下属员工节点而言,又扮演着父节点的角色 ,通过这种父子节点的关系,清晰地展现出公司的层级结构。
当节点不再连接任何子节点时,它就成为了叶子节点,处于树的最末端,是树结构的末梢。在文件系统中,普通文件就是叶子节点的典型代表,它们不再包含其他子目录或文件。以家庭树为例,没有后代的成员就是叶子节点,它们标志着家族分支的结束。 而二叉树,是一种特殊的树结构,每个节点最多拥有两个子节点,分别称为左子节点和右子节点,这种简洁而规整的结构,在许多算法和数据处理中都有广泛应用。
二叉搜索树则进一步对节点的值进行了规定,其左子树中所有节点的值都小于根节点的值,右子树中所有节点的值都大于根节点的值,并且左右子树本身也都是二叉搜索树,这一特性使得二叉搜索树在查找、插入和删除操作上具有高效性,时间复杂度平均为 O (logn) ,就像在一个有序的图书馆书架上查找书籍,我们可以根据书籍的编号范围,快速定位到目标书籍所在的区域。
2.2 图的结构解析
图是由顶点和边组成的复杂数据结构,用于表示对象之间的任意关系。顶点,也叫节点,是图中的基本元素,代表各种实体,比如在社交网络中,每个用户都可以看作是一个顶点。边则是连接顶点的纽带,体现顶点之间的关系。在社交网络中,用户之间的好友关系就可以用边来表示。
根据边的方向性,图可分为无向图和有向图。在无向图中,边没有方向,顶点之间的关系是对称的。就像城市之间的道路,如果两个城市之间有一条双向通行的道路,那么从城市 A 到城市 B 和从城市 B 到城市 A 都可以通过这条道路实现,这条道路就如同无向图中的边。在有向图中,边具有方向性,顶点之间的关系是非对称的。以网页链接为例,网页 A 链接到网页 B,但网页 B 不一定链接到网页 A,这种单向的链接关系就如同有向图中的有向边。
在 C 语言中,图的存储方式主要有邻接矩阵和邻接表。邻接矩阵是用一个二维数组来表示图中顶点之间的连接关系。对于一个具有 n 个顶点的图,其邻接矩阵是一个 n×n 的二维数组。若图中存在从顶点 i 到顶点 j 的边,则矩阵元素 A [i][j] 为 1(对于有权图,则为边的权值),否则为 0。邻接矩阵的优点是简单直观,易于实现,能够快速判断两个顶点之间是否存在边,适用于稠密图,即边数较多的图。但它的空间复杂度较高,对于边数较少的稀疏图,会造成大量的空间浪费。
邻接表则是用链表来存储图的边信息。每个顶点对应一个链表,链表中的每个节点表示与该顶点相邻的其他顶点。这种存储方式适合稀疏图,因为它只存储实际存在的边,能够节省大量空间。在邻接表中查找两个顶点之间是否存在边,需要遍历链表,时间复杂度较高,但在遍历一个顶点的所有邻接顶点时效率较高。例如,在存储大规模的社交网络数据时,由于每个用户的好友数量相对整个用户数量较少,使用邻接表可以大大减少存储空间的占用。
三、二叉树实战演练
3.1 二叉树的遍历
3.1.1 递归实现
在 C 语言中,使用递归实现二叉树的遍历是一种简洁而直观的方法。下面是前序、中序和后序遍历的递归代码实现:
c
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点结构体
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
// 前序遍历:根→左→右
void preorderTraversal(TreeNode* root) {
if (root != NULL) {
printf("%d ", root->val); // 输出根节点值
preorderTraversal(root->left); // 递归遍历左子树
preorderTraversal(root->right); // 递归遍历右子树
}
}
// 中序遍历:左→根→右
void inorderTraversal(TreeNode* root) {
if (root != NULL) {
inorderTraversal(root->left); // 递归遍历左子树
printf("%d ", root->val); // 输出根节点值
inorderTraversal(root->right); // 递归遍历右子树
}
}
// 后序遍历:左→右→根
void postorderTraversal(TreeNode* root) {
if (root != NULL) {
postorderTraversal(root->left); // 递归遍历左子树
postorderTraversal(root->right); // 递归遍历右子树
printf("%d ", root->val); // 输出根节点值
}
}
// 创建新节点
TreeNode* createNode(int val) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
newNode->val = val;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
int main() {
// 手动构建一棵简单的二叉树
TreeNode* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
printf("前序遍历结果: ");
preorderTraversal(root);
printf("\n");
printf("中序遍历结果: ");
inorderTraversal(root);
printf("\n");
printf("后序遍历结果: ");
postorderTraversal(root);
printf("\n");
return 0;
}
上述代码定义了二叉树节点结构体,并实现了前序、中序和后序遍历的递归函数。在main函数中,手动构建了一棵简单的二叉树,并调用这三个遍历函数输出遍历结果。运行该程序,将得到如下输出:
c
前序遍历结果: 1 2 4 5 3
中序遍历结果: 4 2 5 1 3
后序遍历结果: 4 5 2 3 1
3.1.2 实战:构建与验证
接下来,我们通过实战来构建一棵二叉搜索树,并通过中序遍历验证其有序性。
c
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点结构体
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
// 创建新节点
TreeNode* createNode(int val) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
newNode->val = val;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 插入节点到二叉搜索树
TreeNode* insertNode(TreeNode* root, int val) {
if (root == NULL) {
return createNode(val);
}
if (val < root->val) {
root->left = insertNode(root->left, val);
} else if (val > root->val) {
root->right = insertNode(root->right, val);
}
return root;
}
// 中序遍历:左→根→右
void inorderTraversal(TreeNode* root) {
if (root != NULL) {
inorderTraversal(root->left);
printf("%d ", root->val);
inorderTraversal(root->right);
}
}
int main() {
TreeNode* root = NULL;
int values[] = {5, 3, 7, 2, 4, 6, 8};
int n = sizeof(values) / sizeof(values[0]);
// 构建二叉搜索树
for (int i = 0; i < n; i++) {
root = insertNode(root, values[i]);
}
printf("中序遍历二叉搜索树结果: ");
inorderTraversal(root);
printf("\n");
return 0;
}
在这段代码中,insertNode函数用于将节点插入到二叉搜索树中。main函数通过一个数组中的值构建二叉搜索树,并调用inorderTraversal函数进行中序遍历。由于二叉搜索树的性质,中序遍历的结果应该是升序的。运行该程序,输出结果如下:
c
中序遍历二叉搜索树结果: 2 3 4 5 6 7 8
通过输出结果可以验证,二叉搜索树的中序遍历确实是有序的。
3.2 二叉树的应用
3.2.1 实战:查找指定值
在二叉搜索树中查找指定值是其重要应用之一。下面的代码实现了在二叉搜索树中查找指定值的功能,并返回是否存在:
c
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树节点结构体
typedef struct TreeNode {
int val;
struct TreeNode* left;
struct TreeNode* right;
} TreeNode;
// 创建新节点
TreeNode* createNode(int val) {
TreeNode* newNode = (TreeNode*)malloc(sizeof(TreeNode));
newNode->val = val;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
// 插入节点到二叉搜索树
TreeNode* insertNode(TreeNode* root, int val) {
if (root == NULL) {
return createNode(val);
}
if (val < root->val) {
root->left = insertNode(root->left, val);
} else if (val > root->val) {
root->right = insertNode(root->right, val);
}
return root;
}
// 在二叉搜索树中查找指定值
int searchNode(TreeNode* root, int target) {
if (root == NULL) {
return 0; // 未找到
}
if (root->val == target) {
return 1; // 找到
} else if (target < root->val) {
return searchNode(root->left, target);
} else {
return searchNode(root->right, target);
}
}
int main() {
TreeNode* root = NULL;
int values[] = {5, 3, 7, 2, 4, 6, 8};
int n = sizeof(values) / sizeof(values[0]);
// 构建二叉搜索树
for (int i = 0; i < n; i++) {
root = insertNode(root, values[i]);
}
int target = 4;
if (searchNode(root, target)) {
printf("值 %d 存在于二叉搜索树中\n", target);
} else {
printf("值 %d 不存在于二叉搜索树中\n", target);
}
target = 9;
if (searchNode(root, target)) {
printf("值 %d 存在于二叉搜索树中\n", target);
} else {
printf("值 %d 不存在于二叉搜索树中\n", target);
}
return 0;
}
在上述代码中,searchNode函数通过递归的方式在二叉搜索树中查找指定值。如果找到目标值,返回 1;否则返回 0。main函数构建二叉搜索树后,分别查找值 4 和值 9,并输出查找结果。运行该程序,输出如下:
c
值 4 存在于二叉搜索树中
值 9 不存在于二叉搜索树中
通过这个例子,我们展示了二叉搜索树在查找操作上的高效性,平均时间复杂度为 O (logn)。
四、图的实战探索
4.1 图的存储
4.1.1 邻接表实现
邻接表是一种用链表存储图中每个顶点的邻接顶点的数据结构。在 C 语言中,可以通过结构体和指针来实现邻接表。以下是一个简单的无向图邻接表实现示例:
c
#include <stdio.h>
#include <stdlib.h>
// 定义边表节点结构体
typedef struct EdgeNode {
int adjvex; // 邻接顶点的编号
struct EdgeNode* next; // 指向下一个邻接顶点的指针
} EdgeNode;
// 定义顶点表节点结构体
typedef struct VertexNode {
int data; // 顶点数据
EdgeNode* firstedge; // 指向第一个邻接顶点的指针
} VertexNode, AdjList[100]; // 假设图最多有100个顶点
// 定义图结构体
typedef struct {
AdjList vertices;
int numVertices, numEdges; // 图的顶点数和边数
} Graph;
// 创建新的边表节点
EdgeNode* createEdgeNode(int v) {
EdgeNode* newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
newNode->adjvex = v;
newNode->next = NULL;
return newNode;
}
// 初始化图
void initGraph(Graph* g, int n) {
g->numVertices = n;
g->numEdges = 0;
for (int i = 0; i < n; i++) {
g->vertices[i].data = i;
g->vertices[i].firstedge = NULL;
}
}
// 添加边
void addEdge(Graph* g, int v1, int v2) {
EdgeNode* newEdge1 = createEdgeNode(v2);
newEdge1->next = g->vertices[v1].firstedge;
g->vertices[v1].firstedge = newEdge1;
EdgeNode* newEdge2 = createEdgeNode(v1);
newEdge2->next = g->vertices[v2].firstedge;
g->vertices[v2].firstedge = newEdge2;
g->numEdges++;
}
// 打印邻接表
void printAdjList(Graph* g) {
for (int i = 0; i < g->numVertices; i++) {
printf("顶点 %d 的邻接表: ", g->vertices[i].data);
EdgeNode* p = g->vertices[i].firstedge;
while (p != NULL) {
printf("%d -> ", p->adjvex);
p = p->next;
}
printf("NULL\n");
}
}
int main() {
Graph g;
initGraph(&g, 5); // 初始化一个有5个顶点的图
addEdge(&g, 0, 1);
addEdge(&g, 0, 2);
addEdge(&g, 1, 2);
addEdge(&g, 2, 3);
addEdge(&g, 3, 4);
printAdjList(&g);
return 0;
}
在上述代码中,EdgeNode结构体表示边表节点,VertexNode结构体表示顶点表节点,Graph结构体表示整个图。createEdgeNode函数用于创建新的边表节点,initGraph函数用于初始化图,addEdge函数用于向图中添加边,printAdjList函数用于打印邻接表。
邻接表适合稀疏图,因为它只存储实际存在的边,对于稀疏图而言,边数相对顶点数较少,使用邻接表可以节省大量的存储空间。而邻接矩阵对于稀疏图会造成大量的空间浪费,因为它需要为每个顶点对都分配存储空间,无论它们之间是否有边相连。
4.2 图的遍历
4.2.1 实战:DFS 遍历无向图
深度优先搜索(DFS)是一种用于遍历图的算法,它类似于树的前序遍历。从某个顶点开始,DFS 会尽可能深入地访问图中的节点,直到无法继续,然后回溯到上一个节点,继续探索其他路径。以下是使用 C 语言实现 DFS 遍历无向图的代码:
c
#include <stdio.h>
#include <stdlib.h>
// 定义边表节点结构体
typedef struct EdgeNode {
int adjvex; // 邻接顶点的编号
struct EdgeNode* next; // 指向下一个邻接顶点的指针
} EdgeNode;
// 定义顶点表节点结构体
typedef struct VertexNode {
int data; // 顶点数据
EdgeNode* firstedge; // 指向第一个邻接顶点的指针
} VertexNode, AdjList[100]; // 假设图最多有100个顶点
// 定义图结构体
typedef struct {
AdjList vertices;
int numVertices, numEdges; // 图的顶点数和边数
} Graph;
// 创建新的边表节点
EdgeNode* createEdgeNode(int v) {
EdgeNode* newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
newNode->adjvex = v;
newNode->next = NULL;
return newNode;
}
// 初始化图
void initGraph(Graph* g, int n) {
g->numVertices = n;
g->numEdges = 0;
for (int i = 0; i < n; i++) {
g->vertices[i].data = i;
g->vertices[i].firstedge = NULL;
}
}
// 添加边
void addEdge(Graph* g, int v1, int v2) {
EdgeNode* newEdge1 = createEdgeNode(v2);
newEdge1->next = g->vertices[v1].firstedge;
g->vertices[v1].firstedge = newEdge1;
EdgeNode* newEdge2 = createEdgeNode(v1);
newEdge2->next = g->vertices[v2].firstedge;
g->vertices[v2].firstedge = newEdge2;
g->numEdges++;
}
// 访问标记数组,记录每个顶点是否已被访问
int visited[100] = {0};
// DFS函数
void DFS(Graph* g, int v) {
visited[v] = 1; // 标记当前顶点已访问
printf("%d ", v); // 输出当前顶点
EdgeNode* p = g->vertices[v].firstedge;
while (p != NULL) {
int adj = p->adjvex;
if (!visited[adj]) {
DFS(g, adj); // 递归访问未访问的邻接顶点
}
p = p->next;
}
}
int main() {
Graph g;
initGraph(&g, 5); // 初始化一个有5个顶点的图
addEdge(&g, 0, 1);
addEdge(&g, 0, 2);
addEdge(&g, 1, 2);
addEdge(&g, 2, 3);
addEdge(&g, 3, 4);
printf("从顶点0开始的DFS遍历结果: ");
DFS(&g, 0);
printf("\n");
return 0;
}
在这段代码中,DFS函数通过递归实现深度优先搜索。它从指定的顶点开始,标记该顶点为已访问并输出,然后递归访问其所有未访问的邻接顶点。visited数组用于记录每个顶点是否已被访问,以避免重复访问。运行上述代码,将得到从顶点 0 开始的 DFS 遍历结果。
4.2.2 实战:BFS 查找最短路径
广度优先搜索(BFS)是另一种用于遍历图的算法,它类似于树的层序遍历。BFS 从某个顶点开始,逐层地访问图中的节点,先访问距离起始顶点较近的节点,再访问距离较远的节点。在无权图中,BFS 可以用来查找两个顶点之间的最短路径。以下是使用 C 语言实现 BFS 查找无权图中两个顶点最短路径的代码:
c
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 定义边表节点结构体
typedef struct EdgeNode {
int adjvex; // 邻接顶点的编号
struct EdgeNode* next; // 指向下一个邻接顶点的指针
} EdgeNode;
// 定义顶点表节点结构体
typedef struct VertexNode {
int data; // 顶点数据
EdgeNode* firstedge; // 指向第一个邻接顶点的指针
} VertexNode, AdjList[100]; // 假设图最多有100个顶点
// 定义图结构体
typedef struct {
AdjList vertices;
int numVertices, numEdges; // 图的顶点数和边数
} Graph;
// 创建新的边表节点
EdgeNode* createEdgeNode(int v) {
EdgeNode* newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
newNode->adjvex = v;
newNode->next = NULL;
return newNode;
}
// 初始化图
void initGraph(Graph* g, int n) {
g->numVertices = n;
g->numEdges = 0;
for (int i = 0; i < n; i++) {
g->vertices[i].data = i;
g->vertices[i].firstedge = NULL;
}
}
// 添加边
void addEdge(Graph* g, int v1, int v2) {
EdgeNode* newEdge1 = createEdgeNode(v2);
newEdge1->next = g->vertices[v1].firstedge;
g->vertices[v1].firstedge = newEdge1;
EdgeNode* newEdge2 = createEdgeNode(v1);
newEdge2->next = g->vertices[v2].firstedge;
g->vertices[v2].firstedge = newEdge2;
g->numEdges++;
}
// 队列结构体
typedef struct {
int data[100];
int front, rear;
} Queue;
// 初始化队列
void initQueue(Queue* q) {
q->front = q->rear = 0;
}
// 入队操作
void enqueue(Queue* q, int v) {
q->data[q->rear++] = v;
}
// 出队操作
int dequeue(Queue* q) {
return q->data[q->front++];
}
// 判断队列是否为空
bool isQueueEmpty(Queue* q) {
return q->front == q->rear;
}
// 访问标记数组,记录每个顶点是否已被访问
bool visited[100] = {false};
// 记录每个顶点的前驱顶点,用于构建最短路径
int parent[100];
// BFS函数
void BFS(Graph* g, int start, int end) {
Queue q;
initQueue(&q);
visited[start] = true;
parent[start] = -1; // 起始顶点无前驱
enqueue(&q, start);
while (!isQueueEmpty(&q)) {
int v = dequeue(&q);
if (v == end) {
// 找到目标顶点,构建并输出最短路径
printf("从顶点 %d 到顶点 %d 的最短路径: ", start, end);
int current = end;
while (current != -1) {
printf("%d ", current);
current = parent[current];
}
printf("\n");
return;
}
EdgeNode* p = g->vertices[v].firstedge;
while (p != NULL) {
int adj = p->adjvex;
if (!visited[adj]) {
visited[adj] = true;
parent[adj] = v;
enqueue(&q, adj);
}
p = p->next;
}
}
printf("从顶点 %d 到顶点 %d 没有路径\n", start, end);
}
int main() {
Graph g;
initGraph(&g, 5); // 初始化一个有5个顶点的图
addEdge(&g, 0, 1);
addEdge(&g, 0, 2);
addEdge(&g, 1, 2);
addEdge(&g, 2, 3);
addEdge(&g, 3, 4);
BFS(&g, 0, 4);
return 0;
}
在这段代码中,BFS函数使用队列来实现广度优先搜索。从起始顶点开始,将其标记为已访问并加入队列,同时记录其前驱顶点为 - 1。然后不断从队列中取出顶点,访问其未访问的邻接顶点,将它们标记为已访问并加入队列,同时记录它们的前驱顶点。当找到目标顶点时,通过前驱顶点数组回溯构建最短路径并输出。如果遍历完所有可达顶点仍未找到目标顶点,则输出没有路径。
五、总结与展望
在本次关于树与图的 C 语言实战探索中,我们深入了解了树与图的基础概念。树的独特层次结构,从根节点、子节点到叶子节点的层层衍生,以及二叉树、二叉搜索树的特殊性质,为我们处理层级关系数据提供了高效的方式。图则以顶点和边构建起复杂的关系网络,无向图与有向图的区分,邻接矩阵和邻接表的存储方式,使其能够适应各种复杂关系的表示。
在二叉树的实战部分,我们通过递归实现了前序、中序和后序遍历,清晰地展现了二叉树节点的访问顺序。构建二叉搜索树并验证其有序性,以及在二叉搜索树中查找指定值的操作,让我们深刻体会到二叉搜索树在排序和查找方面的高效性。
图的实战中,邻接表的实现让我们掌握了稀疏图的存储方法,深度优先搜索和广度优先搜索算法则为我们提供了遍历图的有效手段。通过 DFS 遍历无向图和 BFS 查找无权图中两个顶点的最短路径,我们解决了实际的图相关问题。
树与图作为重要的数据结构,在 C 语言编程中具有广泛的应用前景。希望读者能够继续深入学习数据结构相关知识,不断拓展自己的编程能力,在实际项目中灵活运用树与图,解决更多复杂的问题。