Trees

潘仁波發表於2020-11-20

樹的含義,樹的特徵

什麼是樹

樹的節點是一對多,並且不會出現迴路

一些名詞解釋

  • 樹的根節點沒有父節點(父親,雙親,雙親節點),一顆樹只有一個根節點
  • 樹中沒有子結點(孩子,兒子,子女)的節點叫做葉子節點
  • 有相同的父節點的節點們叫做兄弟節點
  • 深度:由根節點向下計算到該節點,根節點在第0層
  • 高度:該節點到葉子節點
  • 度:節點的子樹的個樹,葉子節點度為0

二叉樹

如果每個節點有0, 1, 2個子結點,那麼這棵樹就可以稱為二叉樹。二叉樹由左子樹和右子樹構成。

二叉樹的種類

  • 嚴格二叉樹 每個節點必須剛剛好有兩個子結點,或者沒有子節點
  • 滿二叉樹 每個節點必須剛剛好有兩個子結點,而且葉子節點的層數相同
  • 完全二叉樹 除最後一層外的其餘層都是滿的,並且最後一層要麼是滿的,要麼右邊缺少連續若干節點,滿二叉樹是一種完全二叉樹

二叉樹的性質

假設樹的高度是h,根在第0層(如果在1層)

  • 滿二叉樹第i層有 2^i 個節點
  • 滿二叉樹的節點個樹是 2^(h+1) - 1
  • 滿二叉樹中葉子節點的個數是 2^h
  • 完全二叉樹的節點個數介於 2^h 和 2^(h+1) - 1之間
  • 有n個節點的完全二叉樹深度為log2 n

二叉樹的資料結構

struct BinaryTreeNode {
    int data;
    struct BinaryTreeNode *left;
    struct BinaryTreeNode *right;
};

二叉樹的遍歷

  • DLR Process the current node data, process left subtree and then process right subtree
  • LDR Process left subtree, process the current node data and the process right subtree
  • LRD Process left subtree, process right subtree and the process the current node data

根據以上,我們得到三種遍歷方式:

  • 先序遍歷 的順序是DLR
  • 中序遍歷 的順序是LDR
  • 後序遍歷 的順序是LRD

還有一種另外的遍歷方式:

  • 層次遍歷

先序遍歷

先序遍歷流程:

  1. 訪問根節點
  2. 先序遍歷左子樹
  3. 先序遍歷右子樹
void PreOrder(struct BinaryTreeNode *root) {
    if (root) {
        printf("%d ", root->data);
        PreOrder(root->left);
        PreOrder(root->right);
    }
,但是我們不知道在先序遍歷中,左子樹佔了陣列哪些,右子樹佔了哪些,求出這個就解決問題了。}

非遞迴實現:

  1. 處理當前節點
  2. 將當前節點入棧
  3. 處理左子樹
  4. 彈出棧頂元素
  5. 處理該節點的右子樹
void PreOrderNonRecursive(struct BinaryTreeNode *root) {
    struct Stack *S = CreateStack();
    while(1) {
        while(root) {
            printf("%d ". root->data);
            Push(S, root);
            // If left subtree exists, add to stack
            root = root->left;
        }
        if (IsEmptyStack(S)) {
            break;
        }
        root = Pop(S);
        // process right subtree
        root = root->right;
    }
    DeleteStack(S);
}

中序遍歷

中序遍歷流程:

  1. 中序遍歷左子樹
  2. 訪問根節點
  3. 中序遍歷右子樹
void InOrder(struct BinaryTreeNode *root) {
    if (root) {
        InOrder(root->left);
        printf("%d ", root->data);
        InOrder(root->right);
    }
}

非遞迴實現:
與前序遍歷非遞迴類似,不同的是,前序遍歷是在處理左子樹之前處理節點,而中序遍歷處理節點是在彈出之後(表明左子樹的操作已經完成了)

void InOrderNonRecursive(struct BinaryTreeNode *root) {
    struct Stack *S = CreateStack();
    while(1) {
        while(root) {
            Push(S, root);
            root = root->left;
        }
        if (IsEmptyStack(S)) {
            break;
        }
        root = Pop(S);
        printf("%d ", root->data);
        root = root->right;
    }
}

後序遍歷

後序遍歷流程:

  1. 後序遍歷左子樹
  2. 後序遍歷右子樹
  3. 訪問根節點
void PostOrder(struct BinaryTreeNode *root) {
    if (root) {
        PostOrder(root->left);
        PostOrder(root->right);
        printf("%d ", root->data);
    }
}

非遞迴實現:
後序遍歷是左右中的順序,也就是說,在處理完一個節點的左子樹之後,我們需要回到這個節點一次,然後處理完該節點的右字數之後,我們還需要回到這個節點一次,我們應該在第二次回到這個節點的時候去處理這個節點。問題是我們如何區分我們這次返回是從左還是從右
所以我們使用一個 previous 來記錄上一個遍歷過的節點,如果 previous 是當前所在節點的左孩子,那我們就是在剛對當前節點的左子樹操作完成,那我們就要該對它的右子樹操作了。如果 previous 是當前所在節點的右孩子,說明我們對當前所在節點的右子樹處理過了(它的左右子樹都處理過了),我們就可以訪問它了。

void PostOrderNonRecursive(struct BinaryTreeNode *root) {
    struct SimpleArrayStack *S = CreateStack();
    struct BinaryTreeNode *previous = NULL;
    do {
        while (root != NULL) {
            Push(S, root);
            root = root->left;
        }
        while (root == NULL && !IsEmptyStack(S)) {
            root = Top(S);
            if (root->right == NULL || root->right == previous) {
                printf("%d ", root->data);
                Pop(S);
                previous = root;
                root = NULL;
            } else {
                root = root->right;
            }
        }
    } while (!IsEmptyStack(S))
}

層次遍歷

層次遍歷流程:
利用佇列,先進先出,把出隊元素的左孩子和右孩子入隊

void LevelOrder(struct BinaryTreeNode *root) {
    struct BinaryTreeNode *temp;
    struct Queue *Q = CreateQueue();
    if (!root) {
        return;
    }
    EnQueue(Q, root);
    while (!IsEmptyQueue(Q)) {
        temp = DeQueue(Q);
        //Process current node
        printf("%d ", temp->data);
        if (temp->left) {
            EnQueue(Q, temp->left);
        }
        if (temp->right) {
            EnQueue(Q, temp->right);
        }
    }
    DeleteQueue(Q);
}

問題

樹的大部分問題都可以套用遞迴的模板

void func(Tree *root) {
    //PreOrder
    func(root->left);
    //InOrder
    func(root->right);
    //PostOrder
}

遞迴 我們要指導我們定義的函式是做什麼的,利用遞迴來推匯出結果,但不用糾結每次遞迴具體做了什麼

樹相關的問題,最重要的搞清楚當前節點應該做什麼,然後對子結點遞迴,遞迴會讓子節點做相同的事

第一題 找到二叉樹裡最大的元素

思路:先找左子樹裡最大的元素,再找右子樹裡最大的元素,讓他們與根節點的值比較大小

int FindMax(struct BinaryTreeNode *root) {
    int root_val, left, right, max = INT_MIN;
    if (root) {
        root_val = root->data;
        left = FindMax(root->left);
        right = FindMax(root->right);
        if (left > right) {
            max = left;
        } else {
            max = right;
        }
        if (root_val > max) {
            max = root_val;
        }
    }
    return max;
}

第二題 求一棵二叉樹的深度

思路:想一下假如已經知道二叉樹的左子樹和右子樹的深度,如何計算二叉樹的深度?左子樹深度 LD ,右子樹深度 RD ,那麼深度就是 max(LD, RD) + 1。先求左,在求右,最後訪問根(又是一個後序遍歷)

int getDepth(struct BinaryTreeNode *root) {
    int LD, RD;
    if (root) {
        return 0;
    } else {
        LD = getDepth(root->left);
        RD = getDepth(root->right);
        return (LD > RD ? LD : RD) + 1;
    }
}

第三題 填充每個節點的下一個右側節點指標

思路:把每一層的節點都用 next 指標連起來,那對節點的操作就是 root->left->next = root->right (但是這樣做有點小問題,同一層,不一定都是一個節點下的兄弟節點)

這是對一個節點來操作,很明顯,我們這樣做,無法連線同一層全部的相鄰節點,所以一個節點做不到,我們就讓兩個節點來做。

將每兩個相鄰節點都連線起來

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *left;
 *     struct Node *right;
 *     struct Node *next;
 * };
 */

struct Node* connect(struct Node* root) {
    if (!root) {
        return NULL;
    }
    ConnectTwoPoint(root->left, root->right);
    return root;
}

void ConnectTwoPoint(struct Node* node1, struct Node* node2) {
    if (!node1 || !node2) {
        return;
    }
    node1->next = node2;
    ConnectTwoPoint(node1->left, node1->right);
    ConnectTwoPoint(node2->left, node2->right);
    ConnectTwoPoint(node1->right, node2->left);
}

第四題 從前序與中序遍歷序列構造二叉樹

思路:先確定根節點的值,構造出了根節點之後,再遞迴對根節點的左右子樹遞迴,就能構造出左右子樹了

我們可以知道根節點的值就是先序遍歷的第一個元素的值,然後在中序遍歷中,根節點對應位置的左右分別就是根節點的左子樹和右子樹,但是我們不知道在先序遍歷中,左子樹佔了陣列哪些,右子樹佔了哪些,求出這個就解決問題了。

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     struct TreeNode *left;
 *     struct TreeNode *right;
 * };
 */


struct TreeNode* build(int* preorder,int preStart,int preEnd,int* inorder,int inStart,int inEnd) {
    if(preStart > preEnd) {
        return NULL;
    }
    // 構造根節點
    struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode));
    root->val = preorder[preStart];
    int index = 0;
    // 在中序遍歷陣列中找到根節點對應的下標
    for(int i = inStart; i <= inEnd; i++) {
        if(inorder[i] == preorder[preStart]) {
            index=i;
            break;
        }
    }
    // 左子樹元素個數
    int leftSize = index - inStart; 
    // 構造左右子樹
    root->left = build(preorder, preStart + 1, preStart + leftSize, inorder, inStart, index - 1);
    root->right = build(preorder, preStart + leftSize + 1, preEnd, inorder, index + 1, inEnd);
    return root;
 }

struct TreeNode* buildTree(int* preorder, int preorderSize, int* inorder, int inorderSize){
    if(preorderSize == 0) {
        return NULL;
    } 
    return build(preorder, 0, preorderSize-1, inorder, 0, inorderSize-1);
}

哈夫曼樹

哈夫曼樹定義

又稱最優二叉樹,帶權路徑最小

構造方法

  1. 從集合中選出最小的兩個值,構造二叉樹,新的二叉樹的權值等於兩個值的和
  2. 從集合中刪除這兩個值,加入新值
  3. 重複進行 1 2