數據結構復習筆記


前言

最近馬上要到數據結構的期末考試了,就自己總結了下考試的知識點,知識點會有點側重於期考的知識點以致於沒有那么全面和深入,不過是回顧一下所學的知識點罷了。由於是一學期的知識點,雖然不全面但是也是比較繁雜,如有紕漏錯誤煩請指出。
之后有時間了可能會視情況補充完善。
(目前排序部分不太完善)

數據

數據的邏輯結構和物理結構

數據結構的三要素:邏輯結構、存儲結構、數據的運算
數據的邏輯結構和存儲結構是密不可分的兩個方面,一個算法的設計取決於所選定的邏輯結構,二是算法的實現依賴於所采用的存儲結構。

邏輯結構

邏輯結構分為四種類型:集合結構,線性結構,樹形結構,圖形結構。

1、集合結構:集合結構的集合中任何兩個數據元素之間都沒有邏輯關系,組織形式松散。
2、線性結構:數據結構中線性結構指的是數據元素之間存在着“一對一”的線性關系的數據結構。
3、樹狀結構:樹狀結構是一個或多個節點的有限集合。
4、網絡結構:網絡結構是指通信系統的整體設計,它為網絡硬件、軟件、協議、存取控制和拓撲提供標准。

物理結構(存儲結構)

物理結構就是內存的存儲方式,分為以下幾種:

(1)順序存儲結構:是把數據元素存放在地址連續存儲單元里,其數據間的邏輯關系和物理關系是一致的,
其優點是可以實現隨機存取,每個元素占用最少的存儲空間,缺點是只能使用相鄰的一整塊存儲空間,因此可能產生較多的外部碎片
(2)鏈式存儲結構:不要求邏輯上相鄰的元素在物理位置上也相鄰,借助指示元素存儲位置的指針來表示元素之間的邏輯關系。
> 其優點是不會出現碎片現象,能充分利用所有存儲單元;缺點是每個元素因存儲指針而占用額外的存儲空間,且只能實現順序存取。

數據的邏輯結構獨立於其存儲結構
數據的邏輯結構是從面向實際問題的角度出發的,只采用抽象表達方式,獨立於存儲結構,數據的存儲結構有多種不同的選擇;而數據的存儲結構是邏輯結構在計算機上的映射,它不能獨立於邏輯結構而存在。數據結構包括三個要素,缺一不可。

算法特性和時間復雜度

算法的五大特性

算法的特性:

輸入: 算法具有0個或多個輸入
輸出: 算法至少有1個或多個輸出
有窮性: 算法在有限的步驟之后會自動結束而不會無限循環,並且每一個步 驟可以在可接受的時間內完成
確定性:算法中的每一步都有確定的含義,不會出現二義性
可行性:算法的每一步都是可行的,也就是說每一步都能夠執行有限的次數完成

算法的時間復雜度

時間頻度

一個算法執行所耗費的時間,從理論上是不能算出來的,必須上機運行測試才能知道。但我們不可能也沒有必要對每個算法都上機測試,只需知道哪個算法花費的時間多,哪個算法花費的時間少就可以了。並且一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)

時間復雜度

一般情況下,算法中基本操作重復執行的次數是問題規模n的某個函數,用T(n)表示,若有某個輔助函數f(n),使得當n趨近於無窮大時,T(n)/f(n)的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函數,記作T(n)=O(f(n)),它稱為算法的漸進時間復雜度,簡稱時間復雜度

常見數量階
常見數量階
計算時間復雜度

1.基本操作,即只有常數項,認為其時間復雜度為O(1)
2.順序結構,時間復雜度按加法進行計算
3.循環結構,時間復雜度按乘法進行計算
4.分支結構,時間復雜度取最大值 判斷一個算法的效率時,往往只需要關注操作數量的最高次項,其它次要項和常數項可以忽略
5.在沒有特殊說明時,我們所分析的算法的時間復雜度都是指最壞時間復雜度

空間復雜度本處省略,下圖來自《王道考研數據結構》
空間復雜度

線性表

線性表兩種存儲結構

list

線性表的特點:

表中元素個數有限。 表中元素具有邏輯上的順序性,表中元素有其先后次序。 表中元素都是數據元素,每個元素都是單個元素。
表中元素的數據類型都相同,這意味着每個元素占用相同大小的存儲空間。
表中元素具有抽象性,即僅討論元素間的邏輯關系,而不考慮元素究竟表示什么內容。

注意:線性表是一種邏輯結構,表示元素之間一對一的相鄰關系。順序表和鏈表是指存儲結構,兩者屬於不同層面的概念,因此不要將其混淆

InitList(&L):初始化表。構造一個空的線性表
Length(L):求表長。返回線性表工的長度,即L中數據元素的個數。
LocateElem(L,e):按值查找操作。在表L中査找具有給定關鍵字值的元素。
GetElem(L,i):按位査找操作。獲取表工中第1個位置的元素的值。
ListInsert(&L,i,e):插入操作。在表L中的第i個位置上插入指定元素e.
ListDelete(&L,i,&e):刪除操作。刪除表L中第i個位置的元素,並用e返回刪除元素的值
PrintList(I):輸出操作。按前后順序輸出線性表L的所有元素值
Empty(L):判空操作。若L為空表,則返回true,否則返回 false.
DestroyList(&L):銷毀操作。銷毀線性表,並釋放線性表L所占用的內存空間。

順序表

#define LIST_INIT_SIZE 100  //順序表存儲空間的初始分配量
#define LISTINCREMENT 10    //順序表存儲空間的分配增量
typedef struct {
     ElemType *elem;   //存儲空間的基地址
     int length;       //順序表的當前長度
     int listsize;     //數組存儲空間的長度
}SqList;
//初始化
Status InitList(SqList &L){
    L.elem=(ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    if(!L.elem) exit(OVERFLOW);
    L.length=0;
    L.listsize=LIST_INIT_SIZE;
    return OK;
}
//銷毀
void DestroyList(SqList &L) {
  if(L.elem) free(L.elem);
  L.elem=NULL;
}
//清零
void ClearList(SqList &L)
{
       L.length=0;
}
//判空
Status ListEmpty(SqList L)
{
    if(L.length==0) return TRUE;
    else return FALSE;
}
//求表長
int ListLength(SqList L)
{
     return L.length;
}
//取值
Status GetElem(SqList L,int i,ElemType &e){
    if(i<1 || i>L.length) return ERROR;
    e=L.elem[i-1];       
    return OK;
}
//定位1
int LocateElem (SqList L,ElemType e)
{
    i=1;
    while( i<=L.length && L.elem[i-1]!=e)  i++;
    if(i<=L.length) return i;  
    else return 0;       
} 
	//定位2
	/*int LocateElem (SqList L, ElemType e, Status (*compare)(ElemType, ElemType))
	{
		i=1;  p=L.elem;
		while(i<=L.length && !(*compare)(*p++,e)) ++i;
		if(i<=L.length) return i;
		else return 0;
	} 
	//定義比較函數:
	Status GT(ElemType a,ElemType b){
		 if(a>b) return TRUE;
		 else return FALSE;
	}
	*/
//插入
Status ListInsert(SqList &L,int i,ElemType e){
    //插入元素e到順序表L的第i個位置之前
    if(i<1 ||i>L.length+1) return ERROR;
    if(L.length==L.listize)
     {
	 L.elem=(ElemType *) realloc(L.elem, (L.listsize+LISTINCREMENT)*sizeof(ElemType) );
	if(!L.elem) exit(OVERFLOW);
	L.Listsize += LISTINCREMENT;
	 }//重新分配空間
    q=&L.elem[i-1];
    for(p=&(L.elem[L.length-1];p>=q;--p) 
         *(p+1)=*p;
    *q=e;
    ++L.length;
    return OK;
}
Status ListDelete(SqList &L,int i,ElemType &e){
    //刪除順序表L中的第i個元素,其值由e返回;
    if(i<1 ||i>L.length) return ERROR;
    p=&L.elem[i-1];
    e=*p;
    q=L.elem+L.length-1;
    for(++p;p<=q;++p) 
       *(p-1)=*p;
    --L.length;
    return OK;
}
Status ListTraverse(SqList L,Status (*visit)(ElemType))
{
  for(i=0;i<L.length;i++)
    if(!visit(L.elem[i])) return ERROR;
  return OK;
} 
//定義訪問函數:
Status visit(ElemType e){
     printf(e);
     return OK;
}

順序存儲

鏈型表

任何兩個元素的存儲位置沒有固定的聯系 每個元素的存儲位置只能由其直接前驅結點的指針指出
在單鏈表中獲取第i個元素必須從頭指針出發,沿指針鏈依次向后查找。因此,單鏈表是順序存取的存儲結構。

typedef struct LNode{
   ElemType data;       //數據域
   struct LNode *next;  //指針域
}LNode,*LinkList;

Status InitList(LinkList &L) {
    L=(LinkList)malloc(sizeof(LNode));
    if (!L) exit(OVERFLOW);
    L->next=NULL;
    return OK;
}

void DestroyList(LinkList &L) {
    while(L) {
	p=L->next;
	free(L);
	L=p;
    }
}

void ClearList(LinkList L) {
  for(p=L->next;p; p=L->next) {
	L->next=p->next;
	free(p);
  }
}
Status ListEmpty(LinkList L) {
    if( !L->next) return TRUE;
    else return FALSE;
}
int ListLength(LinkList L) {
    p=L->next; n=0;   
    while(p)
    { n++;	p=p->next; }
    return n;
}
Status GetElem(LinkList L, int i, ElemType &e) {
    j=1;
    p=L->next; 
    while(p &&j<i) { p=p->next; ++j;}
    if( !p || j>i) 
      return ERROR;   
    e=p->data ;
    return OK;
 } 
LinkList LocateElem(LinkList L,ElemType e,Status (*compare)(ElemType,ElemType)) {
    p=L->next;
    while( p && !compare(p->data,e) ) p=p->next;
    return p;
}
//定義比較函數:
Status LT(ElemType a,ElemType b) {
     if(a<b) return TRUE;
     else return FALSE;
}
//遍歷
Status ListTraverse(LinkList L,Status (*visit)(ElemType)) {
    for( p=L->next; p; p=p->next )
      if( !visit(p->data) ) return ERROR;
    return OK;
}

鏈式存儲

單循環鏈表、雙循環鏈表和單鏈表遍歷
循環單鏈表和單鏈表的區別在於,表中最后一個結點的指針不是NULL,而改為指向頭結點,從而整個鏈表形成一個環。
循環單鏈表

循環單鏈表遍歷

雙向循環鏈表前后均有結點
雙向循環單鏈表

線性表分析

線性表分析

棧和隊列

棧和隊列

棧

使用棧存儲數據元素,對數據元素的“存”和“取”有嚴格的規定:數據按一定的順序存儲到棧中,當需要調取棧中某數據元素時,需要將在該數據元素之后進棧的先出棧,該數據元素才能從棧中提取出來。
棧操作數據元素只有兩種動作:
數據元素用棧的數據結構存儲起來,稱為“入棧”,也叫“壓棧”。
數據元素由於某種原因需要從棧結構中提取出來,稱為“出棧”,也叫“彈棧”。

棧是后進先出(LIFO)的線性表
ADT棧

Status InitStack(SqStack &S) {
//構造一個空棧,該棧由指針S指示 
  S.base=(SElemType*)malloc ((STACK_INIT_SIZE)*sizeof(SElemType));
    // 棧的連續空間分配
    //S.base=new SElemType[STACK_INIT_SIZE]; 等價
	if(!S.base)	exit(OVERFLOW);
	S.top=S.base; //空棧,初始化棧頂指針
	S.stacksize=STACK_INIT_SIZE; 
	return OK;
}//InitStack 

棧

隊列

隊列:只允許在表的一端進行插入,而在另一端進行刪除的線性表。
隊頭:允許刪除的一端
隊尾:允許插入的一端
空隊列:沒有元素的隊列
設隊列Q=(a1,a2,a3,…an),
a1稱為隊頭元素
an稱為隊尾元素
隊列是先進先出(FIFO) 的線性表。

隊列

ADT隊列

串的基本操作

基本名詞
字符串

ADT字符串

串的存儲方式

順序存儲結構
---- 定長存儲結構
---- 堆分配存儲結構
鏈式存儲結構
---- 塊鏈結構

定長存儲結構

特點:用長度固定的連續單元依次存儲串值的字符序列
串長的表示方法
以下標為0的數組分量存放實際串長——PASCAL
串值后加一個不計入串長的結束標記字符——C、C++中用‘\0’作串的結束標記
用C語言實現第一種表示串長的串的定長存儲結構

   #define MAXSTRLEN    255    //最大串長
   typedef unsigned char SString[MAXSTRLEN+1];

串截斷現象:若串長超出MAXSTRLEN,則超出部分被舍去
串的定長存儲表示下各基本操作的實現的實質:

字符串序列的復制
需求:T,S1,S2都是SSTring型的串變量,現要求將用T返回S1和S2聯接的新串。
算法思想:串T值產生有2種情況:
S1[0]+S2[0]≤MAXSTRLEN:完整聯接
S1[0]+S2[0]>MAXSTRLEN:超出最大串長部分被‘截斷‘
截斷現象在可能增加串長的操作中經常發生,給串相關操作的結果完整性帶來很大隱患

堆分配存儲結構

特點:采用動態字符數組存放串值,此時不必為數組預定義大小,以串長動態分配數組空間
用C語言實現串的堆分配存儲

typedef struct {
   char *ch; 
   int  length;   
}HString;

串的堆分配存儲表示下各基本操作的實現的實質:

字符串序列的復制
優點:
有順序存儲結構的特點
處理方便
操作中對串長沒有限制

Status StrAssign(HString &T, char *chars){
    if(T.ch) free(T.ch);    //釋放舊空間
    for(i=0,c=chars; *c; ++i,++c); //計算串常量chars的長度i      
    if(!i) {  T.ch=NULL; T.length=0;  }  //chars為空串
    else {
        if(!(T.ch=(char *)malloc(i*sizeof(char)))) exit OVERFLOW;
        T.ch[0..i-1]=chars[0..i-1];   //chars的串值依次賦給串變量
        T.length=i;
    }
    return OK;
}// StrAssign 
int Strlength(HString S){//返回串S的長度
    return S.length;
}// Strlength 

int StrCompare(HString S, HString T){
//比較串S和T:若S>T,返回正整數,若S=T返回0,若S<T,返回負整數
    for(i=0;i<S.length && i<T.length; i++)
       if(S.ch[i]!=T.ch[i]) return S.ch[i]-T.ch[i];
    return S.length-T.length;
}// StrCompare 
Status StrConcat(HString &T, HString S1, HString S2){
//由T返回將串S2聯接在串S1的末尾組成的新串
  if(T.ch) free(T.ch);  //釋放舊空間
  if(!(T.ch=(char *)malloc((S1.length+S2.length)*sizeof(char)))) 
	exit OVERFLOW;
  T.ch[0 .. S1.length-1]=S1.ch[0 .. S1.length-1];  
  T.length=S1.length+S2.length;
  for(i=0; i<S2.length; i++) T->ch[S1.length+i]=S2.ch[i];
  return OK;
}// StrConcat 
Status SubString(HString &Sub, HString S, int pos, int len){
   if(pos<1 || pos>S.length || len<0 || len>S.length-pos+1) 
	return ERROR;  //參數不合法
   if(Sub.ch) free(Sub.ch);   //釋放舊空間
   if(!len) {Sub.ch=NULL; Sub.length=0;}  // 子串為空串
   else  {
      if(!(Sub.ch=(char *)malloc(len*sizeof(char)))) exit OVERFLOW;
      Sub.ch[0..len-1]=S.ch[pos-1..pos+len-2];
      Sub.length=len;
   }
   return OK;
}// SubString 

塊鏈存儲表示

塊鏈存儲

#define  CHUNKSIZE  80  // 可由用戶定義的塊大小
  typedef  struct Chunk {  // 結點結構
    char  ch[CUNKSIZE];
    struct Chunk  *next;
  } Chunk;
  typedef struct {  // 串的鏈表結構
     Chunk *head, *tail;  //串的頭和尾指針,便於聯結操作
     int   curlen;           // 串的當前長度
  } LString;

結點大小為1
優點:操作方便;
缺點:存儲密度較低,占用存儲量大。
結點大小>1
優點:存儲密度高;
缺點:插入、刪除字符時,可能會引起結點之間字符的移動,算法實現比較復雜

除了某些特定操作如聯接有一定方便之處,總的來說鏈串不如另外兩種順序存儲結構靈活:
占用空間較多
操作復雜
在實際應用中串的鏈式結構遠不如串的順序結構使用廣泛

串的模式匹配算法KMP

略······

數組

數組下標的計算

由於數組可以是多維的,而順序存儲結構是一維的,因此數組中數據的存儲要制定一個先后次序。通常,數組中數據的存儲有兩種先后存儲方式:

以列序為主(先列后行):按照行號從小到大的順序,依次存儲每一列的元素
以行序為主(先行后序):按照列號從小到大的順序,依次存儲每一行的元素。

根據存儲方式的不同,查找目標元素的方式也不同。如果二維數組采用以行序為主的方式,則在二維數組 anm 中查找 aij 存放位置的公式為:

LOC(i,j) = LOC(0,0) + (i*m + j) * L;

其中,LOC(i,j) 為 aij 在內存中的地址,LOC(0,0) 為二維數組在內存中存放的起始位置(也就是 a00 的位置)。
而如果采用以列存儲的方式,在 anm 中查找 aij 的方式為:

LOC(i,j) = LOC(0,0) + (i*n + j) * L;

特殊矩陣壓縮存儲

特殊矩陣主要有兩類:
-->對稱矩陣
-->稀疏矩陣、上(下)三角矩陣

對稱矩陣

結合數據結構壓縮存儲的思想,我們可以使用一維數組存儲對稱矩陣。由於矩陣中沿對角線兩側的數據相等,因此數組中只需存儲對角線一側(包含對角線)的數據即可。
對稱矩陣的實現過程是,若存儲下三角中的元素,只需將各元素所在的行標 i 和列標 j 代入下面的公式:
公式1

存儲上三角的元素要將各元素的行標i和列標j帶入另外一個公式:
公式2

三元組順序表

   0 12  9  0  0  0  0
   0  0  0  0  0  0  0
  -3  0  0  0  0 14  0
M= 0  0 24  0  0  0  0
   0 18  0  0  0  0  0
  15  0  0 -7  0  0  0

//上面矩陣用三元組表示
i  j  v      
1  2  12
1  3  9
3  1  -3
3  6  14
4  3  24 
5  2  18
6  1  15
6  4  -7
typedef  struct
{
    int i,j;     //行坐標、列坐標
    ElemType e;  //元素
}Triple;

typedef struct
{
    Triple date[MAXSIZE+1];  //0不存儲元素
    int mu,nu,tu;      //行數、列數、非零元個數
}TSMatrix;

轉置

以列序為主序的轉置

void TransposeSMatrix(TSMatrix *T1,TSMatrix *T2)
{
    T2->mu=T1->nu;T2->nu=T1->mu;T2->tu=T1->tu;
    if(T1->tu)
    {
        int q=1,col,p;
        for(col=1;col<=T1->nu;col++)  //矩陣列循環
        {
            for(p=1;p<=T1->tu;p++)    //遍歷所有元素
            {
                if(T1->date[p].j==col)  //當元素在col列時
                {
                    T2->date[q].i=T1->date[p].j;
                    T2->date[q].j=T1->date[p].i;
                    T2->date[q].e=T1->date[p].e;
                    q++;
                }
            }
        }
    }
}
//上述代碼,當矩陣運算為滿時,即tu=mu*nu,其時間復雜度為O(nu*nu*mu)

快速轉置

第一種算法是通過遍歷所有元素的下標,從而確定其在轉置后數組中的位置,快速轉置的思想就是,預先確定每一列第一個非零元在對應轉置后的數組date中的位置;因此需要兩個輔助數組
num[ ]:用來存放每一列的非零元個數
cpot[ ]:存放第一個非零元在轉置后數組date中的位置
num[ ]數組的值很好求,只需要遍歷一次所有元素即可

//可發現
copt[1]=1
copt[col]=copt[col-1]+num[col-1]

void FastTransposeSMatrix(TSMatrix *T1,TSMatrix *T2)
{
    int num[T1->nu],cpot[T1->nu];
    int col,p,q,t;
    T2->mu=T1->nu;T2->nu=T1->mu;T2->tu=T1->tu;
    if(T1->tu)
    {
        //初始化每列非零元個數為0
        for(col=1;col<=T1->nu;col++)
        {
            num[col]=0;
        }
        //求每列非零元個數
        for(t=1;t<=T1->tu;t++)
        {
            ++num[T1->date[t].j];
        }
        //求每列第一個非零元轉置后的位置
        cpot[1]=1;
        for(col=2;col<=T1->nu;col++)
        {
            cpot[col]=num[col-1]+cpot[col-1];
        }
        //遍歷所有元素
        for(p=1;p<=T1->tu;p++)
        {
            col=T1->date[p].j;  //獲取列坐標
            q=cpot[col];        //獲取新位置
            T2->date[q].i=T1->date[p].j;
            T2->date[q].j=T1->date[p].i;
            T2->date[q].e=T1->date[p].e;
            ++cpot[col];   //之所以這個地方要++,因為每列非零元可能不止一個
        }  
    }
}

樹與二叉樹

本處知識點總結有所側重
樹與二叉樹

二叉樹與二叉樹的鏈式結構

二叉樹的性質:
  1.二叉樹的第i層上至多有2i-1個節點
  2.深度為K的二叉樹至多有2k-1個節點
  3.任何一個二叉樹中度數為2的節點的個數比度數為0的節點數目少1.
  4.具有n個節點的完全二叉樹的深度為 |log2N |+1
  5.若完全二叉樹中的某節點編號為 i,
  則若有左孩子編號為 2i,若有右孩子編號為 2i+1,母親節點為 i/2。

完全二叉樹舉例

順序存儲結構:適用於完全二叉樹
特點:用一組地址連續的存儲單元依次自上而下、自左至右存儲完全二叉樹的結點。此時可利用性質5很方便地求出結點之間的關系。
對一般二叉樹,可將其每個結點與完全二叉樹上同一位置上的結點對照,存儲在一維數組的相應分量中。可能對存儲空間造成極大的浪費。

鏈式存儲

  typedef struct BiTNode{
    ElemType data;
    struct BiTNode *lchild,*rchild;
  }BiTNode,*BiTree;

n個結點的二叉樹的二叉鏈表中有n+1個空鏈域

二叉樹的遍歷

遍歷二叉樹:
按某條搜索路徑巡訪二叉樹中的每一個結點,使每一個結點均被訪問一次且僅被訪問一次。

遍歷是二叉樹所有操作中最重要的操作

非線性結構遍歷的困難 :
每個元素的后繼有多個 需要找到一種規律,使得非線性結構的元素排成一個線性序列

應用場合:
在二叉樹中查找具有某種特征的結點 對二叉樹中全部結點逐一進行某種處理

先序遍歷
訪問根結點;
先序遍歷左子樹;
先序遍歷右子樹;

中序遍歷
中序遍歷左子樹;
訪問根結點;
中序遍歷右子樹;

后序遍歷
后序遍歷左子樹;
后序遍歷右子樹;
訪問根節點

二叉樹遍歷
二叉樹遍歷
先序遍歷遞歸實現

void PreorderTraverse(BiTree T, void (*visit)(ElemType)) {
	if(T){
         visit(T->data); 
         PreorderTraverse(T->lchild,visit);                  
         PreorderTraverse(T->rchild,visit);                         
    }
}//PreorderTraverse

先序創建二叉樹

Status CreateBiTree(BiTree &T)
{ //先序創建一棵二叉樹,由指針T指向其根結點的指針
    scanf("%c",&ch);
    if(ch==‘#')  T=NULL;
    else {
	   if(!(T=(BiTree)malloc(sizeof(BiTNode))))  exit(OVERFLOW);
	   T->data=ch;
	   CreateBiTree(T->lchild);
	   CreateBiTree(T->rchild);
    }
    return OK;
}//CreateBiTree

中序遍歷非遞歸實現

void InorderTraverse(BiTree T, void (*visit)(ElemType))
{     InitStack(S); Push(S,T); 
       while(!StackEmpty(S)) 
       {  while(GetTop(S,p)&&p)    Push(S,p->lchild); 
          Pop(S,p); 
          if(!StackEmpty(S))
        {   
         Pop(S,p); 
	      visit(p->data);
	      Push(S,p->rchild); 
             }
        }
} //InOrderTraverse

后序遍歷的非遞歸實現

void PostorderTraverse(BiTree T, void (*visit)(ElemType)) {
      InitStack(S); Push(S,T); 
      while(!StackEmpty(S)) {
          while(GetTop(S,p)&&p)    Push(S,p->lchild); 
          Pop(S,p); 
          while((i=GetTop(S,r)) && r->rchild==p) {
         visit(r->data);
              Pop(S,p); 
          }//while
          if(i) Push(S,r->rchild); 
       }//while
} //PostOrderTraverse

層序遍歷二叉樹

void LevelorderTraverse(BiTree T, void (*visit)(ElemType))
{//層序遍歷二叉樹
    p=T;
    InitQueue(Q);
    if(p) EnQueue(Q,p);
    while(!QueueEmpty(Q))  // 隊列不空
   {
	DeQueue(Q,p);  
	 visit(p->data);
	if(p->lchild)  
	 EnQueue(Q,p->lchild);      
	if(p->rchild)  
	 EnQueue(Q,p->rchild);      
    }
}// LevelorderTraverse 

遍歷時間復雜度

二叉樹與表達式

二叉樹遍歷最早是在對存儲在機內的表達式求值時提出的

用二叉樹表示表達式的方法

例:a+b*(c-d)-e/f
描述表達式的二叉樹遍歷序列
前綴表達式(波蘭式):
        -+a*b-cd/ef
中綴表達式:
        a+b*c-d-e/f
后綴表達式(逆波蘭式):
        abcd-*+ef/-

表達式

a+bc-(d+e)
第一步:按照運算符的優先級對所有的運算單位加括號~
式子變成拉:((a+(b
c))-(d+e))
第二步:轉換前綴與后綴表達式
前綴:把運算符號移動到對應的括號前面
則變成拉:-( +(a (bc)) +(de))
把括號去掉:-+a
bc+de 前綴式子出現
后綴:把運算符號移動到對應的括號后面
則變成拉:((a(bc)* )- (de)+ )-
把括號去掉:abc*-de+- 后綴式子出現

二叉樹的線索化

線索化暫略

樹、二叉樹、森林的互換

樹轉二叉樹

在樹的兄弟結點之間添加一條線
在樹中只保留父結點與第一個孩子結點的連線

樹與二叉樹
森林轉二叉樹:

把森林中的樹都轉換成二叉樹
從第二棵樹開始,把轉換后的二叉樹作為錢一棵樹根結點的右子樹插入到第一棵樹中

二叉樹轉換成樹和森林

1.加線。在所有的兄弟結點之間加一條線。
2.去線。樹中的每個結點,只保留它與第一個孩子結點的連線,刪除其他孩子結點之間的連線。
3.調整。以樹的根結點為軸心,將整個樹調節一下(第一個孩子是結點的左孩子,兄弟轉過來的孩子是結點的右孩子)

哈夫曼樹

赫夫曼樹,也稱“哈夫曼樹”、“最優樹”以及“最優二叉樹”。

樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。通常記作 “WPL” 。
WPL

步驟1
根據給定的n個權值{w1, w2, …, wn},構造n棵二叉樹的集合F = {T1, T2, …, Tn},Ti(1≤i≤n)只有一個帶權值wi的根結點,其左、右子樹均為空。
步驟2
在F中選取兩棵根結點權值最小的二叉樹,分別作為左、右子樹構造一棵新二叉樹。置新二叉樹的根結點的權值為其左、右子樹上根結點的權值之和。
步驟3
在F中刪去這兩棵二叉樹,把新的二叉樹加入F 。
步驟4
重復步驟2和步驟3
直到F中僅剩下一棵樹為止。
這棵樹就是Huffman樹。

typedef struct
{
unsigned int weight; 
unsigned int parent, lchild, rchild; 
}HTNode,*HuffmanTree;
Status CreateHuffmanTree(HuffmanTree &HT, int *w, int n) 
{  if(n<=1) return ERROR; 
   m=2*n-1; 
   HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));//0號單元未用
   for(p=HT+1,i=1;i<=n; ++i,++p,++w)    *p={*w,0,0,0}; 
   for(;i<=m; ++i, ++p)     *p= {0,0,0,0}; 
   for(i=n+1;i<=m; ++i)
   { Select(HT,i-1,s1,s2);
     HT[s1].parent=i; HT[s2].parent=i;       
     HT[i].lchild=s1; HT[i].rchild=s2;   
     HT[i].weight=HT[s1].weight+HT[s2].weight; 
   }	
   return OK;
}

在Huffman樹中沒有度為1的結點,這類樹又稱嚴格二叉樹或正則二叉樹, 此類二叉樹中僅有度為0和度為2的結點。

哈夫曼編碼

介紹

霍夫曼編碼,首先需要根據輸入文本,構造一棵二叉樹,樹的左鏈接表示比特"0",右鏈接表示比特"1",葉子結點表示字符。字符所對應的霍夫曼編碼值就是從根結點到葉子結點的鏈接值。

構建哈夫曼樹

#define LEN 512
struct huffman_node{
        char c;
        int weight;
        char huffman_code[LEN];
        huffman_node * left;
        huffman_node * right;
};
int huffman_tree_create(huffman_node *&root, map<char, int> &word){
        char line[MAX_LINE];
        vector<huffman_node *> huffman_tree_node;

        map<char, int>::iterator it_t;
        for (it_t = word.begin(); it_t != word.end(); it_t++){
                // 為每一個節點申請空間
                huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                node->c = it_t->first;
                node->weight = it_t->second;
                node->left = NULL;
                node->right = NULL;
                huffman_tree_node.push_back(node);
        }


        // 開始從葉節點開始構建Huffman樹
        while (huffman_tree_node.size() > 0){
                // 按照weight升序排序
                sort(huffman_tree_node.begin(), huffman_tree_node.end(), sort_by_weight);
                // 取出前兩個節點
                if (huffman_tree_node.size() == 1){// 只有一個根結點
                        root = huffman_tree_node[0];
                        huffman_tree_node.erase(huffman_tree_node.begin());
                }else{
                        // 取出前兩個
                        huffman_node *node_1 = huffman_tree_node[0];
                        huffman_node *node_2 = huffman_tree_node[1];
                        // 刪除
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        huffman_tree_node.erase(huffman_tree_node.begin());
                        // 生成新的節點
                        huffman_node *node = (huffman_node *)malloc(sizeof(huffman_node));
                        node->weight = node_1->weight + node_2->weight;
                        (node_1->weight < node_2->weight)?(node->left=node_1,node->right=node_2):(node->left=node_2,node->right=node_1);
                        huffman_tree_node.push_back(node);
                }
        }

        return 0;
}

字符頻率統計

int read_file(FILE *fn, map<char, int> &word){
        if (fn == NULL) return 1;
        char line[MAX_LINE];
        while (fgets(line, 1024, fn)){
                fprintf(stderr, "%s\n", line);
                //解析,統計詞頻
                char *p = line;
                while (*p != '\0' && *p != '\n'){
                        map<char, int>::iterator it = word.find(*p);
                        if (it == word.end()){// 不存在,插入
                                word.insert(make_pair(*p, 1));
                        }else{
                                it->second ++;
                        }
                        p ++;
                }
        }
        return 0;
}

哈夫曼樹轉哈夫曼編碼

int get_huffman_code(huffman_node *&node){
        if (node == NULL) return 1;//層序遍歷
        huffman_node *p = node;
        queue<huffman_node *> q;
        q.push(p);
        while(q.size() > 0){
                p = q.front();
                q.pop();
                if (p->left != NULL){
                        q.push(p->left);
                        strcpy((p->left)->huffman_code, p->huffman_code);
                        char *ptr = (p->left)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '0';
                }
                if (p->right != NULL){
                        q.push(p->right);
                        strcpy((p->right)->huffman_code, p->huffman_code);
                        char *ptr = (p->right)->huffman_code;
                        while (*ptr != '\0'){
                                ptr ++;
                        }
                        *ptr = '1';
                }
        }
        return 0;
}

實現步驟

讀取輸入;
統計輸入中每個字符的頻次;
根據頻次,構造Huffman樹;
構造編譯表,用於將字符與變長前綴映射;
將Huffman樹編碼為比特字符串,並寫入輸出流;
將文本長度編碼為比特字符串,並寫入輸出流;
壓縮數據,即使用編譯表翻譯每個文本字符,寫入輸出流。

圖的術語

圖(Graph):由兩個集合V(G)和E(G)組成的,記為G=(V,{E})
其中:
V 是頂點的有窮非空集;
E 是邊(弧)的有限集
約定符號:

V:頂點有窮非空集合
VR:頂點關系的集合
E:邊或弧的集合
n:圖中頂點數目
e:邊或弧的數目
G:圖
N:網

無向完全圖和有向完全圖:
在無向圖中,如果任意兩個頂點之間都存在邊,則稱該圖為無向完全圖。含有n個頂點的無向完全圖有n(n-1)/2條邊。在有向圖中,如果任意兩個頂點之間都存在方向互為相反的兩條弧,則稱該圖為有向完全圖。含有n個頂點的有向完全圖有n(n-1) 條邊。
稀疏圖和稠密圖:
有很少條邊或弧的圖稱為稀疏圖,反之稱為稠密圖,這里的概念是相對而言的。
權和網:
有些圖的邊或弧具有與它相關的數字,這種與圖的邊或弧相關的數叫做權。這些權可以表示從一個頂點到另一個頂點的距離或耗費。這種帶權的圖通常稱為網。
鄰接點:
對於無向圖G= (V,{E}), 如果邊(v,v')屬於E, 則稱頂點v和v‘互為鄰接點,即v和v'相鄰接、邊(v,v')依附於頂點v和v',或者說(v,v')與頂點v和v'相關聯。
度、入度和出度:
點v的度是和v相關聯的邊的數目,記為TD(v)。如上圖左側上方的無向圖,頂點A與B互為鄰接點,邊(A,B) 依附於頂點A 與B 上,頂點A 的度為3。而此圖的邊數是5,各個頂點度的和=3+2+3+2=10,推敲后發現,邊數其實就是各頂點度數和的一半,多出的一半是因為重復兩次計數。
對於有向圖G= (V,{E}),如果弧<v,v'>屬於E,則稱頂點v鄰接到頂點v',頂點v'鄰接自頂點v的弧<v,v'>和頂點v, v'相關聯。以頂點v為頭的弧的數自稱為v的入度,記為ID (v); 以v為尾的弧的數目稱為v的出度,記為OD (v); 頂點v的度為TD(v) =ID(v) +OD (v)。
路徑和路徑的長度:
從頂點v 到頂點v'的路徑是一個頂點序列。路徑的長度是路徑上的邊或弧的數目。有向圖的路徑也是有向的。
回路或環:
第一個頂點到最后一個頂點相同的路徑稱為回路或環。
簡單路徑、簡單回路或簡單環:
序列中頂點不重復出現的路徑稱為簡單路徑。除了第一個頂點和最后一個頂點之外,其余頂點不重復出現的回路,稱為簡單回路或簡單環。
連通、連通圖和連通分量:
在無向圖G中,如果從頂點v到頂點v'有路徑,則稱v和v'是連通的。 如果對於圖中任意兩個頂點vi、vj ∈E, vi,和vj都是連通的,則稱G是連通圖。
稀疏圖:有很少條邊或弧(e<nlog2n)的圖。
稠密圖:有很多條邊或弧的圖。
ADT圖

圖的鄰接矩陣

圖的2種常用的存儲形式:
數組(鄰接矩陣)表示法
鄰接表

鄰接矩陣

#define MAX_VERTEX_NUM 20
typedef  enum {DG,DN,UDG,UDN} GraphKind; 
typedef struct ArcCell
{
	VRType adj;
	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;
Status CreateGraph(MGraph &G)
{//在鄰接矩陣存儲結構上根據圖的種類調用具體構造算法
	printf("please input the kind of graph\n");
	scanf(&G.kind);
	switch(G.kind)
	{
		case DG:return CreateDG(G);//構造有向圖
		case DN:return CreateDN(G);//構造有向網
		case UDG:return CreateUDG(G);//構造無向圖
		case UDN:return CreateUDN(G);//構造無向網
		default:return ERROR;
	}
}
Status CreateUDN(MGraph &G) //在鄰接矩陣存儲結構上,構造無向網G
{	scanf(&G.vexnum,&G.arcnum);//讀入頂點數和邊數目for(i=0;i<G.vexnum;i++)	scanf(&G.vexs[i]);//構造頂點向量
	for(i=0;i<G.vexnum;i++) //鄰接矩陣初始化
	   for(j=0;j<G.vexnum;j++)
	  	G.arcs[i][j]=INFINITY;
	for(k=0;k<G.arcnum;k++)//構造鄰接矩陣
	{	scanf(&v1,&v2,&w);//讀入一條邊依附的頂點及權值
		i=LocateVex(G,v1);j=LocateVex(G,v2);//確定v1、v2在圖中的位置
		G.arcs[i][j]=w;//邊<v1,v2>的權值
		G.arcs[j][i]=G.arcs[i][j];//置<v1,v2>的對稱弧<v2,v1 >
	} 	
   return OK;
}//CreateUDN

圖_鄰接矩陣的分析

圖的鄰接表

圖

#define MAX_VERTEX_NUM 20
typedef struct ArcNode//表結點
{	int adjvex;
	struct ArcNode *nextarc;
	infoType info;
}ArcNode;
typedef struct Vnode//頭結點
{	VertexType data;
	ArcNode *firstarc;
}VNode,AdjList[MAX_VERTEX_NUM];
typedef struct
{	AdjList vertices;
	int vexnum,arcnum;  //圖的當前頂點數和弧數
	int kind;           //圖的種類
}ALGraph;

鄰接表的分析

圖的遍歷

對非線性結構線性化。遍歷算法是求解圖的連通性、拓撲排序、求關鍵路徑等算法的基礎。

圖的兩種遍歷方式(有向圖和無向圖均適用):
深度優先搜索DFS (Depth First Search)
廣度優先搜索BFS (Breadth First Search)

輔助向量visited[]的使用

① 使用原因:
因為圖中任意頂點都可能和其余頂點相鄰接,所以在訪問了某頂點后,可能沿另外的某條路徑搜索,而后又回到此頂點上,為了避免同一頂點被多次訪問,在遍歷圖的過程中,必須記下每個已訪問過的頂點。
② 使用方法:
設置一個輔助數組visited[0..n-1],它的初始值置為“假”,表示頂點未被訪問過,一旦訪問了頂點i,就置visited[i]的值為“真”或者被訪問時的次序號。
Boolean visited[MAX_VERTEX_NUM] ;

深度優先搜索DFS

DFS的過程接近先序遍歷。
算法思想:
①從圖中某個頂點v出發,訪問此頂點;
②依次從v的各個未被訪問的鄰接點出發深度優先遍歷圖,直至圖中所有和v有路徑相通的頂點都被訪問到
③若圖中還有頂點未被訪問(非連通圖),則另選圖中一個未被訪問的頂點作起始點,重復上述過程,直至圖中所有頂點都被訪問到為止。

Boolean visited[MAX_VERTEX_NUM]; 
Status (*VisitFunc)(int v); //全局函數指針變量

void DFSTraverse( Graph G, Status (*Visit)(int v))
 { //對圖G進行深度優先遍歷  
   VisitFunc=Visit;
   for ( v=0; v <G.vexnum; ++v )  visited[v] = FALSE;
   for ( v=0; v <G.vexnum; ++v )
     if ( !visited[v] ) DFS(G,v); 
 }//DFSTraverse
void DFS( Graph G, int v) 
 { //從v出發深度優先遍歷圖G
    visited[v] = TRUE; 
    VisitFunc(v); //訪問頂點v
    for(w=FirstAdjVex(G,v); w>=0; w=NextAdjVex(G,v,w)) 
      if ( !visited[w] )  DFS(G,w);  
 } //DFS

1
2

在遍歷圖時,對每個頂點至多調用一次DFS函數,因為一旦某個頂點被標志成已被訪問,就不再從它出發進行搜索。
遍歷圖的實質上是對每個頂點查找其鄰接點的過程。其耗費的時間取決於所采用的存儲結構。
用鄰接矩陣存儲圖時,查找所有頂點的鄰接點需要O(n2);
用鄰接表存儲圖時,查找所有頂點的鄰接點需要O(e);
深度優先遍歷圖的算法的時間復雜度與采用的存儲結構有關
以鄰接矩陣做圖的存儲結構時,深度優先遍歷的時間復雜度為O(n2)
以鄰接表做圖的存儲結構時,深度優先遍歷的時間復雜度為O(n+e)。

廣度優先搜索BFS

BFS的過程接近層序遍歷。

算法思想:
①從圖中的某個頂點v出發,訪問此頂點;
②依次訪問v的所有未被訪問過的鄰接點,之后按這些鄰接點被訪問的先后次序依次訪問它們的鄰接點,直至圖中所有和v有路徑相通的頂點都被訪問到;
③若此時圖中尚有頂點未被訪問,則另選圖中一個未曾被訪問的頂點作起始點,重復上述過程,直至圖中所有頂點都被訪問到為止。

void BFSTraverse(Graph G,Status(*Visit)(int v)) 
{ //對圖G進行廣度優先遍歷
   for(v=0;v<G.vexnum;++v)  visited[v] = FALSE;  
   InitQueue(Q);
   for( v=0;v<G.vexnum;++v )
	if(!visited[v]) //v沒有被訪問 
	{ visited[v]=TRUE;  	Visit(v);      //訪問v
      EnQueue(Q,v);   //v入隊列
	  while(!QueueEmpty(Q))
	  {  DeQueue(Q,u); //隊頭元素u出隊列
	     for(w=FirstAdjVex(G,u); w>=0; w=NextAdjVex(G,u,w)) 
		if(!Visited[w])  
		{  visited[w]=TRUE; Visit(w); //訪問w
		   EnQueue(Q,w); //w入隊列
		} //if
	  } //while
     } //if
} //BFSTraverse 

利用圖的遍歷算法來得到生成樹或生成森林
對無向連通圖G進行遍歷時,由遍歷過程中歷經的邊的集合和所有頂點的集合一起構成了G的一棵生成樹。
由深度優先遍歷得到的為深度優先生成樹
由廣度優先遍歷得到的為廣度優先生成樹
對於非連通圖,它的每個連通分量的生成樹就組成了非連通圖的生成森林

圖的遍歷算法也可以用來判斷圖的連通性。
對於無向圖來說,若無向圖是連通的,則從任一結點出發, 僅需一次遍歷就能夠訪問圖中的所有頂點;若無向圖是非連通的,則從某一個頂點出發,一次遍歷只能訪問到該頂點所在連通分量的所有頂點,而對於圖中其他連通分量的頂點,則無法通過這次遍歷訪問。對於有向圖來說,若從初始點到圖中的每個頂點都有路徑,則能夠訪問到圖中的所有頂點,否則不能訪問到所有頂點。
故在BFSTraverse ()或DFSTraverse ()中添加了第二個for循環,再選取初始點,繼續進行遍歷,以防止一次無法遍歷圖的所有頂點。對於無向圖,上述兩個函數調用BFS (G,i)或DFS(G,i)的次數等於該圖的連通分量數;而對於有向圖則不是這樣,因為一個連通的有向圖分為強連通的和非強連通的,它的連通子圖也分為強連通分量和非強連通分量,非強連通分量一次調用BFS (G, i)或DFS (G, i)無法訪問到該連通分量的所有頂點。

最小生成樹

一個連通圖的生成樹是一個極小的連通子圖,它含有圖中全部的頂點,但只有足以構成一棵樹的n − 1 n-1n−1條邊,若砍去它的一條邊,則會使生成樹變成非連通圖;若給它增加一條邊,則會形成圖中的一條回路。
對於一個帶權連通無向圖G = ( V , E ) G=(V, E)G=(V,E),生成樹不同,其中邊的權值之和最小的那棵生成樹(構造連通網的最小代價生成樹),稱為G的最小生成樹(Minimum-Spanning-Tree, MST)。

GENERIC_MST(G){
	T=NULL;
	while T 未形成一棵生成樹;
		do 找到一條最小代價邊(u, v)並且加入T后不會產生回路;
			T=T U (u, v);
}

應用MST性質構造最小生成樹的算法:
Prim算法
Kruscal算法

可參考:
https://zhuanlan.zhihu.com/p/136387766

Prim算法(普里姆算法)

設N=(V,{E})是連通網,T=(V,{TE})表示N的最小生成樹,TE為最小生成樹的邊集,初始為空集。則Prim算法的執行過程:
Step1:令U={u},u∈V(u是網中任意一個頂點),TE={};
Step2:在u∈U,v∈V-U的邊(u,v)∈E中尋找一條代價最小的邊(u,v)並入TE,同時將頂點v並入U;
Step3:重復Step2,直至U=V,此時TE中必有n-1條邊,而T={V,{TE}}是N的一棵最小生成樹。

通俗點說就是:從一個頂點出發,在保證不形成回路的前提下,每找到並添加一條最短的邊,就把當前形成的連通分量當做一個整體或者一個點看待,然后重復“找最短的邊並添加”的操作。

為實現普里姆算法,需要附設一個輔助數組closedge,以記錄從U到V-U集合具有最小代價的邊。對每一個v∈V-U(設其在圖中的位置為i),它在數組中存在一個相應分量closedge[i]記錄一個頂點是v另一頂點是U中頂點的邊中代價最小的邊,closedge[i].lowcost域存儲該邊上的權值;closedge[i].adjvex域存儲該邊依附在U集合中的頂點

struct
    {
	ElemType  adjvex;  
   	VRType lowcost;     
    }closedge[MAX_VERTEX_NUM]; 

Prim算法分析:
時間復雜度:O(n2) , 只與頂點數有關, 與網中的邊數無關
適用於求邊稠密的網的最小生成樹

Kruscal算法(克魯斯卡爾算法)

假設連通網N=(V,{E}),T=(V,{TE})表示N的最小生成樹,TE為最小生成樹上邊的集合。初始時令TE為空集。
Step1:令最小生成樹T的初態為只有n個頂點的非連通圖T=(V,{TE}),TE={}。
Step2:從權值最小的邊(u,v)開始,若該邊依附的兩個頂點落在T的不同連通分量上,則將此邊加入到TE中,即TE=TE∪(u,v),否則舍棄此邊,選擇下一條代價最小的邊。
Step3:重復Step2,直至TE所有頂點在同一連通分量上。此時T=(V,{TE})就是N的一棵最小生成樹。

與Prim算法從頂點開始擴展最小生成樹不同,Kruskal 算法是一種按權值的遞增次序選擇合適的邊來構造最小生成樹的方法。

Kruscal算法分析:
時間復雜度:O(e*loge),只與邊數有關, 與網中的頂點數無關
適合於求邊稀疏的網的最小生成樹

Prim算法
Kruscal算法

拓撲排序

基本概念和知識:

AOV(Activity On Vertex)網:有向圖可用來描述一項工程或系統的完成過程。在這種圖中,頂點表示活動,有向弧表示活動之間的優先關系,如
<vi,vj>表示活動vi必須先於活動vj進行。其中vi是vj的直接前驅,vj是vi的直接后繼。若從頂點vi到vk有一條路徑,則vi是vk的前驅、vk是vi的后繼;

有向無環圖(DAG圖):無環的有向圖
對於DAG圖:
描述工程和系統的進行過程
工程能否順利進行--->拓撲排序
估算工程完成必須的最短時間--->有向圖的關鍵路徑問題

有向圖中檢測是否存在環的辦法:
深度優先遍歷
對有向圖進行拓撲排序,若網中所有頂點都在它的拓撲有序序列中,則不存在環。

偏序:若集合 X 上的關系R是傳遞的、自反的、反對稱的,則稱R是集合X上的偏序關系。可指集合中部分成員之間可比較。
全序:若關系R 是集合X 上的偏序關系,如果對於屬於X的每個x,y,必有xRy 或yRx ,則稱R是集合X上的全序關系。可指集合中全部成員之間可比較。
拓撲排序:由集合上的偏序得到該集合上的全序的操作。這個全序被稱為拓撲有序。

拓撲
拓撲排序步驟:
Step1:在有向圖中選一個無前驅的頂點輸出之;
Step2:從有向圖中刪去此頂點及所有以它為尾的弧;
Step3:重復前2步,直到圖中頂點全部輸出,此時圖中無環;或圖不空但找不到無前驅的頂點,此時圖中有環。
拓撲排序算法還是求關鍵路徑的基礎。

僅就邏輯結構:拓撲序列可能不唯一;
但若給定物理結構,按算法得到的拓撲序列唯一。

拓撲排序算法中的圖采用鄰接表作為存儲結構。 輔助數組Indegree[]:記錄每個頂點的入度 輔助結構:暫存入度為零的頂點以避免重復檢測
如棧,隊列,甚至是線性表

 Status TopologicalSort(ALGraph G)    
{  FindIndegree(G,indegree);   
   InitStack(S);  //用到第3章中棧的基本操作 
   for(i=0;i<G.vexnum;++i)   if (!indegree[i])  Push(S,i); 
   count=0;        //對輸出頂點計數 
   while(!StackEmpty(S))
   {  Pop(S,i);    printf(i,G.vertices[i].data); 
      ++count;                  //輸出頂點數加1
      for(p=G.vertices[i].firstarc; p; p=p->nextarc)
      { k=p->adjvex;  if(!(--indegree[k]))  Push(S,k); }
    }//while 
   if(count<G.vexnum)  return ERROR;   
   else  return OK;            //無回路     
}// TopologicalSort 

時間復雜度為O(n+e)

關鍵路徑

AOE-網(Activity On Edge):一個有向無環網,頂點表示事件,弧表示活動,弧上權值表示活動持續的時間。
通常用來估算工程完成時間
路徑長度:AOE網中路徑上各活動持續時間之和。
關鍵路徑:從源點到匯點路徑長度最長的路徑。

設活動ai在有向邊<j,k>上,有:
活動ai的最早開始時間e(i):是從源點v0到vj的最長路徑長度。
活動ai的最遲開始時間l(i):是不推遲工程完成的前提下,該活動允許的最遲開始時間。
e(i)= Ve(j)
l(i)=Vl(k)-dut(<j,k>)
活動ai時間余量:l(i)-e(i)
關鍵活動:l(i)=e(i)的活動。
關鍵路徑上的活動都是關鍵活動
在這里插入圖片描述

關鍵路徑
設有向無環圖G的有向邊<j,k>上:
事件vk的最早發生時間Ve(k)=從源點v0到vk的最長路徑長度
Ve(0)=0;
Ve(k)=Max{Ve(j)+dut(<j,k>), <j,k>∈T,所有j}
在這里插入圖片描述在這里插入圖片描述
事件vj的最遲開始時間Vl(j):保證匯點vn-1在Ve(n-1)時刻完成的前提下,事件vj最遲允許開始的時間。
Vl(n-1) = Ve(n-1)=從源點到匯點的最長路徑長度;
Vl(j)=Min{Vl(k)-dut(<j,k>), <j,k>∈T,所有k}

關鍵路徑

影響關鍵活動的因素是多方面的,任何一項活動持續時間的改變都會影響關鍵路徑的改變
關鍵活動的速度提高是有限度的,只有在不改變網的關鍵路徑的情況下,提高關鍵活動的速度才有效
關鍵路徑可有多條
若網中有幾條關鍵路徑,則單提高某一條關鍵路徑上的關鍵活動的速度並不能導致整個工程縮短工期,必須提高同時在幾條關鍵路徑上的活動的速度才能使整個工程縮短工期

最短路徑

最短路徑問題:從圖中某一頂點到達另一頂點的路徑可能不止一條,求其中一條路徑使得沿此路徑上各弧上的權值總和最小。稱路徑的第一個頂點為源點,最后一個頂點為終點。

參考博客:
https://www.cnblogs.com/Braveliu/p/3458671.html

查找

本部分討論兩類不同的查找表:靜態查找表和動態查找表,給出在不同查找表上進行查找的不同算法和性能分析以及動態查找表的創建方法。

三大查找方法
順序查找,二分法查找(折半查找),分塊查找

基本術語

查找表:為了便於查找而專門設置的一種數據結構,是由同一類型的用於查找的數據元素(或記錄)構成的集合 。
在計算機中進行查找的方法與采用的數據結構有關

對查找表經常進行的操作:

  1. 查詢某個“特定的”數據元素是否在查找表中;
  2. 檢索某個“特定的”數據元素的各種屬性;
  3. 在查找表中插入一個數據元素;
  4. 從查找表中刪除某個數據元素。
    靜態查找表: 僅作上述1)和2)操作的查找表
    動態查找表: 可作上述1)、2)、3)、4)操作的查找表

關鍵字:數據元素中某個數據項的值,用以標識一個數據元素
主關鍵字:可以唯一地標識一個記錄的關鍵字
次關鍵字:能識別若干記錄
查找:根據給定值,在查找表中確定一個其關鍵字等於給定值的數據元素或記錄.
查找成功:表中存在這樣的記錄,則給出該記錄信息或指示該記錄在表中的位置
查找不成功:查找表中不存在這一記錄,給出“空記錄”或“空指針”。

查找算法的性能分析:通常以關鍵字和給定值進行比較的記錄個數的平均值為衡量算法好壞的依據.
查找成功的平均查找長度(ASL)(Average Search Length):查找成功時為確定記錄在查找表中的位置,需和給定值進行比較的關鍵字個數的期望值
Pi: 查找表中第i個記錄的概率,有
Ci:找到表中其關鍵字與給定值相等的第i個記錄時,和給定值比較過的關鍵字的個數.則:
ASL

查找不成功平均查找長度: 確定查找不成功時和給定值進行比較的關鍵字個數的期望值稱為在查找不成功時平均查找長度。
平均查找長度(Average Search Length):查找算法在查找成功時平均查找長度和查找不成功時平均查找長度之和。

靜態查找表的查找

線性表適於表示靜態查找表

線性表通常有兩種存儲方式
順序存儲結構
鏈式存儲結構

可以順序表或線性鏈表表示靜態查找表

基於線性表的查找包括:
順序查找: 適用於兩種存儲結構
有序表的折半查找
索引順序表的分塊查找

順序查找

無序查找算法

從表中第一條/最后一條記錄開始,逐個進行記錄的關鍵字與給定值的比較,若某個記錄的關鍵字和給定值比較相等,則查找成功,返回其在順序表中的位序;反之,若直至最后一條/第一條記錄其關鍵字和給定值比較都不等,則查找不成功,返回0。

輔助信息
監視哨:為能自動檢驗數組下標越界,在0下標處設置哨兵,若查找不成功,則循環會在0下標處自動終止,函數返回0。

 int  Search_Seq( SSTable  ST,  KeyType key )
 {  ST.elem[0]. key= key; //0下標為監視哨
    for(i=ST.length;!EQ(ST.elem[i].key,key ); --i);           
    return i;              
 }//Search_Seq 

在這里插入圖片描述在這里插入圖片描述
無論給定什么關鍵字,順序查找不成功時和給定值進行比較的關鍵字個數均為n+1.
順序查找的缺點:平均查找長度大,n越大,效率越低。
順序查找的優點:算法簡單;適用面廣;不關心記錄是否有序。
順序查找的時間復雜度為O(n)。

折半查找

元素必須是有序的,如果是無序的則要先進行排序操作
折半查找(二分查找):先確定待查記錄所在范圍,逐步縮小范圍,直到找到或找不到該記錄止。
應用范圍:有序順序表。
例: 查找 key = 77 的結點所在的數組元素下標,設指針low和high分別指示待查元素所在范圍的上界和下界,指針mid指示區間的中間位置,即mid=(low+high)/2
折半
折半查找算法

int  Search_Bin ( SSTable  ST,  KeyType key ){
    low = 1 ; high = ST.length ;
    while ( low <= high ) {      
      mid = ( low + ligh ) / 2 ; 
      if  ( EQ(ST.elem[mid]. key, key )  return mid ;
      else if (LT(key,ST.elem[mid].key)) high = mid -1 ;
      else low = mid + 1; 
    }
    return 0 ;
 }  

最壞情況下,關鍵詞比較次數為log2(n+1),且期望時間復雜度為O(log2n);

折半查找的前提條件是需要有序表順序存儲,對於靜態查找表,一次排序后不再變化,折半查找能得到不錯的效率。但對於需要頻繁執行插入或刪除操作的數據集來說,維護有序的排序會帶來不小的工作量,那就不建議使用。

//二分查找(折半查找),版本2
int BinarySearch1(int a[], int value, int n)
{
    int low, high, mid;
    low = 0;
    high = n-1;
    while(low<=high)
    {
        mid = (low+high)/2;
        if(a[mid]==value)
            return mid;
        if(a[mid]>value)
            high = mid-1;
        if(a[mid]<value)
            low = mid+1;
    }
    return -1;
}
 
//折半查找,遞歸版本
int BinarySearch2(int a[], int value, int low, int high)
{
    int mid = low+(high-low)/2;
    if(low > high)
        return -1;
    if(a[mid]==value)
        return mid;
    if(a[mid]>value)
        return BinarySearch2(a, value, low, mid-1);
    if(a[mid]<value)
        return BinarySearch2(a, value, mid+1, high);
}

來源:https://blog.csdn.net/sayhello_world/article/details/77200009

判定樹:用二叉樹描述折半查找過程,樹中每個結點表示一個記錄,用結點中的值為該記錄在表中的位置。每個非終端結點的左子樹表示的是在該結點位置的前半區進行折半查找的過程,其右子樹表示的是在該結點位置的后半區進行折半查找的過程。

查找成功和不成功時和給定值比較的關鍵字個數最多不超過樹的深度:log2n+1

一般情況下,表長為n的折半查找的判定樹的深度和含有n個結點的完全二叉樹的深度相同,設 n=2h-1 且查找概率相等,則折半查找成功的平均查找長度
ASLbs
折半查找優缺點:查找效率高,但僅適用於有序的順序表

分塊查找

索引順序查找(分塊查找):順序表+索引表組成
應用范圍:有序或分塊有序的順序表。
分塊有序:第二個子表中的所有記錄的關鍵字都大於第一個子表的最大關鍵字,依次類推。
分塊查找
查找性能分析:
設長度為n的表均勻分成b塊,每塊含有s個記錄,則b=n/s ,每塊查找概率1/b,塊中每個記錄的查找概率是1/s。則ASLbs=Lb+Lw, 其中:
Lb: 在索引表中確定所在塊的平均查找長度。
Lw :在塊中查找元素的平均查找長度。

分塊查找

動態查找表

二叉排序樹查找

二叉排序樹的定義:
采用鏈式結構存儲的樹表示動態查找表的特點:

鏈式結構適於進行插入和刪除操作;
樹結構本身的排序特性使得查找過程變得高效。

二叉排序樹(二叉查找樹,BST):
空樹或具有下列性質的二叉樹:

根的左子樹若非空,則左子樹上所有結點的關鍵字值均小於根結點的關鍵字值;
根的右子樹若非空,則右子樹上所有結點的關鍵字值均大於根結點的關鍵字值; 它的左右子樹同樣是二叉排序樹。

二叉排序樹的查找過程:
若二叉排序樹為空,則查找失敗,返回空指針;
若二叉排序樹不空,首先將給定值和根結點的關鍵字比較,若相等則查找成功,返回根結點的地址。
若給定值小於根結點的關鍵字值,則在其左子樹上繼續查找;
若給定值大於根結點的關鍵字值,則在其右子樹上繼續查找。

二叉排序樹查找

BiTree SearchBST(BiTree T, KeyType key)
{ if((!T) || EQ(key,T->data.key)) return T;
  if(LT(key,T->data.key)) 
    return SearchBST(T->lchild,key); 
  else return SearchBST(T->rchild,key);
} //SearchBST 

中序遍歷二叉排序樹可得到一個關鍵字的有序序列。
因此一個無序序列可通過構造一棵二叉排序樹而變為一個有序序列

構造BST的過程
查找失敗時元素不斷插入的過程。
二叉排序樹的插入
當樹中不存在關鍵字等於給定值的結點時插入,新插入的結點一定是新添加的葉子結點,並且是查找不成功時查找路徑上訪問的最后一個結點的孩子。

所有新插入的結點都是二叉排序樹上的葉子,因此進行插入時,不必移動其他結點,僅需改動某個結點的指針即可。這相當於在有序序列上插入一個記錄而不需移動其他記錄。

插入算法:
首先進行查找,查找失敗時記錄查找路徑上訪問的最后一個結點(待插入結點的雙親結點)
生成待插入結點,判斷它是其雙親的哪個孩子,將它作為葉子結點插入
若二叉樹為空,則首先單獨生成根結點

Status SearchBST(BiTree T, KeyType key, BiTree f, BiTree &p)
{  //修改后的查找算法
   if(!T)  {  p=f;  return FALSE;  } 
   else if EQ(key,T->data.key)     
     {  p=T;   return TRUE;  }
   else if LT(key,T->data.key)          
	   return SearchBST(T->lchild,key,T,p);    
   else  return SearchBST(T->rchild,key,T,p); 
}// SearchBST 
//插入
Status InsertBST(BiTree &T, ElemType e){
   if(!SearchBST(T,e.key,NULL,p))       
   {
     s=(BiTree)malloc(sizeof(BiTNode));             
     if(!s)   exit(OVERFLOW);
     s->data =e;     s->lchild= s->rchild=NULL; 
     if(!p) T=s;                   
     else if LT(e.key, p->data.key)   p->lchild=s;
     else   p->rchild=s;                 
     return TRUE;
   }
   else return FALSE;                    
} 

二叉排序樹的刪除
 在刪除二叉排序樹上某個結點之后,仍然保持二叉排序樹的特性,即:二叉排序樹中任一結點x,其左(右)子樹中任一結點y(若存在)的關鍵字必小(大)於x的關鍵字。
 

刪除結點有三種情況
  1.被刪除的結點是葉子
  2.被刪除的結點只有左子樹或者只有右子樹
  3.被刪除的結點既有左子樹,也有右子樹

被刪除的結點*p是葉子
由於要刪除的結點p即無左子樹,又無右子樹,
因此刪除結點p之后不會破壞二叉排序樹結構的完整性,
只要將其雙親結點f原來指向p的指針改為指向空即可

f->lchild=NULL;
free( p );

被刪除的結點*p只有左子樹或者只有右子樹
要刪除的結點p只有左子樹PL或者右子樹PR,
這時候只要將p的左子樹PL或p的右子樹PR
直接作為其雙親結點f 的相應左子樹或右子樹即可

f->lchild=p->lchild;(p只有左子樹)
f->lchild=p->rchild;(
p只有右子樹)

被刪除的結點*p既有左子樹,也有右子樹。

令S是刪除前中序遍歷序列中被刪結點*p的直接前驅結點,令指針s指向它 。

第一種處理方法:

f->lchild=p->lchild;
s->rchild=p->rchild;

第二種處理方法:

p->data=s->data;
q->rchild=s->lchild;
free(s);

查找成功的情況:
查找走過一條從根結點到該結點的路徑,與關鍵字比較次數等於路徑長度+1,總的比較次數不超過樹的深度
查找不成功的情況:
查找走過一條從根結點到葉子結點的路徑,與關鍵字比較次數等於路徑長度+1,總比較次數不超過樹的深度

二叉排序樹的平均查找長度分析:
最壞情況:當插入關鍵字有序,二叉排序樹蛻變成單支樹,深度為n,ASL=(n+1)/2
最好情況:二叉排序樹與折半查找判定樹相同,ASL與log2n成正比。
(最好的情況下能達到折半查找的效率)

在等概率情況下,完全二叉樹的查找效率最高。在隨機的情況下,二叉排序樹的平均查找長度和logn是等數量級的

平衡二叉排序樹

基本術語:
平衡二叉樹(AVL樹):
它或者是一棵空樹,或者是滿足下列性質的二叉樹:
(1)其左、右子樹深度之差的絕對值不大於1
(2)其左、右子樹都是平衡二叉樹
結點的平衡因子BF=該結點的左子樹深度-右子樹深度
平衡二叉樹上結點的平衡因子取值={-1,0,+1}
二叉樹上有結點的平衡因子的絕對值大於1,則它就不是平衡的。
為什么引入平衡二叉樹:
若一棵二叉排序樹是平衡的,則樹上任何結點的左右子樹的深度之差不超過1,二叉樹的深度與log2n同數量級。可以保證它的平均查找長度與log2n同數量級
平衡二叉樹

構造平衡的二叉排序樹的方法是:
根據初始序列,從空樹開始插入新結點
在插入過程中,一旦有結點的平衡因子的絕對值大於1(失去平衡),則在保持二叉排序樹特性的前提下,采用平衡旋轉技術,對最小不平衡子樹(其左、右子樹均平衡,只有子樹的根結點不平衡)進行調整,使其平衡。

在插入過程中構造平衡的二叉排序樹
采用平衡旋轉技術,設在二叉排序樹上插入結點而失去平衡的最小子樹根結點指針為a,失去平衡的情況有四種。
旋轉操作的正確性證明:
保持二叉排序樹的性質
即中序遍歷所得關鍵字序列自小到大有序
為什么只對最小不平衡子樹調整?
經旋轉處理的子樹深度與插入之前相同,不影響插入路徑上所有祖先的平衡度

LL型(右單旋轉):
由於在A的左孩子的左子樹上插入結點,A的平衡因子由+1變為+2,需進行一次向右的順時針旋轉操作
LL
RR型(左單旋轉):
由於在A的右孩子的右子樹上插入結點,A的平衡因子由-1變為-2,需進行一次向左的逆時針旋轉操作。
RRLR型(先左后右雙旋轉):
由於在A的左孩子的右子樹上插入結點,A的平衡因子由1變為2,需進行兩次(先左后右)的旋轉操作
LRRL型(先右后左雙旋轉):
由於在A的右孩子的左子樹上插入結點,A的平衡因子由-1變為-2,需進行兩次(先右后左)的旋轉操作。
RL

B-樹與B+樹

B-B-樹也稱B樹。

所有的葉子結點都出現在同一層上,不帶信息(可看成是外部結點或查找失敗的出口,實際並不存在)。

B

從根開始查找,如果 Ki = KEY 則查找成功。
若 Ki < KEY < Ki+1; 查找 Ai 指向的結點
若 KEY < K1; 查找 A0 指向的結點
若 KEY > Kn; 查找 An指向的結點
若 找到葉子,則查找失敗。

B-樹的插入
B-樹的生成從空樹開始,在查找的基礎上逐個插入關鍵字而得。
由於B-樹結點中的關鍵字個數必須≥m/2-1,每次插入一個關鍵字不是在樹中增加一個葉子結點,而是在最底層的某個非終端結點中添加一個關鍵字。此時有可能打破B-樹對結點個數上限的要求,有2種情況:
1、若插入前該結點的關鍵字個數<m-1
2、若插入前該結點的關鍵字個數=m-1

1、若插入前該結點的關鍵字個數<m-1
直接將關鍵字和其它信息按序插入到該結點中。
在這里插入圖片描述2、若插入前該結點的關鍵字個數=m-1,則插入后>m-1
在這里插入圖片描述
在這里插入圖片描述
B-樹的刪除
在B-樹刪除一個關鍵字,首先必須找到待刪關鍵字所在結點,從中刪除之。
若該結點為最下層的非終端結點,且刪除前其關鍵字個數不少於m/2 ,則刪除完成;否則要進行結點的合並。
若該結點不是最底層的非終端結點,被刪關鍵字是其第i個關鍵字,則將Ai-1或Ai所指子樹中的最大(或最小)關鍵字Y移上來代替Ki,然后在相應的結點中刪去Y。

1)刪除前被刪關鍵字所在結點關鍵字數目≥m/2
直接刪除該關鍵字Ki和Ai即可。
2)刪除前被刪關鍵字所在結點關鍵字數目=(m/2)-1 ,而與該結點相鄰的右兄弟(左兄弟)結點中關鍵字數目大於(m/2)-1
將該兄弟結點中最小(最大)關鍵字上移至雙親結點
雙親結點中小於(或大於)且緊靠該上移關鍵字的關鍵字下移至被刪關鍵字所在結點。
3)被刪關鍵字所在結點和其相鄰的兄弟結點中的關鍵字數目均等於(m/2)-1,設該結點有右兄弟且其右兄弟結點地址由雙親結點中的指針Ai指示
刪去關鍵字后,它所在結點的剩余關鍵字和指針,加上雙親結點的關鍵字Ki一起,合並到Ai所指兄弟結點中。
如果因此使得雙親結點的關鍵字個數小於m/2-1,則依次類推作相應處理,即合並操作有可能向上傳遞。結點合並的極端情況是使樹的高度減少1。

在這里插入圖片描述在這里插入圖片描述B+樹:
B+在這里插入圖片描述

散列表

基礎術語:

已講查找表的共同點

記錄關鍵字的值和其在表中的位置之間無確定關系
查找過程是給定值依次和關鍵字集合中各關鍵字的比較
查找效率取決於和給定值進行比較的關鍵字個數。

理想的查找表:不經過比較,一次存取就能得到所要查找的記錄,即根據記錄的關鍵字就能確定其存儲位置
實現途徑:在記錄的關鍵字和其在表中的存儲位置之間建立一種確定的、一對一的關系

散列(Hash)函數
在記錄的關鍵字和其在表中位置之間建立的一種函數關系,即以f(key)作為關鍵字為key的記錄在表中的存儲位置。

散列(Hash)函數是一個映象
將關鍵字的集合映射到某地址集合
通常地址集的大小由關鍵字的個數決定

沖突:不同關鍵字得到同一散列地址,即:
key1key2,而f(key1)=f(key2)
同義詞:在一個散列函數中具有相同函數值的不同關鍵字。

一般情況下,關鍵字的取值范圍比較大,而關鍵字的個數比較小。因此散列函數具有“壓縮”功能。
由於散列函數通常是一個壓縮映像,因此不可避免地會產生“沖突”現象

改進散列函數只能減少沖突,而不能絕對避免沖突
選定處理沖突的方法

散列(Hash)表:根據設定的散列函數H(key)和所選中的處理沖突的方法,將一組關鍵字映象到一個有限的、地址連續的地址集(區間)上,並以關鍵字在地址集中的“像”作為相應記錄在表中的存儲位置,這種表被稱為散列表(哈希表)。
散列(Hash)造表或散列:映象過程
散列(Hash)地址:關鍵字的存儲位置

設計Hash表的步驟
1.考慮選擇一個“好”的、均勻的散列函數
2.選擇一種處理沖突的方法

直接定址法:
取關鍵字或關鍵字的某個線性函數值為散列地址。即H(key)=key或H(key)=a×key+b(a,b為常數)。這種函數也叫自身函數。
特點:
直接定址所得地址集合和關鍵字集合的大小相同。因此不同關鍵字不會發生沖突,但在實際中使用很少。

數字分析法:
取關鍵字分布均勻的若干位或組合作散列地址
特點:
適於關鍵字位數較多而關鍵字個數較少的情況
且關鍵字已知

平方取中法
取關鍵字平方后的中間幾位為散列地址。
依據:
1)通過“平方”擴大差別;
2)平方值的中間幾位受到關鍵字中每一位的影響
特點:
適於無法預知全部關鍵字情況,或關鍵字的每一位都有某些數字重復出現頻度很高
在這里插入圖片描述折疊法:
將關鍵字分割成位數相同的幾部分,取這幾部分的疊加和為散列地址。
有:移位疊加和間界疊加兩種
特點:
適於關鍵字位數很多,且每一位數字分布大致均勻
在這里插入圖片描述除留余數法
取關鍵字被某個不大於散列表長m的數p除后所得余數作為散列地址。即:
H(key) = key MOD p (p≤m)
p的選擇:一般p為≤m且接近m的質數或不含20以內質因數的合數。若p選不好易產生同義詞
特點:
簡單常用,也可與前面各方法結合使用

隨機數法
取關鍵字的隨機函數值為散列地址。即:
H(key)=Random(key)
特點:
當關鍵字長度不等時采用此法比較恰當。

采用散列函數需要考慮的因素
1.計算散列函數所需時間
2.關鍵字長度
3.散列表大小
4.關鍵字各位的分布
5.記錄的查找頻率

處理沖突:
在這里插入圖片描述1.開放定址法:
在這里插入圖片描述
在這里插入圖片描述
2.再哈希法:在這里插入圖片描述
3.鏈地址法
將所有關鍵字為同義詞的記錄存儲在同一單鏈表中。每個地址的鏈表的頭指針組織成一個向量。
4.公共溢出區法:
HashTable[0..m-1]:基本表,每個分量存放一個記錄。
OverTable[0..v]:溢出表,所有關鍵字和基本表中關鍵字為同義詞的記錄,一旦發生沖突, 均填入溢出表。

散列造表的過程
在查找失敗時在失敗位置插入元素
散列表的查找:
查找過程和造表過程一致。給定K值,根據造表時設定的散列函數求得散列地址,若表中此位置上沒有元素,則查找不成功;否則比較關鍵字,若和給定值相等,則查找成功;否則根據造表時設定的處理沖突的方法找“下一地址”,直至散列表某個位置為“空”或者表中所填元素的關鍵字等於給定值時為止。
查找不成功:
若r[i] == NULL
查找成功:若r[i].key == K

分析:
雖然散列表在關鍵字與記錄的存儲位置之間建立了直接映象,但是由於沖突的存在,散列表的查找過程仍是一個給定值和關鍵字進行比較的過程,因此仍可以使用ASL作為散列表查找效率的量度。
查找過程中需和給定值比較的關鍵字個數取決於三個因素:
選用的哈希函數;
選用的處理沖突的方法;
散列表的裝填因子α=n/m
其中
n:表中填入的記錄數;
m:散列表長
(α越大,發生沖突的可能性越大 )
在這里插入圖片描述在這里插入圖片描述

ASL計算:
可參考:
https://www.cnblogs.com/ygsworld/p/10238729.html

排序

排序
八大排序

圖片來源:https://www.cnblogs.com/hokky/p/8529042.html
在這里插入圖片描述

直接插入排序

直接插入排序的核心思想就是:
將數組中的所有元素依次跟前面已經排好的元素相比較,如果選擇的元素比已排序的元素小,則交換,直到全部元素都比較過。
因此,從上面的描述中我們可以發現,直接插入排序可以用兩個循環完成:

第一層循環:遍歷待比較的所有數組元素
第二層循環:將本輪選擇的元素(selected)與已經排好序的元素(ordered)相比較。
如果:selected > ordered,那么將二者交換

我們在這里再次運用到了前面使用過的 “監視哨” 概念

監視哨:為能自動檢驗數組下標越界,在0下標處設置哨兵,若查找不成功,則循環會在0下標處自動終止,函數返回0。
監視哨的意義是防止下標越界,提高速度

在這里插入圖片描述
使用監視哨版的直接插入算法:
在這里插入圖片描述
未使用監視哨:

void print(int a[], int n ,int i){
	cout<<i <<":";
	for(int j= 0; j<8; j++){
		cout<<a[j] <<" ";
	}
	cout<<endl;
}
 
 
void InsertSort(int a[], int n)
{
	for(int i= 1; i<n; i++){
		if(a[i] < a[i-1]){           //若第i個元素大於i-1元素,直接插入。
		                             //小於的話,移動有序表后插入
			int j= i-1;	
			int x = a[i];		 //復制為哨兵,即存儲待排序元素
			a[i] = a[i-1];           //先后移一個元素
			while(x < a[j]){	 //查找在有序表的插入位置
				a[j+1] = a[j];
				j--;		 //元素后移
			}
			a[j+1] = x;		 //插入到正確位置
		}
		print(a,n,i);			//打印每趟排序的結果
	}
	
}
 
int main(){
	int a[8] = {3,1,5,7,2,4,9,6};
	InsertSort(a,8);
	print(a,8,8);
}

直接插入排序算法的性能分析
空間效率:
僅使用了常數個輔助單元,因而空間復雜度為O(1).
時間效率:
在排序過程中,向有序子表中逐個地插入元素的操作進行了nー1趟,每趟操作都分為比較關鍵字和移動元素,而比較次數和移動次數取決於待排序表的初始狀態。
在最好情況下,表中元素已經有序,此時每插入一個元素,都只需比較一次而不用移動元素,因而時間復雜度為O(n).
在最壞情況下,表中元素順序剛好與排序結果中的元素順序相反(逆序),總的比較次數達到最大,為∑i,總的移動次數也達到最大,為∑(i+1)
平均情況下,考慮待排序表中元素是隨機的,此時可以取上述最好與最壞情況的平均值作為平均情況下的時間復雜度,總的比較次數與總的移動次數均約為n2/4。因此,直接插入排序算法的時間復雜度為O(n2).
穩定性:由於每次插入元素時總是從后向前先比較再移動,所以不會出現相同元素相對位置發生變化的情況,即直接插入排序是一個穩定的排序方法。
適用性:直接插入排序算法適用於順序存儲和鏈式存儲的線性表。為鏈式存儲時,可以從前往后查找指定元素的位置。

希爾排序(插入排序)

希爾排序又叫縮小增量排序
基本思想:

先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。

操作方法:

選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1; 按增量序列個數k,對序列進行k 趟排序;
每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1時,整個序列作為一個表來處理,表長度即為整個序列的長度。

希爾排序在這里插入圖片描述
版本2:
來源:https://blog.csdn.net/Void_leng/article/details/87812430

//C語言
void InsertSortWithgap(int arr[], int size, int gap)
{
	assert(arr);
	int i;
	int j;
	int temp;
	for (i = gap; i < size; ++i)//從下標為gap的元素作為帶插入元素
	{
		temp = arr[i];//存儲gap下標的元素
		for (j = i - gap; j >= 0; j -= gap)//從第 0 個元素進行比較
		{
			if (temp > arr[j])//降序
			{
				arr[j + gap] = arr[j];//gap位置的元素換成 j 所在下標的元素
			}
			else
			{
				break;
			}
		}
		arr[j + gap] = temp;//j 所在下標位置存儲  temp
	}
}
	//希爾
void ShellSort(int arr[], int size)
{
	int gap = size;
	while (1)
	{
		gap = gap / 3 + 1;//改變gap 的值
		InsertSortWithgap(arr, size, gap);
		if (gap == 1)
		{
			break;	//gap的值為一時停止
		}
	}
}

希爾排序算法的性能分析
空間效率:
僅使用了常數個輔助單元,因而空間復雜度為O(1).
時間效率:
由於希爾排序的時間復雜度依賴於增量序列的函數,這涉及數學上尚未解決的難題,所以其時間復雜度分析比較困難。當n在某個特定范圍時,希爾排序的時間復雜度約為O(n^1.3)
在最壞情況下希爾排序的時間復雜度為O(n^2).
穩定性:
當相同關鍵字的記錄被划分到不同的子表時,可能會改變它們之間的相對次序,因此希爾排序是一種不穩定的排序方法。例如,圖8.2中49與49的相對次序已發生了變化。
適用性:
希爾排序算法僅適用於線性表為順序存儲的情況。

簡單選擇排序

選擇排序就是在待排序的數據中選擇一個最大的或者最小的放在帶待排序數據的末尾。也可以是已經排好序的數據的開頭。
在這里插入圖片描述簡單選擇排序算法的性能分析:
空間效率:
僅使用常數個輔助單元,故空間效率為O(1).
時間效率:
從上述偽碼中不難看出,在簡單選擇排序過程中,元素移動的操作次數很少,不會超過3(n-1)次,最好的情況是移動0次,此時對應的表已經有序;但元素間比較的次數與序列的初始狀態無關,始終是n(n-1)2次,因此時間復雜度始終是O(n^2)
穩定性:
在第i趟找到最小元素后,和第i個元素交換,可能會導致第個元素與其含有相同關鍵字元素的相對位置發生改變。例如,表L=(2,2,1},經過一趟排序后L={1,2,2},最終排序序列也是L={1,2,2},顯然,2與2的相對次序已發生變化。因此,簡單選擇排序是一種不穩定的排序方法

堆排序(選擇排序)

堆排序就要用到前面學到的數據結構二叉樹的知識了。堆的作用是什么呢?就是尋找最值。所以根節點肯定最大(大堆)或者最小(小堆)。我們把根節點和最后一個結點進行交換,數組最后一個數肯定就是最大或者最小的。最后又在剩下的數據里面建堆。 要注意的是升序建大堆,降序建小堆。

大頂堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

小頂堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

在這里插入圖片描述 堆排序的基本思想是:
將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就為最大值。然后將剩余n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反復執行,便能得到一個有序序列了
在這里插入圖片描述…………

快速排序

快速排序是對冒泡排序的改進,又稱划分交換排序。

基本思想:

在待排序序列中任選一個記錄作為樞軸,通過一趟排序將待排序記錄分割成獨立的兩部分,其中前一部分記錄的關鍵字均小於等於樞軸的關鍵字,后一部分記錄的關鍵字都大於等於樞軸的關鍵字;
分別對這兩個子序列按照同樣方法再進行快速排序(划分),直到分割出的每個子序列只包含一個記錄為止; 此時整個序列達到有序

一趟快排的過程:

選取待排序序列的第一個記錄作為樞軸(或稱支點),將其關鍵字暫存在變量pivotkey中;
設置兩個指針(實際上是整型數,指示數組下標)low和high,其初值位置分別為待排序序列的下界和上界位置;
先從high所指位置開始向左搜索,找到第一個關鍵字小於pivotkey的記錄,將它和樞軸記錄互換位置;
接着從low所指位置開始向右搜索,找到第一個關鍵字大於pivotkey的記錄,將它和樞軸記錄互換位置;
重復以上兩步,直至low=high為止。

一趟快速排序

int Partition(SqList &L,int low,int high)
 {  pivotkey=L.r[low].key;
    while(low<high)                             
    {     
       while(low<high && L.r[high].key>=pivotkey)  --high;
       L.r[low]<->L.r[high]; 
       while(low<high && L.r[low].key<=pivotkey)   ++low;
       L.r[low]<->L.r[high];
    }//while
    return low;       //返回樞軸所在位置 
 }// Partition

在這里插入圖片描述
改進的一趟快速排序

int Partition(SqList &L,int low,int high)
 {  L.r[0]= L.r[low];
    pivotkey=L.r[low].key;
    while(low<high) 
    {  while(low<high && L.r[high].key>=pivotkey)  --high;
       L.r[low]=L.r[high];               
       while(low<high && L.r[low].key<=pivotkey)   ++low;
       L.r[high]=L.r[low];               
    }//while
    L.r[low]=L.r[0];
    return low;                                       
  }// Partition 

快速排序

 void QSort ( SqList &L,int low, int  high )
{  if  (low < high)
  {  pivotloc=Partition(L,low,high); 
     QSort (L, low, pivotloc-1) ;    //對低端子序列遞歸排序
     QSort (L, pivotloc+1, high );   //對高端子序列遞歸排序
  }
}// QSort

void QuickSort ( SqList &L )
{ //對L指示的順序表進行快速排序
  QSort ( L, 1, L.length ); 
}  // QuickSort 

空間復雜度:O(logn),需要一個棧空間
若每趟排序都能將記錄序列均勻分割成長度相近的兩個子序列,則棧的最大深度為 (包括最外層參數進棧)
若每趟排序后樞軸都偏向子序列的一端,則棧的最大深度為n。
改進:在一趟排序后比較分割出的2個子序列的長度,然后先對長度短的子序列進行下一趟快排,這樣棧的最大深度可降為O(logn)
穩定性:不穩定

時間復雜度:O(nlogn)
就平均時間而言,快速排序性能最好
若經過每一趟快速排序得到的兩個子序列的長度基本相等,則快速排序的時間效率最高: O(nlogn)
若初始序列按關鍵字有序或基本有序(正序或逆序),則樞軸總是子序列中關鍵字最小或最大的記錄,這樣每趟快排划分出的兩個子序列都有一個接近空序列,此時快速排序將蛻化為冒泡排序:O(n2)
在這里插入圖片描述

歸並查找


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM