數據結構基礎知識
線性結構
(1)連續儲存(地址在內存中為連續)-----數組
(2)離散儲存(地址在內存中不一定為連續的)-----鏈表
非線性結構
(1)樹
(2)圖
基礎算法(查找排序):
折半查找
排序:
(1)冒泡排序
(2)插入排序
(3)選擇排序
(4)快排
(5)並歸
3.C語言指針(數據結構基礎):
(1)指針
(2)結構體(C++可以使用類)
(3)動態內存的分配與釋放(malloc函數)
//聲明一個整型指針變量p
//注意:此時我們聲明的變量為p,而不是*p
int a=100;
int* p;
int** q;
p=&a;// *p=a;
q=&p;=》q等於p的地址=》*q=p;=》**q=*p;=》**q=a;
此時,以下變量的含義是:
p: p為指向一個整型變量的指針變量,儲存着所指向的整型變量的地址
*p: *是指針運算符,*p為指針變量p所指向的存儲單元中的內容
&a:&為取地址運算符,&a為a的在內存中的地址
q:一個指向指針變量p的指針變量,其中儲存的是p的地址
具體可見下表所示:
變量 | a | p | *p | q | *q | **q |
儲存的值 | 100 | a的地址 | 100 | p的地址 | p | 100 |
概念混淆梳理:
1.指針變量(4字節)地址編號為首字節編號
2.指針變量和指針(地址)不是一個概念,指針變量是用來的存放地址的變量
3.指針變量占四個字節,指針變量大小和操作系統有關,32位系統時占四個字節,64位系統時占八個字節,當然,也與編譯器類型有關,32位編譯器(Win32)編譯的程序占四個字節
附圖:
傳參數改變值案例代碼:
#include "stdio.h" void f(int* i) { *i = 1000; } int main(void) { int p = 100; f(&p); printf("p=%d\n", p); return 0; }
代碼最后執行結果是:p=1000
值傳遞和地址傳遞問題:
值傳遞原理在於將值復制到形參相應儲存單元中,即形參和實參擁有不同的儲存單元(地址),這種方式形參值變化不影響實參,而地址傳遞則是將
一維數組:
一維數組名是一個指針常量:
int a[5]={0,1,2,3,4,5}; //聲明一個大小為5的數組
a是一個指針常量,這是因為a實際上是數組第一個元素的地址,即a[0]的地址,即:
*a = a[0]; // a = &a[0];
有以下式子成立:
a[i] 《=》*(a+i) //原因為一維數組在內存中地址連續,故a[i]的值為地址a加上i后得到的新地址取其指向的值
Tip:關於編譯預處理命令
作用:編譯之前要處理的內容
詳解:stdio: std:標准 io:輸入輸出流
#include <stdio.h> =》引入系統標准庫時使用,此種方式只搜索系統目錄,速度較快
#include “stdio.h” =》引入自定義文件時使用,其先搜索項目目錄,再搜索系統目錄,耗時較長
注意,千萬不要寫成studio(手動滑稽)
被調函數修改主調函數中一維數組的值示例代碼
//只需要傳入被調函數兩個參數,1.數組首元素地址2.數組長度 此時便可以在被調函數中隨意訪問數組元素 #include <stdio.h> void f(int *p ,int len) { p[1] = 9; } int main(void) { int a[5] = { 0,1,2,3,4 }; sizeof(int); f(a,sizeof(a)/sizeof(int)); printf("a[1]為 %d\n", a[1]); return 0; }
結構體:
只有成員,沒有方法
為了表示一些較為復雜的數據結構,而普通的基本類型無法滿足要求
結構體定義與初始化示例代碼:
#include <stdio.h> #include <string.h> struct Student { int sid; char name[200]; //C語言中沒有字符串概念,統一使用字符數組來操作 int age; };//這里不要忘記加上';',這里是語句終止 int main(void) { //這里是第一種賦值方式 struct Student st = { 1000 , "strcpy()" , 20 }; //這里是第二種賦值方式 //struct Student st; //st.sid =1000; //strcpy_s(st.name,"zhangsan");//這里使用可以保證緩沖區大小的安全型函數strcpy_s(),使用strcpy()會報錯,提示不安全 //st.age =20; printf("%d %s %d\n", st.sid, st.name, st.age); return 0; }
使用指針來初始化結構體示例代碼
#include <stdio.h> #include <string.h> struct Student { int sid; char name[200]; //C語言中沒有字符串概念,統一使用字符數組來操作 int age; }; int main(void) { struct Student st; struct Student* Pst; Pst = &st; Pst->sid = 99; //Pst->sid 等價於(*Pst).sid 指針Pst所指向的變量的成員sid Pst->age = 20; strcpy_s(Pst->name, "zhangsan"); printf("%d %s %d\n", st.sid, st.name, st.age); return 0; }
綜上,指針的優越性在於:
當以傳值方式來調用時,需要將變量復制一份,故要在內存中開辟一塊同樣大小的內存,並且會耗費大量的時間。
當以傳遞指針方式來調用時,此時只需要開辟一塊新的內存空間來存放新聲明的指針變量,大小永遠為4個字節,由此可見,當新聲明的變量大小大於四個字節時,使用指針變量無疑是非常合適的,既能節省空間,也可以節省時間。
當然,對於int類型的變量來說,就無所謂,因為其也為四個字節大小。對於結構體來說,指針變量無疑是福音。
線性結構:
(1)數組
(2)鏈表:棧,隊列
動態數組
代碼實現一個動態數組:
#include <stdio.h> #include <stdlib.h> struct Arr { int* pBase; //數組首地址 int len; //數組所容納最大元素個數 int cnt; //當前數組有效元素個數 int increment; //自動增長因子 }; void init_arr(struct Arr *pArr ,int length ); //初始化 bool is_empty(struct Arr* pArr); //檢測數組是否為空 bool is_full(struct Arr* pArr); //檢測數組是否為滿 void show_arr(struct Arr* pArr);//輸出數組元素 bool append_arr(struct Arr* pArr,int val);//在數組末尾追加元素 bool insert_arr(struct Arr* pArr, int pos, int val);//插入到pos位置,pos表示數組中第幾個 bool delete_arr(struct Arr* pArr, int pos, int* pval);//刪除指定位置的元素,pos表示數組中第幾個 void inversion_arr(struct Arr* pArr); //void sort_arr();//數組排序 int main(void) { int pval; struct Arr arr; init_arr(&arr,10); show_arr(&arr); append_arr(&arr, 1); append_arr(&arr, 3); append_arr(&arr, 2); append_arr(&arr, 4); show_arr(&arr); insert_arr(&arr,3,9); show_arr(&arr); delete_arr(&arr,3, &pval); show_arr(&arr); printf("刪除的元素是%d \n", pval); inversion_arr(&arr); show_arr(&arr); return 0; } //初始化動態數組 void init_arr(struct Arr* pArr, int length) { pArr->pBase = (int*)malloc(sizeof(int) * length); if (NULL == pArr->pBase) { printf("動態內存分配失敗!"); exit(-1);//終止程序 } else { pArr->len = length; pArr->cnt = 0; } } //檢測數組是否為空 bool is_empty(struct Arr* pArr) { if (pArr->cnt == NULL) { return true; } else { return false; } } //檢測數組是否為滿 bool is_full(struct Arr* pArr) { if (pArr->cnt == pArr->len) { return true; } else { return false; } } //輸出數組元素 void show_arr(struct Arr* pArr) { if (is_empty(pArr)) { printf("數組為空!\n"); } else { for (int i = 0;i < pArr->cnt;i++) { printf("%d", pArr->pBase[i]); printf("\n"); } } } //在數組末尾追加元素 bool append_arr(struct Arr* pArr, int val) { //數組已滿 if (is_full(pArr)) { return false; } pArr->pBase[pArr->cnt] = val; (pArr->cnt)++; } //插入到pos位置,pos表示數組中第幾個 //插入位置后面的元素全部后移 bool insert_arr(struct Arr* pArr, int pos, int val) { if (pos<1 || pos>pArr->cnt + 1 || is_full(pArr)) //pos最大只能插入到現有元素后一個 { return false; } for (int i = pArr->cnt - 1;i >= pos - 1;--i) { pArr->pBase[i + 1] = pArr->pBase[i]; } pArr->pBase[pos - 1] = val; (pArr->cnt)++; } //刪除指定位置的元素,pos表示數組中第幾個 //刪除位置后面的元素全部前移 bool delete_arr(struct Arr* pArr, int pos, int* pval) { if (is_empty(pArr)) { return false; } if (pos<1|| pos>pArr->cnt) { return false; } *pval = pArr->pBase[pos - 1]; for (int i = pos;i < pArr->cnt;++i) { pArr->pBase[i - 1] = pArr->pBase[i]; } pArr->cnt--; return true; } //將數組中所有元素倒序 void inversion_arr(struct Arr* pArr) { int i = 0; int j = pArr->cnt-1; int t; while (i < j) { t = pArr->pBase[i]; pArr->pBase[i] = pArr->pBase[j]; pArr->pBase[j] = t; ++i; --j; } }
鏈表
預備知識:typedef的用法
typedef struct Student { int sid; char name[100]; char sex; }*PST,ST ;
這里的意思是:將結構體Student重命名為ST,將結構體Student指針類型重命名為*PST,現可聲明如下:
ST st; // struct Student st;
PST pst; // struct Student* pst;
想要確定一個鏈表,我們需要什么參數?(什么可以確定一個鏈表?)
頭指針
通過頭指針我們即可推算出鏈表其他所有信息
下面為一個鏈表的示意圖:
數據域:數據域中儲存的是指針中實際的數據
指針域:指針域中儲存的是下一個節點的地址(指向下一個節點的指針)
頭指針:頭指針是為了方便鏈表操作所添加的節點
首指針:存有數據的鏈表的第一個節點
下面是使用代碼來實現一個指針節點:
#include <stdio.h> typedef struct Node { int data; PNODE pNext;//struct Node* pNext }NODE,*PNODE;
鏈表的分類:
單鏈表:每個節點只有一個指針域
雙鏈表:每一個節點有兩個指針域
循環鏈表:能通過任何一個節點找到其他所有的節點
實現的鏈表算法:
- 遍歷
- 查找
- 清空
- 銷毀
- 求長度
- 排序
- 刪除節點
- 插入節點
對於鏈表的插入節點與刪除節點操作:
1.插入節點:
q->pNext = p->pNext; //q的指針域指向p的下一個節點
p->pNext=q; //p的指針域指向q所指向的節點
2.刪除節點:
r=p->pNext;//將p的下一個節點的地址賦給r
p->pNext=p->pNext->pNext;//p指向下下個節點
int i =r->data;
free(r);//釋放無用內存
return i ;//返回刪除的節點值
代碼實現一個鏈表的基礎操作:
#include <stdio.h> #include <malloc.h> #include <stdlib.h> typedef struct Node { int data; struct Node* pNext;//不能寫為:PNODE pNext }NODE,*PNODE; PNODE create_list(void); //生成一個鏈表 void traverse_list(PNODE pHead); //輸出一個鏈表 bool is_empty(PNODE pHead); //判斷鏈表是否為空 int length_list(PNODE pHead); //計算鏈表節點個數 void sort_list(PNODE pHead); //鏈表排序算法(冒泡排序腳標式) bool insert_list(PNODE pHead, int pos, int val); //在鏈表中指定位置插入一個數據 bool delete_list(PNODE pHead, int pos, int* pval); //在鏈表中指定位置刪除一個數據 int main(void) { PNODE pHead = create_list(); int len = length_list(pHead); printf("鏈表元素個數為%d\n", len); traverse_list(pHead); bool e = is_empty(pHead); printf("鏈表%s", e?"為空":"不為空"); sort_list(pHead); traverse_list(pHead); int val = 0; int pos = 0; printf_s("請輸入要插入到鏈表中的值與位置!"); scanf_s("%d %d", &val, &pos); insert_list(pHead, pos, val); traverse_list(pHead); printf_s("請輸入要刪除鏈表中的元素位置!"); scanf_s("%d",&pos); delete_list(pHead,pos,&val); printf_s("被刪除的元素是%d!", val); return 0; } //生成一個鏈表 PNODE create_list(void) { int len; //生成的鏈表節點個數,用戶輸入 int i; int val;//用戶輸入的節點臨時值 PNODE pHead = (PNODE)malloc(sizeof(NODE));//分配頭結點內存空間 if (pHead == NULL) { printf("分配失敗,程序終止!"); exit(-1);//終止程序 } PNODE pTail = pHead;//尾節點指針先指向頭結點 pTail->pNext = NULL; printf("請輸入您需要生成的鏈表元素個數:"); scanf_s("%d", &len); for (i = 0;i < len;++i) { printf("請輸入第%d個節點的值:",i+1); scanf_s("%d", &val); PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (pNew == NULL) { printf("分配失敗,程序終止!"); exit(-1); } pNew->data = val; //分配數據 pTail->pNext = pNew;//尾節點指針域指向新生成的節點,相當於將新節點鏈到鏈表末尾 pNew->pNext = NULL; pTail = pNew; //新生成的節點變成尾節點,此時尾節點指向新生成的節點 } return pHead;//頭指針便可確定一個鏈表,故返回頭節點即可 } //輸出一個鏈表 void traverse_list(PNODE pHead) { PNODE p = pHead->pNext;//p指向首節點 if (NULL == p) { printf("鏈表為空!"); exit(-1); } while (NULL!=p)//C語言區分大小寫 { printf("%d", p->data); p = p->pNext;//p指向鏈表下一個節點 } printf("\n"); } //判斷鏈表是否為空 bool is_empty(PNODE pHead) { if (NULL == pHead->pNext) { return true; } else { return false; } } //計算鏈表節點個數 int length_list(PNODE pHead) { PNODE p = pHead->pNext;//p指向首節點 int len = 0; while (NULL != p)//C語言區分大小寫 { p = p->pNext;//p指向下一節點 len++; } return len; } //鏈表排序算法(冒泡排序腳標式) void sort_list(PNODE pHead ) { int i, j, t; PNODE p, q; int len = length_list(pHead); for (i = 0, p = pHead->pNext;i < len - 1;++i, p = p->pNext) { for (j=i+1,q=p->pNext;j<len;++j,q=q->pNext) { if (p->data > q->data) { t = p->data; p->data = q->data; q->data = t; } } } return; } //在鏈表中指定位置插入一個數據 //pHead:頭節點指針 //pos:插入位置,值從1開始,插入在第pos個元素前面 //val:插入元素的值 bool insert_list(PNODE pHead ,int pos,int val) { int i=0; PNODE p = pHead; //指針指向到pos位置前一個元素身上,需移動(pos-1)次,由於i從0開始,故為小於pos-1 while (NULL!=p&&i<pos-1) { p = p->pNext; ++i; } if (i > pos - 1 || NULL == p) { return false; } PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (NULL == pNew) { printf_s("內存分配失敗!"); exit(-1); } //將pNew鏈到p后面,將原先的p后面的元素用q保存,最后將這些元素鏈到pNew后面 pNew->data = val; PNODE q = p->pNext; p->pNext = pNew; pNew->pNext = q; return true; } //在鏈表中指定位置刪除一個數據 //pHead:頭節點指針 //pos:刪除位置,值從1開始 //pval:帶出值的指針 bool delete_list(PNODE pHead, int pos, int *pval) { int i = 0; PNODE p = pHead; //指針指向到pos位置前一個元素身上,需移動(pos-1)次,由於i從0開始,故為小於pos-1 while (NULL != p->pNext && i < pos - 1) { p = p->pNext; ++i; } if (i > pos - 1 || NULL == p->pNext) { return false; } //先用q保存要刪掉的節點地址,防止內存泄露,再將下下個節點鏈到p后面即可 PNODE q = p->pNext; *pval = q->data; //刪除后面的節點 p->pNext = p->pNext->pNext; free(q); q = NULL; return true; }
關於泛型:
數據實現不一樣,操作一樣,便是泛型
如何實現:
通過C++中的重載運算符便可以使代碼在表面上看起來是統一的,從而實現泛型。
程序:
數據結構:研究數據存儲問題
(1)個體的存儲
(2)個體關系的存儲
算法:對存儲數據的操作
棧
一種可以實現“先進后出”的存儲結構
分類:
靜態棧
動態棧(鏈表實現)
棧的核心操作:
壓棧(入棧)
出棧
棧的示意圖如下:
為什么不反過來?
原因:入棧代碼確實會更簡單,但出棧代碼會很復雜,pTop上一個節點會找不到,導致pTop無法指向上一個節點。
實現代碼如下:
#include <stdio.h> #include <malloc.h> #include <stdlib.h> typedef struct Node { int data; struct Node* pNext; } NODE,*PNODE; typedef struct Stack { PNODE pTop; PNODE pBottom; }STACK,*PSTACK; void init(PSTACK pS); //初始化一個棧 void push(PSTACK pS, int val); //入棧 bool empty(PSTACK pS); //判斷棧是否為空 void traverse(PSTACK pS); //遍歷一個棧 bool pop(PSTACK pS, int* pval);//出棧 void clear_one(PSTACK pS); //清空棧第一種實現 void clear_two(PSTACK pS); //清空棧第二種實現 int main(void) { STACK S; init(&S); traverse(&S); push(&S,1); push(&S,2); push(&S,3); push(&S,4); push(&S,5); push(&S,6); traverse(&S); int i; pop(&S,&i); pop(&S,&i); printf_s("出棧元素是%d", i); pop(&S,&i); pop(&S,&i); traverse(&S); push(&S, 1); push(&S, 2); push(&S, 3); push(&S, 4); push(&S, 5); push(&S, 6); traverse(&S); clear_one(&S); traverse(&S); push(&S, 1); push(&S, 2); push(&S, 3); push(&S, 4); push(&S, 5); push(&S, 6); traverse(&S); clear_two(&S); traverse(&S); return 0; } //初始化一個棧 void init(PSTACK pS) { pS->pTop = (PNODE)malloc(sizeof(NODE)); if (NULL== pS->pTop) { printf_s("動態分配內存失敗"); exit(-1); } else { pS->pBottom = pS->pTop; //將pTop指向的節點的指針域賦空(使其不鏈着下一個節點) pS->pTop->pNext = NULL; } } //入棧 //pS:指向棧的指針 //val:入棧元素的值 void push(PSTACK pS, int val) { PNODE pNew = (PNODE)malloc(sizeof(NODE)); if (NULL== pNew) { printf_s("動態分配內存失敗"); exit(-1); } pNew->data = val; pNew->pNext = pS->pTop; pS->pTop = pNew; return; } //判斷棧是否為空 bool empty(PSTACK pS) { if (pS->pTop == pS->pBottom) { return true; } else { return false; } } //遍歷一個棧 void traverse(PSTACK pS) { if (empty(pS)) { printf_s("棧為空!"); return; } PNODE p = pS->pTop; while (p!=pS->pBottom) { printf_s("%d \n", p->data); p = p->pNext; } } //出棧 //pS:指向棧的指針 //pval:出棧元素的指針 bool pop(PSTACK pS, int* pval) { if (empty(pS)) { return false; } else { PNODE p = pS->pTop; *pval = p->data; pS->pTop = pS->pTop->pNext; free(p); p = NULL; return true; } } //清空棧第一種實現 //r為一個指針,永遠指向p下一個節點 void clear_one(PSTACK pS) { if (empty(pS)) { printf_s("棧為空!"); return; } PNODE p = pS->pTop; PNODE r = NULL; while (p!=pS->pBottom) { r = p->pNext; free(p); p = r; } pS->pTop = pS->pBottom; return; } //清空棧第二種實現,比較好理解 //r用於保存要釋放內存的節點 void clear_two(PSTACK pS) { if (empty(pS)) { printf_s("棧為空!"); return; } PNODE p = pS->pTop; PNODE r = NULL; while (p != pS->pBottom) { r = p; p = p->pNext; free(r); r = NULL; } pS->pTop = pS->pBottom; return; }
棧的應用:
1.函數調用
2.中斷
3.表達式求值
4.內存分配
5.緩沖處理
6.走迷宮問題
隊列
定義:一種可以實現先進先出的存儲結構
分類:
1.鏈式隊列(鏈表實現)
2.靜態隊列(數組實現)
重點:數組實現的循環隊列
隊列頭:front 指向第一個元素
隊列尾:rear 指向最后一個元素后面的元素
示意圖如下所示:
由圖中可見,rear指向的實際上是一個空的位置,即隊列中最后一個元素后面的位置。
確定一個隊列所需要的參數:
1.隊列頭 front
2.隊列尾 rear
以下三種情況下front,rear的值:
1.初始化時:
(1)front:0
(2)rear:0
2.隊列非空:
(1)front:隊列第一個元素
(2)rear:隊列最后一個有效元素的后一個元素
3.隊列為空:
front,rear值相等,但不一定為零
循環隊列
對於循環隊列,其本質是為了更有效利用連續的存儲空間。
我們需要討論如下問題:
(1)如何進行入隊操作
(2)如何進行出隊操作
(3)如何判斷循環隊列是否為空
(4)如何判斷循環隊列是否已滿
入隊
首先,我們在對一個一般隊列進行入隊操作時,角標r(rear)所要進行的操作是向后移動一個,即:
r=r+1;
但由於現在是循環隊列,當r指向隊尾最后一個存儲空間時,再進行+1的操作就會溢出,若這時隊列頭部已經有空閑空間,我們便可使r指向隊列頭部的空閑空間,這便是循環隊列的入隊操作,對於此操作,我們可以有:
//Arr.length為循環隊列分配的內存元素個數
r=(r+1)% Arr.Length;
出隊
同理,我們在對一個一般隊列進行出隊操作時,角標f(front)所要進行的操作是指向要出隊元素下一個元素的位置,故有:
f=f+1;
當為循環隊列時:
同理,我們有:
f=(f+1)%Arr.Length;
如何判斷循環隊列是否為空:
對於為空的情況,我們先假設隊列中有多個有效元素,經過不斷出隊,f一直會向同個方向移動(即向隊列尾的方向)直到剩余一個有效元素時,此時如果此有效元素出隊,則隊列變為空狀態,f向隊列尾方向移動一個單位,此時r指向的是剛才的有效元素的下一個位置,這時便會f和r指向同一個位置,即f=r的時候,隊列為空。
如何判斷循環隊列是否已滿:
對於判斷循環隊列為滿時,我們首先要知道的是循環隊列實際上是一個類似環形的結構,如下圖所示:
上圖中r指向4,故此時整個循環隊列中只有0,1,2,3四個位置有有效元素,加入我們要進行入隊或者出隊操作,那么f或者r應該向順時針方向移動,所以此時我們們要判斷隊列是否已滿,可以有兩種方式,第一種是聲明一個新的變量cnt,專門用於記錄隊中有效元素個數,入隊+1,出隊-1,用cnt和隊列空間大小比較即可得出此時隊列是否為滿,第二種是先規定循環隊列最后一個位置不能存放元素,意思是當循環隊列差一個元素就滿了時最后一個空間不允許存放有效數據。此時f和r不可以相等,這便有效的將隊列滿的情況與隊列空的情況區分開了,因為隊列為空時的條件為r和f相等,故這種情況下我們會有:
if((r+1)% Arr.Length==f);
這個條件下隊列即為滿
我們通常使用第二種方法
循環隊列C語言代碼實現
#include <stdio.h>; #include <malloc.h>; #include <stdlib.h>; typedef struct Queue { int* pBase; //數組首地址 int front; int rear; } QUEUE; void init(QUEUE* pQ); //初始化隊列 bool empty_queue(QUEUE* pQ); //隊列是否為空 bool full_queue(QUEUE* pQ); //隊列是否為滿 void traverse_queue(QUEUE* pQ); //遍歷隊列 void in_queue(QUEUE* pQ, int val); //入隊 bool out_queue(QUEUE* pQ, int* pval); //出隊 int main() { int i; QUEUE Q; init(&Q); in_queue(&Q, 1); in_queue(&Q, 2); in_queue(&Q, 3); in_queue(&Q, 4); in_queue(&Q, 5); in_queue(&Q, 6); in_queue(&Q, 7); traverse_queue(&Q); out_queue(&Q, &i); printf_s("元素%d已出隊\n", i); out_queue(&Q, &i); printf_s("元素%d已出隊\n", i); out_queue(&Q, &i); printf_s("元素%d已出隊\n", i); traverse_queue(&Q); out_queue(&Q, &i); printf_s("元素%d已出隊\n", i); out_queue(&Q, &i); printf_s("元素%d已出隊\n", i); return 0; } //初始化隊列 void init(QUEUE* pQ) { pQ->pBase = (int*)malloc(sizeof(int) * 6); if (NULL == pQ->pBase) { printf_s("動態分配內存失敗"); exit(-1); } pQ->front = 0; pQ->rear = 0; } //隊列是否為空 bool empty_queue(QUEUE* pQ) { if (pQ->front == pQ->rear) { return true; } else { return false; } } //隊列是否為滿 bool full_queue(QUEUE* pQ) { if ((pQ->rear + 1) % 6 == pQ->front) { return true; } else { return false; } } //遍歷隊列 void traverse_queue(QUEUE* pQ) { int i =pQ->front; while (i != pQ->rear) { printf_s("%d", pQ->pBase[i]); i = (i + 1) % 6; } } //入隊 //val:入隊元素的值 void in_queue(QUEUE* pQ, int val) { if (full_queue(pQ)) { printf_s("隊列已滿,無法入隊!"); return; } else { pQ->pBase[pQ->rear] = val; pQ->rear = (pQ->rear + 1) % 6; } } //出隊 bool out_queue(QUEUE* pQ, int* pval) { if (empty_queue(pQ)) { return false; } else { *pval = pQ->pBase[pQ->front]; pQ->front = (pQ->front + 1) % 6; return true; } }
隊列基礎應用:
所有與時間相關的操作都與隊列相關。
例如:搶票系統等就由隊列來實現
遞歸
表面上是一個函數自己直接/間接調用自己
*遞歸是用棧來實現的
1.為什么遞歸能自己調用自己?
首先我們來看一個場景,有兩個函數A()和B(),現在A函數要調用B函數,那么要做些什么?
(1)將所有實參,返回地址傳遞給被調函數B
(2)為被調函數局部變量(也包括形參)分配存儲空間
(3)將控制權轉移到被調函數入口
當B函數被調用完畢后返回A時:
(1)保存被調函數B的返回結果
(2)釋放被調函數B所占的存儲空間
(3)依照被調函數保存的返回地址將控制權轉移到調用函數A
實際上遞歸的時候也是如此,只不過A,B都是同一個函數罷了
用棧實現遞歸,具體如下圖所示:
A調用B,將B壓棧。B調用完畢,B出棧。此時A位於棧頂,A執行完畢,A出棧。
遞歸要滿足的三個條件:
(1)明確的終止條件
(2)該函數所處理的數據復雜程度在遞減
(3)這個轉化必須是可解的
循環和遞歸的比較:
所有循環都能轉化為遞歸,但是並不一定所有的遞歸問題都能用循環來解決
遞歸與循環的優缺點比較:
遞歸:
(1)優點:易於理解
(2)缺點:速度慢,存儲空間占用多
循環:
(1)優點:速度快,浪費空間幾乎沒有
(2)缺點:不易理解
遞歸應用
例子:
(1)求階乘
#include <stdio.h> long f(long n) { if (1 == n) { return 1; } else { return n * f(n - 1); } } int main() { long a = f(10); printf_s("%d", a); }
(2)高斯問題(求1+2+3…+100之和)
#include <stdio.h> int f(int n) { if (1 == n) { return 1; } else { return n + f(n - 1); } } int main() { int a = f(100); printf_s("%d", a); }
(3)漢諾塔(重點)
漢諾塔說的是有三根柱子A,B,C。 A柱子上有64個從底部到頂部越來越大的圓盤,現在要借助B柱子,來將A柱子上的圓盤全部搬運到C柱子上,且大圓盤不能放在小圓盤上面,請寫出算法。(如下圖所示)
實現邏輯:不管有幾個盤子,我們所做的都是三件事:
1.將前n-1個盤子借助C從A移動到B上
2.這時便可以將n這個盤子從A移動到C上面
3.將B上的n-1個盤子借助A移動到C
實現代碼如下:
#include<stdio.h> void Hannuo(int n, char A, char B, char C); int main(void) { char ch1 = 'A'; char ch2 = 'B'; char ch3 = 'C'; int n = 5; Hannuo(n, ch1, ch2, ch3); } //n:盤子個數 //A:所在柱子 //B:中轉主子 //C:目標柱子 void Hannuo(int n, char A, char B, char C) { if (1 == n) { printf_s("將編號為%d的盤子直接從%c柱子移動到%c的柱子上\n",n,A, C); } else { Hannuo(n - 1, A, C, B); printf_s("將編號為%d的盤子直接從%c柱子移動到%c的柱子上\n",n,A, C); Hannuo(n - 1, B, A, C); } }
樹
**定義:**有且只有一個稱為根的節點,下面有若干互補相交的子樹
通俗的定義:
1.樹是由節點和邊組成
2.一個節點只有一個父節點,但可以有多個子結點
3.但有一個節點例外,該節點沒有父節點,此節點稱為根節點
專業術語:
1.節點
2.父節點
3.子結點
4.子孫節點
5.堂兄弟
6.深度:樹中節點的最大層次稱為樹的深度(從根節點到最底層節點的層數)
根節點是第一層
7.葉子節點:沒有子節點的節點
8.非終端節點:非葉子節點
9.根節點可以是葉子,也可以是非葉子
10.度:子節點的個數稱為度,樹的度為最大的度
樹的分類:
(1)一般樹:任意一個節點的子結點個數不受限制
(2)二叉樹:任意一個節點的子結點個數最多兩個且子結點位置不可更改
(2)森林:n個互不相交樹的集合
二叉樹:
(1)一般二叉樹
(2)滿二叉樹:在不增加樹的層數的前提下,無法再多添加一個節點的二叉樹就是滿二叉樹
(3)完全二叉樹:如果只是刪除了滿二叉樹最底層最右邊連續若干個節點,這樣形成的二叉樹就是完全二叉樹
二叉樹存儲:
(1)連續存儲 (數組)必須為完全二叉樹
(2)鏈式存儲
為什么一顆二叉樹以數組方式存儲時要是完全二叉樹?
因為無法通過接過來推算出原來的樹的結構。
(根據內存順序無法推斷出以前的二叉樹的結構)
完全二叉樹可以根據(1)先序(2)中序(3)后序 來轉化成線性結構
完全二叉樹數組存儲優缺點:
(1)優點:查找某個節點的父節點和子結點(或判斷是否有無)速度快
(2)缺點:耗用內存空間過大
二叉樹鏈式存儲優缺點:
(1)優點:耗用內存小,求子結點方便
(2)缺點:空指針域浪費空間(但很少),求父節點困難
一般樹的存儲(無序):
(1)雙親表示法
(2)孩子表示法
(3)雙親孩子表示法
(4)二叉樹表示法
重點:二叉樹表示法
一般書先轉化為二叉樹來存儲
具體轉換方法:
設法保證任意一個節點的左指針域指向它的第一個孩子,右指針域指向它的下一個兄弟節點,只有能滿足這個條件,就可以把一個普通樹轉化為一個二叉樹
一個普通樹轉化為二叉樹,沒有右子樹,左邊為孩子,右邊為兄弟
森林的儲存
森林的轉化:
將B作為A的兄弟,將G作為B的兄弟即可
左邊的線指向第一個孩子節點,右邊的線指向下一個兄弟節點,結果如下圖所示:
二叉樹先中后序遍歷(以下圖為例)
先序遍歷:
先訪問根節點,再先序訪問左子樹,再先序訪問右子樹
故上圖中訪問順序應為:ABFKGECDQN
中序遍歷:
中序遍歷左子樹,再訪問根節點,最后中序遍歷右子樹
故上圖中訪問順序應為:KFGBEACQDN
后序遍歷:
先后序遍歷左子樹,在后序遍歷右子樹,最后訪問根節點
故上圖中訪問順序應為:KGFEBQNDCA
已知兩種遍歷序列求原始二叉樹:
我們只能通過中序加先序和后序中任意一種求原始二叉樹,通過先,后序無法確定二叉樹原貌
1.已知先序和中序,如何還原原始二叉樹?
先序:ABCDEFGH
中序:BDCEAFHG
求后序?
步驟如下:
(1)先序中A在第一位,故A為根節點
(2)則BDCE為左子樹,FHG為右子樹,因為先訪問左子樹后訪問右子樹
(3)首先看左子樹BDCE,在先序中B先出現,故B為左子樹根節點,因為先序先訪問根節點
(4)在中序中B左邊沒有,右邊是DCE,故以B為節點的二叉樹無左子樹,右子樹為DCE
(5)同理,C為根節點,左右分別為D和E
(6)右子樹推理方式相同
最后得到的二叉樹圖是:
故后序遍歷結果為:DECBHGFA
已知后序和中序求先序遍歷結果:
中序:BDCEAFHG
后序:DECBHGFA
求先序:
(1)在后序中A為最后一個,故A為根節點
(2)根據中序遍歷,左子樹為BDCE,右子樹為FHG
(3)BDCE中在B在最后,故B為左子樹根節點,B在中序中左邊無節點,右邊是DCE,故其無左子樹,右子樹為DCE
(4)接下來操作步驟同理
最后得到的二叉樹圖是: