二叉樹操作(基本操作,線索化,經典問題)

一、樹概念:樹是由一個個節點,根據規則排列在一起的一種數據結構。每一個節點爲一個數據。根據存儲方法分爲順序存儲(使用順序表的方法),鏈式存儲(鏈表的存儲放法)
注:樹中是不帶環的
二、二叉樹
1、二叉樹是節點的集合(集合爲空或非空)或由一個根節點和左右子樹構成
2、二叉樹特點:每個節點最多有兩個孩子(或子樹)(及不存在度大於2的節點)二叉樹的子樹有左右區分,次序不可顛倒。
3、二叉樹分類: 
滿二叉樹:所有葉子節點都在同一層,分支節點存在左右子樹   
完全二叉樹:N-1層不滿,其它層爲完全二叉樹(滿二叉樹是完全二叉樹)
 注:N個節點的完全二叉樹的深度爲lg(N+1)
4、二叉樹的性質:    
    (1)、若規定根節點爲第1層(最好這樣規定比較方便),則非空的二叉樹的第i層最多有 2^(i-1) 個節點 
    (2)、若規定根節點的深度爲1,則深度爲K的二叉樹最多有 2^(K)-1 個節點     
    (3)、任何一棵二叉樹,若葉子節點樹爲n0,節點度爲2的非葉節點數爲n2 則 n0 = n2+1;      
    (4)、具有N個節點的完全二叉樹,從 上--->下,左--->右順序編號(0開始),則對於序號的i的節點有:
           i = 0 :根節點,無雙親      
           i > 0 :雙親序號爲 (i-1)/2      
           左孩子序號爲:2*i+1,(N > 2*i+1)否則無左孩子      
          右孩子序號爲:2*i+2,(N > 2*i+2)否則無右孩子
5、二叉樹存儲    
      順序存儲:採用順序表的的方法存儲數據,(由於每個空節點位置都需佔位,浪費空間,用於構造搜索二叉樹,或堆的存儲(原數據的順序與存儲無關的數據管理))        
      鏈式存儲:採用鏈表存儲,每一個數據爲一個節點。通過指針域將節點鏈接在一起。
三、二叉樹線索化      
    作用:線索化的作用是將樹可以向鏈表一樣遍歷每一個節點  ,而不是用棧或遞歸的方法遍歷    
    注:不同的線索化方式對應不同的線索化的遍歷(並且一棵樹只能線索化一次)      
     前序線索化-----前序線索化遍歷  
     中序線索化-----中序線索化遍歷      
     後序線索化-----後序線索化遍歷

四、數據結構
enum Point{LINK,THREAD};//指針域狀態
typedef char BTDataType; 

typedef struct BinTreeNode 
{ 
	BTDataType _data; //數據
	struct BinTreeNode* _pLeft;  //左子樹
	struct BinTreeNode* _pRight; //右子樹
        //用於線索化
	struct BinTreeNode* _parent; //雙親----用於後繼線索化
	enum Point _leftpag; //左指針域狀態(前驅)
	enum Point _rightpag; //右指針域狀態(後繼)

}BTNode, *PBTNode; 

五、二叉樹操作解析
1、創建二叉樹
傳入樹(tree),數據集(arr),數據個數(size),當前操作的下標(index),非法值(invalue--#)等,採用遞歸的方式構建二叉樹
如果當前操作的下標未越界且數據合法,則創建節點,下標加1進行左子樹操作,下標加1進行右子樹操作
注:由於形參只是實參的一份臨時拷貝,改變形參值不能改變實參值,所以下標index需傳指針
// 創建二叉樹 
void CreateBinTree(PBTNode* pRoot, BTDataType* array, int size, BTDataType invalid)
{
	int index = 0;
	_CreateBinTree(pRoot,array,size,&index,invalid);//遞歸創建,注:形參不能改變實參值,所以需傳地址(index),否則遞歸回退會發生錯誤
}

void _CreateBinTree(PBTNode* pRoot, BTDataType* array, int size, int* index, BTDataType invalid)//改變指針內容,所以傳二級指針
{
	if(array[*index] == invalid)
	{
		return ;
	}
	if(*index < size && array[*index] != invalid)//值合法
	{
		(*pRoot) = BuyBinTreeNode(array[*index]);//創建節點
		++(*index);
		_CreateBinTree(&(*pRoot)->_pLeft,array,size,index,invalid);//改變指針內容,所以傳二級指針
		if((*pRoot)->_pLeft)
			(*pRoot)->_pLeft->_parent = *pRoot;
		++(*index);
		_CreateBinTree(&(*pRoot)->_pRight,array,size,index,invalid);//改變指針內容,所以傳二級指針
		if((*pRoot)->_pRight)
			(*pRoot)->_pRight->_parent = *pRoot;
	}
}

2、創建節點
//創建節點
PBTNode BuyBinTreeNode(BTDataType data)
{
	PBTNode Newnode = NULL;
	Newnode = (PBTNode)malloc(sizeof(BTNode));
	Newnode->_data = data;
	Newnode->_pLeft = NULL;
	Newnode->_pRight = NULL;
	Newnode->_leftpag = LINK;
	Newnode->_rightpag = LINK;
	return Newnode;
}

3、拷貝二叉樹
採用遞歸的方法,前序遍歷原樹,取樹中的數據構建節點,新樹的左遍歷原樹的左,新樹的右遍歷原樹的右
// 拷貝 
PBTNode CopyBinTree(PBTNode pRoot)
{
	PBTNode New = NULL;
	if(pRoot == NULL)
		return NULL;
	else
	{
		New = BuyBinTreeNode(pRoot->_data);//新節點
		New->_pLeft = CopyBinTree(pRoot->_pLeft);
		New->_pRight = CopyBinTree(pRoot->_pRight);
	}
	return New;
}


4、遍歷二叉樹
遍歷二叉樹:
非遞歸---->藉助棧實現  
遞歸------->當子問題處理 
注:層次遍歷使用隊列實現

前序遍歷:根節點--->左子樹---->右子樹
中序遍歷:左子樹--->根節點---->右子樹
後序遍歷:左子樹--->右子樹---->根節點
層次遍歷:上---->下

(1)前序
前序遞歸遍歷:
樹不爲空:遍歷根節點數據,走(遞歸)根的左子樹,走根的右子樹

前序非遞歸遍歷:
樹不爲空:根節點入棧 。循環(棧不爲空),遍歷棧頂的節點,出棧。棧頂節點的右子樹不爲空,右子樹入棧。棧頂節點的左子樹不爲空,左子樹入棧
注:由於棧先進後出的特性,所以右子樹先入棧,後左子樹入棧

(2)中序
中序遞歸遍歷:
根不爲空:走根的左子樹,遍歷根節點的數據,走根的右子樹

中序非遞歸遍歷:
根不爲空:取根節點(cur),
循環cur不爲空或棧不爲空,走到最左側,將經過的每個節點入棧(循環cur不爲空,cur節點入棧,走cur的左)
cur取棧頂,遍歷棧頂元素的數據,出棧
cur走右子樹(將右子樹當子問題處理)

(3)後序
後序遞歸遍歷:
根不爲空:走根的左子樹,走根的右子樹,遍歷根節點的數據

後序非遞歸遍歷:
根不爲空:取根節點(cur)
循環cur不爲空或棧不爲空,走到最左側,將經過的每個節點入棧(循環cur不爲空,cur節點入棧,走cur的左)
cur取棧頂,cur的右爲空並且prev不爲cur(prev:上次遍歷的節點)遍歷cur的數據,prev指向cur,出棧
cur走右子樹(將右子樹當子問題處理)

(4)層次遍歷
層次遍歷類似於先序遍歷,只是使用隊列來實現的而已,代碼類似
根不爲空:根節點入隊列
循環隊列不爲空,取隊頭,遍歷隊頭數據,出隊列

隊頭節點的左不爲空,左節點入隊
隊頭節點的右不爲空,右節點入隊


5、統計樹的節點總個樹
將樹的遍歷輸出,改爲計數即可

6、統計樹的葉子節點個數
將樹的遍歷樹輸出,改爲計數即可(僅當節點左右節點都爲空時,計數才+1)

7、求第K層的節點個數
採用遞歸求第K-1的節點個數的方法解決
根爲空或K==0,返回0。如果 K== 1,返回1
遞歸走根的左子樹 K-1 + 遞歸走根的右子樹 K -1


8、二叉樹的高度

採用遞歸的方法,根的左或右不爲空,返回高的一側(計數器值大的)計數器+1

根不爲空,遞歸走根的左,遞歸走根的右

如果根左或右不爲空,則返回計數器值大的+1

注:由於計數器是函數內部的局部變量,防止函數退出時計數器被銷燬,所以定義成靜態變量


9、鏡像

(1)遞歸鏡像

將先序遍歷改爲交換根節點的左右指針域

(2)非遞歸鏡像

將層次遍歷改爲交換左右指針域即可


10、判讀是否是一棵完全二叉樹
藉助隊列,找出不符合完全二叉樹的條件時退出即可
不是完全二叉樹:
有右孩子,無左孩子
不滿足小於N-1層不滿(超過N-1層不滿)
對層次遍歷部分進行修改:
根節點不爲空入隊列,標記節點爲空
循環隊列不爲空
取隊頭節點,隊頭節點的左爲空且右不爲空,或左不爲空且標記節點不爲空(標記起始爲空,遇到不滿節點時,標記該節點)則不爲完全二叉樹,返回0
如果節點的右爲空(注:不滿節點的缺失只能缺失右,缺失左就是,不是完全二叉樹,或左右孩子都缺失)標記該不滿節點
刪除隊頭
如果隊頭節點(之前取得)的左節點不爲空,左節點入隊列
如果隊頭節點(之前取得)的右節點不爲空,右節點入隊列

循環退出則是完全二叉樹,返回1

11、銷燬
採用後序遍歷的方法銷燬(否則雙親節點先銷燬後,找不到孩子節點)
將後序遍歷改爲釋放空間即可

12、求二叉樹中節點最大的間距

方法1(先計算,後遞歸,順序計算)

從上向下求高度,重複計算的節點太多,時間複雜度:O(N^2)

注:maxlen使用指針傳參
根節點爲空,返回NULL
分別求左右子樹的高度,再相加(add
用中間值(最大值:maxlen)與求和結果進行比較,比中間大,更新中間值,maxlen = add;
遞歸根節點的左,遞歸根節點的右

返回最大值maxlen  

 

方法2(先遞歸,後計算,到着計算,遞歸同時統計高度)類似於求樹的高度---時間複雜度O(N)
遞歸走到空,再統計節點數(樹的高度)

 

根節點爲空,返回0
遞歸根的左,遞歸根的右(注:要保存返回的結果)

如果左的結果+右的結果大於中間值,更新中間值+1


 
返回大的結果(leftszrightsz)+1(三目運算符)

注:中間值使用指針傳參,統計時會多統計依次根節點所以結果還需-1

 

13、求節點的路徑

+遞歸 (參數樹根節點,棧,待求節點)

設置一個標誌位 tmp(起始爲0
 
根爲空,返回0
 
根節點入棧
 
根節點爲求的節點,返回1
 
如果根節點的左不爲空,遞歸根結點的左,並接收返回值(tmp)
 
如果tmp爲假,並且根的右不爲空,遞歸根的右,接收返回值
 
如果tmp爲假,出棧
 
返回tmp;


五、二叉樹線索化
線索化:(遞歸處理)
當前輪處理前驅(前驅節點已遍歷過,並已保存)
下一輪處理後繼(後繼節點未遍歷,不知道後繼節點。可在下輪通過前驅節點處理後繼)
注:由於採用遞歸的方法實現,並且需改變前驅節點的指向(後繼處理),所以前驅節點傳參時,傳二級指針 

1、前序線索化
(1)前序線索化(遞歸)
如果根節點不爲空
處理根節點前驅:
如果根節點的左爲空,根節點左指向前驅節點,根節點的左指針域狀態置爲THREAD
處理根節點後繼:
如果根節點的右爲空且前驅節點不爲空(解決第一個節點無前驅節點問題)
前驅節點的右指向根節點,前驅節點的右指針域狀態置爲THREAD

前驅節點更新爲當前節點
如果當前節點的左爲LINK:遞歸走當前節點的左
如果當前節點的右爲LINK:遞歸走當前節點的右

(2)前序線索化非遞歸:
獲取根節點cur
循環cur不爲空
循環遍歷cur的左側的非葉子節點(cur的左指針域爲LINK)
遍歷葉子節點
cur指向cur的右(當作子樹處理)

(3)前序線索化遍歷(遞歸):
獲取根節點(cur),cur不爲空
循環遍歷到最左側的非葉子節點(cur的左指針域爲LINK)
遍歷葉子節點
循環遍歷連續的後繼節點(cur的右指針域狀態爲THREAD)
如果cur的左指針域未線索化,當作子問題處理(遞歸cur的左)


2、中序線索化
(1)中序線索化(遞歸)
參數根節點,前驅節點
獲取根節點cur,
如果cur不爲空:
遞歸值最左側節點
處理前驅:如果cur的左爲空(cur的左指針域標記爲THREAD,cur的左指向前驅節點prev)
處理後繼:如果cur的右爲空(如果前驅節點不爲空且前驅節點的左指針域狀態爲THREAD,prev的右指針域指向當前節點cur,prev的右狀態標記爲THREAD)
更新前驅節點,前驅節點爲cur
遞歸處理右子樹

(2)中序線索化遍歷(非遞歸)
獲取根節點cur
cur不爲空
走到cur的最左側節點(cur的左指針域爲LINK)
遍歷葉子節點
循環遍歷後繼節點(cur不爲空且cur的右指針域爲THREAD)
走cur的右
注:如果cur爲NULL
遍歷數據

如果cur不爲空
走cur的右,cur = cur->right

3、後續線索化
(1)後序線索化(遞歸):
參數:根節點,前驅節點(二級指針)
如果根節點不爲空
遞歸根節點的左
遞歸根節點的右
//處理前驅節點:
根節點的左爲空
根節點的左指針域狀態置爲THREAD
根節點的左指針域指向前驅節點

//處理後繼節點:
前驅節點不爲空且前驅結點的右爲空
前驅節點的右指針域狀態置爲THREAD
前驅結點的右指向根節點

更新前驅節點(prev = cur)

(2)後繼線索化的遍歷:
後繼線索化的出口爲根節點,需標記左側所走過的節點
獲取根節點cur
循環遍歷,cur不爲空
循環走到最左非葉節點且該節點不爲標記節點(已遍歷的節點)(cur->_leftpag == LINK && prev != cur)
循環遍歷連續的右側節點(處理右單支)(cur && cur->_rigntpag == THREAD),注要標記已遍歷的節點
判斷是否到達根節點(cur == _root)到達遍歷根節點,退出
循環遍歷左節點(處理左單支)(cur && cur->_right == prev),遍歷cur節點,標記cur節點,cur回退(cur = cur->_parent)
如果cur有右節點(cur && cur->_rightpag == LINK)走右子樹(cur = cur->_right)

六、代碼鏈接

https://github.com/weienjun/Ccode/tree/master/BinaryTree























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