1. 程式人生 > >數據結構之二叉樹

數據結構之二叉樹

1+n 進行 一維數組 ali 超過 order ace strong class

概要

參考《大話數據結構》,把常用的基本數據結構梳理一下。

本節介紹二叉樹。

?


定義

??二叉樹(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\)

為度是 \(1\) 的結點數。則樹 \(T\) 的結點總數 \(n=n_0+n_1+n_2\). 我們換個角度數一數樹的連接線數,度為 \(1\) 的分支線為 \(1\) 條,度為 \(2\) 的分支線為 \(2\) 條,即共 \(n_1+2n_2\) 條,又顯然樹的連接樹為結點總數減去 \(1\),即 \(n-1 = n_1+ 2n_2\),即 \(n_0 = n_2 +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\))有:

  1. 如果 \(i=1\),則結點 \(i\) 是二叉樹的根,無雙親;如果 \(i>1\),則其雙親是結點 \(floor(i/2)\).
  2. 如果 \(2i>n\),則結點 \(i\) 無左孩子(結點 \(i\) 為葉子結點);否則其左孩子是結點 \(2i\).
  3. 如果 \(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)是指從根結點出發,按照某種次序依次訪問二叉樹中所有結點,使得每個結點被訪問一次且僅被訪問一次

二叉樹的遍歷方式很多,如果我們限制了從左到右的習慣方式,那麽主要分為四種:

  1. 前序遍歷。規則是若二叉樹為空,則空操作返回,否則先訪問根結點,然後前序遍歷左子樹,再前序遍歷右子樹。如圖,遍歷順序為:ABDGHCEIF.

    技術分享圖片

  2. 中序遍歷。規則是若二叉樹為空,則空操作返回,否則從根結點開始(註意並不是先訪問根結點),中序遍歷根結點的左子樹,然後訪問根結點,最後中序遍歷右子樹。如圖,遍歷順序為:GDHBAEICF.

    技術分享圖片

  3. 後序遍歷。規則是若二叉樹為空,則空操作返回,否則從左到右先葉子後結點的方式遍歷訪問左右子樹,最後是訪問根結點。如圖,遍歷順序為:GHDBIEFCA.

    技術分享圖片

  4. 層序遍歷。規則是若二叉樹為空,則空操作返回,否則從樹的第一層,也就是從根結點開始訪問,從上而下逐層遍歷,在同一層中,按從左到右的順序對結點逐個訪問。如圖,遍歷順序為: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;
}

上邊主要的疑惑是指針的引用,暫時先參考二叉樹建立,指針問題. 遇到修改指針的得註意了。
?

數據結構之二叉樹