資料結構與演算法
資料結構(英語: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語言的實現 :https://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]); } }
下集預告
基礎夯實:基礎資料結構與演算法(二)
參考文獻
單連結串列:http://c.biancheng.net/view/3336.html
雙向連結串列:http://c.biancheng.net/view/3342.html
靜態連結串列:http://c.biancheng.net/view/3339.html
二叉堆(一)之 圖文解析 和 C語言的實現 :https://www.cnblogs.com/skywang12345/p/3610187.html
二叉排序樹(二叉查詢樹)及C語言實現:http://c.biancheng.net/view/3431.html
跳錶C語言實現詳解:https://blog.csdn.net/m0_37845735/article/details/103691814
圖的順序儲存結構(包含C語言實現):http://c.biancheng.net/view/3407.html
歡迎關注訂閱微信公眾號【熊澤有話說】,更多好玩易學知識等你來取
作者:熊澤-學習中的苦與樂
公眾號:熊澤有話說
QQ群:711838388
出處:https://www.cnblogs.com/xiongze520/p/15812527.html
您可以隨意轉載、摘錄,但請在文章內註明作者和原文連結。