大話數據結構筆記——第六章:樹


之前一直在談論一對一的線性結構,現在來研究一對多的數據結構——樹(Tree)

樹的定義

樹(Tree)是n(n>=0)個結點的有限集。n=0時稱爲空樹。在任意一顆非空樹中:

  1. 有且僅有一個特定的稱爲根(Root)的結點。
  2. 當n>1時,其餘結點可分爲m(m>0)個互不相交的有限集T1T_1T2T_2……TmT_m,其中每個集合本身又是一棵樹,並且稱爲根的子樹(SubTree)。

注意:根結點具有唯一性;子樹的個數沒有限制,但它們一定是不相交的。

結點分類

結點擁有的子樹數稱爲結點的(Degree)。度爲0的結點稱爲葉結點(Leaf)或終端結點;度不爲0的結點稱爲非終端結點分支結點。除根節點外,分支結點也稱爲內部結點。樹的度是樹內各結點的度的最大值。

結點間關係

結點的子樹的根稱爲該結點的孩子(Child),相應地,該結點稱爲孩子的雙親(Parent)。同一個雙親的孩子之間互稱爲兄弟(Sibling)。結點的祖先是從根結點到該結點所經分支上的所有結點;反之,以某節點爲根的子樹中的任一結點都稱爲該結點的子孫。

樹的其他相關概念

結點的層次(Level)從根開始定義起,根爲第一層,根的孩子爲第二層。雙親在同一層的結點互爲堂兄弟。樹中結點的最大層次稱爲樹的深度(Depth)或高度
如果將樹中結點的各子樹看成從左至右是有次序的,不能互換的,則稱該樹爲有序樹,否則稱爲無序樹
森林(Forest)是m(m>=0)棵互不相交的樹的集合。

線性結構與樹結構區別

線性結構:

  1. 第一個數據元素:無前驅
  2. 最後一個數據元素:無後繼
  3. 中間數據元素:一個前驅一個後繼

樹結構:

  1. 根節點:無雙親,唯一
  2. 葉結點:無孩子,可以多個
  3. 中間結點:一個雙親多個孩子

樹的抽象數據類型

ADT 樹(tree)
Data
	樹是由一個根節點和若干棵子樹構成。樹中結點具有相同數據類型及層次關係
Operation.
	InitTree(*T):構造空樹T
	DestroyTree(*T):銷燬樹T
	CreateTree(*T,definition):按definition中給出樹的定義來構造樹
	ClearTree(*T):若樹T存在,則將樹T清爲空樹
	TreeEmpty(T):若樹T爲空樹,返回true,否則返回false
	TreeDepth(T):返回T的深度
	Root(T):返回T的根節點
	Value(T,cur_e):cur_e是樹T中一個結點,返回此結點的值
	Assign(T,cur_e,value):給樹T的結點cur_e賦值爲value
	Parent(T,cur_e):若cur_e是樹T的非根結點,則返回它的雙親,否則返回空
	LeftChild(T,cur_e):若cur_e是樹T的非葉結點,則返回它的左孩子,否則返回空
	RightChild(T,cur_e):若cur_e有右兄弟,則返回它的右兄弟,否則返回空
	InsertChild(*T,*p,i,c):其中p指向樹T的某個結點,i爲所指結點p的度加上1,
	非空樹c與T不相交,操作結果爲插入c爲樹T中p指結點的第i棵子樹
	DeleteChild(*T,*p,i):其中p指向樹T的某個結點,i爲所指結點p的度,操作結果
	爲刪除T中p所指結點的第i棵子樹。
endADT

樹的存儲結構

簡單順序存儲結構和鏈式存儲結構都無法對樹進行存儲,但是可以充分利用它們的特點實現對樹存儲結構的表示,這裏介紹三種表示法:雙親表示法、孩子表示法、孩子兄弟表示法

雙親表示法

以一組連續空間存儲樹的結點,同時在每個結點中,附設一個指示器指示其雙親結點在數組中的位置。

data parent

結構代碼:

/*樹的雙親表示法結點結構定義*/
#define MAX_TREE_SIZE 100
typedef int TElemType; //樹節點的數據類型
typedef struct PTNode //結點結構
{
	TElemType data;	//結點數據
	int parent; 	//雙親位置
}PTNode;
typedef struct 
{
	PTNode nodes[MAX_TREE_SIZE]; //結點數組
	int r,n //根的位置和節點數
}PTree;

根據結點的parent指針容易找到其雙親結點,時間複雜度爲O(1),但是如果要知道結點的孩子是什麼,需要遍歷整個結構纔行。
可以增加一個結點給最左邊孩子的域進行改進,可以稱爲長子域,沒有的話設置爲-1。

data parent firstchild

另一個問題,我們可能很關注各兄弟之間的關係,可以增加一個右兄弟域來體現兄弟關係,如果沒有的話就賦值爲-1。

data parent rightsib

這個域到底是指向什麼得可以根據自己需要進行設計,存儲結構的設計是一個非常靈活的過程,一個存儲結構設計得是否合理,取決於基於該存儲結構得運算是否適合、是否方便,時間複雜度好不好等。

孩子表示法

換一種完全不同得考慮方法。由於樹中每個結點可能有多棵子樹,可以考慮用多重鏈表,即每個每個結點有多個指針域,其中每個指針指向一棵子樹的根結點,我們把這種方法叫做多重鏈表表示法,根據結點度的不同,可以有兩種解決方案。

  • 方案一
data child1 child2 …… childn

data是數據域,child1……childn是指針域,這種方案當樹中結點的度相差很大時,會造成空間浪費,很多結點它的指針域可能會空出很多;如果各結點的度差異很小時,這種方案很適用。因此引出第二種方案。

  • 方案二
data degree child1 …… childn

data是數據域,degree爲度域,存儲着每個結點的孩子結點的個數,child1到childn爲指針域。

  • 孩子表示法

把每個結點的孩子結點排列起來,以單鏈表做存儲結構,則n個結點有n個孩子鏈表,如果是葉子結點則此單鏈表爲空。然後n個頭指針有組成一個線性表,採用順序存儲結構,存放進一個一維數組中。
設計兩種結點結構:

child next
										孩子鏈表的孩子結點
data firstchild
										表頭數組的表頭結點

結構代碼:

/*數的孩子表示法結構定義*/
#define MAX_TREE_SIZE 100
typedef struct CTNode //孩子節點
{
	int child;
	struct CTNode *next;
}*ChildPtr;
typedef struct  //表頭結構
{
	TElemType data;
	ChildPtr firstchild;
}CTBox;
typedef struct //樹結構
{
	CTBox nodes[MAX_TREE_SIZE]; //節點數組
	int r,n;//根的位置和結點數
}CTree;

孩子兄弟表示法

通過觀察發現,任意一棵樹,它的結點的第一個孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我們設計兩個指針,分別指向該結點的第一個孩子和此結點的兄弟。

data firstchild rightsib

data是數據域,firstchild是指針域,存儲該結點的第一個孩子結點的存儲地址,rightsib是指針域,存儲該結點的右兄弟結點的存儲位置。

二叉樹的定義

二叉樹(Binary Tree)是n(n>=0)個結點的有限集合,該集合或者爲空集(稱爲空二叉樹),或者由一個根節點和兩棵互不相交的,分別稱爲根節點的左子樹和右子樹的二叉樹組成。

特點

  • 每個結點最多有兩棵子樹,即二叉樹中不存在度大於2的結點。
  • 左子樹和右子樹是有順序的,次序不能任意顛倒。
  • 即使樹中某個結點只有一棵子樹,也要區分它是左子樹還是右子樹。
  • 二叉樹具有五種基本形態:
  • 空二叉樹
  • 只有一個根節點
  • 只有左子樹
  • 只有右子樹
  • 既有左子樹又有右子樹

特殊二叉樹

  • 斜樹:所有子樹都在樹的一邊,左邊或者右邊,分別稱爲左斜樹,右斜樹統稱爲斜樹。
  • 滿二叉樹:在一棵二叉樹中,如果所有分支結點都存在左右子樹,且所有的葉子都在同一層,這樣的二叉樹稱爲滿二叉樹。
  • 完全二叉樹:對一棵具有n個結點的二叉樹按層序編號(從上到下,從左到右),如果編號爲i(1<=i<=n)的結點與同樣深度的滿二叉樹中編號爲i的結點在二叉樹中的位置完全相同,則此二叉樹爲完全二叉樹。
    完全二叉樹特點:
  • 葉子只能出現在最下兩層。
  • 最下層的葉子一定集中在左部連續位置。
  • 倒數兩層,若有葉子結點,一定都在右部連續位置。
  • 如果結點度爲1,則該結點只有左孩子,即不存在只有右子樹的情況。
  • 同樣結點數的二叉樹,完全二叉樹的深度最小。

二叉樹的性質

  1. 在二叉樹的第i層上至多有2i-1結點(i>=1);
  2. 深度爲k的二叉樹至多有2k-1個節點(k>=1);
  3. 對任何一棵二叉樹T,如果其終端結點數爲n0n_0,度爲2的結點數爲n2n_2,則n0n_0=n2n_2+1;
  4. 具有n個結點的完全二叉樹的深度爲|log2log_2n|+1(| |代表取整操作);
  5. 如果對一棵有n個結點的完全二叉樹(其深度爲|log2log_2n|+1)的結點按層序編號(從第1層到第|log2log_2n|+1,每層從左到右),對任一結點i((1<=i<=n)有:
    1. 如果i=1,則結點i是二叉樹的根,無雙親;如果i>1,則其雙親是結點|i/2|。
    2. 如果2i>n,則結點i無左孩子 (結點i爲葉子結點);否則其左孩子是結點2i。
    3. 如果2i+1>n,則結點i無右孩子;否則其右孩子是結點2i+1

二叉樹的存儲結構

二叉樹的順序存儲結構

使用一維數組存儲二叉樹中的結點,並且結點的存儲位置,也就是數組下標要能體現結點之間的邏輯關係。完全二叉樹由於是嚴格的按層序編號,所以使用順序結構可以表現出來二叉樹的結構。對於一般的二叉樹,儘管層序編號不能反映邏輯關係,但是可以將其按完全二叉樹編號,只不過,把不存在的結點設置爲"V"而已。但是注意這種存儲方式對一棵右斜樹來說,會造成很大的存儲空間的浪費。所以順序存儲結構一般只用於完全二叉樹。

二叉鏈表

二叉樹每個結點最多有兩個孩子,所以爲它設計一個數據域和兩個指針域,我們稱這樣的鏈表爲二叉鏈表。

lchild data rchild

data是數據域,lchild和rchild都是指針域,代表着左孩子、右孩子。

typedef struct BiTNode //結點結構
{
	TElemType data; //結點數據
	struct BiTNode *lchild,*rchild; //左右孩子指針
}BiTNode, *BiTree;

如果有需要還可以增加一個指向其雙親的指針域,這樣就稱爲三叉鏈表。

遍歷二叉樹

遍歷原理

二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問二叉樹的所有結點,使得每個結點被訪問一次且僅被訪問一次。由於二叉樹的節點之間不存在唯一的前驅後繼關係,所以二叉樹的遍歷不同於線性結構,將會面臨很多的選擇問題。

二叉樹的遍歷方法

二叉樹遍歷方法很多,不過如果限制了從左到右的習慣方式,那麼主要的遍歷方式能分爲四種

前序遍歷

若二叉樹爲空,則空操作返回,否則先訪問根節點,然後前序遍歷左子樹,再前序遍歷右子樹。也就是左邊的全部遍歷完再遍歷右邊的。

中序遍歷

若二叉樹爲空,則空操作返回,否則從根結點開始(注意並不是先訪問根節點),中序遍歷根節點的左子樹,然後是訪問根節點,最後中序遍歷右子樹。

後序遍歷

若二叉樹爲空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點。

層序遍歷

若二叉樹爲空,則空操作返回,否則從樹的第一層,也就是根節點開始訪問,從上而下逐層遍歷,在同一層上,按從左到右的順序對結點逐個訪問。

對於計算機來說,它只會處理線性序列,這四種遍歷方式,都是把書中的結點變成某種意義的線性序列,這給程序的實現帶來了好處。

前序遍歷算法

/*二叉樹的前序遍歷遞歸算法*/
void PreOrderTraverse(BiTree T)
{
	if (T==NULL)
	{
		return;
	}
	printf("%c",T->data); //顯示結點數據,可以更改爲其他對結點操作
	PreOrderTraverse(T->lchild); //再先序遍歷左子樹
	PreOrderTraverse(T->rchild);//最後先序遍歷右子樹
}

中序遍歷算法

/*二叉樹的中序遍歷遞歸算法*/
void InOrderTraverse(BiTNode T)
{
	if (T==NULL)
	{
		return;
	}
	InOrderTraverse(T->lchild); //中序遍歷左子樹
	printf("%c",T->data );//顯示結點數據,可以更改爲其他對結點操作
	InOrderTraverse(T->rchild); //最後中序遍歷右子樹
}

後序遍歷算法

/*二叉樹的後序遍歷遞歸算法*/
void PostOrderTraverse(BiTNode T)
{
	if (T==NULL)
	{
		return;
	}
	PostOrderTraverse(T->lchild); //先後序遍歷左子樹
	PostOrderTraverse(T->rchild); //再後序遍歷右子樹
	printf("%c",T->data );//顯示結點數據,可以更改爲其他對結點操作
}

推導遍歷結果

在充分理解前、中、後序遍歷的情況下,已知兩個遍歷結果求後一種遍歷結果(但是注意,已知前序和後序遍歷是不能確定一棵二叉樹),相當於根據遍歷結果把樹確定下來。

二叉樹的建立

實現算法:

/*按前序輸入二叉樹中結點的值(一個字符)*/
/*#表示空樹,構造二叉鏈表表示二叉樹T*/
void CreateBiTree(BiTNode *T)
{
	TElemType ch;
	scanf("%c",&ch);
	if (ch=='#')
	{
		*T=NULL;
	}
	else
	{
		*T=(BiTree)malloc(sizeof(BiTNode));
		if (!*T)
		{
			exit(OVERFLOW);
			(*T)->data=ch;
			CreateBiTree(&(*T)->lchild); //構造左子樹
			CreateBiTree(&(*T)->rchild); //構造右子樹
		}
	}

}

線索二叉樹

對於一個n個結點的二叉鏈表,每個結點有指向左右孩子的兩個指針域,也就是說有2n個指針域,而n個結點的二叉樹一共有n-1條分支線數,也就是說,存在n+1個空指針域。將這些空指針域利用起來,存放指向結點在某種遍歷次序下的前驅和後繼地址。把這種指向前驅和後繼的指針稱爲線索,加上線索的二叉鏈表稱爲線索鏈表,相應的二叉樹就稱爲線索二叉樹,其實線索二叉樹變成一個雙向鏈表,我們對二叉樹以某種次序遍歷使其成爲線索二叉樹的過程稱做是線索化。
我們再添上兩個標誌域,注意標誌域只是存放0或1數字的布爾型變量,區分某個指針域到底指的是孩子域還是指向的是前驅域後繼域,具體結構如下圖:

lchild ltag data rtag rchild
  • ltag爲0時指向該結點的左孩子,爲1時指向該結點的前驅。
  • rtag爲0時指向該結點的右孩子,爲1時指向該結點的後繼。

線索二叉樹結構實現

結構定義代碼:

/*二叉樹的二叉線索存儲結構定義*/
typedef enum{Link,Thread} PointerTag; 
/*Link==0表示指向左右孩子的指針*/
/*Thread==1表示指向前驅或後繼的線索*/
typedef struct BiThrNode //二叉線索存儲結點結構
{
	TElemType data; //結點數據
	struct BiThrNode *lchild,*rchild; //左右孩子指針
	PointerTag LTag;
	PointerTag RTag; //左右標誌
}BiThrNode, *BiThrTree;

線索化的實質就是將二叉鏈表中的空指針改爲指向前驅或後繼的線索。由於前驅和後繼信息只有在遍歷該二叉樹時才能得到,所以線索化的過程就是在遍歷的過程中修改空指針的過程

BiThrTree pre; //全局變量,始終指向剛剛訪問過的結點
/*中序遍歷進行中序線索化*/
void InThreading(BiThrTree p)
{
	if (p)
	{
		InThreading(p->lchild); //遞歸左子樹線索化
		if(!p->lchild) //沒有左孩子
		{
			p->LTag = Thread; //前驅線索
			p->lchild = pre; //左孩子指針指向前驅
		}
		if(!pre->rchild) //前驅沒有右孩子
		{
			pre->RTag = Thread; //後繼線索
			pre->rchild = p; //前驅右孩子指針指向後繼(當前結點p)
		}
		pre = p; //保持pre指向p的前驅
		InThreading(p->rchild) //遞歸右子樹線索化
	}
}

有了線索二叉樹後,對它進行遍歷就相當於操作一個雙向鏈表的結構,和雙向鏈表結構一樣,在二叉樹線索鏈表上添加一個頭結點。對鏈表進行遍歷。
遍歷的代碼:

/*T指向頭結點,頭結點的左鏈lchild指向根節點,頭結點的右鏈rchild
指向中序遍歷的最後一個結點,中序遍歷二叉線索鏈表表示二叉樹T*/
Status InOrderTraverse_Thr(BiThrTree T)
{
	BiThrTree p;
	p = T->lchild; //p指向根節點
	while(p != T) //
	{
		while(p->LTag==Link) //當LTag==0時循環到中序序列第一個結點
			p=p->lchild;
		printf("%c",p->data ); //顯示結點數據,可以更改爲其他對結點的操作
		while(p->RTag==Thread && P->rchild!=T)
		{
			p=p->rchild;
			printf("%c",p->data );
		}
		p = p->rchild; //p進至其右子樹根
	}
	return OK;
}

整個代碼等於是一個鏈表的掃描,所以時間複雜度爲O(n)。在實際問題中,如果所採用的二叉樹需要經常遍歷或查找結點時需要某種遍歷序列中的前驅和後繼,那麼採用線索二叉鏈表的存儲結構就是非常不錯的選擇。

樹、森林與二叉樹的轉換

樹轉換爲二叉樹

樹轉化爲二叉樹步驟:

  1. 加線。在所有兄弟結點之間加一條連線。
  2. 去線。對樹中每個結點,只保留它與第一個孩子結點的連線,刪除它與其他孩子結點之間的連線。
  3. 層次調整。以樹的根節點爲軸心,將整棵樹順時針旋轉一定角度,使之結構層次分明。

森林轉換爲二叉樹

步驟:

  1. 把每棵樹轉化爲二叉樹。
  2. 第一顆二叉樹不動,從第二棵二叉樹開始,依次把後一棵二叉樹的根結點作爲前一棵二叉樹的根節點的右孩子,用線連接起來。當所有的二叉樹連接起來後就得到了由森林轉換來的二叉樹。

二叉樹轉換爲樹

  1. 加線。若某結點的左孩子結點存在,則將這個左孩子的右孩子結點、右孩子的右孩子結點,右孩子的右孩子的右孩子的節點……,也就是左孩子的n個右孩子結點都作爲此結點的孩子。將該結點與這些右孩子結點用線連接起來。
  2. 去線。刪除原二叉樹中所有結點與其右孩子結點的連線。
  3. 層次調整。使之結構層次分明。

二叉樹轉化爲森林

步驟:

  1. 從根結點開始,若右孩子存在,則把與右孩子結點的連線刪除,再查看分離後的二叉樹,若右孩子存在,則連線刪除……,直到所有右孩子連線都刪除爲止,得到分離的二叉樹。
  2. 再將每棵分離後的二叉樹轉換爲樹即可。

樹與森林的遍歷

樹的遍歷分爲兩種:

  1. 先根遍歷樹。
  2. 後根遍歷樹。

森林遍歷也爲兩種:

  1. 前序遍歷。
  2. 後序遍歷。
    森林的前序遍歷和二叉樹的前序遍歷結果相同,森林的後序遍歷和二叉樹的中序遍歷結果相同。
    樹的先根遍歷和後根遍歷完全可以借用二叉樹的前序遍歷和中序遍歷算法來實現。

赫夫曼樹及其應用

定義與原理

需要把二叉樹轉化成葉子結點帶權的二叉樹,從樹中一個結點到另一個結點之間的分支構成兩個結點之間的路徑,路徑上的分支數目成爲路徑長度。樹的路徑長度就是從樹根到每一結點的路徑長度之和。如果考慮到帶權的結點,結點的帶權的路徑長度爲從該結點的樹根之間的路徑長度與結點上權的乘積。樹的帶權路徑長度爲樹中所有葉子結點的帶權路徑長度之和。將帶權路徑長度WPL最小的二叉樹稱做赫夫曼樹,或稱最優二叉樹。

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