上一章我們說了常見的10種數據結構,接下來我們說常見的10種算法。
上一章地址:基礎夯實:基礎數據結構與算法(一),不怎么清楚的可以去瞅瞅。
常見的10種算法
數據結構研究的內容:就是如何按一定的邏輯結構,把數據組織起來,並選擇適當的存儲表示方法把邏輯結構組織好的數據存儲到計算機的存儲器里。
算法研究的目的是為了更有效的處理數據,提高數據運算效率。數據的運算是定義在數據的邏輯結構上,但運算的具體實現要在存儲結構上進行。
一般有以下幾種常用運算:
- 檢索:檢索就是在數據結構里查找滿足一定條件的節點。一般是給定一個某字段的值,找具有該字段值的節點。
- 插入:往數據結構中增加新的節點。
- 刪除:把指定的結點從數據結構中去掉。
- 更新:改變指定節點的一個或多個字段的值。
- 排序:把節點按某種指定的順序重新排列。例如遞增或遞減。
1、遞歸算法
遞歸算法:是一種直接或者間接地調用自身的算法。在計算機編寫程序中,遞歸算法對解決一大類問題是十分有效的,它往往使算法的描述簡潔而且易於理解。
- 遞歸就是在過程或函數里調用自身。
- 在使用遞歸策略時,必須有一個明確的遞歸結束條件,稱為遞歸出口。
- 遞歸算法解題通常顯得很簡潔,但遞歸算法解題的運行效率較低。所以一般不提倡用遞歸算法設計程序。
- 在遞歸調用的過程當中系統為每一層的返回點、局部量等開辟了棧來存儲。遞歸次數過多容易造成棧溢出等。所以一般不提倡用遞歸算法設計程序。
下面來詳細分析遞歸的工作原理。
先看看C語言中函數的執行方式,需要了解一些關於C程序在內存中的組織方式:
堆的增長方向為從低地址到高地址向上增長,而棧的增長方向剛好相反(實際情況與CPU的體系結構有關)。
當C程序中調用了一個函數時,棧中會分配一塊空間來保存與這個調用相關的信息,每一個調用都被當作是活躍的。
棧上的那塊存儲空間稱為活躍記錄或者棧幀。
棧幀由5個區域組成:輸入參數、返回值空間、計算表達式時用到的臨時存儲空間、函數調用時保存的狀態信息以及輸出參數,參見下圖:

棧是用來存儲函數調用信息的絕好方案,然而棧也有一些缺點:
棧維護了每個函數調用的信息直到函數返回后才釋放,這需要占用相當大的空間,尤其是在程序中使用了許多的遞歸調用的情況下。
除此之外,因為有大量的信息需要保存和恢復,因此生成和銷毀活躍記錄需要消耗一定的時間。
我們需要考慮采用迭代的方案。幸運的是我們可以采用一種稱為尾遞歸的特殊遞歸方式來避免前面提到的這些缺點。
例題1:計算n!
計算n的階乘,數學上的計算公式為:
n!=n×(n-1)×(n-2)……2×1
使用遞歸的方式,可以定義為:
以遞歸方式實現階乘函數的實現:
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> int main(void) { int sumInt = fact(3); printf("3的階乘為:%d\n", sumInt); system("PAUSE");//結束不退出 } //遞歸求階乘 int fact(int n) { if (n < 0) return 0; else if (n == 0 || n == 1) return 1; else return n * fact(n - 1); }
例題2:斐波那契數列
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> void main(void) { printf("%d \n", fibonacci(10)); system("PAUSE");//結束不退出 } //斐波那契數列,第一二項為1;后面的每一項為前兩項之和 int fibonacci(int a){ if (a == 1 || a == 2) { return 1; } else{ return fibonacci(a - 1) + fibonacci(a - 2); } }
例題3:遞歸將整形數字轉換為字符串
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> void main(void) { char str[100]; int i; printf("enter a integer:\n"); scanf("%d", &i); toString(i, str); puts(str); system("PAUSE");//結束不退出 } //遞歸將整形數字轉換為字符串 int toString(int i, char str[]){ int j = 0; char c = i % 10 + '0'; if (i /= 10) { j = toString(i, str) + 1; } str[j] = c; str[j + 1] = '\0'; return j; }
例題4:漢諾塔
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> //遞歸漢諾塔 void hanoi(int i, char x, char y, char z){ if (i == 1){ printf("%c -> %c\n", x, z); } else{ hanoi(i - 1, x, z, y); printf("%c -> %c\n", x, z); hanoi(i - 1, y, x, z); } } void main(void) { hanoi(10, 'A', 'B', 'C'); system("PAUSE");//結束不退出 }
例題5:猴子吃桃
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> //猴子吃桃,每天吃一半再多吃一個,第十天想吃時候只剩一個 int chitao(int i){ if (i == 10){ return 1; } else{ return (chitao(i + 1) + 1) * 2; } } void main(void) { printf("%d", chitao(5)); system("PAUSE");//結束不退出 }
例題6:N皇后問題
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> /*======================N皇后問題========================*/ #define N 100 int q[N];//列坐標 //輸出結果 void dispasolution(int n) { static int count = 0; printf(" 第%d個解:", ++count); for (int i = 1; i <= n; i++) { printf("(%d,%d) ", i, q[i]); } printf("\n"); } //判斷位置(i,j)能否放皇后 int place(int i, int j) { //第一個皇后總是可以放 if (i == 1) return 1; //其他皇后不能同行,不能同列,不能同對角線 int k = 1; //k~i-1是已經放置了皇后的行 while (k < i) { if ((q[k] == j) || (abs(q[k] - j) == abs(i - k))) return 0; k++; } return 1; } //放置皇后 void queen(int i, int n) { if (i > n)dispasolution(n); else { for (int j = 1; j <= n; j++) { if (place(i, j)==1) { q[i] = j; queen(i + 1, n); } } } } int main() { queen(1, 4); system("PAUSE");//結束不退出 }
2、排序算法
排序是程序設計中常做的操作,初學者往往只知道冒泡排序算法,其實還有很多效率更高的排序算法,比如希爾排序、快速排序、基數排序、歸並排序等。
不同的排序算法,適用於不同的場景,本章最后從時間性能,算法穩定性等方面,分析各種排序算法。
排序算法,還分為內部排序算法和外部排序算法,之間的區別是,前者在內存中完成排序,而后者則需要借助外部存儲器。
這里介紹的是內部排序算法。
冒泡排序:
起泡排序,別名“冒泡排序”,該算法的核心思想是將無序表中的所有記錄,通過兩兩比較關鍵字,得出升序序列或者降序序列。
例如,對無序表{49,38,65,97,76,13,27,49}
進行升序排序的具體實現過程如圖 1 所示:
如下冒泡排序例子
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> //冒泡排序 //交換 a 和 b 的位置的函數 void swap(int *a, int *b); void main() { int array[8] = { 49, 38, 65, 97, 76, 13, 27, 49 }; int i, j; int key; //有多少記錄,就需要多少次冒泡,當比較過程,所有記錄都按照升序排列時,排序結束 for (i = 0; i < 8; i++){ key = 0;//每次開始冒泡前,初始化 key 值為 0 //每次起泡從下標為 0 開始,到 8-i 結束 for (j = 0; j + 1<8 - i; j++){ if (array[j] > array[j + 1]){ key = 1; swap(&array[j], &array[j + 1]); } } //如果 key 值為 0,表明表中記錄排序完成 if (key == 0) { break; } } for (i = 0; i < 8; i++){ printf("%d ", array[i]); } system("PAUSE");//結束不退出 } void swap(int *a, int *b){ int temp; temp = *a; *a = *b; *b = temp; }
快速排序:
快速排序算法是在起泡排序的基礎上進行改進的一種算法,
其實現的基本思想是:通過一次排序將整個無序表分成相互獨立的兩部分,其中一部分中的數據都比另一部分中包含的數據的值小,然后繼續沿用此方法分別對兩部分進行同樣的操作,
直到每一個小部分不可再分,所得到的整個序列就成為了有序序列。
該操作過程的具體實現代碼為:
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> #include <stdlib.h> #define MAX 9 //單個記錄的結構體 typedef struct { int key; }SqNote; //記錄表的結構體 typedef struct { SqNote r[MAX]; int length; }SqList; //此方法中,存儲記錄的數組中,下標為 0 的位置時空着的,不放任何記錄,記錄從下標為 1 處開始依次存放 int Partition(SqList *L, int low, int high){ L->r[0] = L->r[low]; int pivotkey = L->r[low].key; //直到兩指針相遇,程序結束 while (low<high) { //high指針左移,直至遇到比pivotkey值小的記錄,指針停止移動 while (low<high && L->r[high].key >= pivotkey) { high--; } //直接將high指向的小於支點的記錄移動到low指針的位置。 L->r[low] = L->r[high]; //low 指針右移,直至遇到比pivotkey值大的記錄,指針停止移動 while (low<high && L->r[low].key <= pivotkey) { low++; } //直接將low指向的大於支點的記錄移動到high指針的位置 L->r[high] = L->r[low]; } //將支點添加到准確的位置 L->r[low] = L->r[0]; return low; } void QSort(SqList *L, int low, int high){ if (low<high) { //找到支點的位置 int pivotloc = Partition(L, low, high); //對支點左側的子表進行排序 QSort(L, low, pivotloc - 1); //對支點右側的子表進行排序 QSort(L, pivotloc + 1, high); } } void QuickSort(SqList *L){ QSort(L, 1, L->length); } void main() { SqList * L = (SqList*)malloc(sizeof(SqList)); L->length = 8; L->r[1].key = 49; L->r[2].key = 38; L->r[3].key = 65; L->r[4].key = 97; L->r[5].key = 76; L->r[6].key = 13; L->r[7].key = 27; L->r[8].key = 49; QuickSort(L); for (int i = 1; i <= L->length; i++) { printf("%d ", L->r[i].key); } system("PAUSE");//結束不退出 }
更多點擊 排序算法:http://data.biancheng.net/sort/ (插入排序算法、快速排序算法、選擇排序算法、歸並排序和基數排序等)
3、二分查找算法
二分査找就是折半查找,
其基本思想是:首先選取表中間位置的記錄,將其關鍵字與給定關鍵字 key 進行比較,若相等,則査找成功;
若 key 值比該關鍵字值大,則要找的元素一定在右子表中,則繼續對右子表進行折半查找;
若 key 值比該關鍵宇值小,則要找的元素一定在左子表中,繼續對左子表進行折半査找。
如此遞推,直到査找成功或査找失敗(或査找范圍為 0)。
例如:
要求用戶輸入數組長度,也就是有序表的數據長度,並輸入數組元素和査找的關鍵字。
程序輸出查找成功與否,以及成功時關鍵字在數組中的位置。
例如,在有序表 11、13、18、 28、39、56、69、89、98、122 中査找關鍵字為 89 的元素。
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> int binary_search(int key, int a[], int n) //自定義函數binary_search() { int low, high, mid, count = 0, count1 = 0; low = 0; high = n - 1; while (low<high) //査找范圍不為0時執行循環體語句 { count++; //count記錄査找次數 mid = (low + high) / 2; //求中間位置 if (key<a[mid]) //key小於中間值時 high = mid - 1; //確定左子表范圍 else if (key>a[mid]) //key 大於中間值時 low = mid + 1; //確定右子表范圍 else if (key == a[mid]) //當key等於中間值時,證明查找成功 { printf("查找成功!\n 查找 %d 次!a[%d]=%d", count, mid, key); //輸出査找次數及所査找元素在數組中的位置 count1++; //count1記錄查找成功次數 break; } } if (count1 == 0) //判斷是否查找失敗 printf("查找失敗!"); //査找失敗輸出no found return 0; } int main() { int i, key, a[100], n; printf("請輸入數組的長度:\n"); scanf("%d", &n); //輸入數組元素個數 printf("請輸入數組元素:\n"); for (i = 0; i<n; i++) scanf("%d", &a[i]); //輸入有序數列到數組a中 printf("請輸入你想查找的元素:\n"); scanf("%d", &key); //輸入要^找的關鍵字 binary_search(key, a, n); //調用自定義函數 printf("\n"); system("PAUSE");//結束不退出; }
4、搜索算法
搜索算法是利用計算機的高性能來有目的的窮舉一個問題解空間的部分或所有的可能情況,從而求出問題的解的一種方法。
現階段一般有枚舉算法、深度優先搜索、廣度優先搜索、A*算法、回溯算法、蒙特卡洛樹搜索、散列函數等算法。
在大規模實驗環境中,通常通過在搜索前,根據條件降低搜索規模;
根據問題的約束條件進行剪枝;利用搜索過程中的中間解,避免重復計算這幾種方法進行優化。
這里介紹的是深度優先搜索,感興趣的可以百度查詢更多搜索算法。
內容很多,大家可以百度查詢感興趣的用法:也可以點擊 深度優先搜索 查看更多。
深度優先搜索
- 深度優先遍歷首先訪問出發點v,並將其標記為已訪問過;然后依次從v出發搜索v的每個鄰接點w。若w未曾訪問過,則以w為新的出發點繼續進行深度優先遍歷,直至圖中所有和源點v有路徑相通的頂點均已被訪問為止。
- 若此時圖中仍有未訪問的頂點,則另選一個尚未訪問的頂點作為新的源點重復上述過程,直至圖中所有頂點均已被訪問為止。
深度搜索與廣度搜索的相近,最終都要擴展一個結點的所有子結點.
區別在於對擴展結點過程,深度搜索擴展的是E-結點的鄰接結點中的一個,並將其作為新的E-結點繼續擴展,當前E-結點仍為活結點,待搜索完其子結點后,回溯到該結點擴展它的其它未搜索的鄰接結點。
而廣度搜索,則是擴展E-結點的所有鄰接結點,E-結點就成為一個死結點。
5、哈希算法
1. 什么是哈希
Hash,一般翻譯做散列、雜湊,或音譯為哈希,是一個典型的利用空間換取時間的算法,把任意長度的輸入(又叫做預映射pre-image)通過散列算法變換成固定長度的輸出,該輸出就是散列值。
如有一個學生信息表:
學生的學號為:年紀+學院號+班級號+順序排序號【如:19(年紀)+002(2號學院)+01(一班)+17(17號)---à190020117】類似於一個這樣的信息,
當我們需要找到這個學號為【190020117】的學生,在不適用哈希的時候,我們通常是使用一個順序遍歷的方式在數據中進行查詢大類,再查詢子類得到,
這樣的作法很明顯不夠快 ,需要O(n)左右的時間花費,對於大型的數據規模而言這顯然不行,
而哈希的做法是,根據一定的規律(比如說年紀不存在過老和過小的情況,以此將【190020117】進行壓縮成一個簡短的數據如:
【192117】)並且將這個數據直接作用於內存的地址,屆時我們查詢【190020117】只需要進行一次壓縮並訪問【192117】這個地址即可,而這個壓縮的方法(函數),就可以稱之為哈希函數。
一般的對於哈希函數需要考慮如下內容:
- 計算散列地址所需要的時間(即hash函數本身不要太復雜)
- 關鍵字的長度
- 表長(不宜過長或過短,避免內存浪費和算力消耗)
- 關鍵字分布是否均勻,是否有規律可循
- 設計的hash函數在滿足以上條件的情況下盡量減少沖突
2.哈希與哈希表
在理解了哈希的思維之后,我們要了解什么是哈希表,哈希表顧名思義就是經過哈希函數進行轉換后的一張表,
通過訪問哈希表,我們可以快速查詢哈希表,從而得出所需要得到的數據,構建哈希表的核心就是要考慮哈希函數的沖突處理(即經過數據壓縮之后可能存在多數據同一個地址,需要利用算法將沖突的數據分別存儲)。
沖突處理的方法有很多,最簡單的有+1法,即地址數直接+1,當兩個數據都需要存儲進【2019】時,可以考慮將其中的一個存進【2020】
此外還有,開放定址法,鏈式地址發,公共溢出法,再散列法,質數法等等,各方法面對不同的數據特征有不同的效果。
3.哈希的思維
Hash算法是一個廣義的算法,也可以認為是一種思想,使用Hash算法可以提高存儲空間的利用率,可以提高數據的查詢效率,也可以做數字簽名來保障數據傳遞的安全性。
所以Hash算法被廣泛地應用在互聯網應用中。
比如,利用哈希的思維在O(1)的復雜度情況下任意查詢1000以內所有的質數(在創建是否是質數的時候並不是O(1)的復雜度),
注意本樣例只是演示思維,面對本需求可以有更好的空間利用方式(本寫法比較浪費空間,僅供了解)。
如下例子:
【電話聊天狂人】
給定大量手機用戶通話記錄,找出其中通話次數最多的聊天狂人。
輸入格式:
輸入首先給出正整數N(≤105),為通話記錄條數。隨后N行,每行給出一條通話記錄。簡單起見,這里只列出撥出方和接收方的11位數字構成的手機號碼,其中以空格分隔。
輸出格式:
在一行中給出聊天狂人的手機號碼及其通話次數,其間以空格分隔。如果這樣的人不唯一,則輸出狂人中最小的號碼及其通話次數,並且附加給出並列狂人的人數。
輸入樣例:
4
13005711862 13588625832
13505711862 13088625832
13588625832 18087925832
15005713862 13588625832
輸出樣例:
13588625832 3
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> #include <string.h> #include <math.h> #include <stdlib.h> #include <ctype.h> #define MAX 400000 /** 定義 最大 數組 大小 **///(感覺沒啥用 但是最好盡可能開大點,但是不要太大,不要超出系統可建造范圍) typedef struct Node *Hash; /**新的路程又開始了 這次准備用數組來做哈希 還有雙向平方處理沖突**/ struct Node{ char phone[15]; int num; }; int maxInt(int x, int y) { if (x>y) return x; else return y; } char* minstr(char *x, char *y) { if (strcmp(x, y)>0) return y; else return x; } int nextprime(const int n) { int p = (n % 2 == 1) ? n + 2 : n + 1; /**先找一個大於N的奇數**/ int i; while (p<MAX) { for (i = (int)sqrt(p); i >= 2; i--) /**然后再判斷是不是素數**/ if (p%i == 0) break; if (i<2) return p; /**是 那就返回這個數**/ else p += 2;/**不是 那就下一個奇數**/ } } int deal(char *s, int p) /**然后把字符串映射成下標 (映射的方式很多很多,隨便猜一個靠譜的就行了)**/ { int index = (atoi(s + 2)) % p; return index; } int insert(Hash h, int pos, char *s, int p, int Max) /**哈希查找的插入實現 ,分別是哈希數組,數組位置,身份證號,數組最大大小, MAX 看到代碼最后就明白了**/ { int i, posb = pos; /**備份pos值方便雙向平方查找**/ for (i = 1;; i++) { if (strcmp(h[pos].phone, "") == 0) /**如果為pos的值空直接插入**/ { strcpy(h[pos].phone, s); h[pos].num++; Max = max(Max, h[pos].num); break; } else { if (strcmp(h[pos].phone, s) == 0) /**不為空的話,就看看身份證號是不是想等**/ { h[pos].num++; Max = maxInt(Max, h[pos].num); break; } else { //原p%2==1 if (i % 2 == 1) pos = (posb + (i*i)) % p; /**不相等 就找下一個位置 ,分別向后找一次和往前找一次,如此循環**/ else { //原i*i pos = posb - ((i - 1)*(i - 1)); while (pos<0) pos += p; } } } } return Max; } void initial(Hash h, int p) /**把哈希數組初始化 (初始化的動詞英文忘記咋寫了。。。。)**/ { int i; for (i = 0; i<p; i++) { h[i].phone[0] = '\0'; h[i].num = 0; } } int main(){ int Max = 0; int n; /**總數 N 然后就開始找 大於N的最小素數了**/ scanf("%d", &n); /**輸出中把\n也輸入進去 避免下面輸入會出現奇葩的事情**/ int p = nextprime(2 * n); /**突然想起來 每次輸入的都是倆電話號碼,所以 電話號碼最大數是2*n**/ Hash h = (Hash)malloc(p*sizeof(struct Node));/**建立哈希數組**/ initial(h, p); char phone[15]; char phone1[15]; while (n--) { scanf("%s %s", phone, phone1); Max = insert(h, deal(phone, p), phone, p, Max); Max = insert(h, deal(phone1, p), phone1, p, Max); } int i, num = 0; char *Minstr = NULL; for (i = 0; i<p; i++) { if (h[i].num == Max) { if (Minstr == NULL) Minstr = h[i].phone; else Minstr = minstr(Minstr, h[i].phone); num++; } } printf("%s %d", Minstr, Max); if (num>1) printf(" %d", num); system("PAUSE");//結束不退出 }
6、貪心算法
貪心算法(又稱貪婪算法)是指,在對問題求解時,總是做出在當前看來是最好的選擇。
也就是說,不從整體最優上加以考慮,他所做出的僅是在某種意義上的局部最優解。
貪心算法不是對所有問題都能得到整體最優解,但對范圍相當廣泛的許多問題他能產生整體最優解或者是整體最優解的近似解。
貪心算法的基本思路是從問題的某一個初始解出發一步一步地進行,根據某個優化測度,每一步都要確保能獲得局部最優解。每一步只考慮一個數據,他的選取應該滿足局部優化的條件,直到把所有數據枚舉完。
貪心算法的思想如下:
- 建立數學模型來描述問題;
- 把求解的問題分成若干個子問題;
- 對每一子問題求解,得到子問題的局部最優解;
- 把子問題的解局部最優解合成原來解問題的一個解。
與動態規划不同的是,貪心算法得到的是一個局部最優解(即有可能不是最理想的),而動態規划算法得到的是一個全局最優解(即必須是整體而言最理想的),
一個有趣的事情是,動態規划中的01背包問題就是一個典型的貪心算法問題。
如下例子:貪心算法貨幣統計問題
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> #include <malloc.h> void main() { int i, j, m, n, *ns = NULL, *cn = NULL, sum = 0; printf("請輸入總金額m及零錢種類n:"), scanf("%d", &m), scanf("%d", &n); printf("請分別輸入%d種零錢的面額:\n", n); if (!(ns = (int *)malloc(sizeof(int)*n))) return 1; if (!(cn = (int *)malloc(sizeof(int)*n))) return 1; for (i = 0; i<n; i++) scanf("%d", &ns[i]); //------------考慮輸入面額順序不定,先對面額進行降序排列(如按照降序輸入,該段可刪除) for (i = 0; i<n; i++) for (j = i + 1; j<n; j++) if (ns[j]>ns[i]) ns[j] ^= ns[i], ns[i] ^= ns[j], ns[j] ^= ns[i]; for (i = 0; i<n; i++)//貪心算法,從最大面額開始 if (m >= ns[i]) cn[i] = m / ns[i], m = m%ns[i], sum += cn[i], printf("%d元%d張 ", ns[i], cn[i]); printf("\n最少使用零錢%d張\n", sum); system("PAUSE");//結束不退出 }
7、分治算法
分治算法的基本思想是將一個規模為N的問題分解為K個規模較小的子問題,這些子問題相互獨立且與原問題性質相同。
求出子問題的解,就可得到原問題的解。即一種分目標完成程序算法,簡單問題可用二分法完成。
求x的n次冪
復雜度為 O(lgn)O(lgn) 的分治算法
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include "stdio.h" #include "stdlib.h" int power(int x, int n) { int result; if (n == 1) return x; if (n % 2 == 0) result = power(x, n / 2) * power(x, n / 2); else result = power(x, (n + 1) / 2) * power(x, (n - 1) / 2); return result; } void main() { int x = 5; int n = 3; printf("power(%d,%d) = %d \n", x, n, power(x, n)); system("PAUSE");//結束不退出 }
歸並排序
時間復雜度是O(NlogN)O(NlogN),空間復制度為O(N)O(N)(歸並排序的最大缺陷)
歸並排序(Merge Sort)完全遵循上述分治法三個步驟:
- 分解:將要排序的n個元素的序列分解成兩個具有n/2個元素的子序列;
- 解決:使用歸並排序分別遞歸地排序兩個子序列;
- 合並:合並兩個已排序的子序列,產生原問題的解。
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include "stdio.h" #include "stdlib.h" #include "assert.h" #include "string.h" void print_arr(int *arr, int len) { int i = 0; for (i = 0; i < len; i++) printf("%d ", arr[i]); printf("\n"); } void merge(int *arr, int low, int mid, int hight, int *tmp) { assert(arr && low >= 0 && low <= mid && mid <= hight); int i = low; int j = mid + 1; int index = 0; while (i <= mid && j <= hight) { if (arr[i] <= arr[j]) tmp[index++] = arr[i++]; else tmp[index++] = arr[j++]; } while (i <= mid) //拷貝剩下的左半部分 tmp[index++] = arr[i++]; while (j <= hight) //拷貝剩下的右半部分 tmp[index++] = arr[j++]; memcpy((void *)(arr + low), (void *)tmp, (hight - low + 1) * sizeof(int)); } void mergesort(int *arr, int low, int hight, int *tmp) { assert(arr && low >= 0); int mid; if (low < hight) { mid = (hight + low) >> 1; mergesort(arr, low, mid, tmp); mergesort(arr, mid + 1, hight, tmp); merge(arr, low, mid, hight, tmp); } } //只分配一次內存,避免內存操作開銷 void mergesort_drive(int *arr, int len) { int *tmp = (int *)malloc(len * sizeof(int)); if (!tmp) { printf("out of memory\n"); exit(0); } mergesort(arr, 0, len - 1, tmp); free(tmp); } void main() { int data[10] = { 8, 7, 2, 6, 9, 10, 3, 4, 5, 1 }; int len = sizeof(data) / sizeof(data[0]); mergesort_drive(data, len); print_arr(data, len); system("PAUSE");//結束不退出 }
還有更多例子可以百度,這里就不一一舉例了。
8、回溯算法
回溯算法,又稱為“試探法”。解決問題時,每進行一步,都是抱着試試看的態度,如果發現當前選擇並不是最好的,或者這么走下去肯定達不到目標,立刻做回退操作重新選擇。
這種走不通就回退再走的方法就是回溯算法。
例如,在解決列舉集合 {1,2,3} 中所有子集的問題中,就可以使用回溯算法。
從集合的開頭元素開始,對每個元素都有兩種選擇:取還是舍。當確定了一個元素的取舍之后,再進行下一個元素,直到集合最后一個元素。
其中的每個操作都可以看作是一次嘗試,每次嘗試都可以得出一個結果。將得到的結果綜合起來,就是集合的所有子集。
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> //設置一個數組,數組的下標表示集合中的元素,所以數組只用下標為1,2,3的空間 int set[5]; //i代表數組下標,n表示集合中最大的元素值 void PowerSet(int i, int n){ //當i>n時,說明集合中所有的元素都做了選擇,開始判斷 if (i>n) { for (int j = 1; j <= n; j++) { //如果樹組中存放的是 1,說明在當初嘗試時,選擇取該元素,即對應的數組下標,所以,可以輸出 if (set[j] == 1) { printf("%d ", j); } } printf("\n"); } else{ //如果選擇要該元素,對應的數組單元中賦值為1;反之,賦值為0。然后繼續向下探索 set[i] = 1; PowerSet(i + 1, n); set[i] = 0; PowerSet(i + 1, n); } } void main() { int n = 3; for (int i = 0; i<5; i++) { set[i] = 0; } PowerSet(1, n); system("PAUSE");//結束不退出 }
很多人認為回溯和遞歸是一樣的,其實不然。在回溯法中可以看到有遞歸的身影,但是兩者是有區別的。
回溯法從問題本身出發,尋找可能實現的所有情況。和窮舉法的思想相近,不同在於窮舉法是將所有的情況都列舉出來以后再一一篩選,而回溯法在列舉過程如果發現當前情況根本不可能存在,就停止后續的所有工作,返回上一步進行新的嘗試。
遞歸是從問題的結果出發,例如求 n!,要想知道 n!的結果,就需要知道 n*(n-1)! 的結果,而要想知道 (n-1)! 結果,就需要提前知道 (n-1)*(n-2)!。這樣不斷地向自己提問,不斷地調用自己的思想就是遞歸。
使用回溯法解決問題的過程,實際上是建立一棵“狀態樹”的過程。
例如,在解決列舉集合{1,2,3}所有子集的問題中,對於每個元素,都有兩種狀態,取還是舍,所以構建的狀態樹為:
回溯算法的求解過程實質上是先序遍歷“狀態樹”的過程。樹中每一個葉子結點,都有可能是問題的答案。圖 1 中的狀態樹是滿二叉樹,得到的葉子結點全部都是問題的解。
在某些情況下,回溯算法解決問題的過程中創建的狀態樹並不都是滿二叉樹,因為在試探的過程中,有時會發現此種情況下,
再往下進行沒有意義,所以會放棄這條死路,回溯到上一步。在樹中的體現,就是在樹的最后一層不是滿的,即不是滿二叉樹,需要自己判斷哪些葉子結點代表的是正確的結果。
9、動態規划(DP)算法
動態規划過程:每一次決策依賴於當前的狀態,即下一狀態的產生取決於當前狀態。一個決策序列就是在變化的狀態中產生的,這種多階段最優化問題的求解過程就是動態規則過程。
基本思想原理
與分而治之原理類似,將待求解的問題划分成若干個子問題(階段)求解,順序求解各個子問題(階段),前一子問題(階段)為后一子問題(階段)的求解提供有用的信息。
通過各個子問題(階段)的求解,依次遞進,最終得到初始問題的解。一般情況下,能夠通過動態規划求解的問題也可通過遞歸求解。
動態規划求解的問題多數有重疊子問題的特點,為了減少重復計算,對每個子問題只求解一次,將不同子問題(階段)的解保存在數組中。
與分而治之的區別:
分而治之得到的若干子問題(階段)一般彼此獨立,各個子問題(階段)之間沒有順序要求。而動態規划中各子問題(階段)求解有順序要求,具有重疊子問題(階段),后一子問題(階段)求解取決於前一子問題(階段)的解。
與遞歸區別:
與遞歸求解區別不大,都是划分成各個子問題(階段),后一子問題(階段)取決於前一子問題(階段),但遞歸需要反復求解同一個子問題(階段),相較於動態規划做了很多重復性工作。
適用解決問題
采用動態規划求解的問題一般具有如下性質:
- 最優化原理:求解問題包含最優子結構,即,可由前一子問題(階段)最優推導得出后一子問題(階段)最優解,遞進得到初始問題的最優解。
- 無后效性:某狀態以后的過程不會影響以前的狀態,只與當前狀態有關。
- 有重疊子問題:子問題(階段)之間不是獨立的,一個子問題(階段)的解在下一子問題(階段)中被用到。(不是必需的條件,但是動態規划優於其他方法的基礎)
比如斐波那契數列,就是一個簡單的例子。
定義:
Fab(n)= Fab(n-1)+Fab(n-2) Fab(1)=Fab(2)=1;
實現1:
static int GetFab(int n) { if (n == 1) return 1; if (n == 2) return 1; return GetFab(n - 1) + GetFab(n - 2); }
假如我們求Fab(5) 。那我們需要求Fab(4) +Fab(3)。
Fab(4)=Fab(3)+Fab(2).....顯然。 Fab(3)被計算機不加區別的計算了兩次。而且隨着數字的增大,計算量是指數增長的。
如果我們使用一個數組,記錄下Fab的值。當Fab(n)!=null 時。
直接讀取。那么,我們就能把時間復雜度控制在 n 以內。
實現2:
static int[] fab = new int[6]; static int GetFabDynamic(int n) { if (n == 1) return fab[1] = 1; if (n == 2) return fab[2] = 1; if (fab[n] != 0)//如果存在,就直接返回。 { return fab[n]; } else //如果不存在,就進入遞歸,並且記錄下求得的值。 { return fab[n] = GetFabDynamic(n - 1) + GetFabDynamic(n - 2); } }
這就是,動態規划算法的 備忘錄模式。只需要把原來的遞歸稍微修改就行了。
下面是0-1背包問題的解法。
可以對比一下。(一個限重w的背包,有許多件物品。sizes[n]保存他們的重量。values[n]保存它們的價值。求不超重的情況下背包能裝的最大價值)
static int[] size = new int[] { 3, 4, 7, 8, 9 };// 5件物品每件大小分別為3, 4, 7, 8, 9 且是不可分割的 0-1 背包問題 static int[] values = new int[] { 4, 5, 10, 11, 13 };//// 5件物品每件的價值分別為4, 5, 10, 11, 13 static int capacity = 16; static int[,] dp = new int[6, capacity + 1]; static int knapsack(int n, int w) { if (w < 0) return -10000; if (n == 0) return 0; if (dp[n, w] != 0) { return dp[n, w]; } else { return dp[n, w] = Math.Max(knapsack(n - 1, w), knapsack(n - 1, w - size[n - 1]) + values[n - 1]); /* * knapsack(n,w) 指在前N件物品在W剩余容量下的最大價值。 * 這個公式的意思是,對於某一件物品, * 1、如果選擇裝進去,那么,當前價值=前面的n-1件物品在空位w - size(n)下的最大價值(因為空位需要留出,空位也隱含了價值)。 * 再加上這件物品的價值。等於 knapsack(n - 1, w - size[n - 1]) + values[n - 1] * 2、 如果我們選擇不裝進去,那么,在n-1物品的情況下空位仍然在,當前價值 = 前面n-1件物品在空位w下的最大價值。 * 等於knapsack(n - 1, w) * 注意:隨着演算,某一情況下的價值不會一成不變。 * 此時我們做出決策:到底是在裝入時的價值大,還是不裝入時的價值大呢?我們選擇上面兩種情況中值較大的。並記錄在案。 * 最后dp[N,M]就是我們要求的值。 */ } }
10、字符串匹配算法
字符串匹配問題的形式定義:
- 文本(Text)是一個長度為 n 的數組 T[1..n];
- 模式(Pattern)是一個長度為 m 且 m≤n 的數組 P[1..m];
- T 和 P 中的元素都屬於有限的字母表 Σ 表;
- 如果 0≤s≤n-m,並且 T[s+1..s+m] = P[1..m],即對 1≤j≤m,有 T[s+j] = P[j],則說模式 P 在文本 T 中出現且位移為 s,且稱 s 是一個有效位移(Valid Shift)。
比如上圖中,目標是找出所有在文本 T = abcabaabcabac 中模式 P = abaa 的所有出現。
該模式在此文本中僅出現一次,即在位移 s = 3 處,位移 s = 3 是有效位移。
解決字符串匹配的算法包括:
- 朴素算法(Naive Algorithm)、
- Rabin-Karp 算法、
- 有限自動機算法(Finite Automation)、
- Knuth-Morris-Pratt 算法(即 KMP Algorithm)、Boyer-Moore 算法、Simon 算法、Colussi 算法、Galil-Giancarlo 算法、Apostolico-Crochemore 算法、Horspool 算法和 Sunday 算法等)。
字符串匹配算法通常分為兩個步驟:預處理(Preprocessing)和匹配(Matching)。所以算法的總運行時間為預處理和匹配的時間的總和。
上圖描述了常見字符串匹配算法的預處理和匹配時間。
這里設計的很多,大家可以根據需求學習指定算法。
參考文獻:
排序算法:http://data.biancheng.net/sort/
C語言二分查找算法,折半查找算法:http://c.biancheng.net/view/536.html
16圖的搜索算法之先深:https://www.cnblogs.com/gd-luojialin/p/10384761.html
貪心算法:https://blog.csdn.net/yongh701/article/details/49256321
字符串匹配算法:https://www.cnblogs.com/gaochundong/p/string_matching.html
C語言實現字符串匹配KMP算法:https://www.jb51.net/article/54123.htm
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
QQ群:711838388
出處:https://www.cnblogs.com/xiongze520/p/15816597.html
您可以隨意轉載、摘錄,但請在文章內注明作者和原文鏈接。