基礎夯實:基礎數據結構與演算法(一)
數據結構與演算法
數據結構(英語:data structure)是電腦中存儲、組織數據的方式。
數據結構是一種具有一定邏輯關係,在電腦中應用某種存儲結構,並且封裝了相應操作的數據元素集合。它包含三方面的內容,邏輯關係、存儲關係及操作。
不同種類的數據結構適合於不同種類的應用,而部分甚至專門用於特定的作業任務。例如,電腦網路依賴於路由表運作,B 樹高度適用於資料庫的封裝。
為什麼要學習數據結構和演算法?
隨著應用程式變得越來越複雜和數據越來越豐富,幾百萬、幾十億甚至幾百億的數據就會出現,
而對這麼大對數據進行搜索、插入或者排序等的操作就越來越慢,數據結構就是用來解決這些問題的。
常見的10種數據結構
1、數組
數組:數組是一種聚合數據類型,它是將具有相同類型的若干變數有序地組織在一起的集合。
定義結構體數組的方法很簡單,同定義結構體變數是一樣的,只不過將變數改成數組。
或者說同普通數組的定義是一模一樣的,如:
struct STUDENT stu[10];
這就定義了一個結構體數組,共有 10 個元素,每個元素都是一個結構體變數,都包含所有的結構體成員。
結構體數組的引用與引用一個結構體變數在原理上是一樣的。只不過結構體數組中有多個結構體變數,我們只需利用 for 循 環一個一個地使用結構體數組中的元素。
下面編寫一個程式,編程要求:從鍵盤輸入 5 個學生的基本資訊,如姓名、年齡、性別、學號,然後將學號最大的學生的基本資訊輸出到螢幕(如果相同則輸出最後一個)。
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 # include <stdio.h> # include <string.h> /* 編程要求: 從鍵盤輸入 5 個學生的基本資訊,如姓名、年齡、性別、學號,然後將學號最大的學生的基本資訊輸出到螢幕(如果相同則輸出最後一個)。 */ struct STU { char name[20]; int age; char sex; char num[20]; }; void OutputSTU(struct STU stu[5]); //函數聲明, 該函數的功能是輸出學號最大的學生資訊 int main(void) { int i; struct STU stu[5]; for (i = 0; i<5; ++i) { printf("請輸入第%d個學生的資訊:", i + 1); scanf("%s%d %c%s", stu[i].name, &stu[i].age, &stu[i].sex, stu[i].num);/*%c前面要加空格, 不然輸入時會將空格賦給%c*/ } OutputSTU(stu); system("PAUSE");//結束不退出 } void OutputSTU(struct STU stu[5]) { struct STU stumax = stu[0]; int j; for (j = 1; j<5; ++j) { if (strcmp(stumax.num, stu[j].num) < 0) //strcmp函數的使用 { stumax = stu[j]; } } printf("學生姓名:%s 學生年齡:%d 學生性別:%c 學生學號:%s\n", stumax.name, stumax.age, stumax.sex, stumax.num); }
2、鏈表
鏈表:鏈表是一種數據元素按照鏈式存儲結構進行存儲的數據結構,這種存儲結構具有在物理上存在非連續的特點。
鏈表,別名鏈式存儲結構或單鏈表,用於存儲邏輯關係為 “一對一” 的數據。鏈表不限制數據的物理存儲狀態,換句話說,使用鏈表存儲的數據元素,其物理存儲位置是隨機的。
例如,使用鏈表存儲 {1,2,3}
,數據的物理存儲狀態如圖 1 所示:
我們看到,圖 1 根本無法體現出各數據之間的邏輯關係。
對此,鏈表的解決方案是,每個數據元素在存儲時都配備一個指針,用於指向自己的直接後繼元素。如圖 2 所示:
像圖 2 這樣,數據元素隨機存儲,並通過指針表示數據之間邏輯關係的存儲結構就是鏈式存儲結構。
鏈表有 單鏈表、雙向鏈表、靜態鏈表,展開說的話比較多,這裡就演示單鏈表,想要學習更多鏈表直接點擊鏈接進入學習。
鏈表的節點
從圖 2 可以看到,鏈表中每個數據的存儲都由以下兩部分組成:
- 數據元素本身,其所在的區域稱為數據域;
- 指向直接後繼元素的指針,所在的區域稱為指針域;
即鏈表中存儲各數據元素的結構如圖 3 所示:
圖 3 所示的結構在鏈表中稱為節點。也就是說,鏈表實際存儲的是一個一個的節點,真正的數據元素包含在這些節點中,如圖 4 所示:
因此,鏈表中每個節點的具體實現,需要使用 C 語言中的結構體,具體實現程式碼為:
typedef struct Link{ char elem; //代表數據域 struct Link * next; //代表指針域,指向直接後繼元素 }link; //link為節點名,每個節點都是一個 link 結構體
提示,由於指針域中的指針要指向的也是一個節點,因此要聲明為 Link 類型(這裡要寫成 struct Link*
的形式)。
頭節點,頭指針和首元節點
其實,圖 4 所示的鏈表結構並不完整。一個完整的鏈表需要由以下幾部分構成:
- 頭指針:一個普通的指針,它的特點是永遠指向鏈表第一個節點的位置。很明顯,頭指針用於指明鏈表的位置,便於後期找到鏈表並使用表中的數據;
- 節點:鏈表中的節點又細分為頭節點、首元節點和其他節點:
- 頭節點:其實就是一個不存任何數據的空節點,通常作為鏈表的第一個節點。對於鏈表來說,頭節點不是必須的,它的作用只是為了方便解決某些實際問題;
- 首元節點:由於頭節點(也就是空節點)的緣故,鏈表中稱第一個存有數據的節點為首元節點。首元節點只是對鏈表中第一個存有數據節點的一個稱謂,沒有實際意義;
- 其他節點:鏈表中其他的節點;
因此,一個存儲 {1,2,3}
的完整鏈表結構如圖 5 所示:
注意:鏈表中有頭節點時,頭指針指向頭節點;反之,若鏈表中沒有頭節點,則頭指針指向首元節點。
明白了鏈表的基本結構,下面我們來學習如何創建一個鏈表。
鏈表的創建(初始化)
創建一個鏈表需要做如下工作:
- 聲明一個頭指針(如果有必要,可以聲明一個頭節點);
- 創建多個存儲數據的節點,在創建的過程中,要隨時與其前驅節點建立邏輯關係;
例如,創建一個存儲 {1,2,3,4}
且無頭節點的鏈表,C 語言實現程式碼如下:
link * initLink(){ link * p=NULL;//創建頭指針 link * temp = (link*)malloc(sizeof(link));//創建首元節點 //首元節點先初始化 temp->elem = 1; temp->next = NULL; p = temp;//頭指針指向首元節點 //從第二個節點開始創建 for (int i=2; i<5; i++) { //創建一個新節點並初始化 link *a=(link*)malloc(sizeof(link)); a->elem=i; a->next=NULL; //將temp節點與新建立的a節點建立邏輯關係 temp->next=a; //指針temp每次都指向新鏈表的最後一個節點,其實就是 a節點,這裡寫temp=a也對 temp=temp->next; } //返回建立的節點,只返回頭指針 p即可,通過頭指針即可找到整個鏈表 return p; }
3、堆
堆:堆是一種特殊的樹形數據結構,一般討論的堆都是二叉堆。
二叉堆是完全二元樹或者是近似完全二元樹,它分為兩種:最大堆和最小堆。
最大堆:父結點的鍵值總是大於或等於任何一個子節點的鍵值;
最小堆:父結點的鍵值總是小於或等於任何一個子節點的鍵值。示意圖如下:
假設在最大堆[90,80,70,60,40,30,20,10,50]種添加85,需要執行的步驟如下:
如上圖所示,當向最大堆中添加數據時:先將數據加入到最大堆的最後,然後儘可能把這個元素往上挪,直到挪不動為止!
將85添加到[90,80,70,60,40,30,20,10,50]中後,最大堆變成了[90,85,70,60,80,30,20,10,50,40]。
/* * 最大堆的向上調整演算法(從start開始向上直到0,調整堆) * * 註:數組實現的堆中,第N個節點的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 * * 參數說明: * start -- 被上調節點的起始位置(一般為數組中最後一個元素的索引) */ static void maxheap_filterup(int start) { int c = start; // 當前節點(current)的位置 int p = (c-1)/2; // 父(parent)結點的位置 int tmp = m_heap[c]; // 當前節點(current)的大小 while(c > 0) { if(m_heap[p] >= tmp) break; else { m_heap[c] = m_heap[p]; c = p; p = (p-1)/2; } } m_heap[c] = tmp; } /* * 將data插入到二叉堆中 * * 返回值: * 0,表示成功 * -1,表示失敗 */ int maxheap_insert(int data) { // 如果"堆"已滿,則返回 if(m_size == m_capacity) return -1; m_heap[m_size] = data; // 將"數組"插在表尾 maxheap_filterup(m_size); // 向上調整堆 m_size++; // 堆的實際容量+1 return 0; }
更多詳情請點擊:二叉堆(一)之 圖文解析 和 C語言的實現 ://www.cnblogs.com/skywang12345/p/3610187.html
4、棧
棧:棧是一種特殊的線性表,它只能在一個表的一個固定端進行數據結點的插入和刪除操作。
棧是限制插入和刪除只能在一個位置上進行的線性表。其中,允許插入和刪除的一端位於表的末端,叫做棧頂(top),不允許插入和刪除的另一端叫做棧底(bottom)。
對棧的基本操作有 PUSH(壓棧)和 POP (出棧),前者相當於表的插入操作(向棧頂插入一個元素),後者則是刪除操作(刪除一個棧頂元素)。
棧是一種後進先出(LIFO)的數據結構,最先被刪除的是最近壓棧的元素。
棧就像是一個箱子,往裡面放入一個小盒子就相當於壓棧操作,往裡面取出一個小盒子就是出棧操作,取盒子的時候,最後放進去的盒子會最先被取出來,最先放進去的盒子會最後被取出來,這即是後入先出。
下面是一個棧的示意圖:
如下入棧出棧示例:
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> //元素elem進棧 int push(int* a, int top, int elem){ a[++top] = elem; return top; } //數據元素出棧 int pop(int * a, int top){ if (top == -1) { printf("空棧"); return -1; } printf("彈棧元素:%d\n", a[top]); top--; return top; } int main() { int a[100]; int top = -1; top = push(a, top, 1); top = push(a, top, 2); top = push(a, top, 3); top = push(a, top, 4); top = pop(a, top); top = pop(a, top); top = pop(a, top); top = pop(a, top); top = pop(a, top); system("PAUSE");//結束不退出 }
5、隊列
隊列:隊列和棧類似,也是一種特殊的線性表。和棧不同的是,隊列只允許在表的一端進行插入操作,而在另一端進行刪除操作。
與棧結構不同的是,隊列的兩端都”開口”,要求數據只能從一端進,從另一端出,如圖 1 所示:
通常,稱進數據的一端為 “隊尾”,出數據的一端為 “隊頭”,數據元素進隊列的過程稱為 “入隊”,出隊列的過程稱為 “出隊”。
不僅如此,隊列中數據的進出要遵循 “先進先出” 的原則,即最先進隊列的數據元素,同樣要最先出隊列。
拿圖 1 中的隊列來說,從數據在隊列中的存儲狀態可以分析出,元素 1 最先進隊,其次是元素 2,最後是元素 3。
此時如果將元素 3 出隊,根據隊列 “先進先出” 的特點,元素 1 要先出隊列,元素 2 再出隊列,最後才輪到元素 3 出隊列。
棧和隊列不要混淆,棧結構是一端封口,特點是”先進後出”;而隊列的兩端全是開口,特點是”先進先出”。
因此,數據從表的一端進,從另一端出,且遵循 “先進先出” 原則的線性存儲結構就是隊列。
如下示例:
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> int enQueue(int *a, int rear, int data){ a[rear] = data; rear++; return rear; } void deQueue(int *a, int front, int rear){ //如果 front==rear,表示隊列為空 while (front != rear) { printf("出隊元素:%d\n", a[front]); front++; } } int main() { int a[100]; int front, rear; //設置隊頭指針和隊尾指針,當隊列中沒有元素時,隊頭和隊尾指向同一塊地址 front = rear = 0; //入隊 rear = enQueue(a, rear, 1); rear = enQueue(a, rear, 2); rear = enQueue(a, rear, 3); rear = enQueue(a, rear, 4); //出隊 deQueue(a, front, rear); system("PAUSE");//結束不退出 }
6、散列表(哈希表)
散列表:散列表源自於散列函數(Hash function),其思想是如果在結構中存在關鍵字和T相等的記錄,那麼必定在F(T)的存儲位置可以找到該記錄,
這樣就可以不用進行比較操作而直接取得所查記錄。
構造散列函數考慮的因素:
- 執行速度
- 關鍵字長度
- 散列表大小
- 關鍵字的分布情況
- 查找頻率
根據元素集合的特性構造
- 要求一:n 個數據原僅佔用 n 個地址,雖然散列查找是以空間換時間,但仍希望散列的地址空間盡量小
- 要求二:無論用什麼方法儲存,目的都是盡量均勻地存放元素,以避免衝突
解決衝突的辦法:
- 直接定址法
- (取關鍵字的某個線性函數值為散列地址:f (key) = a*key + b )
- 簡單、均勻也不會有衝突但需要事先知道關鍵字的排布,適合表較小且連續的情況
- 數字分析法
- 平方取中法
- 摺疊法
- 除留取余法
- Hash(key) = key % p (p 是整數),設表長為 m ,取 p <= m 且為質數
- 隨機數法
結構:
#define MAXSIZE 1000 #define NULLKEY -65535 typedef struct { int* elem; //數據元素存儲基址,數組 int size; //元素個數 }HashTable; int m = 0;//散列表表長
散列表的創建:
void IniHashTable(HashTable* H) { int i; m = MAXSIZE; H->size = m; H->elem = (int*)malloc(m* sizeof(int)); for (i = 0; i < m; i++) { H->elem[i] = NULLKEY; } }
散列函數:
根據不同的情況改變演算法
int Hash(int key) { return key % m; //除留取余法 }
插入元素:
void InsertHash(HashTable* H, int key) { int addr = Hash(key); //求散列地址 while (H->elem[addr] != NULLKEY) //不為空則衝突 { addr = (addr + 1) % m; //開放地址法的線性探測 } H->elem[addr] = key; //發現有空位後插入 }
查找元素:
int Search(HashTable* H, int key) { int addr = Hash(key); //求散列地址 while (H->elem[addr] != key) //不為空則衝突 { addr = (addr + 1) % m; //開放地址法的線性探測 if (H->elem[addr] == NULLKEY || addr == Hash(key)) { return false; } } return true; }
7、二叉樹
二叉樹:二叉樹是指樹中節點的度不大於2的有序樹,它是一種最簡單且最重要的樹。
二叉樹的遞歸定義為:二叉樹是一棵空樹,或者是一棵由一個根節點和兩棵互不相交的,分別稱作根的左子樹和右子樹組成的非空樹;左子樹和右子樹又同樣都是二叉樹 。
二叉排序樹要麼是空二叉樹,要麼具有如下特點:
- 二叉排序樹中,如果其根結點有左子樹,那麼左子樹上所有結點的值都小於根結點的值;
- 二叉排序樹中,如果其根結點有右子樹,那麼右子樹上所有結點的值都大小根結點的值;
- 二叉排序樹的左右子樹也要求都是二叉排序樹;
例如,圖 1 就是一個二叉排序樹:
使用二叉排序樹查找關鍵字
二叉排序樹中查找某關鍵字時,查找過程類似於次優二叉樹,在二叉排序樹不為空樹的前提下,首先將被查找值同樹的根結點進行比較,會有 3 種不同的結果:
- 如果相等,查找成功;
- 如果比較結果為根結點的關鍵字值較大,則說明該關鍵字可能存在其左子樹中;
- 如果比較結果為根結點的關鍵字值較小,則說明該關鍵字可能存在其右子樹中;
實現函數為:(運用遞歸的方法)
BiTree SearchBST(BiTree T,KeyType key){ //如果遞歸過程中 T 為空,則查找結果,返回NULL;或者查找成功,返回指向該關鍵字的指針 if (!T || key==T->data) { return T; }else if(key<T->data){ //遞歸遍歷其左孩子 return SearchBST(T->lchild, key); }else{ //遞歸遍歷其右孩子 return SearchBST(T->rchild, key); } }
使用二叉排序樹在查找表中做查找操作的時間複雜度同建立的二叉樹本身的結構有關。即使查找表中各數據元素完全相同,但是不同的排列順序,構建出的二叉排序樹大不相同。
例如:查找表 {45,24,53,12,37,93}
和表 {12,24,37,45,53,93}
各自構建的二叉排序樹圖下圖所示:
使用二叉排序樹實現動態查找操作的過程,實際上就是從二叉排序樹的根結點到查找元素結點的過程,所以時間複雜度同被查找元素所在的樹的深度(層次數)有關。
為了彌補二叉排序樹構造時產生如圖 5 右側所示的影響演算法效率的因素,需要對二叉排序樹做「平衡化」處理,使其成為一棵平衡二叉樹。
更多詳情點擊 二叉排序樹(二叉查找樹)及C語言實現
8、跳錶
跳錶:跳錶是一個隨機化的數據結構,可以被看做二叉樹的一個變種,它在性能上和紅黑樹,AVL樹不相上下,但是跳錶的原理非常簡單,目前在Redis和LeveIDB中都有用到。
跳錶使用空間換時間的設計思路,通過構建多級索引來提高查詢的效率,實現了基於鏈表的「二分查找」。
跳錶是一種動態數據結構,支援快速的插入、刪除、查找操作,時間複雜度都是 O(logn)。跳錶的空間複雜度是 O(n)。
不過,跳錶的實現非常靈活,可以通過改變索引構建策略,有效平衡執行效率和記憶體消耗。
詳細見:跳錶C語言實現詳解
9、圖
圖:圖是另一種非線性數據結構。在圖結構中,數據結點一般稱為頂點,而邊是頂點的有序偶對。
使用圖結構表示的數據元素之間雖然具有「多對多」的關係,但是同樣可以採用順序存儲,也就是使用數組有效地存儲圖。
使用數組存儲圖時,需要使用兩個數組,一個數組存放圖中頂點本身的數據(一維數組),另外一個數組用於存儲各頂點之間的關係(二維數組)。
不同類型的圖,存儲的方式略有不同,根據圖有無權,可以將圖劃分為兩大類:圖和網 。
圖,包括無向圖和有向圖;
網,是指帶權的圖,包括無向網和有向網。
存儲方式的不同,指的是:在使用二維數組存儲圖中頂點之間的關係時,如果頂點之間存在邊或弧,在相應位置用 1 表示,反之用 0 表示;
如果使用二維數組存儲網中頂點之間的關係,頂點之間如果有邊或者弧的存在,在數組的相應位置存儲其權值;反之用 0 表示。
結構程式碼表示:
#define MAX_VERtEX_NUM 20 //頂點的最大個數 #define VRType int //表示頂點之間的關係的變數類型 #define InfoType char //存儲弧或者邊額外資訊的指針變數類型 #define VertexType int //圖中頂點的數據類型 typedef enum{DG,DN,UDG,UDN}GraphKind; //枚舉圖的 4 種類型 typedef struct { VRType adj; //對於無權圖,用 1 或 0 表示是否相鄰;對於帶權圖,直接為權值。 InfoType * info; //弧或邊額外含有的資訊指針 }ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; typedef struct { VertexType vexs[MAX_VERtEX_NUM]; //存儲圖中頂點數據 AdjMatrix arcs; //二維數組,記錄頂點之間的關係 int vexnum,arcnum; //記錄圖的頂點數和弧(邊)數 GraphKind kind; //記錄圖的種類 }MGraph;
例如,存儲圖 1 中的無向圖(B)時,除了存儲圖中各頂點本身具有的數據外,還需要使用二維數組存儲任意兩個頂點之間的關係。
由於 (B) 為無向圖,各頂點沒有權值,所以如果兩頂點之間有關聯,相應位置記為 1 ;反之記為 0 。構建的二維數組如圖 2 所示。
在此二維數組中,每一行代表一個頂點,依次從 V1 到 V5 ,每一列也是如此。比如 arcs[0][1] = 1 ,表示 V1 和 V2 之間有邊存在;而 arcs[0][2] = 0,說明 V1 和 V3 之間沒有邊。
對於無向圖來說,二維數組構建的二階矩陣,實際上是對稱矩陣,在存儲時就可以採用壓縮存儲的方式存儲下三角或者上三角。
通過二階矩陣,可以直觀地判斷出各個頂點的度,為該行(或該列)非 0 值的和。例如,第一行有兩個 1,說明 V1 有兩個邊,所以度為 2。
存儲圖 1 中的有向圖(A)時,對應的二維數組如圖 3 所示:
更多詳情點擊 圖的順序存儲結構(包含C語言實現)
10、Trie樹
Trie樹:又稱單詞查找樹,Trie樹,字典樹,是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字元串(但不僅限於字元串),
所以經常被搜索引擎系統用於文本詞頻統計。它的優點是:利用字元串的公共前綴來減少查詢時間,最大限度地減少無謂的字元串比較,查詢效率比哈希樹高。
如下程式碼,定義結構體、初始化Trie樹、插入、查找、刪除:
#define _CRT_SECURE_NO_WARNINGS //避免scanf報錯 #include <stdio.h> #define MAX 26 //26個字母 #define SLEN 100 //節點中存儲的字元串長度 //Trie結構體定義 struct Trie { struct Trie *next[MAX]; char s[SLEN]; //節點處存儲的字元串 int isword; //節點處是否為單詞 char val; //節點的代表字元 } *root; //初始化Trie樹 struct Trie *init() { struct Trie *root = (struct Trie *)malloc(sizeof(struct Trie)); int i; for (i = 0; i < MAX; i++) { root->next[i] = NULL; } root->isword = 0; root->val = 0; return root; } //按照指定路徑path 插入字元串 s void insert(char path[], char s[]) { struct Trie *t, *p = root; int i, j, n = strlen(path); for (i = 0; i < n; i++) { if (p->next[path[i] - 'a'] == NULL) { t = (struct Trie *)malloc(sizeof(struct Trie)); for (j = 0; j < MAX; j++) { t->next[j] = NULL; t->isword = 0; } t->val = path[i]; p->next[path[i] - 'a'] = t; } p = p->next[path[i] - 'a']; } p->isword = 1; strcpy(p->s, s); } //按照指定路徑 path 查找 char *find(char path[], int delflag) { struct Trie *p = root; int i = 0, n = strlen(path); while (p && path[i]) { p = p->next[path[i++] - 'a']; } if (p && p->isword) { p->isword = delflag; return p->s; } return NULL; } //刪除整棵Trie樹 void del(struct Trie *root) { int i; if (!root) return; for (i = 0; i < MAX; i++) { if (root->next[i]) del(root->next[i]); free(root->next[i]); } }
下集預告
基礎夯實:基礎數據結構與演算法(二)
參考文獻
單鏈表://c.biancheng.net/view/3336.html
雙向鏈表://c.biancheng.net/view/3342.html
靜態鏈表://c.biancheng.net/view/3339.html
二叉堆(一)之 圖文解析 和 C語言的實現 ://www.cnblogs.com/skywang12345/p/3610187.html
二叉排序樹(二叉查找樹)及C語言實現://c.biancheng.net/view/3431.html
跳錶C語言實現詳解://blog.csdn.net/m0_37845735/article/details/103691814
圖的順序存儲結構(包含C語言實現)://c.biancheng.net/view/3407.html
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
QQ群:711838388
出處://www.cnblogs.com/xiongze520/p/15812527.html
您可以隨意轉載、摘錄,但請在文章內註明作者和原文鏈接。