資料結構之通用樹結構的實現

顧小豆發表於2018-02-17

之前我們講了樹的定義和操作,這節我們講一下如何實現這些操作。既然要考慮如何實現,那就得說說樹的儲存結構。

大家都知道樹是非線性資料結構,顯然我們無法用陣列來表示樹的邏輯結構。那我們應該怎麼辦呢?通過什麼來表示呢?其實可

以設計結構體陣列對結點間的關係進行表述。如下圖所示:


從上圖發現,將根結點的雙親定義為-1,表示其沒有雙親;將根的孩子結點的雙親定義為0,表示其雙親是根結點;將根結點孩

子1的孩子結點的雙親定義為1,表示其雙親是根結點的孩子結點1;還有雙親定義為根結點孩3,以此類推就會有4、5、6、7...

等等雙親。通過Child來存放其孩子結點的在結構體陣列中下標,這樣根據雙親Parent和孩子結點Child就能輕鬆的訪問樹結構裡

所有元素了。

接下來我們考慮兩個現實的問題:1.樹結構需要新增刪除結點,陣列儲存是否足夠靈活?2.每個結點的子結點可以有多個,如何

儲存?到此是不是感覺到很頭疼呢?既然要實現樹結構的操作,當然希望是一個通用的樹結構的操作,可以任意插入結點、刪除

結點等。那我們到底應該怎麼辦呢?

大家都知道,之前我們學習線性表時說過,連結串列是可以無限擴充套件,任意插入的,不限長度的,這裡我們是否可以也用連結串列代替這

個結構陣列呢?

Of course!我們當然可以利用連結串列組織樹中的各個結點,連結串列中的前後關係不代表結點間的邏輯關係,結點的邏輯關係由child數

據域描述,child資料域儲存其他結點的儲存地址。既然提到連結串列,來我們這裡就得複用之前已經實現的連結串列程式碼,參考線性表之

單連結串列

這裡我們的定義兩個結構體,連結串列結點結構體和樹結點結構體。如下圖:


樹中每一個結點包含一個指向父結點的指標(如下圖紅色箭頭所示)。


樹中每一個結點都是同一個連結串列中的資料元素,根結點是連結串列的首元素,根結點的孩子結點從左往右分別是連結串列的第二個、第三

個。。。等元素,根結點的孩子結點的孩子結點緊跟在連結串列後面但是要注意:樹結點在連結串列中的位置不代表樹的任何邏輯關

系。


下面我們來看一下樹的詳細架構圖:


由上圖我們發現要實現樹結構的操作,勢必需要兩個連結串列,一個用於記錄樹所有元素,我們這裡稱為組織連結串列、另一個是孩子鏈

表用來描述樹結構的邏輯關係。

說了這麼多,我們接下來一步一步的來實現上一節描述的那些操作。

1.建立樹

// 建立樹  
GTree* GTree_Create()
{
    return LinkList_Create();    // 呼叫連結串列建立函式,定義一個空組織連結串列,並返回
}

這裡沒啥好說的,不明白的小夥伴可以參考之前的章節。

2.銷燬樹和清空樹

// 銷燬樹
void GTree_Destroy(GTree* tree)
{
    GTree_Clear(tree);
    LinkList_Destroy(tree);
}
// 清空樹
void GTree_Clear(GTree* tree)
{
     GTree_Delete(tree, 0);
}

銷燬樹就是將所有的樹結構清空,並且釋放所有申請記憶體,代表著這個東西不存在了,清空樹就是將樹變成空樹,也就是將其從

根結點刪除。

3.插入資料

根據結構圖我們知道,樹中的一個元素不僅要在組織連結串列中,還要在其組織連結串列的孩子連結串列中,這就需要申明兩個連結串列結點結構

體記憶體來用於插入連結串列(比如結構圖中的成員B不僅在組織連結串列中,而且還包含在A成員的孩子連結串列中),實現方法如下:

首先定義插入所需要的樹結點結構體變數,用於處理插入元素,接著進行入口引數合法性檢查,如果不合法,直接返回NULL;

如果合法,開始插入元素:

1.定義存放樹結點、孩子結點和插入元素結點結構體變數,並且分別為其定義的結構體申請記憶體,同時獲取當前插入位置雙親的

元素地址;

2.對申請記憶體合法性進行檢查;

    2.1.記憶體申請成功:

          2.1.1.處理插入元素,儲存插入元素資料、初始化插入元素雙親(預設為NULL,無雙親)、建立孩子結點連結串列;

          2.1.2.將樹結點和孩子結點地址指向插入元素結點地址,並在樹結點連結串列(組織連結串列)尾部插入樹結點元素;

          2.1.3.檢查一下插入位置是否合法,合法,將插入元素的雙親指向當前元素(雙親)的結點地址,在樹孩子結點連結串列(孩子

          連結串列)尾部插入孩子結點元素,否則,釋放申請的孩子結點記憶體,退出插入操作;

    2.2.記憶體申請失敗,釋放記憶體,退出插入操作;

實現程式碼如下:

// 在樹結構指定的位置pPos插入元素data
// pPos:要插入元素的雙親的位置,即在那個雙親下插入
int GTree_Insert(GTree* tree, GTreeData* data, int pPos)
{
    // 定義連結串列結點結構體
    LinkList* list = (LinkList*)tree;
    // 定義返回變數,並且檢查引數合法性
    int ret = (list != NULL) && (data != NULL) && (pPos < LinkList_Length(list));
    // 合法性OK
    if( ret )
    {
        TLNode* trNode = (TLNode*)malloc(sizeof(TLNode));     // 定義連結串列結點結構體,並申請記憶體,存放樹結點連結串列成員
        TLNode* cldNode = (TLNode*)malloc(sizeof(TLNode));    // 定義連結串列結點結構體,並申請記憶體,存放孩子結點連結串列成員
        TLNode* pNode = (TLNode*)LinkList_Get(list, pPos);    // 定義連結串列結點結構體,儲存pPos位置的元素
        GTreeNode* cNode = (GTreeNode*)malloc(sizeof(GTreeNode));     // 定義樹結點結構體,並申請記憶體,存放要插入元素內容
        // 記憶體申請合法性檢查
        ret = (trNode != NULL) && (cldNode != NULL) && (cNode != NULL);
        // 記憶體申請成功
        if( ret )
        {
            cNode->data = data;                   // 儲存要插入的元素地址
            cNode->parent = NULL;                 // 雙親地址指向NULL
            cNode->child = LinkList_Create();     // 建立孩子連結串列
            
            trNode->node = cNode;                 // 樹結點指向要插入元素結點地址             
            cldNode->node = cNode;                // 樹孩子結點指向要插入元素結點地址  
            // 在樹結點連結串列(組織連結串列)尾部插入樹結點元素
            LinkList_Insert(list, (LinkListNode*)trNode, LinkList_Length(list));
            // 要插入的位置合法,不是根結點
            if( pNode != NULL )
            {
                cNode->parent = pNode->node;      // 將要插入元素的雙親指向當前元素的結點地址                
                // 在樹孩子結點連結串列(孩子連結串列)尾部插入孩子結點元素
                LinkList_Insert(pNode->node->child, (LinkListNode*)cldNode, LinkList_Length(pNode->node->child));
            }
            else
            {            	 
            	free(cldNode);
            }
        }
        // 記憶體申請不成功,釋放記憶體
        else
        {
            free(trNode);
            free(cldNode);
            free(cNode);
        }
    }
    
    return ret;
}

4.列印樹結構

上面我們講解了樹結構插入的操作,但是因為樹結構是非線性的,所有我們也不知道是否操作的正確,就算檢視地址也因為非線

性不連續的而行不通,所以我們應該寫一個顯示函式,可以將插入樹結構的資料按照層次(自定義格式)列印出來。

樹結構的顯示,歸根結底就是遍歷樹成員,將其列印出來。由於樹的定義是非線性、採用遞迴的形式的,所以樹結構的顯示必然

也需要遞迴來實現。實現的思路如下:

首先先列印根結點,然後遍歷根結點的孩子結點,遍歷的過程中列印孩子結點,列印孩子結點時就會遍歷該孩子結點的孩子結

點,直至遍歷結束,所需的樹結構也就列印出來了。

實現程式碼如下:

// 按指定格式顯示樹結構,使用文字形式,縮放的方式
// pFunc  顯示方式式函式的地址
// 孩子結點顯示空格增量
// 孩子結點顯示格式'-'
/* eg:pFunc:%c   gap = 2 div = '-'
      A
      --B
      ----E
      ----F
      --C
      --D 
      ----H
      ----I
      ----J
*/
void GTree_Display(GTree* tree, GTree_Printf* pFunc, int gap, char div)
{
	  // 定義樹結點結構體,用於存放獲取的根結點成員,即組織連結串列的首元素
    TLNode* trNode = (TLNode*)LinkList_Get(tree, 0);
    // 獲取根結構成員成功,入口引數合法,呼叫顯示遞迴函式
    if( (trNode != NULL) && (pFunc != NULL) )
    {  
        recursive_display(trNode->node, pFunc, 0, gap, div);
    }
}


顯然,我們遞迴時直接呼叫GTree_Display()函式是實現不了列印樹結構的,所以我們單獨定義了一個實現遞迴函式。實現程式碼

如下:

// 顯示樹結構遞迴函式
static void recursive_display(GTreeNode* node, GTree_Printf* pFunc, int format, int gap, char div)
{
    int i = 0;
    // 合法性檢查
    if( (node != NULL) && (pFunc != NULL) )
    {
    	  // 迴圈列印縮排格式
        for(i=0; i<format; i++)
        {
            printf("%c", div);
        }
        // 列印樹結構儲存的成員資料
        pFunc(node->data);
        // 空行
        printf("\n");
        // 遍歷孩子連結串列,列印孩子連結串列成員資料 
        for(i=0; i<LinkList_Length(node->child); i++)
        {
            // 獲取每一個連結串列元素,用於遞迴列印
            TLNode* trNode = (TLNode*)LinkList_Get(node->child, i);
            // 呼叫遞迴函式,按格式列印孩子結點資料
            recursive_display(trNode->node, pFunc, format + gap, gap, div);
        }
    }
}

遍歷孩子結點時呼叫顯示遞迴函式,這裡還有一個pFunc可能大家感到迷茫,莫名其妙,其實這是自定義的一個函式型別,用於

定義列印內容的格式(字元、字串、整數等),程式碼如下:

typedef void (GTree_Printf)(GTreeData*);

void printf_data(GTreeData* data)
{
    printf("%c", (int)data);
}

列印的結果如下圖:


5.刪除結點

通過上面插入結點的操作,我們知道樹結構的插入分兩步,第一步往組織連結串列裡插入結點、第二步向孩子連結串列中插入結點,所以

樹結構是連結串列中巢狀著連結串列分層次、不確定元素個數和要刪除結點的性質的(可能是分支結點、也可能是葉結點,還有可能直接

就是根結點)。如果是分子節點,那麼它的孩子結點可能還不止一個,同時還可能有子孫結點,所以我們就得依次一個一個、一

層一層的刪除,確保要刪除結點、其孩子結點和子孫結點全部被刪除。也就是說刪除也要分兩步,首先刪除組織連結串列裡的結點,

再將組織連結串列裡結點的孩子結點也要刪除,然後用上遞迴的思想繼續刪除子孫結點,直至刪除完畢為止。實現方法如下:

首先要從組織連結串列中找到要刪除的結點地址,然後儲存要刪除的結點資料,最後呼叫刪除遞迴函式。實現程式碼如下:

// 刪除樹結構指定位置pos的元素
GTreeData* GTree_Delete(GTree* tree, int pos)
{
    // 定義樹結點結構體變數,獲取要刪除結點當前位置的元素地址
    TLNode* trNode = (TLNode*)LinkList_Get(tree, pos);
    // 定義返回變數
    GTreeData* ret = NULL;
    // 要刪除結點的位置元素合法
    if( trNode != NULL )
    {
        ret = trNode->node->data;                 // 儲存當前位置元素資料地址,用於函式返回
        
        recursive_delete(tree, trNode->node);     // 呼叫遞迴,刪除結點    
    }
    
    return ret;
}

遞迴刪除函式實現方法如下:

1.合法性檢查;

2.找到要刪除結點的雙親指標;

3.從組織連結串列中刪除結點;

    3.1.遍歷連結串列,尋找要刪除的結點;

          3.1.1.找到後就刪除該結點;

           3.1.2.釋放該結點記憶體;

           3.1.3.標記已經找到刪除結點,並跳出遍歷連結串列操作;

   3.2.找不到要刪除的元素直接進行後面的操作;

4.從孩子連結串列中刪除孩子結點及其子孫結點;

    4.1.遍歷連結串列,尋找要刪除的結點;

           4.1.1.找到後就刪除該結點;

           4.1.2.釋放該結點記憶體;

           4.1.3.標記已經找到刪除結點,並跳出遍歷連結串列操作;

   4.2.找不到要刪除的元素直接進行後面的操作;

5.如果存在子孫結點,呼叫遞迴函式刪除所有的子孫結點。

實現程式碼如下:

// 結點刪除遞迴函式
// list:要刪除的樹結構
// node:要刪除的結點
static void recursive_delete(LinkList* list, GTreeNode* node)
{
	  // 入口引數合法性檢查OK
    if( (list != NULL) && (node != NULL) )
    {
    	  // 定義一個樹結點結構體變數,存放要刪除位置的雙親地址
        GTreeNode* parent = node->parent;         
        int index = -1;         // 查詢變數
        int i = 0;              // 迴圈變數
        // 遍歷樹結構組織連結串列,找到要刪除結點的位置
        for(i=0; i<LinkList_Length(list); i++)
        {
        	  // 定義結點連結串列結構體變數,存放遍歷的樹連結串列成員
            TLNode* trNode = (TLNode*)LinkList_Get(list, i);
            // 找到了要刪除的結點位置
            if( trNode->node == node )
            {
                LinkList_Delete(list, i);     // 刪除該位置的樹結構成員
                
                free(trNode);                 // 釋放記憶體,防止記憶體洩漏
                
                index = i;                    // 標記要刪除結點在組織連結串列中的位置
                
                break;                        // 跳出遍歷樹結構操作
            }
        }
        // 刪除位置真實存在
        if( index >= 0 )
        {  
        	  // 有雙親
            if( parent != NULL )
            {
            	   // 遍歷孩子結點連結串列,將其從雙親的孩子結點中刪除
                 for(i=0; i<LinkList_Length(parent->child); i++)
                 {
        	  				 // 定義結點連結串列結構體變數,存放遍歷的雙親的孩子結點連結串列成員
                     TLNode* trNode = (TLNode*)LinkList_Get(parent->child, i);
                     // 找到要刪除的孩子結點
                     if( trNode->node == node )
                     {
                     	   // 將其從雙親的孩子連結串列中刪除
                         LinkList_Delete(parent->child, i);
                         // 釋放記憶體,防止記憶體洩漏
                         free(trNode);
                         // 跳出遍歷孩子結點連結串列操作
                         break;
                     }
                 }               
            }
            // 如果存在子孫結點,遞迴刪除其他結點
            while( LinkList_Length(node->child) > 0 )
            {
            	  // 獲取子孫結點連結串列首元素
                TLNode* trNode = (TLNode*)LinkList_Get(node->child, 0);
                // 呼叫遞迴函式,繼續刪除
                recursive_delete(list, trNode->node);
            }
            // 銷燬子孫結點連結串列
            LinkList_Destroy(node->child);
            // 釋放記憶體,防止記憶體洩漏
            free(node);
        }
    }
}

6.獲取指定的結點

實現程式碼如下:

// 獲取樹結構指定位置pos的元素
GTreeData* GTree_Get(GTree* tree, int pos)
{
    // 定義樹結點結構體變數,存放要獲取結點當前位置的元素地址
    TLNode* trNode = (TLNode*)LinkList_Get(tree, pos);
    GTreeData* ret = NULL;    
    // 要獲取的結點的位置元素合法,返回該結點成員資料
    if( trNode != NULL )
    {
        ret = trNode->node->data;
    }
    
    return ret;
}

從上面程式碼上可以發現,獲取結點有點類似刪除結點,就是少了刪除的操作。

7.獲取根結點

獲取根結點其實就是獲取樹結構的首元素,實現程式碼如下:

// 獲取樹結構的根結點元素
GTreeData* GTree_Root(GTree* tree)
{
    return GTree_Get(tree, 0);      // 呼叫獲取樹結點函式
}

8.獲取樹結構的高度

獲取樹結構的高度,我們可以這樣考慮,先求出根結點的子結點的高度,然後找到最大值後再加上1不就是整個樹的高度了;但

是有一個問題就是,子結點還會有子結點,那怎麼辦呢?好辦呀,按照如上的方法、利用遞迴的思想,一層層的求子結點的高

度,最後返回的結果就是整個樹的高度了,實現程式碼如下:

// 獲取樹結構的高度
int GTree_Height(GTree* tree)
{
    // 定義樹結點結構體變數,存放子樹結點
    TLNode* trNode = (TLNode*)LinkList_Get(tree, 0);
    int ret = 0;
    // 根結點合法,呼叫獲取樹結構高度遞迴函式,返回高度
    if( trNode != NULL )
    {
        ret = recursive_height(trNode->node);
    }
    
    return ret;
}

遞迴函式實現程式碼如下:

// 獲取樹結構高度遞迴函式
static int recursive_height(GTreeNode* node)
{
    int ret = 0;
    // 結點合法
    if( node != NULL )
    {
        int subHeight = 0;       // 子結點
        int i = 0;               // 迴圈變數
        // 遍歷子樹的孩子結點連結串列
        for(i=0; i<LinkList_Length(node->child); i++)
        {
            // 依次取出子樹結點的孩子結點
            TLNode* trNode = (TLNode*)LinkList_Get(node->child, i);
            // 呼叫求高度遞迴函式,計運算元結點
            subHeight = recursive_height(trNode->node);
            // 求計算結果的最大值,邊計算邊找最大值
            if( ret < subHeight )
            {
                ret = subHeight;
            }
        }
        // 計算結果加1就是整個樹的高度
        ret = ret + 1;
    }
    
    return ret;
}

9.獲取樹結構的全體成員數量

獲取樹結構的全體成員數量也就是獲取整個組織連結串列的長度,實現程式碼如下:

// 獲取樹結構的元素數量
int GTree_Count(GTree* tree)
{
    return LinkList_Length(tree);
}

10.獲取樹結構的度

獲取樹結構的度的實現方法和獲取樹結構的高度的實現思想差不多,也是先將根結點的度求出來,然後找求根結點所有子樹結點

的度,然後找出最大值,就是樹的度數了,還是要利用遞迴的思想。實現程式碼如下:

// 獲取樹結構的度
int GTree_Degree(GTree* tree)
{
    // 定義樹結點結構體變數,存放子樹結點
    TLNode* trNode = (TLNode*)LinkList_Get(tree, 0);
    int ret = -1;    
    // 根結點合法,呼叫獲取樹結構度遞迴函式,返回高度
    if( trNode != NULL )
    {
        ret = recursive_degree(trNode->node);
    }
    
    return ret;
}

遞迴函式程式碼實現如下:

// 獲取樹結構度遞迴函式
static int recursive_degree(GTreeNode* node)
{
		int ret = -1;
    
    // 結點合法
    if( node != NULL )
    {
        int subDegree = 0;        // 子結點度
        int i = 0;
        // 獲取子結點的長度,即度
        ret = LinkList_Length(node->child);
        // 變數子樹孩子結點的成員
        for(i=0; i<LinkList_Length(node->child); i++)
        {
        	  // 依次取出子樹結點的孩子結點
            TLNode* trNode = (TLNode*)LinkList_Get(node->child, i);            
            // 呼叫求度遞迴函式,計運算元結點度
            subDegree = recursive_degree(trNode->node);            
            // 求計算結果的最大值
            if( ret < subDegree )
            {
                ret = subDegree;
            }
        }
    }
    
    return ret;
}

本節中的樹結構是一種通用的資料結構。

利用連結串列組織樹結點優點:能夠便利的存取結點。

利用連結串列組織樹結點缺點:連結串列的維護具有一定複雜性。

樹結構的非線性特性和遞迴定義的特性是樹結構實現難度較大的根本原因。

至此,有關樹結構的基本操作已經實現了,因是初學,有描述不妥的地方還清大神加以指正。

通用樹結構C程式碼實現


相關文章