二叉樹基礎知識全集(數據結構)

前言

普通樹的存儲、運算都存在一定的困難,所以我們想能不能另闢捷徑,我們來討論一棵簡單樹——二叉樹。

在這裏插入圖片描述


一.二叉樹的定義、特點和形態

1.二叉樹的定義

(形式化定義,與樹一樣也是遞歸定義的)

二叉樹是有限的節點集合——遞歸定義
① 這個集合或者是空。
②或者由一個根節點和兩棵互不相交的稱爲左子樹右子樹的二叉樹組成。(非空)

例如:
在這裏插入圖片描述

2.二叉樹的特點

(1) 結點的度小於等於2
(2)爲有序樹(子樹有序,不能顛倒)

(所以二叉樹也不算是普通樹的特例,因爲二叉樹本身的特點是它是一棵有序樹)

3.二叉樹的五種基本形態

在這裏插入圖片描述

二.二叉樹的性質(五種)

1.性質1:

在二叉樹的第i層上至多有 2的i-1次方 個結點
(證明用數學歸納法)

2.性質2:

深度爲k的二叉樹至多有 2的k次方-1個 結點
①每一層最少要有1個結點,因此,最少結點數爲 k。
②最多結點個數借助性質1:用求等比級數前k項和的公式:
2的0次方+2的1次方+2的2次方+…+2的k-1次方=2的k次方-1

(記住結論即可)

3.性質3:

對於任何一棵二叉樹,若度爲2的結點數有n2個,則葉子結點數n0(因爲葉子結點數度爲0,所以我們把它寫成n0)必定爲n2+1 (即n0=n2+1

(二叉樹除了度爲0、2的結點外還有度爲1的結點)

證明:若設度爲 1 的結點有 n1 個,總結點數爲n,總邊數爲B


在這裏插入圖片描述
從圖中我們可以看出,對於二叉樹來說,除了根結點是沒有邊伸向他的,其他的結點都有一條邊伸向他,所以我們得出:B = n −1。也就是每條邊都伸向一個結點。


在這裏插入圖片描述

接下來,我們從上往下看,我們可以發現:我們算邊數的時候,相當於結點是度爲2的伸出兩條邊,度爲1的伸出一條邊,度爲0的結點沒有伸出邊來,因此,我們得出:B = n2 * 2 + n1 * 1


對於B = n −1 ,B = n2 * 2 + n1 * 1這兩個式子,我們劃個等式n −1 = n2 * 2 + n1 * 1,得到n = n2 * 2 + n1 * 1 + 1,又因爲n = n2 + n1 + n0,所以再將這兩個式子劃個等式,得到n0 = n2 + 1

滿二叉樹

(滿二叉樹的直接意思就是每一層都充滿了結點)

(1)定義:
在一棵二叉樹中,如果所有分支節點都有左孩子節點和右孩子節點,並且葉節點都集中在二叉樹的最下一層,這樣的二叉樹稱爲滿二叉樹

(最後一層都是葉子,越往上的層沒有葉子,但也都充滿着)

在這裏插入圖片描述

(2)特點:

①每層都“充滿”了結點;
②深度爲k 的滿二叉樹,有2的k次方 -1個結點(假設一共有k層);(最多的)
③葉子節點都在最下一層;
④只有度爲0和度爲2的節點,沒有度爲1的結點;
⑤一旦n(這棵樹結點的個數)或h(這棵樹結點的高度)確定,樹型就確定了h=log2(n+1) (2是低數)

完全二叉樹

(1)定義:
在一棵二叉樹中,深度爲k ,有n個結點,除第 k 層外,其它各層 (1~k-1) 的結點數都達到最大個數,第k層從右向左連續缺若干結點,稱爲完全二叉樹

(跟滿二叉樹比起來,假設深度爲k,那麼只有第k層是不滿的,其他各層都是滿的,並且第k層是從右往左缺若干個結點)
在這裏插入圖片描述

(2)特點:

①葉子只能出現在層次最大的兩層
②最大層次上的葉子,都在最左邊(因爲右缺)
③如果有度爲1的節點,只能有個,且該節點只有左孩子,沒有右孩子
④按層編號後,一旦出現某結點(編號爲i)爲葉子或只有左孩子,則編號大於i的結點,均爲葉子
⑤完全二叉樹一旦n確定,其樹形就確定了,可以計算出高度h以及n0、n1和n2,其中n1=0或1,當n爲偶數時,n1=1,當n爲奇數n1=0。另:h=[log2(n+1)] ([]爲上取整)

4.性質4:

具有 n (n≥0) 個結點的完全二叉樹的深度爲 [log2
(n+1)] ([]爲上去整)

(即一棵二叉樹的結點個數確定之後,我們就能確定他的深度)

證明:
在這裏插入圖片描述

(2的k減1次方-1是上面k-1層結點數,n肯定是>他的,而根據興性質2,二叉樹的最大結點個數爲2的k次方-1個,所以n是<=2的k次方-1個的,也就是可以右缺,也可以滿,把成立的這個式子變形再取對數即可得到)

5.性質5:

將一棵有n個結點的完全二叉樹自頂向下,同一層自左向右連續給結點編號1, 2, …, n,則有以下關係:

①若i = 1, 則 i 無雙親 (實際上表示i=1時爲根結點)

②若i > 1, 則 i 的雙親爲**【i/2】**(在這裏【】是下取整)
例如:5的雙親爲5/2下取整爲2.

③若2*i <= n, 則 i 的左子女2*i, 若2*i+1 <= n, 則 i 的右子女2*i+1
例如:5的25<=10,所以5的左子女爲10, 25+1>10,所以5沒有右子女
同理,4的左子女爲8,右子女爲9(先要滿足條件)

④若 i 爲奇數, 且i != 1, 則其左兄弟爲 i-1

⑤若 i 爲偶數, 且i != n, 則其右兄弟爲 i+1

在這裏插入圖片描述

三.二叉樹的存儲

1.二叉樹的順序存儲

(1)特殊二叉樹:完全二叉樹的順序存儲

在這裏插入圖片描述

①按滿二叉樹編號次序存儲結點,依次存放二叉樹中的數據元素。
②編號爲i的節點的左孩子編號爲2i右孩子編號爲(2i+1)除樹根節點外,編號爲i的結點的雙親節點的編號爲[i/2](下取整)

(一棵完全二叉樹,我們按照完全二叉樹的編號給他編,從上到下,從左往右,依次1,2,3…給每個結點編上號,然後按照編號的位置把它放到內存中去,下標是1的時候放的是A,下標是2的時候放的是B,那麼這樣放有什麼好處呢?
我們會發現用這個方式存儲,會使二叉樹中每一個結點的位置蘊含着雙親和孩子之間的關係。比如:C它的編號是3,那麼他的左孩子就是2i,就是6存的F,右孩子就是2i+1,7存的G)

因此,完全二叉樹的順序存儲是通過結點的位置下標來蘊含着結點和結點之間的關係的。

(2)對於一般二叉樹的順序存儲

在這裏插入圖片描述

①先用空節點補全成爲完全二叉樹
②再對節點編號 (是層序編號)
③最後確定存儲

但是會存在着很多空閒的空間沒有被利用。

(3)極端情形

只有右單支的二叉樹

在這裏插入圖片描述
這時候如果還要按照順序存儲來存就要補空節點了,需要補很多空的,所以他存在着很大的空間浪費

二叉樹的順序存儲特點:
• 結點間關係蘊含在其存儲位置中
• 浪費空間,適於存滿二叉樹和完全二叉樹

2.二叉樹的鏈式存儲

二叉樹的鏈式存儲又有幾種方式,二叉鏈表和三叉鏈表還有一個靜態鏈表,二叉鏈表就是有兩個指針。

在這裏插入圖片描述

(1)二叉鏈表

在這裏插入圖片描述
(這是一個結點,這個結點的存儲方式中間是數據域,兩邊是一個指向左孩子一個指向右孩子的指針)

✓每個結點有3個成員,
data域存儲結點數據,
leftChildrightChild分別存放指向左子女和右子女的指針

(對於這棵樹來說,我們要把它表示成二叉鏈表的形式,首先要寫出他的存儲結構,存儲結構實際上就是他的結點的定義)

代碼表示:

typedef struct BiNode
{
	TElemType data;   //定義數據域 
	struct BiNode *lchild,*rchild;  //定義了左孩子右孩子兩個指針 
}BiNode,*BiTree;    //定義了這樣一個node 這樣一個二叉樹 

相當於根結點是這個鏈表的首元結點,然後有一個頭指針指向他,或者說中間再加上一個頭結點。

在這裏插入圖片描述
(有左孩子的時候指向左孩子,有右孩子的時候指向右孩子,沒有的時候指針域是空的。)

(2)三叉鏈表

在這裏插入圖片描述
(三叉鏈表是每個結點有四個成員,在二叉鏈表的基礎上還增加了一個指針域是指向雙親結點的)

✓每個結點有4個成員,
data域存儲結點數據,
leftChildrightChild分別存放指向左子女和右子女的指針。
✓每個結點增加一個指向雙親的指針parent,使得查找雙親也很方便。

代碼表示:

typedef struct TriTNode
{ 
	TelemType data;
	struct TriTNode *lchild,*parent,*rchild; //增加了一個parent指針
}TriTNode,*TriTree;

在這裏插入圖片描述

(有專門的指針域去指向他的雙親)

三叉鏈表比起二叉鏈表呢雖然是增加了一些指針域一些空間,但是這種存儲方式可以使我們一下子找到某個結點的雙親,而二叉鏈表要找 雙親的時候必須從鏈表的首元結點依次往後順着去找纔可以。因此,三叉鏈表這個存儲方式增加了一些存儲空間,但對於某些算法提高了效率。

例:對這棵二叉樹進行表示
在這裏插入圖片描述

(3)靜態鏈表

除了上述兩種表示之外,還有一種叫靜態鏈表。
實際上就是一個數組順序存儲,但是數組中的每一個結點除了包含這些數據域以外,還包含他的雙親結點的地址,還有左孩子右孩子的地址,其實就是下標。

在這裏插入圖片描述

四.二叉樹的遍歷

1.遍歷的定義

(1)指按某條搜索路線遍訪每個結點且不重複(又稱周遊)。
(2)是樹結構插入、刪除、修改、查找和排序運算的前提
是二叉樹一切運算的基礎和核心

2.二叉樹的三種遍歷

在這裏插入圖片描述

(1)先序遍歷:根節點–>左子樹–>右子樹。 ABDC

(2)中序遍歷:左子樹–>根節點–>右子樹。 BDAC

(3)後序遍歷:左子樹–>右子樹–>根節點。 DBCA

(先中後序就是指先根中根和後根)

3.二叉樹的遍歷算法(遞歸)

遍歷可以用遞歸,也可以用非遞歸,在這裏呢我們討論遞歸的方式
在這裏插入圖片描述

(1)先序遍歷

①算法思路:

若二叉樹爲空,則空操作
否則
訪問根結點 (D)
先序遍歷左子樹 (L)
先序遍歷右子樹 (R)

②過程:
在這裏插入圖片描述
(D是根的意思,L是左子樹,R是右子樹。對於上面樹來說,我們先訪問根A,然後再訪問左子樹L,對於左子樹也是先序遍歷,先訪問左子樹的根B,然後遍歷他的左子樹爲空返回,然後再遍歷他的右子樹根爲D,左子樹右子樹都爲空返回,再返回回來,再訪問這棵總樹的右子樹R,右子樹也是先序遍歷,先遍歷他的根C,然後C的左和右都爲空,返回,這樣就結束了。)

實際上遞歸算法寫起來比較簡單,但是執行過程是比較複雜的。這樣我們就得到了一個先序遍歷序列A B D C。

③回憶遞歸算法:

例:求n!算法

long Factorial ( long n )      //封裝爲函數 
{
	if ( n == 0 ) return 1;//基本項(先判斷,遞歸返回項,我們把它叫做基本項) 
	else return n * Factorial (n-1); //歸納項  (否則,返回遞歸的歸納項) 

}

用這個算法回憶一下遞歸算法怎麼寫

代碼描述:

Status PreOrderTraverse(BiTree T)   //封裝爲函數 

{
	if (T==NULL) return OK; //空二叉樹
	else
	{ 
		cout<<T->data; //訪問根結點
		PreOrderTraverse(T->lchild); //遞歸遍歷左子樹
		PreOrderTraverse(T->rchild); //遞歸遍歷右子樹
	}
}

⑤代碼具體執行過程:

在這裏插入圖片描述

(2)中序遍歷

①算法思路:

若二叉樹爲空,則空操作
否則:
中序遍歷左子樹 (L)
訪問根結點 (D)
中序遍歷右子樹 (R)

②過程:

在這裏插入圖片描述

代碼描述

Status InOrderTraverse(BiTree T)   //封裝爲函數 
{
	if (T==NULL) return OK; //空二叉樹
	else
	{ 
		InOrderTraverse(T->lchild); //遞歸遍歷左子樹
		cout<<T->data; //訪問根結點
		InOrderTraverse(T->rchild); //遞歸遍歷右子樹
	} 
}
(3)後序遍歷

①算法思路:

若二叉樹爲空,則空操作
否則
後序遍歷左子樹 (L)
後序遍歷右子樹 (R)
訪問根結點 (D)

②過程:

在這裏插入圖片描述

代碼描述

Status PostOrderTraverse(BiTree T)  //封裝爲函數 
{
	if (T==NULL) return OK; //空二叉樹
	else
	{ 
		PostOrderTraverse(T->lchild); //遞歸遍歷左子樹
		PostOrderTraverse(T->rchild); //遞歸遍歷右子樹
		cout<<T->data; //訪問根結點
	} 
}

(三種遍歷算法前面都一樣,就是遞歸訪問根結點的順序不同而已)

4.遍歷表示表達式

對於這棵樹實際上給我們展現的是一個表達式,外結點(葉子結點)都是運算數(操作數),這些內結點(也就是度不是零的這些結點分支節點)表示的是運算符,對這麼一棵樹進行表示:
在這裏插入圖片描述

先序遍歷: - + a * b - c d / e f 前綴表示

中序遍歷 :a + b * c - d - e / f 中綴表示

後序遍歷 :a b c d - * + e f / - 後綴表示

層序遍歷 :- + / a * e f b – c d

(分別對它進行不同的遍歷,得到了不同的遍歷序列,需要熟悉掌握用這是四種不同的遍歷方法寫出樹不同的遍歷序列)

5.總結

在這裏插入圖片描述
先序:ABDEFGC
中序:DBFEGAC
後序:DFGEBCA

(1)如果去掉輸出語句,從遞歸的角度看,三種算法是完全相同

(2)三種算法的訪問路徑是相同的,只是訪問結點的時機不同

(3)從虛線的出發點到終點的路徑上,每個結點經過3次

第1次經過時訪問=先序遍歷
第2次經過時訪問=中序遍歷
第3次經過時訪問=後序遍歷

(4)複雜度:

時間效率:O(n)
//三種遍歷都是這樣,因爲每個結點只訪問一次

空間效率:O(n)
//棧佔用的最大輔助空間,因爲在遞歸的過程中我們實際上用到的是棧,遞歸工作棧。

五.二叉樹的(遍歷)應用

在討論二叉樹的遍歷時我們採用的是遞歸的方法,當然,我們也可以採用非遞歸的方法,採用遞歸的方法是在系統中它建立了一個遞歸工作棧(也就是它的存儲),如果我們採用非遞歸的方式,隨着我們二叉樹的結點訪問的序列不斷深入的時候,他訪問的路徑又有回退,這時候我們是要自己建立一個棧的,通過棧模仿遞歸的過程,實現非遞歸。由二叉樹遍歷遞歸的方法,我們來看一下二叉樹的幾個應用。

1.求二叉樹的結點個數

可以用逐個訪問找到結點,也是用到遞歸遍歷,所以遍歷是一個基礎。

算法思路:

➢ 如果是空樹,則結點個數爲0; (遞歸返回)
➢ 否則,結點個數爲左子樹的結點個數+右子樹的結點個數再+1(根結點)。
(左子樹的結點個數也是左子樹的左子樹的結點個數+他的右子樹的結點個數+1,一層一層遞歸進去,然後再返回)

在這裏插入圖片描述
代碼描述:

int NodeCount(BiTree T)
{ 
	if (T == NULL ) return 0;   //如果是空樹,返回0 
	else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}

2.求二叉樹葉子結點的個數

同樣也是這個思路,左子樹的葉子結點個數+右子樹的葉子結點個數

算法思路:

➢ 如果是空樹,則葉子結點個數爲0;
➢ 如果是葉子結點,則個數爲1;
➢ 否則,結點個數爲左子樹的葉子結點個數+右子樹的葉子結點個數

代碼描述:

int LeafCount(BiTree T)
{
	if(T==NULL) //如果是空樹返回0
	return 0;
	if (T->lchild == NULL && T->rchild == NULL)
	return 1; //如果是葉子結點返回1 (T的左子樹和右子樹都是空的時候,證明此時T爲葉子) 
	else return LeafCount(T->lchild) + LeafCount(T->rchild);  //否則,返回左子樹的葉子+右子樹的葉子 
}

3.建立一棵二叉樹

(1)按先序遍歷序列建立二叉樹的二叉鏈表

例如:A B C # # D E # G # # F # # #

在這裏插入圖片描述

算法思路:

✓以遞歸方式建立二叉樹。
✓輸入結點值的順序必須對應二叉樹結點先序遍歷的順序。
✓約定以輸入序列中不可能出現的值作爲空結點的值以結
束遞歸

(先序遍歷當然是先建根了,剪完根之後,再建他的左子樹,右子樹,左子樹也是先建他的根,再建他的左子樹,右子樹,所以,二叉樹的建立也是以遞歸的方式。
注意:這個先序遍歷序列也一定要留出葉子結點左右孩子的位置,葉子結點的左右孩子也要給他補全,實際上是作爲遞歸結束的方式,到葉子的時候改結束了,在這我們用#表示)

代碼描述:

void CreateBiTree(BiTree &T)
{
	cin>>ch;   //輸入一個字符
	if (ch==’#’) T=NULL; //遞歸結束,建空樹
	else
	{
		T=new BiTNode; T->data=ch; //生成根結點
		CreateBiTree(T->lchild); //遞歸創建左子樹
		CreateBiTree(T->rchild); //遞歸創建右子樹
	}
}

4.構造一棵二叉樹

(1)同一棵二叉樹具有唯一先序序列、中序序列和後序序列。

(3)不同的二叉樹可能具有相同的先序序列、中序序列和後序序列。
在這裏插入圖片描述
在這裏插入圖片描述
• 僅由一個先序序列(或中序序列、後序序列),無法確定這棵二叉樹的樹形
• 思考:給定先序、中序和後序遍歷序列中任意兩個,是否可以唯一確定這棵二叉樹的樹形?

(4)結論:
在這裏插入圖片描述

若二叉樹中各結點的值均不相同,則:
• 由二叉樹的前序序列和中序序列,可唯一確定一棵二叉樹(確定後序)
• 由二叉樹的後序序列和中序序列,可唯一確定一棵二叉樹(確定前序)
• 由前序序列和後序序列不一定能唯一地確定一棵二叉樹。

5.思考

在n個結點的二叉鏈表中,有n+1 個空指針域。

在這裏插入圖片描述
分析:必有2n個鏈域。除根結點外,每個結點有且僅有一個雙親,所以只會有n-1個結點的鏈域存放指針,指向非空子女結點。空指針數目=2n-(n-1)=n+1

結點個數爲n的二叉樹,他的指針域還有n+1個在空閒着,這些空指針很浪費,而二叉樹最常用的操作是遍歷。如何利用好空餘指針域,解決最關鍵最常用遍歷的效率問題?

六.二叉樹的線索化——線索化二叉樹

1.什麼是二叉樹的線索化?

(1)二叉樹的遍歷可將二叉樹中所有結點排列成一個線性序列,由序列可以找到某個結點的前驅和後繼

在這裏插入圖片描述
比如說這棵二叉樹,我們進行中序遍歷的時候,得到了中序遍歷序列
bdaec,拿其中任何一個結點比如a,我們就知道a的前驅是b,a的後繼是e。

這樣每次通過遍歷序列找前驅後繼浪費時間,我們可以改進一下。

(2) 事先做預處理,將某種遍歷順序下的前驅、後繼關係記在二叉樹的存儲結構中,以便高效地找出某結點的前驅、後繼。

(也就是說我們想要在這個結點的存儲過程中就已經存下了這個結點的前驅和後繼)

(3)這種對結點前驅、後繼關係的記錄,稱爲線索

主要目的:提高遍歷效率

2.如何記錄線索呢?

(1) 方法一:存儲結構增加兩個域:前驅指針pred和後繼指針succ
在這裏插入圖片描述
(在二叉樹結點存儲的時候,在原來的二叉鏈表的情況下,又增加了兩個指針域,一個指向前驅,一個指向後繼,這樣的話,每個結點的前驅和後繼馬上就能找到了,在O(1)的時間複雜度下)

缺點:當結點數很大很多時存儲消耗較大。

(2)方法二:利用空鏈域n+1個空鏈域)改造二叉樹結點
在這裏插入圖片描述
①改造二叉樹的結點,將 pred 指針和 succ 指針壓縮到 leftChildrightChild 的空閒指針中;
(當一個結點沒有左孩子或者右孩子的時候,他的指針域是空着的,那就讓他利用起來,指向前驅和後繼,那麼什麼時候指向的是前驅後繼,什麼時候指向的是子女)

②增設兩個標誌 ltagrtag,指明指針是指示子女還是前驅/後繼;

③若 lTag=0, lchild域指向左孩子;若 lTag=1, lchild域指向其前驅(線索)。
④若 rTag=0, rchild域指向右孩子;若 rTag=1, rchild域指向其後繼(線索)。

3.如何進行線索化?

中序遍歷序列:bdaec

(要先寫出序列,再畫二叉樹)
在這裏插入圖片描述
在這裏插入圖片描述

(首先,我們寫出他的存儲結構,加了兩個整型的標誌位,是0的時候,表示指向子女,是1的時候,表示指向線索(前驅和後繼)的。先看b,他沒有左孩子,又是第一個結點,因此前面是1、空,他有右孩子,因此是0,指針域指向右孩子d;再看d,d的前驅是b,d是一個葉子結點,既沒有左孩子也沒有右孩子,因此前面是指向前驅,1,、b,他的後繼是a,因爲他沒有右孩子,1、a;再看a,a沒有前驅和後繼,兩個指針分別指向左孩子b和右孩子c,所以標誌位爲0、0;接下來是e,e是葉子結點,沒有左孩子右孩子,所以前面是1、指向前驅a,後面是1、指向後繼c;最後一個結點是c,c有左孩子,所以0、指向e,他沒有右孩子,而且他是遍歷序列當中的最後一個元素了,因此右面是1、指向空)

typedef struct node 
{ 
	ElemType data; 
	int ltag,rtag;   //兩個標誌位
	struct node *lchild; 
	struct node *rchild; 
} TBTNode;

討論:在上述線索化二叉樹記錄的線索中,當訪問到b結點時能通過線索找到其後繼結點嗎?爲什麼?如何才能找到?

b裏面存的是他的右孩子,沒有存他的後繼結點,右孩子不見得是他的後繼,無法直接找到,要想找到後繼,我們只能再通過遍歷序列來找b後面是誰。
這個討論告訴我麼:線索化二叉樹是把空閒的指針利用起來,來存儲前驅和後繼的線索,但並不是每個結點的前驅和後繼都能夠一下子找到,因爲有的時候他存的是指向左子女和右子女的指針,有的時候是指向前驅和後繼的指針。所以他只能說是在一定程度上提高了遍歷的效率。


後續

以上就是二叉樹的全部基礎知識的了,學習了二叉樹的定義、性質、存儲、遍歷以及他的應用和線索化,其中最重要的就是他的遞歸遍歷了,之後我們還會再去學習二叉樹的另一種應用——哈夫曼樹。
最後,一句調侃語句送給正在肝數據結構的你我
——待我長髮及腰,分如二叉樹梢,早起滿冠枯草,睡前一頭蓬毛。

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