二叉樹實現及相關操作知識梳理

樹型結構

樹

前言

樹型結構在生活中是非常常見的一種結構,應用範圍很廣,就用一個簡單的例子來說。計算機中的文件目錄就是一個樹型結構,一般創建一個文件,如果文件中沒有文件那麼就相當於一個空樹,如果裏面有文件,就相當於這個文件的子樹,以此類推,就形成了樹型結構的文件目錄。
在學習中我們主要學習二叉樹的一些特性,把樹細化學習。

二叉樹的概念

二叉樹是結點的有限集合,該集合或者爲空,或者由一個
根節點加上兩顆對稱的左子樹和右子樹的二叉樹組成。
二叉樹的每個結點上最多有兩個子樹
二叉樹有五種形態,及只有根結點,空樹,只有左子樹,只有右子樹,左右子樹都有。

二叉樹的分類

二叉樹有完全二叉樹和滿二叉樹兩種

  • 滿二叉樹是所有分支節點都存在左右子樹,並且葉子結點在同一層上。
  • 完全二叉樹是具有N個結點的二叉樹的結構與滿二叉樹前N個結點的結構相同

    怎麼理解呢?
    滿二叉樹完全二叉樹
    左圖爲滿二叉樹,有圖爲完全二叉樹。

    二叉樹性質

  • 二叉樹最大結點數:若規定只有一個根結點的時候,深度爲1,那麼深度爲K的樹,最大結點數爲 (2^K) -1 (K>=0)

  • 二叉樹的第i層上的結點數:若規定只有一個根結點的時候,層數爲1,那麼一顆非空樹的第i層最多有 2^(i-1) (i>0)

  • 葉子結點和非葉子結點的關係:對於任何一顆二叉樹,如果葉子結點個數爲N0,度爲2的非葉子結點個數爲N1,則有 N1 = N0 + 1;

  • 關於完全二叉樹的深度:具有N結點的二叉樹的的深度K爲log2(n+1)上取整數

  • 順序存出中父結點和子結點的關係:子結點要找到對應的父結點,(i - 1)/2;父找到左孩子i*2 + 1;找到右孩子i*2 + 1;

    二叉樹的創建

    創建一顆二叉樹,以順序表,通過先序遍歷的方式還原一顆二叉樹(順序表中必須有標識NULL的特殊字符存在)
    具體思路是採用遞歸的方式,用一個index來記錄樹創建過程中創建到數組中的哪個位置,遞歸的用先序遍歷的方式去創建。
    具體代碼:

    // 創建一個結點
    TreeNode* CreateTreeNode(TreeType value)
    {
     TreeNode* new_node = (TreeNode*)malloc(sizeof(TreeNode));
     // 用assert來判斷new_node空間是否開闢成功。
     // 在這裏我們直接讓程序掛掉。(不同場景處理方式不同)
     assert(new_node);
     new_node->data = value;
     new_node->lchild = NULL;
     new_node->rchild = NULL;
     return new_node;
    }
    // 二叉樹遞歸體,遞歸的建立一顆二叉樹
    TreeNode* _CreateTree(TreeType arr[], size_t size, int* index, TreeType nulltype)
    {
     if (index == NULL || size < 1)
     {
         return NULL;
    }
    if (arr[*index] == nulltype)
    {
        return NULL;
    }
    // 先創建根結點
    TreeNode* root = Create Tree Node(TreeType value);
    ++(*index);
    // 遞歸的創建左子樹
    TreeNode* root->lchild = _CreateTree(arr, size, index, nulltype);
    ++(*index);
    // 遞歸的創建右子樹
    TreeNode* root->rchild = _CreatTree(arr,size, index, nulltype);
    return root;
    }
    // 函數主體
    TreeNode* CreateTree(TreeType arr[], size_t size, TreeType nulltype)
    {
    if (size < 1)
    {
        return NULL;
    }
    // 用來記錄創建到數組中的哪個元素上
    int index = 0;
    return _CreateTree(arr, size, &index, nulltype);
    }

    二叉樹的遍歷

    創建好二叉樹後,我們訪問二叉樹中的某個結點,就必須經過遍歷。
    我們實現一下,二叉樹的遞歸版的前、中、後序遍歷和非遞歸版的前、中、後遍歷。

遞歸版

  • 先序遍歷(前序遍歷)

    void PreOrder(TreeNode* root)
    {
        if (root == NULL)
        {
            return;
        }
        // 先打印根結點
        printf("%c  ", root->data);
        // 遞歸的打印左右子樹
        PreOrder(root->lchild);
        PreOrder(root->rchild);
    }
  • 中序遍歷

    void InOrder(TreeNode* root)
    {
         if (root == NULL)
         {
            return;
         }
         // 先遞歸的找到最左邊的孩子
         InOrder(root->lchild);
         // 打印最左邊的孩子,遞歸棧出棧打印根結點
         printf("%c  ", root->data);
         // 遞歸進右子樹
         InOrder(root->rchild);
    }
    
  • 後續遍歷

    void PostOrder(TreeNode* root)
    {
        if (root == NULL)
        {
            return;
        }
        // 先遞歸,找最左的孩子,打印,然後遞歸棧出棧,再進行最左子樹的右子樹進行遞歸
        PreOrder(root->lchild);
        PreOrder(root->rchild);
        printf("%c  ", root->data);
    }

    非遞歸版

    注意:這裏我們在寫非遞歸版,直接用順序棧的函數接口,不做棧的實現。
    如果不知道棧的實現,戳這裏棧的實現

  • 前序遍歷
    前序遍歷非遞歸採用手動棧來進行操作,棧的特性是先入後出,取棧頂元素,所以我們抓住這兩個特性。 前序遍歷,是根左右順序遍歷。
    1)先讓根結點入棧,取棧頂元素打印,成功進(2),失敗退出循環。
    2)進行出棧
    3)再進行入右孩子,入左孩子。
    就這樣循環,直到棧爲空,也就是取棧頂失敗。遍歷完成。
    代碼如下:

    void PreOrederByLoop(TreeNode* root)
    {
         if (root == NULL)
         {
                 return;
         }
         // 創建一個棧。
         SeqStack stack;
         // 初始化一個棧(用C語言實現的棧)
         InitStack(&stack);
         // 先入根結點
         PushStack(&stack,  root);
         TreeNode* top = NULL;
         while (FindTopStack(&stack, &top)
         {
                 printf("%c ", top->data);
                 PopStack(&stack);
                 // 先入右孩子,後入左孩子
                 if (top->rchild != NULL)
                 {
                         PushStack(&stack, top->rchild);
                 }
                 if (top-lchild != NULL)
                 {
                         PushStack(&stack, top->lchild);
                 }
          }
    }
    
  • 中序遍歷
    中序遍歷,是採用手動棧。將函數放入一個while(1)的循環中。
    1)先循環的去從根結點到最左孩子的入棧。
    2)進行取棧頂元素,並判斷是否取棧頂元素成功,
    3)如果失敗break,退出while(1)訓話。如果成功,打印棧頂元素,並出棧
    4)進行右孩子的判斷是否爲空,如果不爲空,進行讓循環指針指向取棧頂元素的右孩子。
    代碼:

void InOrderByLoop(TreeNode* root)
{
        if (root == NULL)
        {
            // 非法輸入
            return;
        }
        SeqStack Stack;
        InitStack(&stack);
        TreeNode* cur = root;
        while (1)
        {
            // 從根結點到最左邊的孩子,並逐一入棧
            while (cur != NULL)
            {
                PushStack(&stack, cur);
                cur = cur->lchild;
            }
            // 取棧頂元素
            TreeNode* top = NULL;
            FindTopStack(&stack, &top);
            if (top == NULL)
            {
                // 取失敗,退出循環。因爲因爲已經遍歷完了。
                break;
            }
            // 打印並出棧。
            printf("%c ", top->data);
            PopStack(&stack);
            // 讓cur嘗試的去找最左孩子的右孩子。
            cur = top->rchild;
        }
}
  • 後序遍歷
    後序遍歷,藉助手動創建的棧,採用while(1)循環作爲大的循環條件。
    1)循環的從根結點到最左孩子,並且逐一入棧。
    2)取棧頂元素。如果取失敗就break;
    3)判斷是否存在最左孩子是否存在右孩子。還要判斷右孩子是否等於上一個取棧頂的元素,這樣做是防止重複遍歷。
    4)如果不存在右孩子或者右孩子不等於上次取棧頂元素,那麼可以打印並且出棧。
    5)否則就讓cur 指向棧頂元素的右孩子
    代碼:
void PostOrder(TreeNode* root)
{
    if (root == NULL)
    {
        // 非法輸入
        return;
    }
    SeqStack satck;
    InitStack(&stack);
    // 用來記錄上一個top的元素
    TreeNode* pre = NULL;
    TreeNode* cur = root;
    while (1)
    {
        // 循環從根到最左孩子的入棧
        while (cur != NULL)
        {
            PushStack(&stack, cur);
            cur = cur->lchild;
        }
        // 取棧頂元素
        TreeNode* top = NULL;
        FindTopStack(&stack, &top);
        if (top == NULL)
        {
            // 取失敗退出,說明遍歷結束
            break;
        }
        // 滿足個兩個條件中的一個就可以打印並且出棧。
        if (top->rchild == NULL || top->rchild == pre)
        {
            printf("%c ", top->data);
            PopStack(&stack);
            pre = top;
        }
        else
        {
            // 存在右孩子,繼續入右孩子。
            cur = top->rchild;
        }
    }
}
  • 層序遍歷
    對於層序遍歷,也是一個很重要的點,需要掌握。層序遍歷就是,從上到下,從左到右依次的進行遍歷。那麼前面我們說的,前中後序遍歷,都是藉助棧的先進後出,可以訪問棧頂元素的特點來進行的遍歷。然而對於層序遍歷用隊列的先進先出,訪問隊首的方式遍歷。對隊列不熟悉的可以戳這裏鏈式隊列的實現循環順序隊列的實現
    1)先讓根結點入隊。
    2)以取隊守元素爲條件進行循環
    3)打印並出隊
    4)判斷是否左孩子爲空,不爲空就讓左孩子入隊,
    5)再判斷右孩子是否爲空,不爲空就讓右孩子入隊
    代碼:
void LevelOrder(TreeNode* root)
{
    if (root == NULL)
    {
        // 非法判斷
        return;
    }
    SeqQueue queue;
    InitQueue(&queue);
    PushQueue(&queue, root);
    // 取棧頂元素,並且取隊守元素是否爲空。
    TreeNode* head = NULL;
    while (FindHead(&stack, &head))
    {
        // 打印並且出隊
        printf("%c ", head->data);
        PopQueue(&queue);
        if (head->lchild != NULL)
        {
            PushQueue(&queue, head->lchild);
        }
        if (head->rchild != NULL)
        {
            PushQueue(&queue, head->rchild);
        }
    }
}
  • 判斷是否是完全二叉樹
    判斷是否是完全二叉樹,更多的是在層序遍歷的基礎上變化而來,那麼我們也需要藉助隊這個數據結構來進行。判斷完全二叉樹的依據就是在滿二叉樹的下一層,從左到右,要麼只有左孩子,要麼左右都有,不能只有右沒有左。
    1)我們需要創建隊列,進行初始化,將root結點入隊
    2)取隊首元素,判斷是否爲NULL,如果爲NULL,直接break。
    3)出隊。入左孩子和右孩子。
    4)循環判斷隊列是否大於0
    如果是,就讓原來的隊列出隊,並且取隊首元素,判斷是否爲空,是就返回0。不是就繼續
    如果不是就返回1表示是完全二叉樹
    代碼實現:

    int IsCompleteTree(TreeNode* root)
    {
        if (root == NULL)
        {
             // 非法判斷
             return 0;
        }
        SeqQueue queue;
        InitQueue(&queue);
        // 用來取隊首元素
        TreeNode* head = NULL;
        PushQueue(&queue, root);
        while (SizeQueue(&queue) > 0)
        {
            FindHeadQueue(&queue, head);
            if (head == NULL)
            {
                break;
            }
            PopQueue(&queue);
            PushQueue(&queue, head->lchild);
            PushQueue(&queue, head->rchild);
        }
        while (SizeQueue(&queue) > 0)
        {
            // 當上面循環跳出,進入本循環,隊首元素爲NULL先出隊後取
            PopQueue(&queue);
            FindHeadQueue(&queue, &head);
            if (head != NULL)
            {
                reuturn 0;
            }
        }
        return 1;
    }
    

    以上爲二叉樹基礎知識整理。

發佈了74 篇原創文章 · 獲贊 35 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章