1. 程式人生 > 實用技巧 >樹與二叉樹知識點總結(一)

樹與二叉樹知識點總結(一)

樹和二叉樹

樹的定義

  樹是\(N(N>0)\)個結點的有限集合,\(N=0\)時,稱為空樹,這是一種特殊情況,在任意一顆非空樹中應滿足:

  1. 有且僅有一個特定的稱為根的結點
  2. \(N>1\)時,其餘結點可分為\(m(m>0)\)個互不相交的有限集合\(T_1,T_2,……,T_m\),其中每個集合本身又是一棵樹,並且稱為根結點的子樹

通俗理解總結:

  • 有且僅有一個根節點
  • 根節點沒有前驅結點,其他節點有且只有一個前驅結點
  • 所有節點可以有零個或者多個後繼結點
  • 樹是一種遞迴的資料結構,適合於表示所有層次結構的資料

樹的基本術語

  • 結點關係

    • 祖先結點:根結點到該結點的唯一路徑的任意節點
    • 子孫結點
    • 雙親結點:根結點到該結點的唯一路徑上最接近該結點的結點
    • 孩子結點
    • 兄弟節點:具有相同雙親結點的結點
  • 樹的度:樹中所有結點的度數的最大值

  • 結點的層次、深度、高度

    • 層次:根為第一層、它的孩子為第二層,以此類推
    • 深度:根結點開始自頂向下累加
    • 高度:葉子結點開始自底向上累加
  • 樹的高度(深度)、路徑、路徑長度

    • 樹的高度(深度):樹中結點的最大層數

    • 路徑:又兩個結點之間所經過的結點序列構成的

    • 路徑長度:路徑上所經過的邊的個數

      由於樹種的分支是有向的,即從雙親指向孩子,所以數中的路徑是自上而下的,同一雙親的兩個孩子之間不存在路徑

樹的性質

  1. 樹中的結點數等於所有結點的度數加1。

證明:

  不難想象,除根結點以外,每個結點有且僅有一個指向它的前驅結點。也就是說每個結點和指向它的分支一一對應。
  假設樹中一共有\(b\)個分支,那麼除了根結點,整個樹就包含有\(b\)個結點,所以整個樹的結點數就是這\(b\)個結點加上根結點,設為\(n\),則\(n=b+1\)。而分支數\(b\)也就是所有結點的度數,證畢。

  1. 度為\(m\)的樹中第\(i\)層上至多結點樹如下

\[Node(max)=m^{i-1} (i>1) \]

證明:(數學歸納法)
  首先考慮\(i=1\)的情況:第一層只有根結點,即一個結點,\(i=1\)帶入式子滿足。
  假設第\(i-1\)

層滿足這個性質,第\(i-1\)層最多有\(m^{i-2}\)個結點,又因為樹的度為\(m\),所以對於第\(i-1\)層的每個結點,最多有\(m\)個孩子結點。所以第\(i\)層的結點數最多是\(i-1\)層的\(m\)倍,所以第\(i\)層上最多有\(m ^{i-1}\)個結點。

  1. 高度為\(h\)\(m\)叉樹至多的節點數如下

\[Node(max)=m^{h-1}+m^{h-1}+m^{h-1}+···+m+1=\frac{(m^h-1^)}{m-1} \]

  1. 具有\(n\)個結點的\(m\)叉樹的最小高度如下

\[high(min)=\lceil log_m{n(m-1)+1}\rceil \]

  1. 樹結點與度之間的關係有

\[TBranch=1n_1+2n_2+···+mn_2(度為m的結點引出m條分支) \]

\[CNode=n_0+n_1+n_2+···+n_m=總分支樹+1 \]

樹的儲存結構

順序儲存結構

  雙親表示法:用一組連續的儲存空間儲存樹的結點,同時在每個結點中,用一個變數儲存該結點的雙親結點在陣列中的位置。

如圖所示:

程式碼如下:

typedef char ElemType;
typedef struct TNode{
    ElemType data;  //節點資料
    int parent;  //該結點雙親在陣列中的下標
}Tnode;  //結點資料型別

#define MaxSize 100
typedef struct{
    TNode nodes[MaxSize];  //節點陣列
    int n;  //節點數量
}Tree;  //樹的雙親表示結構

優點:可以很快得到每個結點的雙親結點

缺點:求結點的孩子需要遍歷整個結構

鏈式儲存結構

孩子表示法:

把每個結點的孩子結點排列起來儲存成一個單鏈表。所以\(n\)個結點就有\(n\)個連結串列;
如果是葉子結點,那這個結點的孩子單鏈表就是空的;
然後\(n\)個單鏈表的的頭指標又儲存在一個順序表(陣列)中。

如圖所示:

程式碼如下:

typedef char ElemType;
typedef struct CNode{
    int child;  //該孩子在表頭陣列的下標
    struct CNode *next;  //指向該結點的下一個孩子結點
}CNode,*Child;  //孩子節點的資料型別

typedef struct{
    ElemType data;  //結點資料域
    Child firstchild;  //指向該結點的第一個孩子結點
}TNode;  //孩子結點的資料型別

優點:尋找子女非常直接

缺點:尋找雙親需要便利\(N\)個結點的孩子連結串列指標域所只想的\(N\)個孩子連結串列

孩子兄弟表示法:

  孩子兄弟表示法:顧名思義就是要儲存孩子和孩子結點的兄弟,具體來說,就是設定兩個指標,分別指向該結點的第一個孩子結點和這個孩子結點的右兄弟結點。

如圖所示:

程式碼如下:

typedef char ElemType;
typedef struct CSNode{
    ElemType data;  //結點資料域
    struct CSNode *firstchild,*rightsib;  //指向該結點的第一個孩子結點和該結點的右兄弟結點
}CSNode;  //孩子兄弟點的資料型別

優點:方便實現轉換為二叉樹,易於查詢結點的孩子

缺點:從當前結點查詢其雙親結點比較麻煩


二叉樹

二叉樹的定義

二叉樹是\(n(n≥0)\)個結點的有限集合:

  1. 或者為空二叉樹,即 \(n=0\)
  2. 或者由一個根結點和兩個互不相交的被稱為根的左子樹和右子樹組成。左子樹和右子樹又分別是一棵二叉樹。

通俗理解:

  每個結點至多有兩顆子樹,左右子樹的順序不能顛倒,二叉樹與度為2的有序樹不同,不同的原因是度為2的樹要求每個節點最多隻能有兩棵子樹,並且至少有一個節點有兩棵子樹。二叉樹的要求是度不超過2,節點最多有兩個叉,可以是1或者0。

二叉樹的五種基本形態

  • 空樹
  • 只有一個根結點
  • 根結點只有左子樹
  • 根結點只有右子樹
  • 根結點既有左子樹又有右子樹

擁有特殊形態的二叉樹

  • 斜樹:每個結點只有左結點或者每個結點只有右結點
  • 滿二叉樹:樹種每一層都含有最多的結點,對於編號\(i\)的結點,期雙親結點為\(\lfloor i/2\rfloor\)
  • 完全二叉樹:每一個結點都與高度為h的滿二叉樹編號\(1-n\)相同;如果\(i≤n/2\)下,則結點\(i\)為分支結點,否則為葉子結點
  • 二叉排序樹:左子樹均小於根結點,右子樹均大於根結點
  • 平衡二叉樹:左右子樹的深度之差不超過1

二叉樹的性質

  1. 非空二叉樹上的葉子結點數等於度為2的節點數加一,即 \(n_0=n_2+1\)
  2. 非空二叉樹上第\(k\)層上至多有\(2^{k-1}\)個結點\((k≥1)\)
  3. 高度為\(h\)的二叉樹至多有\(2^k - 1\)個結點\((h≥1)\)
  4. 具有\(n\)\((n>0)\)結點的完全二叉樹的高度為\(\lceil log_2{n+1}\rceil\)\(\lfloor log_2n\rfloor+1\)

二叉樹的儲存結構

順序儲存結構

  二叉樹的順序儲存結構就是用一組地址連續的儲存單元依次自上而下、自左至右儲存完全二叉樹上的結點元素。

如圖所示:

  優點:適合完全二叉樹和滿二叉樹,序號可以反映出結點之間的邏輯關係,可以節省空間
  缺點:適合一般二叉樹,只能新增一些空結點,空間利用率低

鏈式儲存結構

  二叉樹每個結點最多兩個孩子,所以設計二叉樹的結點結構時考慮兩個指標指向該結點的兩個孩子。

如圖所示:

程式碼如下:

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

二叉樹的遍歷

先序遍歷(\(NLR\))

過程:

  1. 訪問根結點
  2. 先序遍歷左子樹
  3. 先序遍歷右子樹

程式碼如下:

遞迴程式碼如下:

void PreOrder(BiTree T)
{
//先序遍歷演算法
if(T!=NULL){
	vist(T);  //訪問根結點,如:printf("%c",T->data);
	PreOrder(T->lchild);  //遞迴遍歷左子樹
	PreOrder(T->rchild);  ////遞迴遍歷右子樹
	}
}

非遞迴程式碼如下

void Preorder2(BiTree T){
//先序遍歷非遞迴演算法
InitStack(S);  //需要藉助一個遞迴棧
BiTree p=T;  //p是遍歷指標
	while(p||!IsEmpty(S)){  //棧不空或p不空時迴圈
		if(p){  //一路向左
			visit(p);  //訪問當前結點
			Push(S,p);  //入棧
			p=p->lchild;  //左孩子不空,一直向左走
			}
		else{  //出棧,並轉向出棧結點的右子樹,可改成if(!IsEmpty(S))
			Pop(S,p);  //棧頂元素出棧
			p=p->rchild;  //向右子樹走,p賦值為當前結點的右孩子
			} // 返回while迴圈繼續進入if-else語句
	}
}

中序遍歷(\(LNR\))

  1. 中序遍歷左子樹
  2. 訪問根結點
  3. 中序遍歷右子樹

遞迴程式碼如下:

void InOrder(BiTree T)
{
//先序遍歷演算法
if(T!=NULL){
    PreOrder(T->lchild);  //遞迴遍歷左子樹
    vist(T);  //訪問根結點,如:printf("%c",T->data);
    PreOrder(T->rchild);  ////遞迴遍歷右子樹
	}
}

非遞迴程式碼如下:

void Inorder2(BiTree T){
//中序遍歷非遞迴演算法
	InitStack(S);//需要藉助一個遞迴棧
	BiTree p=T;
	while(p||!IsEmpty(S)){  //棧不空或者P不空時迴圈
		if(p){
			Push(S,p);
			p=p->lchild;
			}
		else{
			Pop(S,p);
			visit(p);
			p=p->rchild;
			}
		}
	}

後序遍歷(\(LRN\))

  1. 後序遍歷左子樹
  2. 後序遍歷右子樹
  3. 訪問根結點

遞迴程式碼如下:

void PostOrder(BiTree T)
{
//先序遍歷演算法
if(T!=NULL){
    PreOrder(T->lchild);  //遞迴遍歷左子樹
    PreOrder(T->rchild);  ////遞迴遍歷右子樹
    vist(T);  //訪問根結點,如:printf("%c",T->data);    
	}
}

非遞迴程式碼如下(重難點!!!):

void PostOrder(BiTree T)){
    InitStack(S);
    BiTree p=T; //工作指標
    r=NULL;//指向最近訪問過的節點,輔助指標
    while(p||!IsEmpty(S)){
    	if(p){ 
            //1、從根結點到最左下角的左子樹都入棧
			Push(S,p);
			p=p->lchild;
			}
		else{  //返回棧頂的兩種情況
			GetTop(S,P);//彈出棧頂元素
			if(p->rchild&&p->rchild!=r){
				//1、右子樹存在且未訪問過,
				p=p->rchild;//轉右
				push(S,p); //壓入棧
				p=p->lchild;//走到最左
					}
			else{
                //2、右子樹已經訪問或空,接下來出棧訪問結點
				pop(S,p);  //將結點彈出
				visit(p->data);   //訪問該結點
                  r=p;  //指標訪問過的右子樹根結點
                  p=NULL;//訪問完之後就重置P,每次從棧中彈出一個,防止進入第一個if
				}
		}
	}
}

難點:要保證左孩子和右孩子都已被訪問並且左孩子在右孩子前訪問才能訪問根結點