😃C语言的算法总结😃
一个好的程序员必须深精算法之道,这样才能编写出高效且简洁的程序。算法学的好,程序将写的简介又高效,下面是本猿花大量时间整理的算法总结,希望对正在学习的你有所帮助,有什么错误的地方也希望各位大佬指点,咱们共同成长,一起携手奔赴美好的明天。
什么是算法呢?
定义
:算法(Algorithm)就是定义良好的计算过程,它取一个或一组值为输入,并产生一个或一组作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出的结合。
目录总览
🔥排序算法:
1.冒泡排序
定义:冒泡排序(Bubble Sort)是一种简单的排序算法,它的基本思想是重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。在每次遍历中,它都会找出未排序序列中最大(或最小)的元素,并将其放到已排序序列的起始位置。这个过程像水泡在水中上浮一样,因此得名冒泡排序。
冒泡排序的时间复杂度为O(n^2),虽然它的实现方法简单易懂,但是对于大规模的数据排序来说效率较低,通常不被作为实际应用的排序算法,仅在教学中作为基本排序算法进行讲解。
代码示例:
void bubble_sort(int a[], int n) {
int i, j, tmp;
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - i - 1; j++) {
if (a[j] > a[j + 1]) {
tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}
2.快速排序
定义:快速排序的基本思路是选取一个基准值(pivot),将数组分为左右两部分,左边的元素都小于等于基准值,右边的元素都大于等于基准值,然后分别对左右两部分递归地进行快速排序。在本代码中,我们选取了数组左端的元素作为基准值,用双指针法将左右两部分分开,并将小于基准值的元素移到左边、大于基准值的元素移到右边,最后将基准值插入到左右两部分的分界点上。
代码示例:
#include <stdio.h>
void quick_sort(int arr[], int left, int right) {
if (left >= right) {
return;
}
int pivot = arr[left];
int i = left, j = right;
while (i < j) {
while (i < j && arr[j] > pivot) {
j--;
}
if (i < j) {
arr[i++] = arr[j];
}
while (i < j && arr[i] < pivot) {
i++;
}
if (i < j) {
arr[j--] = arr[i];
}
}
arr[i] = pivot;
quick_sort(arr, left, i - 1);
quick_sort(arr, i + 1, right);
}
int main() {
int arr[] = {5, 1, 7, 3, 9, 2, 8, 4, 6};
int n = sizeof(arr) / sizeof(arr[0]);
quick_sort(arr, 0, n - 1);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
3.插入排序
定义:插入排序的基本思路是将数组分为已排序部分和未排序部分,每次从未排序部分取出一个元素,插入到已排序部分中的合适位置,直到未排序部分为空。在本代码中,我们从数组的第二个元素开始,将其插入到前面已排序部分中的合适位置,然后继续取下一个未排序元素进行插入操作,直到所有元素都被插入到已排序部分中。插入操作时,我们从后往前比较已排序部分中的元素,将大于当前元素的元素向右移动,直到找到合适的插入位置。
代码示例:
#include <stdio.h>
void insertion_sort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int temp = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
}
int main() {
int arr[] = {5, 1, 7, 3, 9, 2, 8, 4, 6};
int n = sizeof(arr) / sizeof(arr[0]);
insertion_sort(arr, n);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
4.选择排序
定义:选择排序的基本思路是将数组分为已排序部分和未排序部分,每次从未排序部分中选取最小(或最大)的元素,插入到已排序部分的末尾,直到未排序部分为空。在本代码中,我们从数组的第一个元素开始,将其与后面的元素进行比较,找到最小的元素,然后将其与第一个元素交换位置,这样第一个元素就是已排序部分中的最小元素。然后继续从第二个元素开始,重复上述操作,直到所有元素都被插入到已排序部分中。
#include <stdio.h>
void selection_sort(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;
}
}
if (min_idx != i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}
int main() {
int arr[] = {5, 1, 7, 3, 9, 2, 8, 4, 6};
int n = sizeof(arr) / sizeof(arr[0]);
selection_sort(arr, n);
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
🔥查找算法:
1.二分查找
定义:二分查找(Binary Search)是一种在有序数组中查找某一特定元素的搜索算法。它的算法思想是将数组分成两个部分,判断中间元素与目标元素的大小关系,如果相等则返回中间元素的位置,如果中间元素大于目标元素,则在左半部分继续查找,否则在右半部分查找,直到找到目标元素或者整个数组都被查找完毕。
以下是C语言中的二分查找示例代码:
int binary_search(int arr[], int n, int target) {
int left = 0;
int right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
else if (arr[mid] > target) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return -1; // 表示未找到目标元素
}
注释:
其中,arr为有序数组,n为数组元素个数,target为目标元素。该函数返回目标元素在数组中的位置,如果未找到则返回-1。
2.顺序查找
定义顺序查找(Sequential Search),也称为线性查找,是一种简单的查找算法,适用于各种类型的数据结构,包括数组和链表。顺序查找的思想是从数据结构的起始位置开始,依次比较每个元素,直到找到目标元素或者查找完整个数据结构。
以下是C语言中的顺序查找示例代码:
int sequential_search(int arr[], int n, int target) {
for (int i = 0; i < n; i++) {
if (arr[i] == target) {
return i;
}
}
return -1; // 表示未找到目标元素
}
注释:
其中,arr为数组,n为数组元素个数,target为目标元素。该函数返回目标元素在数组中的位置,如果未找到则返回-1。
顺序查找的时间复杂度为O(n),因为需要依次比较每个元素。在数据量较小或者数据结构无序的情况下,顺序查找是一种简单有效的查找算法。
3.哈希查找
定义:哈希查找(Hash Search)是一种利用哈希表进行快速查找的算法,它通过哈希函数将关键字映射到哈希表中的一个位置,然后在该位置上进行查找操作。哈希查找的时间复杂度通常是O(1),因此它是一种非常高效的查找算法。
哈希查找的基本操作包括插入、查找和删除。插入操作就是将一个元素插入到哈希表中,查找操作就是在哈希表中查找一个元素,删除操作就是从哈希表中删除一个元素。
以下是C语言中哈希查找的实现示例代码:
#include <stdio.h>
#include <stdlib.h>
#define TABLE_SIZE 100 // 哈希表大小
/* 定义哈希表节点 */
typedef struct Node{
int key;
int value;
struct Node *next; // 指向下一个节点的指针
} Node;
/* 哈希函数 */
int hash(int key) {
return key % TABLE_SIZE;
}
/* 创建哈希表节点 */
Node* create_node(int key, int value) {
Node *node = (Node*) malloc(sizeof(Node));
node->key = key;
node->value = value;
node->next = NULL;
return node;
}
/* 插入元素 */
void insert(Node* hash_table[], int key, int value) {
int index = hash(key);
Node *node = hash_table[index];
while (node != NULL) {
if (node->key == key) {
node->value = value; // 如果已经存在该元素,更新其值
return;
}
node = node->next;
}
// 如果不存在该元素,创建新节点并插入到链表头部
Node *new_node = create_node(key, value);
new_node->next = hash_table[index];
hash_table[index] = new_node;
}
/* 查找元素 */
int search(Node* hash_table[], int key) {
int index = hash(key);
Node *node = hash_table[index];
while (node != NULL) {
if (node->key == key) {
return node->value;
}
node = node->next;
}
return -1; // 如果未找到元素,返回-1
}
/* 删除元素 */
void delete(Node* hash_table[], int key) {
int index = hash(key);
Node *node = hash_table[index];
Node *prev = NULL;
while (node != NULL) {
if (node->key == key) {
if (prev == NULL) {
hash_table[index] = node->next;
}
else {
prev->next = node->next;
}
free(node); // 释放节点内存
return;
}
prev = node;
node = node->next;
}
}
/* 主函数 */
int main() {
Node* hash_table[TABLE_SIZE] = {NULL}; // 初始化哈希表
insert(hash_table, 3, 10);
insert(hash_table, 5, 20);
insert(hash_table, 9, 30);
printf("%d\n", search(hash_table, 3)); // 输出10
printf("%d\n", search(hash_table, 5)); // 输出20
printf("%d\n", search(hash_table, 7)); // 输出-1
delete(hash_table, 5);
printf("%d\n", search(hash_table, 5)); // 输出-1
return 0;
}
注释:
该示例代码中,Node结构体表示哈希表中的节点,其中key表示关键字,value表示值,next指向下一个节点。哈希函数采用取模运算,将关键字映射到哈希表中的一个位置。insert函数用于插入一个元素到哈希表中,如果已经存在该元素则更新其值。search函数用于查找一个元素,如果未找到则返回-1。delete函数用于删除一个元素。在主函数中,初始化哈希表后插入几个元素,然后查找和删除元素并输出结果。
🔥图论算法:
1.最短路径算法
Dijkstra算法
Dijkstra算法是一种用于计算加权图中单源最短路径的贪心算法。它通过找到当前起点到每个顶点的最短路径来逐步扩展搜索范围,直到找到目标顶点的最短路径。该算法的时间复杂度一般为O(|E|+|V|log|V|),其中|V|和|E|分别表示图的顶点数和边数。
Dijkstra算法的基本思想是维护两个集合:一个是已确定最短路径的顶点集合S,另一个是未确定最短路径的顶点集合V-S。初始时,S只包含起点,V-S包含其余所有顶点。然后,对于V-S中的每个顶点,计算起点到该顶点的距离,并更新起点到该顶点的最短路径和路径长度。具体操作如下:
-
初始化起点的最短路径为0,其他顶点的最短路径为正无穷。
-
从V-S中选择距离起点最近的顶点v,将v加入S中,并更新从起点到V-S中所有顶点的最短路径和路径长度。具体操作如下:
a. 遍历v的所有邻居节点w,若w不在S中,则计算起点到w的距离(等于起点到v的距离加上v到w的距离),如果该距离小于w当前的最短路径,则更新w的最短路径和路径长度。
b. 重复步骤a,直到v的所有邻居节点都被遍历完。 -
重复步骤2,直到所有顶点都被加入S中,或者找到目标顶点的最短路径。
以下是Dijkstra算法的示例代码,其中graph表示加权无向图的邻接矩阵,start表示起点的编号,end表示目标顶点的编号:
import sys
def dijkstra(graph, start, end):
n = len(graph) #图的顶点数
dist = [sys.maxsize] * n #起点到每个顶点的最短距离,初始化为无穷大
visited = [False] * n #标记每个顶点是否已确定最短路径
dist[start] = 0 #起点到自己的距离为0
for i in range(n):
u = -1 #当前距离起点最近的顶点编号
min_dist = sys.maxsize #当前距离起点最近的顶点到起点的距离
for j in range(n): #遍历所有顶点,找到距离起点最近的顶点
if not visited[j] and dist[j] < min_dist:
u = j
min_dist = dist[j]
if u == -1 or u == end: #如果已经找到目标顶点或无法再找到新的顶点,则退出循环
break
visited[u] = True #将距离起点最近的顶点加入S集合中
for v in range(n): #遍历u的所有邻居节点
if not visited[v] and graph[u][v] != 0:
#计算起点到v的距离,如果小于当前的最短距离,则更新dist[v]
new_dist = dist[u] + graph[u][v]
if new_dist < dist[v]:
dist[v] = new_dist
return dist[end] #返回起点到目标顶点的最短距离
注释:
该示例代码中,dist表示起点到各个顶点的最短距离,visited表示每个顶点是否已确定最短路径。在每次循环中,找到距离起点最近的顶点u,并将其加入S集合中。然后遍历u的所有邻居节点v,如果起点到v的距离小于当前的最短距离,则更新dist[v]。重复进行直到找到目标顶点或无法再找到新的顶点。最后返回起点到目标顶点的最短距离。
Floyd算法
定义:Floyd算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与Dijkstra算法类似。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。
以下是程序示例:
#include<stdio.h>
#include<stdlib.h>
#define max 1000000000
int d[1000][1000],path[1000][1000];
int main()
{
int i,j,k,m,n;
int x,y,z;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)
for(j=1;j<=n;j++){
d[i][j]=max;
path[i][j]=j;
}
for(i=1;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
d[x][y]=z;
d[y][x]=z;
}
for(k=1;k<=n;k++)
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) {
if(d[i][k]+d[k][j]<d[i][j]) {
d[i][j]=d[i][k]+d[k][j];
path[i][j]=path[i][k];
}
}
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
if (i!=j) printf("%d->%d:%d\n",i,j,d[i][j]);
int f, en;
scanf("%d%d",&f,&en);
while (f!=en) {
printf("%d->",f);
f=path[f][en];
}
printf("%d\n",en);
return 0;
}
注释:
1、从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。
2、对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比已知的路径更短。如果是更新它。
把图用邻接矩阵G表示出来,如果从Vi到Vj有路可达,则G[i][j]=d,d表示该路的长度;否则G[i][j]=无穷大。定义一个矩阵D用来记录所插入点的信息,D[i][j]表示从Vi到Vj需要经过的点,初始化D[i][j]=j。把各个顶点插入图中,比较插点后的距离与原来的距离,G[i][j] = min( G[i][j], G[i][k]+G[k][j] ),如果G[i][j]的值变小,则D[i][j]=k。在G中包含有两点之间最短道路的信息,而在D中则包含了最短通路径的信息。
比如,要寻找从V5到V1的路径。根据D,假如D(5,1)=3则说明从V5到V1经过V3,路径为{V5,V3,V1},如果D(5,3)=3,说明V5与V3直接相连,如果D(3,1)=1,说明V3与V1直接相连。
2.最小生成树算法
Prim算法
定义:Prim算法是一种用于解决最小生成树问题的贪心算法。它的基本思想是从图中的任意一个顶点出发,不断地选择与当前生成树距离最小的边所连接的顶点加入到生成树中,直到所有的顶点都被加入为止。在这个过程中,需要维护一个集合,表示已经被加入到生成树中的顶点,以及一个数组,表示每个顶点到生成树的距离。Prim算法的时间复杂度为O(n^2),其中n为图中顶点的个数。
Prim算法的实现步骤如下:
- 随机选择一个起点,将其加入到生成树中。
- 根据当前生成树中的顶点,以及它们与其他顶点之间的边,计算出每个顶点到生成树的距离,并存储在一个数组中。
- 从距离数组中选出距离最小的顶点,将其加入到生成树中,并将其与生成树中的顶点相连。
- 重复步骤2和步骤3,直到所有的顶点都被加入到生成树中。
以下是Prim算法的具体实现代码:
// Prim算法实现
void prim(int n, int graph[][MAX], int start) {
int i, j, k;
int lowcost[MAX]; // 存储每个顶点到生成树的距离
int closest[MAX]; // 存储每个顶点距离生成树最近的顶点
bool visited[MAX]; // 标记每个顶点是否已经被访问过
// 初始化
for (i = 1; i <= n; i++) {
lowcost[i] = graph[start][i];
closest[i] = start;
visited[i] = false;
}
visited[start] = true;
// 依次加入n-1个顶点
for (i = 1; i < n; i++) {
int min = INF;
int u = start;
// 找到距离生成树最近的顶点
for (j = 1; j <= n; j++) {
if (!visited[j] && lowcost[j] < min) {
min = lowcost[j];
u = j;
}
}
// 将该顶点加入生成树
visited[u] = true;
// 更新距离数组和最近顶点数组
for (k = 1; k <= n; k++) {
if (!visited[k] && graph[u][k] < lowcost[k]) {
lowcost[k] = graph[u][k];
closest[k] = u;
}
}
}
}
注释:
其中,n表示图中顶点的个数,graph表示图的邻接矩阵,start表示起点。在实现中,我们使用了一个INF常量表示无穷大,用于初始化距离数组。
Kruskal算法
定义:Kruskal算法是一种用于解决最小生成树问题的贪心算法。它的基本思想是将所有的边按照权重从小到大排序,然后依次加入到生成树中,如果加入某条边会形成环,则不加入该边。Kruskal算法的时间复杂度为O(mlogm),其中m为边的个数。
Kruskal算法的实现步骤如下:
- 将所有的边按照权重从小到大排序。
- 依次遍历每条边,如果加入该边会形成环,则不加入该边,否则将该边加入生成树中。
- 重复步骤2,直到生成树中包含n-1条边,其中n为图中顶点的个数。
以下是Kruskal算法的具体实现代码:
// Kruskal算法实现
void kruskal(int n, int m, Edge edges[]) {
int i, j;
int count = 0; // 记录生成树中已经包含的边的个数
int parent[MAX]; // 记录每个顶点的祖先
Edge result[MAX]; // 存储生成树中的边
// 初始化每个顶点的祖先为自身
for (i = 1; i <= n; i++) {
parent[i] = i;
}
// 将所有的边按照权重从小到大排序
sort(edges, edges + m);
// 依次遍历每条边,加入生成树中
for (i = 0; i < m; i++) {
int u = edges[i].u;
int v = edges[i].v;
int w = edges[i].w;
// 判断加入该边是否会形成环
int root1 = find(u, parent);
int root2 = find(v, parent);
if (root1 != root2) {
// 加入该边,并更新祖先数组
result[count++] = edges[i];
parent[root1] = root2;
}
// 如果生成树中已经包含n-1条边,则结束遍历
if (count == n - 1) {
break;
}
}
// 输出生成树中的边及其权重
for (i = 0; i < count; i++) {
printf("(%d, %d) %d\n", result[i].u, result[i].v, result[i].w);
}
}
注释:
其中,n表示图中顶点的个数,m表示边的个数,edges表示边的数组。在实现中,我们使用了一个find函数来查找一个顶点的祖先,以判断加入某条边是否会形成环。
🔥字符串匹配算法:
1.暴力匹配
定义:暴力匹配(Brute Force Matching),也称为朴素匹配,是一种简单直接的字符串匹配算法。它的基本思想是从文本串的第一个字符开始,逐个字符地与模式串进行比较,如果遇到不匹配的字符,则将文本串向后移动一位,继续进行比较。如果匹配成功,则返回匹配的位置。
暴力匹配算法的时间复杂度为O(mn),其中m和n分别为模式串和文本串的长度。在最坏情况下,需要比较m * n次,因此该算法的效率较低,适用于较短的字符串匹配。在实际应用中,通常使用更高效的字符串匹配算法,如KMP算法和Boyer-Moore算法等。
以下是暴力匹配的代码实现:
def violence_match(text, pattern):
n = len(text)
m = len(pattern)
for i in range(n - m + 1):
j = 0
while j < m and text[i + j] == pattern[j]:
j += 1
if j == m:
return i
return -1
注释:
其中,text为文本串,pattern为模式串,n为文本串的长度,m为模式串的长度。在循环中,i表示文本串的起始位置,j表示模式串的当前位置。在while循环中,如果当前字符匹配成功,则继续匹配下一个字符,否则跳出循环,从文本串的下一个位置重新开始匹配。如果匹配成功,则返回模式串在文本串中的起始位置,否则返回-1表示匹配失败。
2.KMP算法
定义:KMP算法是一种用于字符串匹配的算法,其全称为“Knuth-Morris-Pratt算法”,是由Donald Knuth、Vaughan Pratt、James H. Morris在1977年联合发表的。KMP算法的主要思想是在模式串匹配的过程中,利用已经匹配过的信息,尽可能减少匹配的次数。它通过预处理模式串,得到一个next数组,用于指导匹配过程中的跳转。
KMP算法的核心是next数组的求解,其定义为:next[i]表示在模式串中,以i结尾的前缀子串中,最长的既是前缀又是后缀的字符串的长度。在匹配过程中,如果当前字符匹配失败,则可以利用next数组中的值,跳过已经匹配过的部分,从模式串的next值对应的位置继续匹配。
KMP算法的时间复杂度为O(m+n),其中m为模式串的长度,n为文本串的长度。虽然KMP算法的预处理过程需要O(m)的时间复杂度,但由于可以减少匹配的次数,因此在实际应用中,KMP算法比暴力匹配算法更加高效。
KMP算法的代码实现比较复杂,需要预处理next数组,以下是KMP算法的代码实现:
def kmp_match(text, pattern):
n = len(text)
m = len(pattern)
next = get_next(pattern)
i = 0
j = 0
while i < n and j < m:
if j == -1 or text[i] == pattern[j]:
i += 1
j += 1
else:
j = next[j]
if j == m:
return i - j
else:
return -1
def get_next(pattern):
m = len(pattern)
next = [-1] * m
i = 0
j = -1
while i < m - 1:
if j == -1 or pattern[i] == pattern[j]:
i += 1
j += 1
next[i] = j
else:
j = next[j]
return next
注释:
其中,text为文本串,pattern为模式串,n为文本串的长度,m为模式串的长度。在匹配过程中,i表示文本串的当前位置,j表示模式串的当前位置。在while循环中,如果当前字符匹配成功,则继续匹配下一个字符,否则利用next数组中的值,跳过已经匹配过的部分,从模式串的next值对应的位置继续匹配。如果匹配成功,则返回模式串在文本串中的起始位置,否则返回-1表示匹配失败。
在get_next函数中,通过对模式串进行预处理,得到next数组。在while循环中,如果当前字符匹配成功,则继续计算next值,否则利用next数组中的值跳过已经匹配过的部分。
3.BM算法
定义:BM算法是一种用于字符串匹配的算法,其全称为“Boyer-Moore算法”,是由Robert S. Boyer和J Strother Moore在1977年发表的。BM算法的主要思想是在匹配过程中,利用坏字符规则和好后缀规则,尽可能减少匹配的次数。它通过预处理模式串,得到一个坏字符表和一个好后缀表,用于指导匹配过程中的跳转。
BM算法的核心是坏字符规则和好后缀规则的实现。坏字符规则指的是,当文本串中的某个字符与模式串中的某个字符不匹配时,利用已经匹配过的信息,尽可能将模式串向右移动,以跳过不可能匹配的字符。好后缀规则指的是,当模式串的后缀与文本串中的某个子串匹配时,利用已经匹配过的信息,尽可能将模式串向右移动,以跳过不必要的匹配。
BM算法的时间复杂度为O(m+n),其中m为模式串的长度,n为文本串的长度。虽然BM算法的预处理过程需要O(m)的时间复杂度,但由于可以利用坏字符规则和好后缀规则,跳过不可能匹配的部分,因此在实际应用中,BM算法比KMP算法和暴力匹配算法更加高效。
BM算法的代码实现比较复杂,需要预处理坏字符表和好后缀表,以下是BM算法的代码实现:
def bm_match(text, pattern):
n = len(text)
m = len(pattern)
bc = get_bad_char(pattern)
gs = get_good_suffix(pattern)
i = 0
while i <= n - m:
j = m - 1
while j >= 0 and text[i+j] == pattern[j]:
j -= 1
if j == -1:
return i
x = j - bc[ord(text[i+j])]
y = 0
if j < m - 1:
y = move_by_gs(j, m, gs)
i += max(x, y)
return -1
def get_bad_char(pattern):
bc = [-1] * 256
m = len(pattern)
for i in range(m):
bc[ord(pattern[i])] = i
return bc
def get_good_suffix(pattern):
m = len(pattern)
gs = [m] * m
suff = get_suffix(pattern)
for i in range(m - 1, -1, -1):
if suff[i] == i + 1:
for j in range(m - i - 1):
if gs[j] == m:
gs[j] = m - i - 1
gs[m - suff[i] - 1] = m - i - 1
return gs
def get_suffix(pattern):
m = len(pattern)
suff = [0] * m
suff[m - 1] = m
for i in range(m - 2, -1, -1):
j = i
while j >= 0 and pattern[j] == pattern[m - 1 - i + j]:
j -= 1
suff[i] = i - j
return suff
def move_by_gs(j, m, gs):
k = m - j - 1
if gs[k] < m:
return j - gs[k] + 1
for r in range(j + 2, m):
if is_prefix(m, pattern, r):
return r - j
return m
def is_prefix(m, pattern, r):
n = m - r
for i in range(n):
if pattern[i] != pattern[r + i]:
return False
return True
注释:
其中,text为文本串,pattern为模式串,n为文本串的长度,m为模式串的长度。在匹配过程中,i表示文本串的当前位置,j表示模式串的当前位置。在while循环中,如果当前字符匹配成功,则继续向前匹配,否则利用坏字符表和好后缀表中的值,将模式串向右移动。如果匹配成功,则返回模式串在文本串中的起始位置,否则返回-1表示匹配失败。
在get_bad_char函数中,通过对模式串进行预处理,得到坏字符表。在get_good_suffix函数中,通过对模式串进行预处理,得到好后缀表。在get_suffix函数中,计算出模式串的后缀数组。在move_by_gs函数中,利用好后缀表中的值,计算出模式串需要向右移动的距离。在is_prefix函数中,判断一个字符串是否为另一个字符串的前缀。
🔥动态规划算法:
1.背包问题
定义:背包问题是一种经典的动态规划问题,通常用于优化问题中。它的基本思想是在给定的一组物品和一个固定容量的背包中,如何选择物品放入背包中,使得背包中物品的总价值最大。其中,每个物品都有一个固定的重量和价值,背包的容量也是固定的。这个问题可以分为两种不同的情况:0-1背包问题和完全背包问题。0-1背包问题中,每个物品只有一个,要么选择要么不选;而完全背包问题中,每个物品可以选择多次。背包问题是动态规划算法的经典案例之一,其解法可以通过设计状态、状态转移方程和边界条件来实现。
背包问题的实现通常采用动态规划算法。具体实现步骤如下:
-
定义状态:将问题定义为一个状态,通常用一个二维数组来表示,其中数组的第一维表示可选物品的序号,第二维表示背包的容量。
-
确定状态转移方程:根据问题的定义和状态的定义,推导出状态转移方程。对于0-1背包问题,状态转移方程为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
其中,dp[i][j]表示前i个物品放入容量为j的背包中所能获得的最大价值,w[i]表示第i个物品的重量,v[i]表示第i个物品的价值。 -
确定边界条件:初始化dp数组,通常将dp[0][j]和dp[i][0]的值设置为0。
-
求解最优解:通过状态转移方程和边界条件,计算出dp数组中所有元素的值。最终,dp[N][C]就是问题的最优解,其中N表示物品的数量,C表示背包的容量。
-
回溯求解方案:从dp[N][C]开始,根据状态转移方程逆推出每个物品是否放入背包的方案,得到最终的解决方案。
上述实现步骤是0-1背包问题的实现方法,对于完全背包问题和多重背包问题,可以根据问题的定义和状态的定义,相应地修改状态转移方程。
以下是0-1背包问题的动态规划代码实现:
def knapsack(w, v, C):
N = len(w)
dp = [[0 for j in range(C+1)] for i in range(N+1)]
for i in range(1, N+1):
for j in range(1, C+1):
if j < w[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1])
# 回溯求解方案
res = []
i, j = N, C
while i > 0 and j > 0:
if dp[i][j] != dp[i-1][j]:
res.append(i-1)
j -= w[i-1]
i -= 1
return dp[N][C], res[::-1]
其中,w和v分别表示物品的重量和价值,C表示背包的容量。函数返回的第一个值是问题的最优解,第二个值是选择物品的编号(从0开始)。
2.最长公共子序列问题
定义:最长公共子序列(Longest Common Subsequence,LCS)问题是一种经典的动态规划问题,通常用于求解两个序列的最长公共子序列。所谓最长公共子序列,是指两个序列中都存在的、长度最长的子序列。例如,序列“ABCD”和“BD”中的最长公共子序列是“BD”。
LCS问题的基本思路是将两个序列分别按照顺序排列,并将它们转化为一个二维矩阵。矩阵中的每个元素代表两个序列中对应位置的字符组合,如果它们相等,则在矩阵中标记为1,否则标记为0。接下来,从矩阵的左上角开始,按照规定的顺序遍历矩阵,根据矩阵中的标记信息,计算出两个序列的最长公共子序列的长度。
以下是代码例程:
#include<iostream>
#include<string.h>
using namespace std;
#define MAXSIZE 100
int B[MAXSIZE][MAXSIZE]; //B数组用来追踪最长子序列
void Trace(char*X,int i,int j){
if(i == 0||j == 0){
return;
}
if(B[i][j] == 0){//0代表左斜向上
Trace(X,i-1,j-1);
cout<<X[i-1]<<" ";
}
else if(B[i][j] == 1){//1代表向上
Trace(X,i-1,j);
}
else{//2代表向左
Trace(X,i,j-1);
}
}
void LCS(char *X,char *Y,int m,int n){
int C[m+1][n+1];
for(int i = 0;i <= m;i++){
for(int j = 0;j <= n;j++){
if(i ==0||j == 0){
C[i][j] = 0;
}
else if(X[i-1] == Y[j-1]){
C[i][j] = C[i-1][j-1] + 1;
B[i][j] = 0; //0代表左斜向上
}
else{
if(C[i-1][j] >= C[i][j-1]){
C[i][j] = C[i-1][j];
B[i][j] = 1; //1代表向上
}
else{
C[i][j] = C[i][j-1];
B[i][j] = 2; //2代表向左
}
}
}
}
}
int main(){
char X[] = "ABCBDAB";
char Y[] = "BDCABA";
/*当把一二行的代码换成注释中的代码(即X,Y互换),结果可能会有变化
char Y[] = "ABCBDAB";
char X[] = "BDCABA";
*/
int m = strlen(X),n = strlen(Y);
LCS(X,Y,m,n);
Trace(X,m,n);
return 0;
//实际上是有三个解:
//BCBA
//BDAB
//BCAB
}
注释:
这个问题满足优化原则和子问题重叠性,可以用动态规划解决它。
最长公共子序列的长度的递推方程(令m = i;n = j;)
可惜的是这个方法有一些缺陷,当最长公共子序列的解不唯一的时候,这个代码只能求出一个解。
🔥分治算法:
1.归并排序
定义:归并排序是一种基于分治思想的排序算法,其基本思想是将待排序的序列划分成若干子序列,分别进行排序,最后将排好序的子序列合并成一个有序序列。
归并排序的具体实现步骤如下:
-
将待排序序列不断递归地划分成左右两个子序列,直到每个子序列只有一个元素。
-
对于每个子序列,逐一比较左右两部分的元素,将它们排序并合并成一个有序序列。
-
不断将有序序列合并成更大的有序序列,直到最终合并成整个序列有序。
-
归并排序的时间复杂度为O(nlogn),是一种稳定的排序算法。由于归并排序需要额外的空间存储子序列和合并后的序列,因此空间复杂度为O(n)。
以下是归并排序的Python代码实现:
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
i, j = 0, 0
res = []
while i < len(left) and j < len(right):
if left[i] <= right[j]:
res.append(left[i])
i += 1
else:
res.append(right[j])
j += 1
res += left[i:]
res += right[j:]
return res
注释:
其中,merge_sort()函数实现了归并排序的主要逻辑,merge()函数实现了合并两个有序序列的功能。在归并排序中,递归调用merge_sort()函数实现序列的划分和排序,最终调用merge()函数将排好序的子序列合并成一个有序序列。
2.快速排序
快速排序是一种常见的排序算法,它基于分治思想。它的基本思路是将待排序的序列分成两部分,其中一部分的元素都比另一部分的元素小,然后对这两部分分别进行快速排序,最终将整个序列排序。
具体实现时,选择一个基准元素(通常是第一个或最后一个元素),将序列中的元素分成两部分,一部分比基准元素小,一部分比基准元素大。然后递归地对两部分分别进行快速排序,最终将整个序列排序。
快速排序的时间复杂度为O(nlogn),是一种效率较高的排序算法。
以下是快速排序的示例代码(使用递归实现):
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[0] # 选择第一个元素作为基准元素
left = [x for x in arr[1:] if x < pivot] # 小于基准元素的放在左边
right = [x for x in arr[1:] if x >= pivot] # 大于等于基准元素的放在右边
return quick_sort(left) + [pivot] + quick_sort(right) # 递归排序左右两部分,并将结果合并
注释:
以上代码中,arr 是待排序的数组。在每一轮排序中,我们选择第一个元素作为基准元素 pivot,然后将数组分成两部分,一部分是小于 pivot 的元素,另一部分是大于等于 pivot 的元素。然后递归地对左右两部分进行排序,并将结果合并。
需要注意的是,上面的代码是使用了 Python 的列表推导式,如果使用其他编程语言,可能需要使用循环来实现。
🔥搜索算法:
1.深度优先搜索
定义:深度优先搜索(Depth-First Search,简称 DFS)是一种图遍历算法,它从一个源节点开始,沿着一条路径往下走,直到无法继续为止,然后回退到前一个节点,选择另一条路径继续走下去,直到遍历完整个图。
具体实现时,可以使用递归或栈来实现 DFS。递归实现时,从源节点开始,递归地访问与它相邻的节点,直到访问到一个没有未被访问的相邻节点为止。然后回溯到前一个节点,继续访问它的未被访问的相邻节点,直到遍历完整个图。
DFS 适用于遍历深度比较大的图,可以用于解决很多图论问题,如连通性、路径查找、拓扑排序等。
需要注意的是,DFS 可能会遍历到重复的节点,因此需要使用一个标记数组来记录每个节点是否已经被访问过,防止重复遍历。
以下是使用递归实现的深度优先搜索代码示例:
def dfs(graph, node, visited):
visited[node] = True # 标记当前节点已经被访问
print(node, end=' ')
for neighbor in graph[node]:
if not visited[neighbor]:
dfs(graph, neighbor, visited) # 递归访问相邻节点
注释:
以上代码中,graph 是图的邻接表表示,node 是当前节点,visited 是标记数组,表示每个节点是否已经被访问过。
在每一轮 DFS 中,我们首先将当前节点 node 标记为已经被访问过,并输出它的值。然后遍历与它相邻的节点,如果它的相邻节点 neighbor 没有被访问过,就递归地访问它。
需要注意的是,以上代码中没有考虑图不连通的情况,如果图不连通,需要对每个未被访问的节点再进行一次 DFS。此外,如果要求输出遍历路径,可以使用一个列表来记录遍历路径,每次访问一个节点时,将它加入遍历路径中即可。
2.广度优先搜索
定义:
广度优先搜索(Breadth-First Search,简称 BFS)是一种图遍历算法,它从一个源节点开始,首先访问它的所有相邻节点,然后依次访问它们的相邻节点,直到遍历完整个图。
具体实现时,可以使用队列来实现 BFS。从源节点开始,将它加入队列中,然后依次从队列中取出节点,访问它的所有相邻节点,将它们加入队列中,直到队列为空为止。
BFS 适用于遍历深度比较小的图,也可以用于解决很多图论问题,如连通性、最短路径等。
需要注意的是,BFS 可能会访问到重复的节点,因此需要使用一个标记数组来记录每个节点是否已经被访问过,防止重复访问。
以下是使用队列实现的广度优先搜索代码示例:
from collections import deque
def bfs(graph, start):
visited = [False] * len(graph) # 标记数组,表示每个节点是否已经被访问过
queue = deque([start]) # 使用队列来存储待访问的节点
visited[start] = True # 标记起始节点已经被访问
while queue:
node = queue.popleft() # 取出队列中的第一个节点
print(node, end=' ')
for neighbor in graph[node]:
if not visited[neighbor]:
queue.append(neighbor) # 将未被访问的相邻节点加入队列
visited[neighbor] = True # 标记相邻节点已经被访问过
注释:
以上代码中,graph 是图的邻接表表示,start 是起始节点。
在每一轮 BFS 中,我们首先从队列中取出第一个节点 node,输出它的值。然后遍历与它相邻的节点,如果它的相邻节点 neighbor 没有被访问过,就将它加入队列中,并标记它已经被访问过。
需要注意的是,以上代码中没有考虑图不连通的情况,如果图不连通,需要对每个未被访问的节点再进行一次 BFS。此外,如果要求输出遍历路径,可以使用一个字典来记录每个节点的前驱节点,每次访问一个节点时,将它的前驱节点加入字典中即可。
🔥数字算法:
1.欧几里得算法(求最大公约数)
定义:欧几里得算法(Euclidean algorithm)也称辗转相除法,是求最大公约数的一种方法。它的基本思想是:两个整数的最大公约数等于其中较小的数和两数的差的最大公约数。例如,求 28 和 14 的最大公约数,用欧几里得算法可以这样做:
28 ÷ 14 = 2 余 0
14 ÷ 0 = 0 余 0
因为余数为 0,所以最大公约数是 14。
欧几里得算法可以递归实现,也可以用循环实现。
下面是用递归实现的 Python 代码:
def gcd(a, b):
if b == 0:
return a
else:
return gcd(b, a % b)
print(gcd(28, 14)) # 输出 14
下面是用循环实现的 Python 代码:
def gcd(a, b):
while b != 0:
a, b = b, a % b
return a
print(gcd(28, 14)) # 输出 14
2.质数判断
定义:质数是指只能被1和自身整除的正整数,例如2、3、5、7、11等。判断一个数是否为质数,可以采用以下方法:
-
从2开始,依次判断该数能否被2到该数-1之间的任意一个数整除,如果能,说明不是质数;如果都不能,说明是质数。
-
对于一个大于2的整数n,如果它不是质数,那么它必然存在一个小于等于n/2的质因子。因此,只需要判断该数能否被2到n/2之间的任意一个质数整除即可。
-
另一种更高效的方法是,判断该数能否被2到sqrt(n)之间的任意一个数整除,如果能,说明不是质数;如果都不能,说明是质数。这是因为如果该数存在大于sqrt(n)的因子,那么它一定有一个小于sqrt(n)的因子,因此只需要判断小于等于sqrt(n)的因子即可。
例如,判断5是否为质数,可以用方法1,从2开始依次判断5能否被2、3、4整除,发现都不能,因此5是质数。判断15是否为质数,可以用方法2,判断15能否被2、3、5、7整除,发现能够被3和5整除,因此15不是质数。判断31是否为质数,可以用方法3,判断31能否被2、3、5、7、11整除,发现都不能,因此31是质数。
在C语言中,可以用以下代码实现判断一个数是否为质数:
#include <stdio.h>
#include <math.h>
int is_prime(int n) {
if (n <= 1) {
return 0; // 1不是质数
}
int i;
for (i = 2; i <= sqrt(n); i++) {
if (n % i == 0) {
return 0; // 如果n能被i整除,说明不是质数
}
}
return 1; // 如果n不能被任何小于等于sqrt(n)的数整除,说明是质数
}
int main() {
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
if (is_prime(n)) {
printf("%d是质数\n", n);
} else {
printf("%d不是质数\n", n);
}
return 0;
}
注释:
该程序中,is_prime函数用于判断一个数是否为质数,如果是质数返回1,否则返回0。在函数中,首先判断n是否小于等于1,如果是则不是质数;然后用for循环从2到sqrt(n)依次判断n能否被整除,如果能则不是质数,否则是质数。在主函数中,先读入一个正整数n,然后调用is_prime函数判断n是否为质数,并输出结果。
3.素数筛选
素数筛选是一种算法,用于找到一定范围内的所有素数。
在C语言中,可以使用以下代码实现素数筛选:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
void sieve_of_eratosthenes(int n)
{
bool prime[n+1];
memset(prime, true, sizeof(prime));
for (int p=2; p*p<=n; p++)
{
if (prime[p] == true)
{
for (int i=p*2; i<=n; i += p)
prime[i] = false;
}
}
for (int p=2; p<=n; p++)
if (prime[p])
printf("%d ", p);
}
int main()
{
int n = 50;
sieve_of_eratosthenes(n);
return 0;
}
注释:
这段代码使用了埃拉托色尼筛法,先将所有数标记为素数,然后从2开始,将它的倍数标记为非素数。最后输出所有标记为素数的数字。
🔥数据压缩算法:
1.哈夫曼编码
哈夫曼编码是一种压缩算法,它通过将出现频率高的字符编码为较短的二进制码,来减小数据的存储空间。
以下是用C语言实现哈夫曼编码的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node {
char data;
int freq;
struct node *left;
struct node *right;
};
struct node *newNode(char data, int freq) {
struct node *temp = (struct node *)malloc(sizeof(struct node));
temp->data = data;
temp->freq = freq;
temp->left = temp->right = NULL;
return temp;
}
void swap(struct node **a, struct node **b) {
struct node *temp = *a;
*a = *b;
*b = temp;
}
void heapify(struct node **heap, int size, int i) {
int smallest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left < size && heap[left]->freq < heap[smallest]->freq) {
smallest = left;
}
if (right < size && heap[right]->freq < heap[smallest]->freq) {
smallest = right;
}
if (smallest != i) {
swap(&heap[i], &heap[smallest]);
heapify(heap, size, smallest);
}
}
void buildHeap(struct node **heap, int size) {
int i = (size - 1) / 2;
while (i >= 0) {
heapify(heap, size, i);
i--;
}
}
struct node *extractMin(struct node **heap, int *size) {
struct node *min = heap[0];
heap[0] = heap[*size - 1];
*size = *size - 1;
heapify(heap, *size, 0);
return min;
}
void insert(struct node **heap, int *size, struct node *node) {
*size = *size + 1;
int i = *size - 1;
while (i > 0 && node->freq < heap[(i - 1) / 2]->freq) {
heap[i] = heap[(i - 1) / 2];
i = (i - 1) / 2;
}
heap[i] = node;
}
void printCodes(struct node *root, int *code, int top) {
if (root->left) {
code[top] = 0;
printCodes(root->left, code, top + 1);
}
if (root->right) {
code[top] = 1;
printCodes(root->right, code, top + 1);
}
if (!root->left && !root->right) {
printf("%c: ", root->data);
for (int i = 0; i < top; i++) {
printf("%d", code[i]);
}
printf("\n");
}
}
void huffmanCoding(char *data, int *freq, int size) {
struct node **heap = (struct node **)malloc(sizeof(struct node *) * size);
int heapSize = 0;
for (int i = 0; i < size; i++) {
heap[i] = newNode(data[i], freq[i]);
heapSize++;
}
buildHeap(heap, heapSize);
while (heapSize > 1) {
struct node *left = extractMin(heap, &heapSize);
struct node *right = extractMin(heap, &heapSize);
struct node *temp = newNode('$', left->freq + right->freq);
temp->left = left;
temp->right = right;
insert(heap, &heapSize, temp);
}
int code[100];
printCodes(heap[0], code, 0);
}
int main() {
char data[] = {'a', 'b', 'c', 'd', 'e', 'f'};
int freq[] = {5, 9, 12, 13, 16, 45};
int size = sizeof(data) / sizeof(data[0]);
huffmanCoding(data, freq, size);
return 0;
}
注释:
在上面的代码中,我们定义了一个结构体 node 来表示哈夫曼树的节点,包含字符数据、出现频率和左右子节点。我们还定义了一些辅助函数来创建节点、交换节点、构建最小堆、提取最小值、插入节点和打印编码。最后,我们使用一个示例来测试我们的哈夫曼编码实现。在这个示例中,我们使用了字符数组 data 和频率数组 freq 来表示字符和它们出现的频率,然后调用 huffmanCoding 函数来计算编码并输出结果。
2.LZW算法
定义:LZW算法是一种无损数据压缩算法,它可以将一些重复出现的数据序列压缩成更短的编码序列,从而减少存储空间和传输带宽的需求。LZW算法的基本思想是在压缩过程中动态地构建一个字典表,将出现过的数据序列存储为一组索引,并将这些索引序列输出为压缩数据。
在C语言中实现LZW算法的基本步骤如下:
-
初始化一个空的字典表,其中包括所有可能的单个字符和一些特殊的控制字符。
-
从输入数据流中读取字符序列,并将它们与字典表中的现有序列进行匹配。
-
如果匹配成功,则将匹配序列的索引输出为压缩数据,并将匹配序列与下一个字符组成一个新的序列,继续匹配。
-
如果匹配失败,则将当前字符输出为压缩数据,并将当前字符与下一个字符组成一个新的序列,继续匹配。
-
如果新的序列在字典表中不存在,则将它添加到字典表中,并将上一个序列的索引输出为压缩数据。
-
重复步骤2-5,直到输入数据流结束。
-
输出字典表中所有序列的索引作为压缩数据的结尾标记。
下面是一个简单的LZW算法的C语言实现示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DICT_SIZE 4096
int dict[MAX_DICT_SIZE][2];
int dict_size = 256;
void init_dict() {
// 初始化字典表,包括所有可能的单个字符
for (int i = 0; i < 256; i++) {
dict[i][0] = i;
dict[i][1] = i;
}
}
int find_dict_entry(int prefix, int suffix) {
// 在字典表中查找匹配项
for (int i = 0; i < dict_size; i++) {
if (dict[i][0] == prefix && dict[i][1] == suffix) {
return i;
}
}
return -1;
}
void add_dict_entry(int prefix, int suffix) {
// 将新的序列添加到字典表中
dict[dict_size][0] = prefix;
dict[dict_size][1] = suffix;
dict_size++;
}
void compress(FILE* input, FILE* output) {
init_dict();
int prefix = fgetc(input);
if (prefix == EOF) {
return;
}
int suffix = fgetc(input);
if (suffix == EOF) {
fputc(prefix, output);
return;
}
int code = find_dict_entry(prefix, suffix);
while (suffix != EOF) {
if (code >= 0) {
// 匹配成功,继续读取下一个字符
prefix = code;
suffix = fgetc(input);
code = find_dict_entry(prefix, suffix);
} else {
// 匹配失败,输出索引并将新序列添加到字典表中
fputc(prefix >> 4, output);
fputc((prefix << 4) | (suffix >> 8), output);
fputc(suffix & 0xff, output);
add_dict_entry(prefix, suffix);
prefix = suffix;
suffix = fgetc(input);
code = find_dict_entry(prefix, suffix);
}
if (dict_size >= MAX_DICT_SIZE) {
// 字典表已满,重新初始化
init_dict();
}
}
// 输出字典表中所有序列的索引作为结尾标记
fputc(prefix >> 4, output);
fputc(prefix << 4, output);
}
int main(int argc, char** argv) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <input_file> <output_file>\n", argv[0]);
exit(1);
}
FILE* input = fopen(argv[1], "rb");
if (input == NULL) {
fprintf(stderr, "Error: cannot open input file %s\n", argv[1]);
exit(1);
}
FILE* output = fopen(argv[2], "wb");
if (output == NULL) {
fprintf(stderr, "Error: cannot open output file %s\n", argv[2]);
exit(1);
}
compress(input, output);
fclose(input);
fclose(output);
return 0;
}
以上仅是常见的算法,实际上C语言可以实现的算法种类非常丰富,具体还需根据实际需要进行选择。