文章目錄
串
KMP
【例題】模式串"ababaaababaa",求nextval數組(下標從1開始,0記錄長度)
【先求next數組】
- 先畫出表格(下標、T、next),並賦值特殊情況next[1]=0
- 看下標
j
那列,記下前面的子串T[1]-T[j-1],找出最長的前後綴- 沒有相同的前後綴 --> next[j] = 1
- 有相同的前後綴 --> 取最長的前後綴,長度爲len --> next[j]=len+1
【再求nextval數組】
- 畫出nextval行,並賦值特殊情況nextval[1]=0
- 【算法:求單元格(j, nextval)】先鎖定下標爲j的那一列 --> 然後看第j列,next那一行(j, next)的值t --> 然後看第t列,T那一行(t, T)–> 用(t, T)和(j, T)比較
- (t, T)==(j, T) 的話:(j, nextval)=(t, nextval)
- (t, T)≠(j, T)的話:(j, nextval)=(j, next)
- 【舉例:求單元格(5,nextval)】鎖定下標爲5的那一列 --> (5, next)=3 --> (3, T)=a --> (3, T)=a與(5, T)=a比較 --> 相同,則(5, nextval)=(3, nextval)=3
- 【舉例:求單元格(6, nextval)】鎖定下標爲6的那一列 --> (6, next)=4 --> (4, T)=b --> (4, T)=b與(6, T)=a比較 --> 不同,則(6, nextval)=(6, next)
下標 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
T | a | b | a | b | a | a | a | b | a | b | a | a |
next | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 | 3 | 4 | 5 | 6 |
nextval | 0 | 1 | 0 | 1 | 0 | 4 | 2 | 1 | 0 | 1 | 0 | 4 |
// T[0]存着字符串長度
void getnext(char *T, int next[]) {
int j=1,t=0;
next[1] = 0; //特殊狀態
while (j<T[0]) { //已知next[j]求next[j+1],所以j不能等於T[0]
if (t==0 || T[j]==T[t]) {
next[j+1] = t+1;
++j;++t;
}
else t = next[t];
}
}
// KMP搜索:下標0處存長度
int KMP(char *str, char *substr, int next[]) {
int i=1,j=1;
while (i<=str[0] && j<=substr[0]) {
if (j==0 || str[i]==substr[j]) {
++i;++j;
} else {
j = next[j]; //失配,回溯
}
}
if (j>substr[0]) return i-substr[0]; //匹配成功
else: return 0; //匹配失敗
}
樹
【選擇題】舉特例,認真審題
1.一顆二叉樹的度可以小於2
2.
3.
4.
5.
6.
7.度爲m的哈夫曼樹,葉子結點爲n,非葉子結點爲
8.前序序列、後序序列不能確定唯一的二叉樹
樹
樹和森林的先序 —> 對應二叉樹的先序
樹和森林的後序 —> 對應二叉樹的中序
typedef struct CSNode{
Elem data;
struct CSNode *firstchild, *nextsibling;
}CSNode, *CSTree;
二叉樹遍歷
【本質】遍歷是把樹變成一個一維的數組(即線性化)
【技巧】在遍歷的途中,我們可以新增一個pre,表示當前遍歷的結點p,在數組的前一個結點。這個思想在很多地方都有應用。比如檢查二叉樹是否爲排序二叉樹,二叉樹線索化
void LevelOrder(BiTree root) { //層次遍歷
BiTNode *que[MAXSIZE]; int front,rear; //隊列
BiTNode *p;
front=rear=0;
if (root==NULL) return ; //空樹,此句容易少寫
que[rear]=root; rear=(rear+1)%MAXSIZE; //根結點入隊列
while (front!=rear) { //不爲空
p=que[front]; front=(front+1)%MAXSIZE; //出隊列
printf("%c", p->data); //訪問
if(p->lchild!=NULL) {
que[rear]=p->lchild; //左子樹入隊列
rear=(rear+1)%MAXSIZE;
}
if (p->rchild!=NULL) {
que[rear]=p->rchild; //右子樹入隊列
rear=(rear+1)%MAXSIZE;
}
}
}
void PreOrder_norec(BiTree root) { //先序非遞歸
// 思路:模擬遍歷時指針p的移動
// 操作:1. 從根結點開始一直往左走,每經過一個結點先訪問,然後放入棧;
// 2. 往左走不動了,結點出棧,然後往右走一步,繼續上一步,直到往右走不動了
BiTNode *stack[MAXSIZE]; int top=-1; //棧
BiTNode *p = root; //指針
if (root==NULL) return ;
do {
while (p!=NULL) { //一直往左走
printf("%c", p->data); //訪問
stack[++top]=p; //入棧
p=p->lchild; //往左走
}
// 往左走不動了
if (top!=-1) {
p=stack[top--]; //出棧
p=p->rchild; //往右走一步
}
} while (top!=-1 || p!=NULL);
}
void InOrder_norec(BiTree root) { //中序非遞歸
// 思路:模擬遍歷時指針p的移動
// 操作:從根結點開始一直往左走,每經過一個結點不訪問,放入棧;
// 往左走不動了,結點出棧,然後訪問該結點,並往右走一步,繼續上一步,直到往右走不動的時候
BiTNode *stack[MAXSIZE]; int top=-1;
BiTNode *p = root;
do {
while (p!=NULL) { //一直往左走
stack[++top]=p;
p=p->lchild;
}
// 往左走不動了
if (top!=-1) {
p=stack[top--]; //出棧
printf("%c", p->data); //訪問
p=p->rchild; //往右走一步
}
} while (top!=-1 || p!=NULL);
}
void PostOrder_norec(BiTree root) { //後序非遞歸
/*PreOrder_norec中利用的思想是模擬遍歷途中指針p的移動,但此方法有一個缺點,無法實現後序遍歷
另一種思想:根據訪問次序入棧並輸出
【先序遍歷的操作步驟】訪問根,然後把右、左入棧(先入棧後輸出,所以將右子樹入棧)
【後序遍歷】那麼次方法下後序遍歷應該怎麼做?
1. 後序序列:左 右 根 -> 逆後序序列:根 右 左
2. 想到先序序列:根 左 右。故後序序列可效仿先序序列非遞歸的方法
操作步驟:根結點入棧;然後出棧,將左右子樹加入棧中;直到棧空爲止。由此得到逆後序序列;然後將逆後序序列倒過來輸出
*/
BiTNode *stack[MAXSIZE]; int top=-1; //棧
BiTNode *inPostOrder[MAXSIZE]; int top2=-1; //第二個棧:用於存儲逆後序序列
BiTNode *p;
if (root==NULL) return ; //雖然在這裏,這句話可以省略。但還是要養成判斷臨界條件的好習慣
stack[++top] = root; //根結點入棧
while (top!=-1) {
p=stack[top--]; //出棧
inPostOrder[++top2]=p; //訪問
if (p->lchild!=NULL) stack[++top]=p->lchild; //右子樹入棧
if (p->rchild!=NULL) stack[++top]=p->rchild; //左子樹入棧
}
while (top2!=-1) { //倒序輸出逆後序序列
p = inPostOrder[top2--];
printf("%c", p->data); //正式訪問
}
}
二叉樹線索化
【本質】把二叉樹的空結點用起來,左子樹表示線性化結果中前面的結點,右子樹表示後一個結點
【線索化的實現思路】創建一個pre,表示當前結點p線性化結果的前一個結點
1.pre初始爲NULL
2.進入p時,有兩種視角。一個是站在pre往後看p,一個是站在p往前看pre
【代碼】
typedef struct TBTNode{ //Thread BiTree Node
char data;
struct TBTNode *lchild, *rchild;
int ltag,rtag; //1爲是線索,0爲不是線索
}TBTNode, *TBTree;
void CreatePreThread(TBTNode *root) {
// 對root所指的空間進行修改,故這裏不要引用
TBTNode *pre=NULL; //遍歷時的前一個結點
if(root==NULL) return ;
PreThread(root, pre); //線索化
// 【注意!】這裏要處理最後一個結點,最後一個結點的右子樹一定是空的
pre->rchild=NULL; pre->rtag=1;
}
void PreThread(TBTNode *p, TBTNode *&pre) {
//對p所指的存儲空間進行修改,所以p不用引用
//pre表示:在線性化序列中,T的前一個結點。是對pre變量的值進行修改,需要引用
if (p==NULL) return ;
// 站在T的視角,即可以往前看pre
if (p->lchild==NULL) { //不管p是不是線性序列的第一個,如果是空都要線索化
p->ltag=1; p->lchild=pre; //線索化
}
// 站在pre的視角,即可往後看T
if (pre!=NULL && pre->rchild==NULL) { //pre的rchild爲空
pre->rtag=1; pre->rchild=p; //線索化
}
//前進,下一個結點
pre=p;
//因爲是前序,已經做了線索化,所以不是線索的情況再遞歸
if (p->ltag==0) PreThread(T->lchild, pre);
if (p->rtag==0) PreThread(T->rchild, pre);
}
【補充】二叉樹線索化的重要問題還有它的基本操作
1.求樹的第一個結點,最後一個結點
2.求當前結點p的前一個或後一個
3.遍歷
【思路】嚴格抓住二叉樹線性結果序列,明確它的前一個後一個是在哪個位置來寫代碼。後兩個問題可以調用第一個問題來處理
// 以中序線索二叉樹爲例
TBTNode *First(TBTNode *p) { //第一個:p左子樹中第一個沒有左孩子的結點
if (p==NULL) return NULL;
while (p->ltag==0) p=p->lchild; //如果左邊有結點,一直往左走,直到沒有結點
return p;
}
TBTNode *Last(TBTNode *p) { //最後一個:右子樹當中沒有右孩子的結點
if (p==NULL) return NULL;
while (p->rtag==0) p=p->rchild;
return p;
}
TBTNode *Next(TBTNode *p) { //下一個
if (p==NULL) return NULL;
if (p->rtag==1) return p->rchild; //是線索直接返回
else return First(p->lchild);
}
TBTNode *Prior(TBTNode *p) { //上一個:左邊最後一個
if (p==NULL) return NULL;
if (p->lchild==1) return p->lchild; //是線索直接返回
else return Last(p->lchild); //左邊最後一個
}
void Order(TBTNode *root) {
for (TBTNOde *p=First(root); p!=NULL; p=Next(p)) Visit(p);
}
哈夫曼樹
1.哈夫曼m叉樹中只有和的結點,故有公式
2.個葉子結點的哈夫曼樹,共有個結點
3.權值越大的結點,距離根越近;帶權路徑最短(葉子結點權重*長度 求和)
4.由一組結點得到的哈夫曼樹可能不唯一(有相同結點、左右子樹互換)
5.構樹:每次從左到右選擇最小的m個記錄合成一個;若結點不夠用0補充,但要重新構樹
typedef struct node{
int w;
struct node *lchild,*rchild;
}BiTNode, *BiTree;
BiTree CreateHufferman(int num[], int n);
int GetWPL(BiTree T, int pathLen);
void GetCode(BiTree T, int pathLen);
BiTree CreateHufferman(int num[], int n) {
BiTree *trees; //trees用於保存哈夫曼樹構造過程的n棵樹
int min1,min2; //min1最小結點的下標,min2第二小結點的下標
BiTNode *tmp; int i,j;
trees = (BiTree *)malloc(sizoef(BiTree) * n); //BiTree[]指針數組統一管理哈夫曼的葉子結點
for (i=0; i<n; i++) { //創造哈夫曼的葉子結點,並放入trees統一管理
tmp = (BiTNode *)malloc(sizeof(BiTNode));
tmp->w = num[i];
tmp->lchild = tmp->rchild = null;
trees[i] = tmp;
}
for (i=1; i<n; i++) {//構造哈夫曼樹,循環n-1次
min1 = min2 = -1;
for (j=0; j<n; j++) { //找出從左到右的第一、二個結點
if (trees[j]!=NULL) {
if (min1==-1) min1 = j; //第一個結點
else if (min2==-1) min2 = j; //第二個結點
else break; //找完,退出
}
}
for (j=min2; j<n; j++) { //找到最小結點和次小結點
if (trees[j]!=NULL) {
if (trees[j]->w < min1) { //比最小結點還小
min2 = min1; //注意!最小結點變成了次小結點
min1 = j;
} else if (trees[j]->w < min2) { //比最小結點大,比次小結點小
min2 = j;
}
}
}
// 合併min1,min2
tmp = (BiTNode *)malloc(sizeof(BiTNode));
tmp->w = trees[min1]->w + trees[min2]->w;
tmp->lchild = trees[min1];
tmp->rchild = trees[min2];
trees[min1]=tmp;
trees[min2]=NULL;
}
free(trees);
return tmp;
}
// 調用:GetWPL(hfmTree, 0); 根結點的路徑長度爲0
int GetWPL(BiTree T, int pathLen) {
if (T==NULL) return 0; //檢測空樹情況
if (T->lchild==NULL && T->rchild==NULL) { //若是葉子結點
return T->w * pathLen;
}
return GetWPL(T->lchild, pathLen+1) + GetWPL(T->rchild, pathLen+1);
}
// 調用:GetCode(hfmTree, 0); 根結點的路徑長度爲0
void GetCode(BiTree T, int pathLen) {
static char code[2*MAX-1]; //靜態變量保存編碼值
if (T==NULL) return ; //檢測空樹情況
if (T->lchild==NULL && T->rchild==NULL) { //葉子結點
code[pathLen] = ‘\0’; //賦值結束符
printf(“結點值%d 編碼值:%s\n”, code); //輸出此葉子結點編碼情況
return ; //退出
}
code[pathLen]=‘0’; GetCode(T->lchild, pathLen+1); //往左走
code[pathLen]=‘1’; GetCode(T->rchild, pathLen+1); //往右走
}
圖
1.【完全圖】任意兩頂點都有邊(無向完全條,有向)
2.【連通圖】任意兩點可互通(無向叫連通圖,有向叫強連通圖)
3.【極大的連通子圖(極大是相對於頂點而言)】不能再大,再加入一個頂點就不滿足任意兩點互相連通了(無向叫連通分量、極大連通子圖;有向叫強連通分量、極大強連通圖)
4.【極小的連通子圖(極小是相對於邊而言)】子圖中兩兩已經連通且邊最小,再加一個就會構成環(無向圖叫極小連通子圖,有向圖沒有這種東西)
存儲結構
// 鄰接矩陣
typedef struct{
char vexs[MAXSIZE]; //頂點信息
float arcs[MAXSIZE][MAXSIZE]; //鄰接表
int vernum,arcnum;
}MGraph; //鄰接矩陣
// 鄰接表
typedef struct ArcNode{
int adjvex; //邊所指的頂點
int w; //邊的權重信息
struct ArcNode *next; //下一個弧
}ArcNode; //邊信息
typedef struct VNode{
char data; //頂點信息
ArcNode *firstarc; //第一條弧
}VNode; //頂點
typedef struct{
VNode vexs[MAXSIZE]; //頂點集
int vernum, arcnum;
}ALGraph; //鄰接表
遍歷
// DFS_rec O(n+e)
int visit[MAXSIZE] = {0};
void DFS(ALGraph G) {
int i;
for (i=0; i<G.vernum; ++i) visit[i]=0;
for (i=0; i<G.vernum; ++i) //此圖可能是非連通圖,從一個頂點可能走不到所有頂點
if (visit[i]==0) DFSTool(G, i);
}
void DFSTool(ALGraph G, int v) {
ArcNode *p;
visit[v]=1;
printf(“%c\t”, G.vexs[v].data);
for (p=G.vexs[v].firstarc; p; p=p->nextarc) {
if (visit[p->adjvex]==0) DFSTool(G, p->adjvex);
}
}
// DFS_norec
void DFS_norec(ALGraph G) {
int visit[MAXSIZE]; //訪問數組
int stack[MAXSIZE]; int top=-1; //棧
int i,j;
ArcNode *p;
for (i=0; i<G.vernum; ++i) visit[i]=0; //訪問數組初始化
for (i=0; i<G.vernum; ++i) { //遍歷頂點
if (visit[i]) continue; //此結點已經訪問過,退出
//此結點還未被訪問過,從它開始
top=-1; //棧清空
stack[++top]=i; //此結點入棧
visit[i]=1; printf(“%c\t”, G.vers[i].data); //訪問此結點
while (top!=-1) { //棧中還有元素
j = stack[top]; //得到棧頂,不出棧!
for (p=G.vers[j].firstarc; p; p=p->next) { //從這個頂點開始遍歷
if (visit[p->adjvex]==0) { //該結點沒有訪問過
visit[p->adjvex]=1; printf(“%c\t”, G.vers[p->adjvex].data); //訪問
stack[++top]=p->adjvex; //入棧
break; //退出此次循環,開始從p->adjvex頂點往下走
}
}
if (p==NULL) —-top; //如果上面for循環正常結束(即p==NULL),表明stack[top]的鄰邊已經遍歷完,那就要讓它出棧
}
}
}
// BFS O(n+e)
int visit[MAXSIZE]; //外面定義訪問數組
void BFS(ALGraph G) {
int i;
for (i=0; i<G.vernum; ++i) visit[i]=0; //初始化訪問數組
for (i=0; i<G.vernum; ++i) //不一定是連通圖,第一個結點不能夠到達任意一個結點
if (visit[i]==0) BFSTool(G, i);
}
void BFSTool(ALGraph G, int v) {
int que[MAXSIZE]; int front,rear; //隊列
ArcNode *p;
int i;
front=rear=0; //隊列初始化
que[rear]=v; rear=(rear+1)%MAXSIZE; //v入隊列
while (front!=rear) { //隊列不爲空
i=que[front]; front=(front+1)%MAXSIZE; //出隊列
visit[i]=1; printf(“%c\t”, G.vexs[i].data); //訪問
for (p=G.vexs[i].firstarc; p; p=p->nextarc) //把該結點所有領接節點入隊列
if (visit[p->adjvex]==0) {
que[rear]=p->adjvex;
rear=(rear+1)%MAXSIZE;
}
}
}
【時間複雜度】有頂點都進隊一次,所有邊都被訪問一次,因此總次數爲n+e,即O(n+e)
無向連通圖的最小生成樹(Prim、Kruskal)
1.無向連通圖的最小生成樹:刪除無向連通圖的邊,讓其只剩下n-1條且任意兩點還是連通的
2.最小生成樹:邊權重之和最小
3.最小生成森林:對非連通圖而言的最小生成森林
Prim普利姆 | Kruskal克魯斯卡爾 | |
---|---|---|
思想 | ①以頂點爲操作對象,每次選擇一個頂點併入子圖; ②頂點的選擇依據:子圖到其他頂點的最短路徑 |
①以邊爲操作的主要單位:每次併入一條邊; ②選擇邊的依據:當前最小邊,且不構成迴路 |
數據結構 | ①子圖vest[i]:結點i是否已併入生成樹; ②lowcost[i]:結點i到子樹的最短邊權重 |
①Road:存儲圖的所有邊 ②用並查集檢查是否構成回邊 |
適合 | 稠密圖(時間複雜度與頂點有關係,與邊數沒有關係) | 稀疏圖(時間複雜度由邊數決定) |
時間複雜度 | (併入n-1個頂點,每次併入要選出最小者–>n²) | (算法主要操作是對邊e的排序上,取決於排序算法的時間複雜度) |
// Prim:以頂點爲單位,每併入一個頂點(vset[]),更新到其餘各點的最短開銷(lowcost[])
#define INF 10e7
int Prim(MGraph G, int v0) {
int vset[MAXSIZE]; //vset[i]==1即表示頂點i已併入最小生成樹
// 當前生成樹到頂點w的最小邊爲w-lownode[w],其權重爲lowcost[w]
int lowcost[MAXSIZE];
int lownode[MAXSIZE];
int sum; //最小生成樹的開銷
int min, w; //選出每次最優的頂點
int i,j;
// 初始化
for (i=0; i<G.vernum; i++) {
vset[i] = 0;
lowcost[i] = INF;
}
// 將v0併入生成樹
vset[v0]=1;
sum=0;
for (i=0; i<G.vernum; i++) {
lowcost[i] = G.arcs[v0][i];
lownode[i] = v0;
}
// 還需要n-1個頂點併入
for (i=2; i<=G.vernum; i++) {
// 找當前生成樹離其他頂點的最短距離
min = INF;
for (j=0; j<G.vernum; j++) {
if (vset[j]==0 && lowcost[j]<min) {
min = lowcost[j];
w = j;
}
}
// 將w併入
vset[w] = i; //第i個併入樹的結點
sum += lowcost[w];
// 更新lowcost
for (j=0; j<G.vernum; j++) {
if (vset[j]==0 && G.arcs[w][j]<lowcost[j]) //新併入的結點到剩餘個點會不會更近
lowcost[j]=G.arcs[k][j];
lownode[j]=w;
}
}
// 若要創建出此最小生成樹,可以根據lowcost[]和lownode[]創建
return sum;
}
// Kruskal:每次選擇最短邊,併入時檢查是否會構成迴路
typedef struct{
int a,b; //邊的兩點
int w; //邊的權重
}Road; //邊
Road roads[MAXSIZE]; //邊集
int parent[MAXSIZE]; //並查集:i結點的父親是parent[i];parent[i]==i表示i即爲根
int getRoot(int a) {
while (a!=parent[a]) a=parent[a]; //a的父親是parent[a]
//如果是它自己,則表示找到根
return a;
}
int Kruskal(MGraph G, Road roads[]) {
int i, a_root, b_root;
int sum=0;
for (i=0; i<G.vernum; ++i) parent[i]=i;
sort(roads, G.arcnum); //將roads按照w從小到大排列
for (i=0; i<G.arcnum; ++i) { //遍歷所有的邊
a_root = getRoot(roads[i].a); //得到頂點a的根
b_root = getRoot(roads[i].b); //得到頂點b的根
if (a_root!=b_root) { //不產生迴路
parent[a] = b; //更新並查集,將a接到b的下面
sum += road[i].w; //更新權重
//若要創建出這個最小生成樹,可以在這裏創建a,b,並連接邊
}
}
return sum;
}
有向圖的強聯通分量
【強聯通】在有向圖中,兩頂點相互可達
【強聯通圖】有向圖中任意兩點相互可達
【強聯通分量】非強聯通圖的子圖,且子圖中任意兩點相互可達
【極大強連通分量】強連通分量中頂點最多的那個
【Kosaraju算法】
1.求圖G的逆圖R
2.對逆圖R進行DFS,得到DFS生成森林,求得生成森林的後序序列postorder
3.以postorder[]從後面往前的順序對原圖G進行DFS,即得到好幾個生成樹trees,這些生成即是強聯通分量
4.若要求極大的強連通分量,求trees中頂點最多的那個
關節點與雙連通圖
【關結點】若刪除V 與 V上的所有邊後,圖不在連通,則v就是該圖的關節點
【重(雙)連通圖】沒有關節點的圖稱爲雙連通圖
【判斷頂點V是否爲關節點】
1.刪除V和V上的所有邊獲得子圖SG
2.從SG的一個頂點出發DFS,得到遍歷結果visit[]
3.看visit[]中除了V是否全部訪問到。全部訪問到則表明V不是關結點
【判斷是否爲雙連通圖】判斷每個頂點是否爲關結點。都不是,則是雙連通圖
最短路徑
迪傑斯特拉算法
【手算答題模版】
用dist[]存放最短路徑長度,path[]存放最短路徑
// Dijkstra:求一個頂點到其餘各點的距離
void Dijkstra(MGraph G, int v0, int dist[], int path[]) {
// v0到w的最短距離爲dist[],路徑爲v0->...->path[w]->w
int set[MAXSIZE]; //已考慮的頂點
int min, i, j, u;
// 初始化、從v0到其他各點
for (i=0; i<G.vernum; ++i) {
dist[i] = G.arcs[v0][i]; //初始化距離:v0->i
set[i] = 0; //還未考慮任何頂點
if (G.arcs[v0][i]<INF) path[i] = v0; //若v0->i有路,則當前的最短路徑即是i->v0
else path[i] = -1; //沒有路,即爲-1
}
set[v0]=1; path[v0]=-1; //把結點v0設置爲已考慮
for (i=1; i<G.vernum; ++i) { //還需要考慮n-1個頂點
//找出當前權重最短的邊
min = INF;
for (j=0; j<G.vernum; ++j) {
if (set[j]==0 && dist[j]<min ) {
u = j;
min = dist[j];
}
}
set[u]=1; //將此邊的另一個頂點併入
//更新最短路徑dist[]:查看頂點u做爲中轉站,路線會不會更近
for (j=0; j<G.vernum; ++j) {
if (set[j]==0 && dist[u]+G.arcs[u][j]<dist[j]) {
dist[j] = dist[u] + G.arcs[u][j];
path[j] = u;
}
}
}
}
void PrintPath(int path[], int a) {
int stack[MAXSIZE], top=-1;
// 以由葉子結點到根結點的順序將其入棧
while (path[a]!=-1) {
stack[++top] = a;
a = path[a];
}
stack[++top] = a;
// 倒過來輸出
while (top!=-1) {
printf(“%d ”, stack[top—-]);
}
printf(“\n”);
}
弗洛伊德算法
【手算答題模版】
void PrintPath(MGraph G, int u, int v, int path[][max]) {
int midNode;
if ( path[u][v]==-1 ) {
print("<%C,%c>", G.vers[u], G.vers[v]); //直接輸出
} else {
mindNode = path[u][v];
PrintPath(u, mid, path); //mid前半段路徑
PrintPath(mid, v, path); //mid後半段路徑
}
}
void Floyd(MGraph G, int A[][maxSize], int Path[][maxSize]) {
int i,j,k;
// 初始化
for (i=0; i<G.vernum; i++) {
for (j=0; j<G.vernum; j++) {
A[i][j] = G.arcs[i][j];
Path[i][j] = -1;
}
}
for (k=0; k<G.vernum; ++k) { //中間經過k點有沒有更近呢?
for (i=0; i<G.vernum; ++i) {
for (j=0; j<G.vernum; ++j) {
// i->j 對比 i->k->j
if (A[i][j]>A[i][k] + A[k][j]) {
A[i][j] = A[i][k] + A[k][j];
Path[i][j] = k;
}
}
}
}
}
AOV網
【AOV網】Activity On Vertex network。有向無環圖;頂點表示活動;邊表示活動的次序
【拓撲排序序列】判斷有向圖是否有迴路
// 拓撲排序:O(n+e)刪除入度爲0的頂點和邊,直到入度爲0的邊
int TopSort(ALGraph &G) {
int v, w, n;
int stack[MAXSIZE], top=-1; //存放入度爲0的頂點
ArcNode *p;
// 將圖中入度爲0的頂點入棧
for (v=0; v<G.vernum; ++v) {
if (G.vers[v].cnt==0) stack[++top] = v;
}
n = 0; //已刪除n個結點
while (top!=-1) { //還有入度爲0的頂點
v = stack[top—-]; //出棧
// 刪除此頂點
++n;
for (p=G.vers[v].firstarc; p; p=p->nextarc) { //刪除鄰邊
w = p->adjvex;
—- (G.vers[w].cnt);
if (G.vers[w].cnt==0) stack[++top] = w; //若又出現入度爲0的頂點,加入棧
}
}
if (n==G.vernum) return 1; //拓撲排序成功,爲AOV網
else return 0; //拓撲排序失敗,該有向圖不爲AOV網,有迴路
}
// 逆拓撲有序序列:出度爲0的頂點先輸出,即DFS時第一個退出的頂點
int visit[MAXSIZE];
void RTopSort_DFS(ALGraph &G, int v) {
ArcNode *p;
visit[v] = 1;
// 訪問v的頂點
for (p=G.vers[v].firstarc; p; p=p->nextarc) {
if (visit[p->adjvex]==0) RTopSort_DFS(G, p->adjvex);
}
// v的頂點都訪問完了,說明頂點v沒有下一條邊了,也就意味着此時v的出度爲0
printf(“%c”, G.vers[v].data ); //輸出
}
AOE網
【AOE網】Activity On edge network有向無環圖;頂點表示事件;邊表示活動且邊有權值
【ve】即事件v的最早發生時間(earliest)。不休息緊鑼密鼓的把V上游活動全部做完,即取起點到V路徑中的max({上一個頂點ve+邊}的最大值,ve(源點)=0
)
【vl】即事件v的最遲發生時間(latest)。中途想休息一下,但是不能讓工期延長的最遲動工時間({下一個頂點的vl-邊}的min,vl(匯點)=ve(源點)
)
【ee】即活動e的最早發生時間。中途不休息趕緊把上游活動做完需要的時間(ee(i)=ve(邊的頭頂點)
)
【el】即活動e的最遲發生時間。中途想休息一下,但是不能讓工期延長的最遲動工時間(vl(邊的尾頂點)-邊權重
)
【關鍵活動】中途不能休息,一休息就會延長工期的活動,即ee==el
的活動
【關鍵路徑】關鍵活動所構成從源點到匯點的路徑(可能有多條),此爲整個工程的完成的最短時間(即中途一刻也不休息,完成工程的時間)
【代碼】
// 方法一:關鍵路徑即圖中源點到匯點的最長路徑
// 用拓撲排序求的源點S和匯點E;修改最短路徑算法爲求最長路徑算法,求得SE之間的最長路徑;並通過回溯求出所有的路徑
// 方法二:利用關鍵路徑算法
typedef struct ArcNode{
int adjvext;
int w;
struct ArcNode *nextarc;
}ArcNode;
typedef struct{
char data;
int cnt; //記錄該結點的入度
ArcNode *firstarc;
}VNode;
typedef struct{
VNode vers[MAXSIZE];
int vernum, arcnum;
}ALGraph;
// 計算每個結點的入度
void CntVNodeInDegree(ALGraph &G) {
for (int v=0; v<G.vernum; v++) {
for (ArcNode *p=G.vers[v].firstarc; p; p=p->next) {
int w = p->adjvex; //邊v->w
G.vers[w].cnt++; //w的入度+1
}
}
};
// 拓撲排序:order[]爲拓撲有序序列,返回1表示圖爲有向無環圖
int TopSort(ALGraph &G, int order[]) {
int stack[MAXSIZE], top=-1; //存放入度爲0的頂點
int v,w,n;
// 將圖中入度爲0的頂點入棧
for (i=0; i<G.vernum; ++i)
if (G.vers[i].cnt==0) stack[++top] = i;
n=0; //已刪除n個頂點
while(top!=-1) { //還有入度爲0的頂點
v = stack[top—-]; //出棧
//刪除此頂點
order[n] = v; n++;
for (ArcNode *p=G.vers[v].firstarc; p; p=p->next) {
w = p->adjvex;
—-(G.vers[w].cnt);
if (G.vers[w].cnt==0) stack[++top]=w;
}
}
if (n==G.vernum) return 1;
else return 0;
}
// 關鍵路徑
int CriticalPath(ALGraph &G, int **map) {
// map[G.vernum][G.vernum]爲G的鄰接矩陣;map[v][w]=1表示v->w爲關鍵活動。初始值爲INF,表示不是關鍵活動
int topo[G.vernum];
int ve[G.vernum] = {0}, vl[G.vernum] = {0};
int v, w;
ArcNode *p;
// 拓撲排序:檢查是否爲有向無環圖,並獲得拓撲序列存到topo[]中
if (TopSort(G, topo)==0 ) return 0;
// 計算每個頂點的ve:ve[]初始值爲0,ve[w]=Max{ve[v] + vw的邊權重}
for (i=0; i<G.vernum; i++) ve[i]=0; //ve[]初始值爲0
for (i=0; i<G.vernum; i++) { //遍歷拓撲有序序列
v = topo[i]; //得到拓撲有序序列的頂點
// 計算它的下一個頂點:v->w
for (p=G.vers[v].firstarc; p; p=p->next) {
w = p->adjvex; //它的鄰邊v->w
if (ve[w] < ve[v]+p->w) //ve(尾) ve(頭)+邊。取最大值
ve[w] = ve[v]+p->w; //ve(尾)=ve(頭)+邊
}
}
// 計算每個頂點的vl:vl[]初始值爲ve[匯點],vl[w]=min{vl[v] - vw邊的權重}
v = topo[G.vernum-1]; //匯點
for (i=0; i<G.vernum; ++i) vl[i]=ve[v]; //vl[]初始值爲ve[匯點]
for (i=G.vernum-1; i>=0; i—-) { //從後往前遍歷拓撲有序序列
v = topo[i]; //拓撲有序序列的頂點
// 計算它的下一個頂點:v->w
for (p=G.vers[v].firstarc; p; p=p->next) {
w = p->adjvex; //它的鄰邊v->w
if (vl[v] > vl[w]-p->w) //vl(頭) vl(尾)-邊。取最小值
vl[v] = vl[w]-p->w; //vl(頭)=vl(尾)-邊
}
}
// 求關鍵活動
int ee, el; //當前活動的ee、el
for (v=0; v<G.vexnum; v++) { //遍歷每個頂點
for (p=G.vers[i].firstarc; p; p=p->nextarc) { //遍歷該頂點的邊
w = p->adjvex; //它的鄰邊v->w
ee = ve[v]; //ee(vw)=ve(v)
el = vl[w]-p->w; //el(vw)=vl(尾)-邊
if (ee==el) { //爲關鍵活動
printf(“(%c->%c)\t”, G.vers[v].data, G.vers[w].data);
map[v][w] = 1; //v-w邊爲關鍵活動
}
}
}
return 1;
}
void PrintAllCriticalPath(ALGraph &G, int **map, int start, int end) {
static char path[MAXSIZE] = {‘\0’};
static int index = 0;
// 關鍵路徑不只一條,打印出所有的關鍵路徑
path[index++] = G.vers[start].data;
if (start==end) {
path[index] = ‘\0’;
printf(“%s\n”, path);
} else {
for (ArcNode *p=G.vers[start].firstarc; p; p=p->nextarc) {
int w = p->adjvex;
if (map[start][w]==1) {
PrintAllCriticalPath(G, map, w, end);
}
}
}
index—-;
}
排序
性能 | 分析 |
---|---|
平均時間複雜度 | 特殊:基數排序O(d(n+rd));d爲關鍵字位數,r爲一位的關鍵字取值範圍,n爲數量“快些以nlog2n的速度歸隊”:快(快速排序)、些(希爾排序)、歸(歸併排序)、隊(堆排序) |
最壞時間複雜度 | 1. 快速排序的時間複雜度爲O(n2) 2. 其他都和平均情況相等 |
最好時間複雜度 | 1. 直接插容易插變成O(n),起泡起的好變成O(n) 2. “容易插”、“起的好”都是指初始序列已經有序 |
空間複雜度 | 1. 快速排序O(log2n) 2. 歸併排序O(n) 3. 基數排序O(rd) 4. 其他都是O(1) |
穩定性 | 【助記】考研複習痛苦啊,心情不穩定(不穩定的算法),快(快速排序)些(希爾排序)選(簡單選擇排序)一堆(堆排序)好友來聊天吧 。這4種不是穩定的,其他自然都是穩定的 |
排序原理 | 1. 經過一趟排序,就能保證一個元素到達最終位置:起泡、快速(交換類的兩種),簡單選擇、堆(選擇類的兩種) 2. 排序方法的元素比較次數和原始序列無關:簡單選擇排序、折半插入排序 3. 排序方法的排序趟數和原始序列有關:交換類的排序 4. 比較類的算法,在最壞的情況下能達到最好的時間複雜度爲O(nlog2n) |
其他 | 簡單普通的排序方法的升級版的平均複雜度都爲O(nlog2n),最壞的情況都是和沒改進的時候一樣O(n^2),除了堆排序 |
插入排序
// 直接插入排序
void InsortSort(int arr[], int n) {
int i,j,tmp;
for (i=1; i<n; i++) { //i從左到右:[1, n-1]
tmp = arr[i]; //取下arr[i]
for (j=i-1; j>=0 && tmp<arr[j]; j—-) { //j是i的前一個元素:[0, i-1]
// 【注意】tmp與arr[j]的對比必須放在for中判斷。若分開判斷,最好最壞情況都是O(n^2)
// 【注意】若tmp==arr[j],則不交換,保持原定相對位置,保證排序的穩定性
arr[j+1] = arr[j];
}
arr[j+1]=tmp; //tmp比arr[j]小,或相等。則tmp放在arr[j]的一位
}
}
// 折半插入排序
void BiInsertSort(int arr[], int n) {
int i,j,tmp;
int low,high,mid;
for (i=1; i<n; i++) { //i從左到右:[1,n-1]
tmp = arr[i]; //取下arr[i]
low=0; high=i-1; //確定折半查找的範圍
while (low<=high) { //【注意】low==high也需要判斷low位置與tmp的大小
mid = (low+high)/2;
if (arr[mid]<=tmp) low=mid+1;
//【注意】若arr[]出現與tmp相同的數字,則插入位置在後面,保持排序穩定性
else high=mid-1;
}
// 插入位置是arr[low],則把[low, i-1]的元素後移
for (j=i; j>low; j—-) {
arr[j] = arr[j-1];
}
arr[low]=tmp; //放下tmp
}
}
// 希爾排序
// 【增量選擇原則】最後一個值一定取1;增量序列中值應儘量沒有除1之外的公因子
void ShellSort(int arr[], int n) {
int i,j,tmp;
int gap;
for (gap=n/2; gap>0; gap/=2) { //默認增量序列:除以2且大於0
for (i=gap; i<n; i+=gap) { //【注意】i是從左到右的第二個元素,位移間隔是gap
tmp = arr[i]; //取下arr[i]
for (j=i-gap; j>=0 && tmp<arr[j]; j-=gap) {//【注意】j是i的前一個元素,位移間隔是gap
// 【注意】若tmp==arr[j],則arr[j]不後移,保持原始相對位置,保證排序的穩定性
arr[j+gap] = arr[j]; //注意:tmp比arr[j]小,則arr[j]後移
}
arr[j+gap]=tmp; //注意;arr[j]比tmp大,則tmp應該放在j的後一位
}
}
}
交換排序
// 冒泡排序
void BubbleSort(int arr[], int n) {
int i,j,tmp;
int flag=1; //此趟是否有發生交換
for (i=n-1; i>=1 && flag; i—-) { //無序序列的右邊界,每趟完成最後邊多填一個有序元素,一共需要n-1趟
flag = 0; //標記這一趟沒有交換髮生
for (j=0; j<i; j++) { //遍歷無序序列,判斷是否要交換,[0, i-1]
if (arr[j]>arr[j+1]) {
tmp=arr[j];arr[j]=arr[j+1];arr[j+1]=tmp;
flag=1;
}
}
}
}
// 快速排序
void QuickSort(int arr[], int low, int high) {
int i,j, tmp;
if (low<high) { //low==high表明只剩下一個元素,不需要排序了
tmp = arr[low]; //哨兵取最左邊這個,i的位置空出來了
//找到tmp存放的位置,使小於tmp的放左邊,大於tmp的放右邊
i=low; j=high;
while (i<j) {
// i的位置空出來了。從右往左走,直到能放在i位置上的元素
while (i<j && arr[j]>tmp) j—-; //arr[j]比tmp大,繼續往左走
if (i<j) { //找到比tmp的小的元素arr[j]
arr[i]=arr[j]; //放到i的位置,則j的位置空出來了
i++;
}
// j的位置空出來了。從左到右走,直到能放在j位置上的元素
while (i<j && arr[i]<tmp) i++; //arr[i]比tmp小,繼續往右走
if (i<j) { //找到比tmp大的元素arr[i]
arr[j]=arr[i]; //放到j的位置,則i的位置又空出來了
—-j;
}
}
arr[i]=tmp; //tmp的位置找到,此時小於tmp的元素在左邊,大於tmp的在右邊
QuickSort(arr, low, i-1);
QuickSort(arr, i+1, high);
}
}
選擇排序
// 簡單選擇排序
void SelectSort(int arr[], int n) {
int i,j,tmp;
int min_index;
for (i=0; i<n; ++i) { //i爲無序序列的左邊界,每趟排序無序序列縮小一個
// 在無序序列[i, n-1]中選擇最小值
min_index = i; //最小值下標
for (j=i+1; j<n; ++j) {
if (arr[min_index]>arr[j])
min_index = j;
}
// 把最小值交換到無序序列的最左邊arr[i]
tmp=arr[i];
arr[i]=arr[min_index];
arr[min_index]=tmp;
}
}
// 堆排序:從小到大排序,要從數組頭取最大值放到最後,所以要用大頂堆
void HeapSort(int arr[], int n) { //元素在arr[0]-arr[n-1]
int i,tmp;
// 建堆:從最後一個非葉子結點開始調整
for (i=n/2-1; i>=0; i—-) //最後一個非葉子結點是n的父親
Sift(arr, i, n-1); //調整[i, n-1]
// 堆排序
for (i=n-1; i>=1; i—) { //從大頂堆中取n-1次最值:i表示無序序列的後邊界
// 堆頂和堆尾交換,將arr[i]併入有序序列
tmp = arr[0]; //從大頂堆中取出堆頂(最大值)
arr[0] = arr[i];//將堆中最後一個元素放在堆頂
arr[i] = tmp; //將最大值放入大頂堆的最右邊
Sift(arr, 0, i-1); //調整[0, i-1]
}
}
void Sift(int arr[], int low, int high) { //arr[low]不滿足堆定義,則在[low, high]範圍內調整(即下沉)
int i,j,tmp;
i=low; //此次要調整的元素
tmp=arr[i]; //備份要調整的元素,i的位置空出來了
for (j=2*i+1; j<=high; j=2*i+1) { //j指向i的左孩子
// 因爲是大頂堆,則左右孩子中要取最大的那個
if (j<high && arr[j]<arr[j+1]) { //如果i有右孩子 && 右孩子大
j=j+1; //則j指向i的右孩子
}
if (tmp<arr[j]) { //元素<它的孩子。但大頂堆的規則是元素>孩子,則需要進行調整
arr[i]=arr[j]; //孩子上浮到arr[i]
i=j; //元素的位置換到j
} else { //元素>它的孩子。符合大頂堆,即調整結束
break; //退出調整
}
}
arr[i]=tmp; //把tmp放在調整結束的位置i
}
歸併排序
// 歸併排序:對A[]中的[low, high]進行歸併排序
void MergeSort(int A[], int low, int high) {
if (low<high) {
int mid = (low+high)/2;
MergeSort(A, low, mid); //歸併前半段
MergeSort(A, mid+1, high); //歸併後半段
Merge(A, low, mid, high); //將[low, mid],[mid+1, high]合併
}
}
// 將[low, mid]、[mid+1, high]兩端有序序列歸併成一段有序序列
void Merge(int A[], int low, int mid, int high) {
int *tmp, i,j,k;
tmp = (int *)malloc( sizeof(int)*(high-low+1) ); //暫存排序結果
k=0; //合併結果的指針
for (i=low, j=mid+1; i<=mid && j<=high; ) {
tmp[k++] = A[i]<=A[j] ? A[i++] : A[j++]; //小的先存
}
// 處理剩餘部分
while (i<=mid) tmp[k++] = A[i++];
while (j<=high) tmp[k++] = A[j++];
// 將結果拷貝到A[]
k=0; //合併結果指針
for (i=low; i<=high; i++) A[i]=tmp[k++];
free(tmp); //釋放暫時空間
}
外部排序
外部排序歸併操作法 | 涉及的內容 | 說明 | 文件內容 |
---|---|---|---|
文件中存有n個數,但內存大小隻能存放m個數 | 15 19 04 83 12 27 11 25 16 34 26 07 10 90 06 |
||
第一階段 | 置換-選擇排序 | 將文件中的數據處理成多個有序子序列 | 【第一種結果】每個子序列長度相同:4 12 15 19 83 | 11 16 25 27 34 | 6 7 10 26 90 【第二種結果】每個子序列長度不同: 4 12 15 19 25 27 34 83 | 7 10 11 16 26 90 | 6 |
可選步驟 | 最佳歸併樹(生成哈夫曼樹的方法) | 計算最優的歸併方案 | 文件中生成了3個有序子序列,但長度都不相同。在進行二路歸併時,策略就有很多:①12先歸併,再和3 ②13先歸併,再和2 ③23先歸併, 再和1 歸併方案很多,如何取最優的歸併方案呢? |
第二階段 | 敗者樹(選擇每個歸併段的最小值) | 對文件中的多個有序子序列進行歸併操作 | 4 6 7 10 11 12 15 16 19 25 26 27 34 83 90 |
【空間複雜度】O(1)
【時間複雜度】
步驟 | 說明 | 時間複雜度 | I/O操作 |
---|---|---|---|
【第一階段】初始歸併段的生成 | 置換選擇排序 | 選擇最值那一步的時間複雜度要根據考試要求選擇算法而定 | 所有記錄都要進行兩次I/O操作 |
【可選階段】最佳歸併樹 | 生成哈夫曼樹 | ||
【第二階段】選擇最值 | 用敗者樹選擇最值 | 1. 【敗者樹建樹】O(klog2k) 2. k路歸併的敗者樹高度⌈log2k⌉+1,從k個記錄中選擇最值需要進行⌈log2k⌉此比較,即時間複雜度是O(log2k) |
|
【第二階段】歸併 | 歸併 | m個初始歸併段進行k路歸併,歸併的趟數⌈logkm⌉ | 每次歸併,所有記錄都要進行兩次I/O操作 |
查找
鏈接:查找彙總