資料結構之二叉樹的建立

顧小豆發表於2018-02-24

文章開頭首先要感謝一下國嵌嵌入式教育的工作者們。

建立二叉樹 

二叉樹不僅比通用樹結構簡練,而且同時擁有通用樹相同的操作。要想建立二叉樹,首先就得了解一下二叉樹的儲存結構。已知二叉樹的儲存結構分為順序儲存結構和鏈式儲存結構。其中鏈式儲存結構又分為二叉連結串列和三叉連結串列。

1. 順序儲存結構:

按照順序儲存結構的定義,在此約定用一組地址連續的儲存單元一次自上而下、自左至右儲存完全二叉樹上的節點元素,即將完全二叉樹上編號為i的結點元素儲存在如上定義的一維陣列中下標為i-1的分量中。如下表1所示為圖1所示的完全二叉樹的順序儲存結構:

  圖1

表1

1

2

3

4

5

6

7

8

9

10

對於一般二叉樹,則將其每一個結點與完全二叉樹的結點相對照,儲存在一維陣列的相應分量中,如下表2所示圖2所示的非完全二叉樹的順序儲存結構::

  圖2

表2

1

2

3

4

5

0

6

0

7

8

圖中“0”表示不存在此結點。由此可見順序儲存結構僅適用於完全二叉樹。因為,在最壞的情況下,一個深度為k且只有k個結點的單枝樹(樹中不存在度為2的結點)卻需要長度為2k-1的一維陣列。造成儲存記憶體的浪費。

鏈式儲存結構:

設計不同的結點結構可構成不同形式的鏈式儲存結構。由二叉樹的定義得知,二叉樹的結點由一個資料元素和分別指向其左、右子樹的兩個分支構成,則表示二叉樹的連結串列中的結點至少包含3個域:資料域和左、右指標域。如下所示:

Lchid

Data

Rchild

有時,為了便於找到結點的雙親,則還可在結點結構中增加一個指向其雙親結點的指標域,如下所示:

Lchild

Data

Parent

Rchild

利用這兩種結點結構所得的二叉樹的儲存結構分別稱之為二叉連結串列三叉連結串列。連結串列的頭指標指向二叉樹的根結點。因此,我們可知在含有n個節點的二叉連結串列中有n+1個空鏈域用於存放二叉樹結點。到此,我們應該明白可以通過單連結串列的實現思想來建立二叉樹。單連結串列內容參考單連結串列C語言實現

我們接下來講一下如何通過二叉連結串列鏈式儲存結構對二叉樹進行操作,其重點就是如何在二叉樹中定位結點的位置?那麼如何定位呢?

現實生活中我們總會遇到問路的情況,指路的人一般都會說直走、到哪個路口左拐,到哪個路口右拐。這就讓我們聯想到二叉樹的結點是否可以類比成現實中的路口,它的兩個子樹是否可以當做左拐或右拐呢?

因此,前輩們想出了指路法定位結點:


指路法通過根結點與目標結點的相對位置進行定位,指路法可以避開二叉樹遞迴的性質線性定位。

思想:在C語言中可以利用bit位進行指路。。。



用結構體來定義二叉樹中的指標域,二叉樹的頭結點與資料域也可以用結構體實現。

二叉樹的重點操作是定位,下面我們看一下定位操作:


位的關鍵技巧:

1.利用二進位制中的0和1分別表示left和right;

2.位運算是實現指路法的基礎。

至此,我們就可以來實現二叉樹的相關操作了,上程式碼。

首先定義相關結構體及其他變數

#define BT_LEFT 0            //  左邊
#define BT_RIGHT 1           //  右邊
// 定義新資料型別,用於封裝函式
typedef void BTree;
typedef unsigned long long BTPos;
// 定義二叉樹左右指標結構體
typedef struct _tag_BTreeNode BTreeNode;
struct _tag_BTreeNode
{
    BTreeNode* left;         // 二叉樹左結點指標
    BTreeNode* right;        // 二叉樹右結點指標
};

// 定義二叉樹根結點結構體
typedef struct _tag_BTree TBTree;
struct _tag_BTree
{
    int count;                // 記錄二叉樹結點個數
    BTreeNode* root;          // 二叉樹結點指標結構體,指向根結點
};
1.建立二叉樹
// 建立二叉樹
BTree* BTree_Create() // O(1)
{
    // 定義二叉樹結點結構體變數,並申請記憶體
    TBTree* ret = (TBTree*)malloc(sizeof(TBTree));
    // 申請成功,初始化二叉樹為空樹
    if( ret != NULL )
    {
        ret->count = 0;
        ret->root = NULL;
    }
    
    return ret;
}

建立二叉樹比較簡單,等同於簡單的單連結串列建立方法。所以銷燬和清空二叉樹也會單連結串列的操作雷同。

2.銷燬與清空單連結串列

// 銷燬二叉樹
void BTree_Destroy(BTree* tree) // O(1)
{
    free(tree);       // 釋放記憶體
}
// 清空二叉樹
void BTree_Clear(BTree* tree) // O(1)
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 引數合法性OK,將二叉樹置為空樹
    if( btree != NULL )
    {
        btree->count = 0;
        btree->root = NULL;
    }
}

3.插入結點

插入結點是二叉樹操作的重點,程式碼如下:

// 在二叉樹指定位置pos插入結點node
// pos:定位的方向,二進位制:0表示左,1表示右
// count:定位次數,移動指標次數
// flag:插入方向 BT_LEFT or BT_RIGHT
int BTree_Insert(BTree* tree, BTreeNode* node, BTPos pos, int count, int flag) // O(n) 
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;    
    // 入口引數合法性檢查,插入的二叉樹不為空,插入的結點不為空,插入方向正確
    int ret = (btree != NULL) && (node != NULL) && ((flag == BT_LEFT) || (flag == BT_RIGHT));
    int bit = 0;
    // 入口引數合法性ok
    if( ret )
    {
    	// 定義二叉樹左右指標結構體臨時變數
        BTreeNode* parent = NULL;
    	// 定義二叉樹左右指標結構體變數,存放當前結點地址
        BTreeNode* current = btree->root;
        // 初始化插入結點的左右指標地址,預設為NULL
        node->left = NULL;
        node->right = NULL;
        // 開始定位  定位次數不為零,插入的位置不是根結點
        while( (count > 0) && (current != NULL) )
        {
            // 取定位方向引數最右邊bit位用於判斷左右
            bit = pos & 1;
            pos = pos >> 1;
            // 臨時變數更新當前指標,儲存插入結點位置的雙親指標
            parent = current;
            // 左邊,向左移動指標
            if( bit == BT_LEFT )
            {
                current = current->left;        // 將當前結點指標指向左邊子結點指標
            }
            // 右邊,向右移動指標
            else if( bit == BT_RIGHT )
            {
                current = current->right;       // 將當前結點指標指向右邊子結點指標
            }
            // 定位次數減1
            count--;
        }
        // 定位完成後,判斷待插入結點的插入位置
        // 左邊
        if( flag == BT_LEFT )
        {
            node->left = current;           // 將帶插入結點的左指標指向當前結點
        }
        // 右邊
        else if( flag == BT_RIGHT )
        {
            node->right = current;          // 將待插入結點的右指標指向當前結點
        }
        // 當前結點指標不為空,即不是根結點
        if( parent != NULL )
        {
            // 左邊
            if( bit == BT_LEFT )
            {
                parent->left = node;         // 將插入結點位置的雙親指標的左指標指向待插入結點
            }
            // 右邊
            else if( bit == BT_RIGHT )
            {
                parent->right = node;        // 將插入結點位置的雙親指標的右指標指向待插入結點
            }
        }
        // 插入的是首結點,即根結點
        else
        {
            btree->root = node;              // 將根結點指向待插入結點
        }
        // 二叉樹結點個數加1
        btree->count++;
    }
    
    return ret;
}

通過上面的程式碼,我們發現二叉樹的插入與單連結串列的參入操作實現方式基本雷同,不同的就是二叉樹指標分左右,雙指標操作,而單連結串列操作的指標只有一個next。插入的重點是先根據引數定好位置同時時刻更新找到的新位置,然後根據插入的方向將找到的位置的左或右左右指標指向要插入的結點地址,將要插入的結點地址的左右指標指向相對應的原來位置的左右指標位置。

4.顯示二叉樹

插入的二叉樹,就要驗證一下插入的是否正確,所以我們通過二叉樹的顯示函式來顯示二叉樹的元素,來驗證插入的正確性。程式碼如下:

// 顯示二叉樹
void BTree_Display(BTree* tree, BTree_Printf* pFunc, int gap, char div) // O(n)
{
	  // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 合法性檢查OK,呼叫顯示遞迴函式
    if( btree != NULL )
    {
        recursive_display(btree->root, pFunc, 0, gap, div);
    }
}

同通用樹一樣,二叉樹的顯示函式也需要遞迴來實現,遞迴實現程式碼如下:

// 顯示遞迴函式
static void recursive_display(BTreeNode* node, BTree_Printf* pFunc, int format, int gap, char div) // O(n)
{
    int i = 0;
    // 合法性檢查OK
    if( (node != NULL) && (pFunc != NULL) )
    {
    	// 列印格式符
        for(i=0; i<format; i++)
        {
            printf("%c", div);
        }
        // 列印內容
        pFunc(node);
        
        printf("\n");
        // 存在左子樹結點或存在右子樹結點
        if( (node->left != NULL) || (node->right != NULL) )
        {
 
            recursive_display(node->left, pFunc, format + gap, gap, div);      // 呼叫遞迴函式列印左子樹結點
            recursive_display(node->right, pFunc, format + gap, gap, div);     // 呼叫遞迴函式列印右子樹結點
        }
    }
    // 根結點為空
    else
    {    	  
    	  // 列印格式符
        for(i=0; i<format; i++)
        {
            printf("%c", div);
        }
        printf("\n");
    }
}

與通用樹不同的地方就是二叉樹需要分左右顯示。

5.刪除指定結點

// 刪除指定位置的結點
BTreeNode* BTree_Delete(BTree* tree, BTPos pos, int count) // O(n)
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 定義返回變數
    BTreeNode* ret = NULL; 
    int bit = 0;    
	  // 入口引數合法性檢查ok
    if( btree != NULL )
    {
    	  // 定義二叉樹左右指標結構體臨時變數
        BTreeNode* parent = NULL;
    	  // 定義二叉樹左右指標結構體變數,存放當前結點地址
        BTreeNode* current = btree->root;        
        // 開始定位  定位次數不為零,刪除的位置不是根結點
        while( (count > 0) && (current != NULL) )
        {
            // 取定位方向引數最右邊bit位用於判斷左右
            bit = pos & 1;
            pos = pos >> 1;
            
            // 臨時變數更新當前指標,儲存刪除結點位置的雙親指標
            parent = current;            
            // 左邊,向左移動指標
            if( bit == BT_LEFT )
            {
                current = current->left;         // 將刪除結點位置的雙親指標的左指標指向待刪除結點
            }
            // 右邊,向右移動指標
            else if( bit == BT_RIGHT )
            {
                current = current->right;        // 將刪除結點位置的雙親指標的右指標指向待刪除結點
            }
            // 定位次數減1             
            count--;
        }        
        // 當前結點指標不為空,即不是根結點
        if( parent != NULL )
        {
            // 左邊
            if( bit == BT_LEFT )
            {
                parent->left = NULL;         // 將結點左子樹位置指向空指標,即刪除左結點
            }
            // 右邊
            else if( bit == BT_RIGHT )
            {
                parent->right = NULL;        // 將結點右子樹位置指向空指標,即刪除右結點
            }
        }
        // 刪除的是首結點,即根結點
        else
        {
            btree->root = NULL;              // 將根結點指向空指標,即刪除所有結點
        }
        // 返回刪除元素結點
        ret = current;
        // 更新二叉樹結點個數,結點個數減去刪除的結點個數
        btree->count = btree->count - recursive_count(ret);
    }
    
    return ret;
}

刪除結點的操作重點和插入一樣,首先通過引數定位到要刪除的結點,然後根據要刪除的方向將該結點的左或右指標指向NULL即可。需要主要的是二叉樹的數量更新不是單單的減1就行的,這得需要根據刪除的結點來計算出該結點相應子樹下的所有結點個數,然後減去這個個數才行btree->count = btree->count - recursive_count(ret);
,所以我們同個一個遞迴函式求結點個數。遞迴函式程式碼如下:

// 求二叉樹結點子樹結點個數
static int recursive_count(BTreeNode* root) // O(n)
{
    int ret = 0;
    // 合法性檢查ok
    if( root != NULL )
    {
        ret = recursive_count(root->left) + 1 + recursive_count(root->right);    // 呼叫遞迴函式計算個數
    }
    // 返回個數
    return ret;
}

6.獲取指定結點

// 獲取指定位置的結點
BTreeNode* BTree_Get(BTree* tree, BTPos pos, int count) // O(n)
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 定義返回變數
    BTreeNode* ret = NULL; 
    int bit = 0;
    
    // 入口引數合法性檢查ok
    if( btree != NULL )
    {
    	// 定義二叉樹左右指標結構體變數,存放當前結點地址
        BTreeNode* current = btree->root;        
        // 開始定位  定位次數不為零,刪除的位置不是根結點
        while( (count > 0) && (current != NULL) )
        {
            // 取定位方向引數最右邊bit位用於判斷左右
            bit = pos & 1;
            pos = pos >> 1;
            
            // 左邊,向左移動指標
            if( bit == BT_LEFT )
            {
                current = current->left;
            }
            // 右邊,向右移動指標
            else if( bit == BT_RIGHT )
            {
                current = current->right;
            }
            
            // 定位次數減1   
            count--;
        }
        
        ret = current;
    }
    
    return ret;
}

獲取指定位置的結點,首先就是定位置到要獲取的位置,然後返回該結點的地址即可。

7.獲取根結點

// 獲取根結點
BTreeNode* BTree_Root(BTree* tree) // O(1)
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 定義返回變數
    BTreeNode* ret = NULL;
    // 入口引數合法性檢查ok,返回根結點地址
    if( btree != NULL )
    {
        ret = btree->root;
    }
    
    return ret;
}

獲取根結點就是獲取二叉樹的頭結點,直接返回頭結點地址即可。

8.獲取二叉樹的高度(深度)

// 獲取二叉樹高度
int BTree_Height(BTree* tree) // O(n)
{
    // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 定義返回變數
    int ret = 0;    
    // 入口引數合法性檢查ok,呼叫遞迴函式求二叉樹高度
    if( btree != NULL )
    {
        ret = recursive_height(btree->root);
    }
    // 返回二叉樹高度
    return ret;
}

和通用樹一樣,二叉樹的高度獲取也需要遞迴來實現,遞迴函式程式碼如下:

// 求二叉樹高度遞迴函式
static int recursive_height(BTreeNode* root) // O(n)
{
    // 定義返回值變數
    int ret = 0;
    // 入口引數合法性檢查OK
    if( root != NULL )
    {
        int lh = recursive_height(root->left);       // 求左子樹高度
        int rh = recursive_height(root->right);      // 求右子樹高度
        // 求左右子樹高度最大值
        ret = ((lh > rh) ? lh : rh) + 1;
    }
    // 返回二叉樹高度
    return ret;
}

分別求左右子樹的高度,然後比較出最大值返回。

9.獲取二叉樹的度

// 獲取二叉樹度
int BTree_Degree(BTree* tree) // O(n)
{
	  // 定義二叉樹結點結構體變數,強制轉換入口引數
    TBTree* btree = (TBTree*)tree;
    // 定義返回變數
    int ret = 0;    
	  // 入口引數合法性檢查ok,呼叫求二叉樹度遞迴函式
    if( btree != NULL )
    {
        ret = recursive_degree(btree->root);
    }
    // 返回二叉樹的度
    return ret;
}
和通用樹一樣,二叉樹的度獲取也需要遞迴來實現,遞迴函式程式碼如下:
// 求二叉樹度遞迴函式,最大度為2
static int recursive_degree(BTreeNode* root) // O(n)
{
	  // 定義返回值變數
    int ret = 0;    
    // 入口引數合法性檢查OK
    if( root != NULL )
    {
    	  // 左子樹結點不為空,度加1
        if( root->left != NULL )
        {
            ret++;
        }        
    	  // 右子樹結點不為空,度加1
        if( root->right != NULL )
        {
            ret++;
        }
        // 度為1,呼叫遞迴函式求其他結點度
        if( ret == 1 )
        {
            int ld = recursive_degree(root->left);       // 求左子樹結點的度
            int rd = recursive_degree(root->right);      // 求右子樹結點的度
            // 求左子樹結點度的最大值
            if( ret < ld )
            {
                ret = ld;
            }            
            // 求右子樹結點度的最大值
            if( ret < rd )
            {
                ret = rd;
            }
        }
    }
    // 返回度
    return ret;
}

因為二叉樹的度最大也就是2,所以只有在當前求得度為1時才需要呼叫遞迴函式來求其他子樹結點的度。

驗證程式碼如下:

#include <stdio.h>
#include <stdlib.h>
#include "BTree.h"

/* run this program using the console pauser or add your own getch, system("pause") or input loop */
// 定義二叉樹資料域結構體
struct Node
{
    BTreeNode header;
    char v;
};
// 定義列印內容函式
void printf_data(BTreeNode* node)
{
    if( node != NULL )
    {
        printf("%c", ((struct Node*)node)->v);
    }
}

int main(int argc, char *argv[])
{
	  // 建立二叉樹
    BTree* tree = BTree_Create();
    // 定義要插入結點元素
    struct Node n1 = {{NULL, NULL}, 'A'};
    struct Node n2 = {{NULL, NULL}, 'B'};
    struct Node n3 = {{NULL, NULL}, 'C'};
    struct Node n4 = {{NULL, NULL}, 'D'};
    struct Node n5 = {{NULL, NULL}, 'E'};
    struct Node n6 = {{NULL, NULL}, 'F'};    
    // 插入結點元素
    BTree_Insert(tree, (BTreeNode*)&n1, 0, 0, 0);
    BTree_Insert(tree, (BTreeNode*)&n2, 0x00, 1, 0);
    BTree_Insert(tree, (BTreeNode*)&n3, 0x01, 1, 0);
    BTree_Insert(tree, (BTreeNode*)&n4, 0x00, 2, 0);
    BTree_Insert(tree, (BTreeNode*)&n5, 0x02, 2, 0);
    BTree_Insert(tree, (BTreeNode*)&n6, 0x02, 3, 0);
    // 列印相應提示內容
    printf("Height: %d\n", BTree_Height(tree));
    printf("Degree: %d\n", BTree_Degree(tree));
    printf("Count: %d\n", BTree_Count(tree));
    printf("Position At (0x02, 2): %c\n", ((struct Node*)BTree_Get(tree, 0x02, 2))->v);
    printf("Full Tree: \n");
    // 顯示二叉樹
    BTree_Display(tree, printf_data, 4, '-');
    // 測試刪除結點功能
    BTree_Delete(tree, 0x00, 1);   
    // 列印相應提示內容
    printf("After Delete B: \n");
    printf("Height: %d\n", BTree_Height(tree));
    printf("Degree: %d\n", BTree_Degree(tree));
    printf("Count: %d\n", BTree_Count(tree));
    printf("Full Tree: \n");    
    // 顯示刪除結點後的二叉樹
    BTree_Display(tree, printf_data, 4, '-');
    
    // 測試清空二叉樹功能
    BTree_Clear(tree);    
    // 列印相應提示內容
    printf("After Clear: \n");
    printf("Height: %d\n", BTree_Height(tree));
    printf("Degree: %d\n", BTree_Degree(tree));
    printf("Count: %d\n", BTree_Count(tree));    
    // 顯示清空後的二叉樹
    BTree_Display(tree, printf_data, 4, '-');
    // 銷燬二叉樹
    BTree_Destroy(tree);
    
	return 0;
}

通過理解相關實現程式碼,我們發現通過指路法可以方便的定位二叉樹中的結點,基於指路法的二叉樹在插入,刪除和獲取操作的實現細節上與單連結串列相似。

整體實現程式碼:二叉樹C語言實現程式碼

相關文章