數據結構之二叉樹
概要
參考《大話數據結構》,把常用的基本數據結構梳理一下。
本節介紹二叉樹。
?
定義
??二叉樹(Binary Tree)是 \(n\) (\(n \geqslant 0\))個結點的有限集合,該集合或者為空集(稱為空二叉樹),或者由一個根結點和兩棵互不相交的、分別稱為根結點的左子樹和右子樹的二叉樹組成。
二叉樹的特點:
- 每個結點最多有兩棵子樹,所以二叉樹中不存在度大於 \(2\) 的結點。
- 左子樹和右子樹是有順序的,次序不能任意顛倒。
- 即使樹中某結點只有一棵子樹,也要區分它是左子樹還是右子樹。
??所有的結點都只有左子樹的二叉樹叫左斜樹。所有結點都是只有右子樹的二叉樹叫右斜樹。這兩者統稱為斜樹
??在一棵二叉樹中,如果所有分支結點都存在左子樹和右子樹,並且所有葉子都在同一層上,這樣的二叉樹稱為滿二叉樹。
??對一棵具有 \(n\) 個結點的二叉樹按層序編號,如果編號為 \(i\) (\(1 \leqslant i \leqslant n\))的結點與同樣深度的滿二叉樹中編號為 \(i\) 的結點在二叉樹中位置完全相同,則這棵二叉樹稱為完全二叉樹。如圖:
註意:首先滿二叉樹一定是一棵完全二叉樹,但完全二叉樹又不一定是滿的,其次按層序編號相同的結點是一一對應的。從這裏得出一些完全二叉樹的特點:
- 葉子結點只能出現在最下兩層
- 最下層的葉子一定集中在左部連續位置
- 倒數二層,若有葉子結點,一定都在右部連續位置
- 如果結點度為 \(1\),則該結點只有左孩子,即不存在只有右子樹的情況
- 同樣結點數的二叉樹,完全二叉樹的深度最小
?
二叉樹的性質
??性質 1: 在二叉樹的第 \(i\) 層上至多有 \(2^{i-1}\) 個結點(\(i \geqslant 1\))。
??性質 2: 深度為 \(k\) 的二叉樹至多有 \(2^k-1\) 個結點(\(k \geqslant 1\))。
??性質 3: 對任何一棵二叉樹 \(T\),如果其終端結點數為 \(n_0\),度為 \(2\) 的結點數為 \(n_2\),則 \(n_0 = n_2 +1\).
說白了就是葉子結點數比度為 \(2\) 的結點數多一個。我們設 \(n_1\)
??性質 4: 具有 \(n\) 個結點的完全二叉樹的深度為 \(floor(\log_2n)+1\)(\(floor(x)\) 表示不超過 \(x\) 的最大整數)。
??性質 5: 如果對一棵有 \(n\) 個結點的完全二叉樹(其深度為 \(floor(\log_2n)+1\))的結點按層序編號(從第一層到 \(floor(\log_2n)+1\) 層,每層從左到右),對任一結點 \(i\)(\(1 \leqslant i \leqslant n\))有:
- 如果 \(i=1\),則結點 \(i\) 是二叉樹的根,無雙親;如果 \(i>1\),則其雙親是結點 \(floor(i/2)\).
- 如果 \(2i>n\),則結點 \(i\) 無左孩子(結點 \(i\) 為葉子結點);否則其左孩子是結點 \(2i\).
- 如果 \(2i+1>n\),則結點 \(i\) 無右孩子,否則其右孩子是結點 \(2i+1\).
可以與上圖結點理解。
?
二叉樹的存儲結構
二叉樹的順序存儲結構
二叉樹的順序存儲結構是用一維數組存儲二叉樹的結點,並且結點的存儲位置,也就是數組的下標要能體現結點之間的邏輯關系。比如下面一棵完全二叉樹:
將這棵二叉樹存入到數組中,相應的下標對應其同樣的位置,如下圖:
對於一般的二叉樹,盡管層序編號不能反映邏輯關系,但是可以將其按完全二叉樹編號,只不過,把不存在的結點設置為空就行了。但是像極端的情況比如深度為 \(k\) 的右斜樹,它只有 \(k\) 個結點,卻需要分配 \(2^k-1\) 個存儲單元空間,造成了極大的空間浪費,所以順序存儲結構一般只用於完全二叉樹。
?
二叉鏈表
二叉樹每個結點最多有兩個孩子,所以為它設計一個數據域和兩個指針域是比較自然的想法,我們稱這樣的鏈表叫做二叉鏈表。如圖:
其中 data 是數據域, lchild 和 rchild 都是指針域,分別存放指向左孩子和右孩子的指針。二叉鏈表的結點結構定義代碼如下:
typedef int TElemType; // TElemType 類型根據實際情況而定,這裏假設為 int
struct BiTNode
{
TElemType data;
BiTNode *lchild, *rchild;
};
如果有需要,還可以再增加一個指向雙親的指針域,那樣就稱之為三叉鏈表。
?
遍歷二叉樹
??二叉樹的遍歷(traversing binary tree)是指從根結點出發,按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問一次且僅被訪問一次。
二叉樹的遍歷方式很多,如果我們限制了從左到右的習慣方式,那麽主要分為四種:
前序遍歷。規則是若二叉樹為空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。如圖,遍歷順序為:ABDGHCEIF.
中序遍歷。規則是若二叉樹為空,則空操作返回,否則從根結點開始(註意並不是先訪問根結點),中序遍歷根結點的左子樹,然後訪問根結點,最後中序遍歷右子樹。如圖,遍歷順序為:GDHBAEICF.
後序遍歷。規則是若二叉樹為空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點。如圖,遍歷順序為:GHDBIEFCA.
層序遍歷。規則是若二叉樹為空,則空操作返回,否則從樹的第一層,也就是從根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。如圖,遍歷順序為:ABCDEFGHI.
?
二叉樹樹的建立與遍歷代碼實現
如果我們要在內存中建立一個如下左圖這樣的二叉樹,為了能讓每個結點確認是否有左右孩子,我們對它進行擴展,就右圖的樣子,也就是將二叉樹中每個結點的空指針引出一個虛結點,其值為一個特定值,比如“#”。我們稱這種處理後的二叉樹為原二叉樹的擴展二叉樹。擴展二叉樹就可以做到一個遍歷序列確定一棵二叉樹。比如右圖的遍歷序列就為 AB#D##C##.
有了這樣的準備就可以著手建立二叉樹了。創建二叉樹同遍歷方法一樣,也有不同的創建方法。現在我們用前序實現二叉樹的建議。代碼如下:
#include<iostream>
#include<cstdio> //getchar()
using namespace std;
typedef char TElemType; // TElwmType 類型根據實際情況而定,這裏假設為 int
struct BiTNode
{
TElemType data;
BiTNode *lchild, *rchild;
};
void CreateBiTree(BiTNode* (&T)) //這裏傳入的是指針的引用,因為牽扯到修改指針的值
{
//讀入字符串 ch, 前序構造樹
//cout<<"請輸入創建一棵二叉樹的結點數據"<<endl;
TElemType ch = getchar();
//cin>>ch;
if (ch == '#') //其中getchar()為逐個讀入標準庫函數
T = NULL;
else
{
T = new BiTNode;//產生新的子樹
T->data = ch;
CreateBiTree(T->lchild);//遞歸創建左子樹
CreateBiTree(T->rchild);//遞歸創建右子樹
}
}
void PreOrderTraverse(BiTNode* T)
{
//前序遍歷
if(T) //結點不為空時執行
{
cout<< T->data;
PreOrderTraverse(T->lchild);
PreOrderTraverse(T->rchild);
}
else
cout<<"";
}
void InOrderTraverse(BiTNode* T)
{
//中序遍歷
if(T) //結點不為空時執行
{
InOrderTraverse(T->lchild);
cout<< T->data;
InOrderTraverse(T->rchild);
}
else
cout<<"";
}
void PostOrderTraverse(BiTNode* T)
{
//後序遍歷
if(T) //結點不為空時執行
{
PostOrderTraverse(T->lchild);
PostOrderTraverse(T->rchild);
cout<< T->data;
}
else
cout<<"";
}
int main()
{
//const char* ch = "AB#D##C##";
BiTNode *T;
cout<<"創建樹結構,請輸入字符,‘#’ 代表空:"<<endl;
CreateBiTree(T); //輸入 AB#D##C##
cout<<"創建完畢"<<endl;
cout<<T->data<<endl;
cout<<"前序遍歷結果:"<<endl;
PreOrderTraverse(T);//輸出 ABDC
cout<<endl;
cout<<"中序遍歷結果:"<<endl;
InOrderTraverse(T); //輸出 BDAC
cout<<endl;
cout<<"後序遍歷結果:"<<endl;
PostOrderTraverse(T);//輸出 DBCA
cout<<endl;
return 0;
}
上邊主要的疑惑是指針的引用,暫時先參考二叉樹建立,指針問題. 遇到修改指針的得註意了。
?
數據結構之二叉樹