[數據結構與算法] 彙總-notes(考試記憶版)

KMP

KMP筆記鏈接

【例題】模式串"ababaaababaa",求nextval數組(下標從1開始,0記錄長度)

【先求next數組】

  1. 先畫出表格(下標、T、next),並賦值特殊情況next[1]=0
  2. 看下標j那列,記下前面的子串T[1]-T[j-1],找出最長的前後綴
    1. 沒有相同的前後綴 --> next[j] = 1
    2. 有相同的前後綴 --> 取最長的前後綴,長度爲len --> next[j]=len+1

【再求nextval數組】

  1. 畫出nextval行,並賦值特殊情況nextval[1]=0
  2. 【算法:求單元格(j, nextval)】先鎖定下標爲j的那一列 --> 然後看第j列,next那一行(j, next)的值t --> 然後看第t列,T那一行(t, T)–> 用(t, T)和(j, T)比較
    1. (t, T)==(j, T) 的話:(j, nextval)=(t, nextval)
    2. (t, T)≠(j, T)的話:(j, nextval)=(j, next)
  3. 【舉例:求單元格(5,nextval)】鎖定下標爲5的那一列 --> (5, next)=3 --> (3, T)=a --> (3, T)=a與(5, T)=a比較 --> 相同,則(5, nextval)=(3, nextval)=3
  4. 【舉例:求單元格(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.n0=1+n2+2n3+...+(m1)nmn_0=1+n_2+2*n_3+...+(m-1)*n_m
3.n=2h1n=2^h-1
4.=2h1層結點樹=2^{h-1}
5.C2nn/(n+1)C_{2n}^n/(n+1)
6.h=log2n+1=log2(n+1)h=\lfloor{log_2n}\rfloor+1=\lceil{log_2(n+1)}\rceil
7.度爲m的哈夫曼樹,葉子結點爲n,非葉子結點爲(n1)/(m1)\lceil{(n-1)/(m-1)}\rceil
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叉樹中只有n0n_0nmn_m的結點,故有公式n0=1+(m1)nmn_0=1+(m-1)*n_m
2.n0n_0個葉子結點的哈夫曼樹,共有2n012*n_0-1個結點
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.【完全圖】任意兩頂點都有邊(無向完全n(n1)/2n(n-1)/2條,有向n(n1)n(n-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:存儲圖的所有邊
②用並查集檢查是否構成回邊
適合 稠密圖(時間複雜度與頂點有關係,與邊數沒有關係) 稀疏圖(時間複雜度由邊數決定)
時間複雜度 O(n2)O(n^2)(併入n-1個頂點,每次併入要選出最小者–>n²) O(eloge)O(eloge)(算法主要操作是對邊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操作

查找

鏈接:查找彙總

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章