简介:严蔚敏教授编写的《数据结构》教材是计算机专业教学的基石,提供了数据结构和算法的理论与实践。本配套实现程序深入讲解了数组、链表、栈、队列、树、图等数据结构和多种排序、搜索、图算法的C语言实现,帮助读者通过实际编程加深对这些概念的理解,并提高编程及问题解决能力。掌握这些基础知识对于优化程序性能和通过编程面试至关重要。
1. 数据结构基础介绍
数据结构是计算机存储、组织数据的方式,它旨在以更加高效的方式访问和修改数据。对于任何想要在 IT 领域深耕的从业者来说,掌握数据结构都是基础且关键的一步。
数据结构的定义和重要性
数据结构不仅包括了数据元素本身,还涵盖了元素之间的关系,以及对数据的多种操作。例如数组、链表、栈、队列、树、图等,这些结构能够帮助我们更好地处理和分析数据,是算法设计和软件开发不可或缺的组成部分。
常见的数据结构类型
线性结构
线性结构是一种简单直观的数据结构,它包括数组、链表等,用于存储一系列的元素,元素之间存在一对一的线性关系。
树形结构
树形结构是对一系列具有层次关系的数据进行建模的方法,它包括二叉树、多叉树、B树等。树形结构在数据库索引、文件系统等领域有广泛的应用。
图形结构
图形结构由节点(顶点)和连接这些节点的边组成,它用于描述实体之间的复杂关系,如社交网络、交通网络等。
哈希结构
哈希结构通过哈希函数将数据映射到表中的位置,适用于快速查找。哈希表是一种常见的实现方式,广泛应用于数据缓存、数据库索引等场景。
理解这些基础数据结构的定义、特性和应用,是深入学习数据结构和算法的起点。这将有助于我们在解决实际问题时,能够选择合适的工具,实现更加高效的软件开发和数据分析。在后续章节中,我们将探索更多关于数据结构的细节及其在算法中的应用。
2. 算法概念和应用
2.1 算法的基本概念
2.1.1 算法定义
算法是一组定义明确的指令,用于完成特定任务或解决特定问题。它是计算机程序的核心,由一系列计算步骤和决策规则构成。算法的设计需要考虑问题的具体需求、数据的输入输出、运算的准确性和效率。
2.1.2 算法特性
算法具有以下特性:
- 有穷性 :算法的步骤在有限时间内必须能够完成。
- 确定性 :每一步骤都清晰明确,不会出现歧义。
- 可行性 :算法的每一步都是可执行的。
- 输入 :算法至少有一个输入。
- 输出 :算法至少有一个输出,是对应输入的解。
2.1.3 算法效率
算法效率是指算法在计算机上执行所需的时间和空间资源。在时间上,主要关注算法的运行时间复杂度;在空间上,关注算法的空间复杂度。效率高的算法是那些在较少的时间和空间内完成任务的算法。
2.2 算法的设计和分析
2.2.1 设计算法的步骤
- 明确问题:清楚算法需要解决的问题是什么。
- 设计策略:确定算法的基本方法和策略。
- 确定数据结构:选择合适的数据结构存储信息。
- 编写程序:将算法逻辑转换为程序代码。
- 测试和调试:确保算法正确无误地执行。
- 分析和优化:评估算法性能并进行优化。
2.2.2 算法的时间复杂度
时间复杂度是衡量算法运行时间的一个指标,通常用大O符号表示。例如,一个线性搜索算法的时间复杂度是O(n),它表示运行时间随着输入数据量n的增长而线性增长。常见的有O(1)、O(log n)、O(n)、O(n log n)、O(n^2)等。
2.2.3 空间复杂度的分析
空间复杂度关注算法执行过程中临时占用存储空间的大小。它同样用大O符号来表示,如O(1)表示常数空间复杂度,算法占用的空间不随输入数据的规模变化。
2.3 算法的应用场景
2.3.1 数据处理
算法在数据处理中扮演重要角色。例如,使用排序算法将数据集整理成有序状态,以便于快速检索、插入和删除操作。数据压缩、数据加密等也需要复杂的算法来实现。
2.3.2 问题解决
在科学计算、人工智能、机器学习等领域,算法被用来解决优化问题、预测分析等。复杂问题如旅行商问题(TSP)或图的着色问题等,通常通过设计特定的算法来寻求最优化的解决方案。
以上是第二章的核心内容,强调了算法定义、特性、效率的概念,以及算法设计和分析的方法。同时,本章还介绍了算法在数据处理和问题解决中的实际应用场景。在接下来的章节中,我们将深入探讨排序、搜索和图算法的实现及其应用。
3. C语言实现数据结构和算法
3.1 C语言基础回顾
3.1.1 C语言数据类型
C语言提供了多种数据类型,包括基本类型(如整型和浮点型)、构造类型(如数组、结构体、联合体和枚举)、指针类型以及void类型。基本类型根据其表示的数据大小和范围可以分为整型(int)、字符型(char)、浮点型(float和double)等。这些类型是构成更复杂数据结构的基础。
int main() {
int integerVar = 10;
char charVar = 'A';
float floatVar = 3.14f;
double doubleVar = 3.14159;
// 指针类型和void类型在后续章节中进行介绍
}
3.1.2 C语言控制结构
控制结构是C语言中用来控制程序执行流程的语句,主要包括选择结构、循环结构和跳转语句。选择结构如 if-else
和 switch
语句用于条件判断,循环结构如 for
、 while
和 do-while
用于重复执行代码块,跳转语句如 break
、 continue
和 goto
用于程序控制流的非顺序执行。
int main() {
int num = 5;
if (num > 0) {
printf("Number is positive.\n");
} else {
printf("Number is non-positive.\n");
}
for (int i = 0; i < num; i++) {
if (i == 2) continue; // 跳过当前循环的剩余部分
printf("%d ", i);
}
}
3.1.3 C语言函数和模块化编程
函数是C语言中实现模块化编程的基本单位。它允许我们将代码分割成独立的模块,每个模块实现特定的功能。函数的定义包括返回类型、函数名、参数列表和函数体。模块化编程有助于提高代码的可读性、可维护性和可复用性。
#include <stdio.h>
int add(int a, int b) {
return a + b; // 返回两数之和
}
int main() {
int sum = add(3, 4);
printf("Sum is: %d\n", sum);
return 0;
}
3.2 C语言中数据结构的实现
3.2.1 线性表的实现
线性表是具有相同数据类型的一组数据的有序集合。在C语言中,线性表通常可以使用数组来实现。数组提供了固定大小的连续内存空间,支持随机访问。
#define MAX_SIZE 100
int linearList[MAX_SIZE];
int size = 0;
void insert(int index, int value) {
if (index < 0 || index > size) {
printf("Insertion position out of range.\n");
return;
}
if (size == MAX_SIZE) {
printf("Linear list is full.\n");
return;
}
for (int i = size; i > index; i--) {
linearList[i] = linearList[i - 1];
}
linearList[index] = value;
size++;
}
3.2.2 栈和队列的实现
栈是一种后进先出(LIFO)的数据结构,而队列是一种先进先出(FIFO)的数据结构。在C语言中,可以通过数组或链表来实现它们。
#define STACK_SIZE 100
int stack[STACK_SIZE];
int top = -1;
void push(int value) {
if (top == STACK_SIZE - 1) {
printf("Stack is full.\n");
return;
}
stack[++top] = value;
}
int pop() {
if (top == -1) {
printf("Stack is empty.\n");
return -1;
}
return stack[top--];
}
3.2.3 树和图的实现
树是由节点组成的层级结构,图是由顶点(节点)和边组成的复杂网络结构。在C语言中,树可以通过结构体和指针来实现,而图通常用邻接矩阵或邻接表表示。
typedef struct Node {
int data;
struct Node* children[MAX_SIZE];
int numChildren;
} Node;
void addChildren(Node* parent, Node* child) {
parent->children[parent->numChildren++] = child;
}
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->numChildren = 0;
return newNode;
}
3.3 C语言中算法的实现
3.3.1 排序算法的实现
排序算法用于将一组数据按照特定的顺序进行排列。常见的排序算法有冒泡排序、选择排序、插入排序等。C语言实现排序算法时,可以利用数组或者链表。
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;
}
}
}
}
3.3.2 搜索算法的实现
搜索算法用于在一个数据集合中查找特定的元素。线性搜索是最基本的搜索算法,而二分搜索则是在有序数组中使用的一种效率更高的搜索方法。
int binarySearch(int arr[], int l, int r, int x) {
while (l <= r) {
int m = l + (r - l) / 2;
if (arr[m] == x) {
return m;
}
if (arr[m] < x) {
l = m + 1;
} else {
r = m - 1;
}
}
return -1;
}
3.3.3 图算法的实现
图算法在处理复杂数据结构时非常有用,例如在社交网络分析、地图导航等领域。图算法实现包括图的遍历、最短路径、最小生成树等。
void DFSUtil(Node* v, int visited[]) {
visited[v->data] = 1;
printf("%d ", v->data);
for (int i = 0; i < v->numChildren; i++) {
Node* child = v->children[i];
if (!visited[child->data]) {
DFSUtil(child, visited);
}
}
}
void DFS(Node* graph[], int numVertices) {
int* visited = (int*)calloc(numVertices, sizeof(int));
for (int v = 0; v < numVertices; ++v) {
if (!visited[v]) {
DFSUtil(graph[v], visited);
}
}
free(visited);
}
通过本章节的介绍,读者应该对C语言中基本的数据结构和算法的实现有了初步的理解。下一章节将详细探讨排序算法的种类和实现方式。
4. ```
第四章:排序算法的种类和实现
4.1 排序算法概述
4.1.1 排序算法的分类
排序算法可以根据不同的标准进行分类。按照算法的时间复杂度,可以分为比较类排序和非比较类排序。比较类排序依赖于元素间的比较来确定元素顺序,而非比较类排序则不依赖于元素间的比较。比较类排序的时间复杂度下限为O(n log n),如快速排序、归并排序等。非比较类排序包括计数排序、基数排序等,它们可以达到线性时间复杂度,但适用于特定情况。
4.1.2 各种排序算法的特点
每种排序算法都有自己的特点和适用场景。例如,冒泡排序易于实现,但在大数据集上效率低下;插入排序在几乎已经排序的数组上表现良好;快速排序在平均情况下表现优秀,但最差情况下可能退化至O(n^2);归并排序提供了稳定的O(n log n)排序,适合外部排序;堆排序是原地排序,但不是稳定的排序算法。了解每种排序算法的特点对于选择合适的排序方法至关重要。
4.2 常用排序算法的C语言实现
4.2.1 冒泡排序
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行的,直到没有再需要交换的元素为止。
C语言实现代码
void bubbleSort(int arr[], int n) {
int i, j, temp;
for (i = 0; i < n-1; i++) {
for (j = 0; j < n-i-1; j++) {
if (arr[j] > arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
该算法的逻辑是在相邻元素中进行比较,如果前一个元素比后一个元素大,就交换它们的位置。通过多轮遍历,每次遍历都将一个最大元素放置到正确的位置。该算法在数据量较少时效率尚可接受,但在数据量大时效率较低。
4.2.2 选择排序
选择排序算法是一种原址比较排序算法。它的工作原理是:首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
C语言实现代码
void selectionSort(int arr[], int n) {
int i, j, min_idx, temp;
for (i = 0; i < n-1; i++) {
min_idx = i;
for (j = i+1; j < n; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
temp = arr[min_idx];
arr[min_idx] = arr[i];
arr[i] = temp;
}
}
该算法的逻辑是在未排序序列中选出最小(或最大)元素,与序列的第一个元素交换位置。然后,再从剩余未排序元素中继续寻找最小(或最大)元素,与未排序序列的第一个元素交换位置。如此往复,直到所有元素均排序完毕。
4.2.3 插入排序
插入排序是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
C语言实现代码
void insertionSort(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
该算法的逻辑是每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
4.3 高级排序算法的C语言实现
4.3.1 快速排序
快速排序是由C. A. R. Hoare在1960年提出的一种划分交换排序。它采用分治法的策略来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。快速排序使用一个分隔值将数组分为两个子数组,左边的元素比分隔值小,右边的元素比分隔值大,然后递归地对这两个子数组进行快速排序。
C语言实现代码
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high);
quickSort(arr, low, pivot - 1);
quickSort(arr, pivot + 1, high);
}
}
该算法的逻辑是选择一个基准值(pivot),通过一趟排序将待排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
4.3.2 归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。作为一种典型的分治法策略,归并排序将输入数组分成两个大小相等的子数组,分别对它们进行排序,然后将结果合并起来。归并排序的效率为O(n log n),它是稳定的排序算法。
C语言实现代码
void merge(int arr[], int l, int m, int r) {
int i, j, k;
int n1 = m - l + 1;
int n2 = r - m;
int L[n1], R[n2];
for (i = 0; i < n1; i++)
L[i] = arr[l + i];
for (j = 0; j < n2; j++)
R[j] = arr[m + 1 + j];
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++;
}
}
该算法的逻辑是将数组分成两半,对每部分递归地应用归并排序,然后将排序好的两部分合并。合并操作是将两个已排序的数组归并成一个更大的有序数组。
4.3.3 堆排序
堆排序是一种基于比较的排序算法,通过构建二叉堆(一种特殊的完全二叉树),并利用堆这种数据结构的特性进行排序。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。
C语言实现代码
void heapify(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) {
int swap = arr[i];
arr[i] = arr[largest];
arr[largest] = swap;
heapify(arr, n, largest);
}
}
该算法的逻辑是首先将待排序的数组构造成一个大顶堆(或小顶堆),然后将堆顶元素与堆的最后一个元素交换,再重新调整剩余元素为一个新的大顶堆(或小顶堆)。重复这个过程直到所有元素都被排序。
## 4.3.4 对比各排序算法性能
为了比较不同的排序算法性能,我们可以从时间复杂度、空间复杂度、稳定性以及适用场景等方面进行分析。
### 时间复杂度和空间复杂度
- **冒泡排序**: 时间复杂度最差为O(n^2),最好为O(n),空间复杂度为O(1)。
- **选择排序**: 时间复杂度总是O(n^2),空间复杂度为O(1)。
- **插入排序**: 时间复杂度最差和平均为O(n^2),最好为O(n),空间复杂度为O(1)。
- **快速排序**: 时间复杂度平均为O(n log n),最差为O(n^2),空间复杂度为O(log n)。
- **归并排序**: 时间复杂度为O(n log n),空间复杂度为O(n)。
- **堆排序**: 时间复杂度总是O(n log n),空间复杂度为O(1)。
### 稳定性和适用场景
- **冒泡排序、插入排序**: 稳定排序,适用于数据规模较小的场景。
- **快速排序、归并排序**: 不稳定排序,快速排序由于其优异的平均性能,是实际应用中最常用的排序算法之一。归并排序在稳定性和排序速度之间提供了一个较好的平衡,适合于外部排序。
- **堆排序**: 不稳定排序,由于空间复杂度低,适合于对大数据集进行原地排序。
5. 搜索算法的种类和实现
5.1 搜索算法概述
5.1.1 搜索算法的基本概念
搜索算法是用于在数据集合中找到特定项的算法。在计算机科学中,搜索算法通常用来确定一个元素是否存在于一个数据结构中。其基本目标是找到一个值或一组值,或者检查某个值是否存在。
5.1.2 搜索算法的分类
搜索算法可以根据数据结构的不同被分为两大类:基于无序集合的搜索和基于有序集合的搜索。无序集合的搜索主要依赖线性搜索,而有序集合则可以采用二分搜索、广度优先搜索(BFS)和深度优先搜索(DFS)等。此外,还有一些高级搜索算法,比如A*搜索算法,广泛应用于路径查找、人工智能等领域。
5.2 基本搜索算法的C语言实现
5.2.1 线性搜索
线性搜索是最基本的搜索算法。它遍历所有元素直到找到所需的项或遍历完所有元素。其时间复杂度为O(n),其中n为集合的大小。
#include <stdio.h>
// 线性搜索函数
int linearSearch(int arr[], int size, int target) {
for (int i = 0; i < size; i++) {
if (arr[i] == target) {
return i; // 返回找到元素的索引
}
}
return -1; // 未找到元素时返回-1
}
int main() {
int data[] = {1, 3, 5, 7, 9};
int index = linearSearch(data, 5, 7);
if (index != -1) {
printf("Found target at index: %d\n", index);
} else {
printf("Target not found.\n");
}
return 0;
}
5.2.2 二分搜索
二分搜索是一种在有序数组中查找特定元素的高效算法。它通过比较数组中间元素与目标值的大小来缩小搜索范围,时间复杂度为O(log n)。
#include <stdio.h>
// 二分搜索函数
int binarySearch(int arr[], int size, int target) {
int left = 0;
int right = size - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid; // 返回找到元素的索引
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到元素时返回-1
}
int main() {
int data[] = {1, 3, 5, 7, 9, 11, 13, 15};
int index = binarySearch(data, 8, 7);
if (index != -1) {
printf("Found target at index: %d\n", index);
} else {
printf("Target not found.\n");
}
return 0;
}
5.3 高级搜索算法的C语言实现
5.3.1 广度优先搜索
广度优先搜索(BFS)是一种从根节点开始,逐层向外扩展的搜索算法,适用于图结构数据。通过使用队列来跟踪待访问的节点,遍历图中的所有节点。
5.3.2 深度优先搜索
深度优先搜索(DFS)是一种沿着图的分支进行搜索的方法,它会尽可能深地搜索图的分支。实现时使用递归或栈来跟踪路径。
5.3.3 A*搜索算法
A*搜索算法是一种启发式搜索算法,用于寻找最短路径。它结合了最佳优先搜索和Dijkstra算法的特点,适用于路径查找和图形界面的交互设计。
以上就是搜索算法的种类和实现。在实际应用中,选择合适的搜索算法能够极大地优化问题求解的效率。下一章节将介绍图算法的种类和实现,我们将探讨图遍历和搜索算法、最短路径算法以及最小生成树算法等。
简介:严蔚敏教授编写的《数据结构》教材是计算机专业教学的基石,提供了数据结构和算法的理论与实践。本配套实现程序深入讲解了数组、链表、栈、队列、树、图等数据结构和多种排序、搜索、图算法的C语言实现,帮助读者通过实际编程加深对这些概念的理解,并提高编程及问题解决能力。掌握这些基础知识对于优化程序性能和通过编程面试至关重要。