二叉樹的學習筆記

0. 什麼是樹

數據的基本單位是數據元素,在涉及到數據處理時數據元素之間的關係稱之爲結構,咱們依據數據元素之間關係的不一樣,將數據結構分爲線性結構非線性結構 兩種。html

  • 線性結構:按照某種次序排列在一個序列中,在線性結構中,根據數據元素存取方法的不一樣分爲三種。
    1. 直接存取:直接存取指定項而不須要訪問其前驅,如數組,文件
    2. 順序存取:只能按序訪問直到指定元素,如棧,隊列,鏈表
    3. 字典結構:字典map,關鍵碼索引
  • 非線性結構:每一個數據元素都有可能與其餘零個或多個元素髮生聯繫,可分爲層次結果和羣結構。層次結構中的典型結構就是樹形結構。羣結構中元素之間是無順序結構,如集合,圖,網絡結構等。

這上面所劃分的是數據結構的邏輯結構,而數據是如何在計算機中存儲的,這涉及到數據的物理結構,邏輯結構是從解決問題出發,涉及到功能的簡歷,物理結構設計到響應速度,處理、修改、存儲時間等問題。node

  • 順序存儲結構:邏輯上相鄰元素存放到物理位置上相鄰的存儲單元
  • 連接存儲結構:邏輯上相鄰的元素位置上不必定相鄰,經過指針指示
  • 索引存儲結構:存儲時創建附加索引表,(關鍵碼,地址)
  • 散列存儲結構:經過散列計算存儲地址

樹:N個結點的有限集合.png

1. 基本性質

  1. 二叉樹不存在度大於2的節點,二叉樹是有序的樹,須要區分左節點和右節點
  2. 設度爲0 , 1, 2 的結點的個數爲N0,N1,N2,則結點總數爲N = N0 + N1 + N2
  3. 設B爲二叉樹的分支總數,(除了根節點都有一個接入的分支),N = B+1,而分支總數由度爲1或2的結點練出,則有 B= N1 + 2N2;
  4. 葉子結點的個數 N0 = N2 +1;
  5. 非空二叉樹的第K層最多有 2 ^(k-1) 個節點;
  6. 高度爲H的二叉樹最多有 2^H -1 個節點;
  7. 結點爲 i 所在的層次爲 log2(i) +1;
  8. 兩種基本二叉樹

二叉樹.png

2. 二叉樹存儲結構

  • 順序存儲結構:數組(通常僅適用於徹底二叉樹)
  • 鏈式存儲結構:數據域 + 指針域(leftchild + rightchild)

3. 二叉樹的遍歷

二叉樹遍歷是最基本的操做,分爲先序遍歷,中序遍歷、後序遍歷,以及層次遍歷。ios

3.1 遞歸遍歷方式

  • 前序遍歷
void pre_order(BTree *root){
    if (root != NULL){
        visit(root); //cout<<root->value
        pre_order(root->leftchild);
        pre_order(root->rightchild);
    }
}
  • 中序遍歷
void in_order(BTree* root){
    if (root != NULL){
        in_order(root->leftchild);
        visit(root);
        in_order(root->rightchild);
    }
}
  • 後序遍歷
void post_order(BTree * root){
    if (root != NULL){
        post_order(root->leftchild);
        post_order(root->rightchild);
        visit(root);
    }
}
  • 遞歸調用的代價較高,遞歸的實現是調用函數的自己,每次調用都要保存局部變量,調用函數地址,返回值等等,因此遞歸算法很簡潔,相對來講開銷比較大。

3.2 非遞歸的遍歷方式

  • 前序遍歷
#include<iostream>
#include<string>
#include<stack>
using namesapce std;

typedef struct node{
    int data;
    struct node *leftchild, *rightchild;
}BTree;

void pre_order(BTree *root){
    stack<BTree*> s;
    BTree *p = root;

    //當棧非空或者p指針非空時執行
    while (p != NULL || !s.empty()){
        while (p != NULL){
            cout << p->data;
            s.push(p);
            p = p->leftchild;
        }
        if (!s.empty()){
            p = s.top();
            s.pop();
            p->p->rightchild;
        }
    }
}
  • 中序遍歷
void in_order(BTree * root){
    stack<BTree *> s;
    BTree * p = root;

    while (p != NULL || !s.empty()){
        while (P != NULL){
            s.push(p);
            p = p->leftchild;
        }
        if (!s.empty()){
            p = s.top;
            cout << p->data;
            s.pop();
            p = p->rightchild;
        }
    }
}
  • 後序遍歷
    方法一:當用棧來存儲結點的時候,須要分清楚是返回根結點時,是從左子樹返回的,仍是從右子樹返回的,所以須要使用一個輔助指針,也記錄是否被訪問過。
void post_order(BTree *root){
    stack<BTree *> s;
    BTree *p = root, *r = NULL;

    while (p != NULL || !s.empty()){
        if (p != NULL){
            s.push(p);
            p = p->leftchild;  // 最左邊
        }
        else{
            p = s.top();
            //右子樹存在且沒有被訪問過
            if (p->rightchild != NULL && p->rightchild != r){
                p = p->rightchild;
                s.push(p);
                p = p->leftchild;
            }
            else{
                p = s.top();
                //彈出結點並訪問
                s.pop();
                cout << p->data;
                r = p;
                p = NULL;

            }
        }
    }
}

方法二:
對於任一結點,線將其入棧,若是結點P不存在左孩子和右孩子,則能夠直接訪問它,若是P上存在左孩子或者右孩子,可是左孩子或者右孩子已經訪問過了,則直接訪問該結點,不然,將P的右孩子和左孩子依次入棧。
這種方法要簡單一些。算法

void post_order(BTree *root){
    stack<BTree *> s;
    BTree *cur = NULL; //當前結點
    BTree *pre = NULL; //前一次訪問的結點

    s.push(root);
    while (!s.empty()){
        cur = s.top();
        if ((cur->leftchild == NULL && cur->rightchild == NULL) ||
            (pre != NULL && (pre == cur->leftchild || pre == cur->rightchild))){
            //若是當前結點沒有孩子結點或者孩子結點已經被訪問過
            cout << cur->data;
            s.pop();
            pre = cur;
        }
        else{
            if (cur->rightchild != NULL)
                s.push(cur->rightchild);
            if (cur ->rightchild != NULL)
                s.push(cur->leftchild);
        }
    }
}

3.3 層次遍歷

《編程之美:分層遍歷二叉樹》的另外兩個實現編程

// 輸出以root爲根結點的第level層中的全部結點(從左向右)
// 成功返回 1
// 遞歸實現
int printNodeAtLevel(BinaryNode *root, int level){
    if (!root || level < 0)
        return 0;
    if (level == 0){  // 根結點爲0深度
        cout << root->data << " ";
        return 1;
    }
    return printNodeAtLevel(root->leftchild,level -1) + 
        printNodeAtLevel(root->rightchild,level-1);

}
void layer_order(BTree *tree){
    if (tree == NULL)
        return;

    queue<BTree *> q;
    q.push(tree);

    BTree * p = NULL; 
    while (!q.empty()){
        p = que.front();
        visit(p);
        que.pop();
        if (p->leftchild != NULL)
            que.push(p->leftchild);
        if (p->rightchild != NULL)
            que.push(p->rightchild);
    }
}

// 本身實現隊列
# define max 1000;
typedef struct sequeue{
    BTree data[max];
    int front;
    int rear;
}sequeue;

void enter(sequeue *q, BTree t){
    if (q->rear == max){
        cout << "the queue is full" << endl;
    }
    else{
        q->data[q->rear] = t;
        q->rear++;
    }
}

BTree del(sequeue *q, BTree t){
    if (q->front == q->rear){
        return NULL;
    }
    else{
        q->frot++;
        return q->data[q->front -1]
    }
}

void level_tree(bintree t){
    sequeue q;
    Btree temp;
    q.front = q.rear = 0;
    if (!t){
        printf("the tree is empty\n");
        return;
    }
    enter(&q, t);
    while (q.front != q.rear){
        t = del(&q);
        printf("%c ", t->data);
        if (t->lchild){
            enter(&q, t->lchild);
        }
        if (t->rchild){
            enter(&q, t->rchild);
        }
    }
}

4. 二叉樹基本使用

4.1 判斷兩棵二叉樹是否相等

比較兩棵二叉樹是否相等,注意是否能夠旋轉的問題數組

bool isequal(BinaryNode *root1, BinaryNode *root2){
    if (root1 == NULL && root2 == NULL)
        return true;
    if (!root1 || !root2)
        return false;
    if (root1->data == root2->data)
        return isequal(root1->leftchild, root2->leftchild) &&
            isequal(root1->rightchild, root2->rightchild);
    else
        return false;
}

//若是左右節點能夠旋轉
return (isequal(root1->leftchild, root2->leftchild) && isequal(root1->rightchild, 
    root2->rightchild)) || isequal(root1->leftchild, root2->rightchild) &&
    isequal(root1->rightchild, root2->leftchild);

4.2 求二叉樹的深度

本質上是二叉樹的遍歷,先求出左右子樹的深度,才能求出根結點的深度網絡

int tree_height(BinaryNode *root){
    int left, right;
    if (root == NULL) return -1;
    
    left = tree_height(root->leftchild);
    right = tree_height(root->rightchild);
    return (left > right) ? left : right;
}

4.3 求二叉樹結點的最大距離

《編程之美: 求二叉樹中節點的最大距離》的另外一個解法數據結構

  • 距離的定義:若是將二叉樹看作是一個圖,父子結點之間的連線是雙線的,定義距離是兩結點之間邊的個數。

最大距離.png

  • 分析:計算一個二叉樹的最大距離有兩種狀況:
    A:路徑通過左子樹最深結點,經過根結點,再到右子樹的最深結點
    B:路徑不穿過根結點,而是左子樹或者右子樹的最大路徑,取其大者
    只要計算這兩種狀況的路徑距離,並取最大值,就是該二叉樹的最大距離

狀況A和狀況B.png
所以狀況A須要子樹的最大深度,B須要子樹的最大距離。函數

struct result{
    int nMaxDistance; // 最大距離
    int nMaxDepth; // 最大深度
};

result getMaximumDistance(BinaryNode *root){
    if (!root){
        result empty = { 0, -1 }; //最大深度初始化爲 -1,調用者要對它加1,而後變爲0
        return empty;
    }
    result res;
    result lhs = getMaximumDistance(root->leftchild);
    result rhs = getMaximumDistance(root->rightchild);
    res.nMaxDepth = max(lhs.nMaxDepth + 1, rhs.nMaxDepth + 1);
    res.nMaxDistance = max(max(lhs.nMaxDistance, rhs.nMaxDistance),
        lhs.nMaxDepth + rhs.nMaxDepth + 2);
    return res;

}

4.4 由遍歷序列重建二叉樹

  • 先序遍歷的第一個結點必定是二叉樹的根結點,後序遍歷序列的最後一個結點必定是根結點,在中序遍歷中,根結點將中序序列分爲兩個左右子序列
  • 只要知道二叉樹的中序序列和任意其餘一種序列(前序,後序和層次),均可以惟一肯定一棵二叉樹
  • 輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。
BinaryNode *Construct(int *preorder, int *inorder, int length){
    if (preorder == NULL || inorder == NULL || length <= 0)
        return NULL; // 注意return NULL
    // 前序 - 首:尾  中序 - 首:尾
    return ConstructCore(preorder, preorder + length - 1, inorder, inorder + length - 1);
}
BinaryNode *ConsturctCore(int *startPreorder, int *endPreorder,
                int * startInorder, int * endInorder){
    //前序遍歷第一個數字是根結點的值
    int rootvalue = startPreorder[0];
    BinaryNode *root = new BinaryNode();//創建結點
    root->data = rootvalue;
    root->leftchild = root->rightchild = NULL;

    if (startPreorder == endPreorder){ // 邊界條件
        if (startInorder == endInorder && *startPreorder == *startInorder)
            return root;
        else
            throw std:exception("Invalid input");//非法輸入
    }

    // 在中序遍歷中找到根結點的值
    int *rootInorder = startInorder;
    while (rootInorder <= endInorder && *rootInorder != rootvalue)
        ++rootInorder;
    if (rootInorder == endInorder && *rootInorder != rootValue)
        throw std::exception("Invaild input");
    
    int leftLength = rootInorder - startInorder;
    int *leftPreorderEnd = startPreorder + leftLength;
    if (leftLength > 0)
        //構建左子樹 遞歸
        //中序遍歷後根據根節點獲取的 左子樹元素  rootinorder是中序遍歷中的根元素的位置
        root->leftchild = ConstructCore(startPreorder + 1, leftPreorderEnd, startInorder, rootInorder - 1);
    if (leftLength < endPreorder - startPreorder)
        root->rightchild = ConstructCore(leftPreorderEnd + 1, endPreorder, rootInorder, endInorder);
    return root;

}

4.5 從二叉樹中查找值爲x的結點

bool findBTree(BinaryNode *root, ElemType &x){
    if (root == NULL)
        return false;
    if (root->data == x){
        x = root->data;
        return true;
    }
    else{
        if (findBTree(root->leftchild, x))
            return true;
        if (findBTree(root->rightchild, x))
            return true;
        return false;
    }
}

4.6 統計二叉樹中等於給定值x的結點的個數

int Count(BinaryNode *root, ElemType &x){
    if (root == NULL)
        return -1;
    if (root->data == x){
        return Count(root->leftchild, x) + Count(root->rightchild, x) + 1;
    }
    else
        return Count(root->leftchild, x) + Count(root->rightchild, x);
}

4.7 返回x結點所在的層號

int findlevel(BinaryNode *root, int x){
    if (root == NULL)
        return -1;
    if (root->data == x) // 根結點的層號爲1
        return 1;
    else{
        int leftlevel = findlevel(root->leftchild, x);
        if (leftlevel >= 1)
            return leftlevel + 1;
        
        int rightlevel = findlevel(root->rightchild, x);
        if (rightlevel >= 1)
            return rightlevel + 1;
    }
    else{
        return -1;
    }
}

4.8 從二叉樹中找到全部結點的最大值

int MaxValue(BinaryNode * root){
    if (root == NULL)
        return 0;

    int leftmax, rightmax;
    leftmax = MaxValue(root->leftchild);
    rightmax = MaxValue(root->rightchild);
    int submax = (leftmax >= rightmax) ? leftmax : rightmax;
    return (submax >= root->data) ? submax : root->data;
}

4.9 求二叉樹中全部結點數

int TreeCount(BinaryNode *root){
    if (root == NULL)
        return -1;
    else
        return Treecount(root->leftchild) + TreeCount(root->rightchild) + 1;
}

4.10 求二叉樹中全部葉子結點數目

int LeafCount(BinaryNode *root){
    if (root == NULL)
        return -1;
    if (root->leftchild == NULL && root->rightchild == NULL)
        return 1;
    else
        return LeafCount(root->leftchild) + LeafCount(root->rightchild);
}

4.11 二叉樹路徑上結點的和等於定值

一棵二叉樹每一個結點包含一個整數,請設計一個算法輸出全部知足條件的路徑,此路徑上全部結點的和等於定值,注意此類路徑不要求必須從根結點開始post

void printbuffer(vector<int> buffer, int level, int end){
    for (int i = level, i <= end; i++)
        cout << buffer[i] << " ";
    cout << endl;
}

// 輸入:二叉樹 sum定值 存儲的vector<buffer>
void findsum(BinaryNode *root, int sum, vector<int> buffer, int level){
    if (root == NULL)
        return;
    int temp = sum;
    buffer.push_back(root->data);
    for (int i = level; i >= 0; i--){
        temp -= buffer[i];
        if (temp ==0)
            printbuffer(buffer,level,i)
    }
    findsum(root->leftchild, sum, buffer, level + 1);
    buffer.pop_back();
    level -= 1;
    findsum(root->rightchild, sum, buffer, level + 1);
}

5. 二叉排序樹(樹的應用)

  • 二叉排序樹(Binary sort/search Tree)/BST, BST 具備以下性質:(1)若根節點具備左子樹,則左子樹全部結點都比根結點要小 (2)若根結點有右子樹,則右子樹的全部結點都比根結點大 (3)左右子樹也分別爲BST;
  • 左子樹值<根結點值<右子樹值,因此對BST進行中序遍歷,能夠獲得一個遞增的有序序列。
  • 構造一個二叉排序樹的目的:提升查找,插入,刪除的速度;
  • 如何判斷一棵二叉樹是不是二叉排序樹
// 二叉搜索樹的中序遍歷是遞增的
int prev = INT_MIN;
int judgeBST(BinaryNode *root){
    int b1, b2;
    if (root == NULL)
        return 1;
    else{
        b1 = judgeBST(root->rightchild);
        if (b1 == 0 || prev >= root->data)
            return 0;
        prev = root->data;
        b2 = judgeBST(root->rightchild);
        return b2;
    }
}

二叉排序樹(二叉查找樹)經常使用操做

  • 經常使用操做:插入結點,先進行查找,查找到要插入的位置;查找結點;刪除結點(刪除結點相對來講比較複雜,不能使二叉樹的特性不知足BST特性)
  • 刪除結點:
    1. 刪除葉子結點:直接刪除
    2. 刪除只具備單孩子結點:刪除後須要將孩子結點與本身的父親結點相連
    3. 刪除具備左右孩子的結點:刪除後誰作根結點,通常是右結點的左子樹的最左孩子
// 查找是否含有key值
// 這是迭代寫法 也能夠遞歸寫法
bool searchBST(BinarySortTree *root,int key){
    if (root == NULL)
        return false;
    BinarySortTree *current = root;
    while (root != NULL){
        if (key == current->data)
            return true;
        else if (key < current->data)
            current = current->leftchild;
        else
            current = current->rightchild;
    }
    return false;
}

// 插入結點
// 若是遇到重複的元素能夠根據本身的定義將它歸到左子樹或者右子樹
void insertBST(BinarySortTree *root,int key){
    BinarySortTree *current = root;
    BinarySortTree *pre = NULL;

    while (current != NULL){
        pre = current;
        if (key < current->data)
            current = current->leftchild;
        else if (key>current->data)
            current = current->rightchild;
        else
            return;
    }

    BinarySortTree newnode;
    newnode = (BinarySortTree)malloc(sizeof(BinarySortTreeNode));
    newnode->data = key;
    newnode->leftchild = newnode->rightchild = NULL;
    
    // prev 是要安放的結點的父親結點
    if (current == NULL)
        current->data = key;
    else if (key < pre->data)
        pre->leftchild = newnode;
    else
        pre->rightchild = newnode;
}

6. 平衡二叉樹(樹的應用)

  • 爲了不樹的高度增長過快,下降二叉排序樹的性能,咱們規定在插入和刪除二叉樹結點時,保證任意結點左右子樹的高度差不會超過1,並將這樣的二叉樹稱之爲平衡二叉樹(AVL)
  • 左子樹與右子樹的高度差稱之爲平衡因子,平衡因子的值只能爲-1,0,1
  • 平衡二叉樹能夠是一棵空樹,它的左子樹和右子樹都是平衡二叉樹,且左右子樹的高度差絕對值不超過1.
    1. 思路1:遍歷樹的每一個結點時,調用函數tree_height獲得它的左右子樹的深度,若是每一個結點的左右子樹的深度相差不超過1,按照定義就是一棵平衡二叉樹
    2. 思路2:後序遍歷,在遍歷每個結點的時候記錄下它的高度,能夠一邊遍歷一邊判斷每一個結點是否是平衡的
    3. 思路3:求出根結點的最大深度和最小深度,最大深度和最小深度之差就是任一子樹深度差的最大值
//後序遍歷
bool isBalanced(BinaryNode* root, int *pDepth){
    if (root == NULL){
        *pDepth = 0;
        return true;
    }
    int left, right;
    if (isBalanced(root->leftchild, &left) && isBalanced(root->rightchild, &right)){
        int diff = left - right;
        if (diff <= 1 && diff >= -1){
            *pDepth = 1 + (left>right ? left : right);
            return true;
        }
    }
    return false;
}

//最大深度和最小深度的差
int maxDepth(BinaryNode *root){
    if (root == NULL)
        return 0;
    return 1 + max(maxDepth(root->leftchild), maxDepth(root->rightchild));
}

int minDepth(BinaryNode *root){
    if (root == NULL)
        return 0;
    return 1 + min(minDepth(root->leftchild), minDepth(root->rightchild));
}

bool isBalanced(BinaryNode *root){
    return (maxDepth(root) - minDepth(root) <= 1);
}

7. 哈夫曼樹和哈夫曼編碼

[轉載]Huffman樹

  • 哈夫曼編碼是一種可變長度編碼,其特色是對頻率高的字符賦予短編碼,頻率低的字符賦予較長的編碼,從而使得平均編碼長度減短,從而起到壓縮數據的效果。

8. 紅黑樹

  • 紅黑樹是衆多平衡搜索樹中的一種,可保證在最壞狀況下插入,搜索,刪除等集合操做時間複雜度爲O(lgn);
  • 紅黑樹是二叉搜索樹,它在葉子結點上增長了一個存儲位來表示結點的顏色(red/black)
  • 紅黑性質:
    1. 每一個結點或是紅色的,或是黑色的
    2. 根結點是黑色的
    3. 每一個葉結點是黑色的
    4. 若是一個結點是紅色的,則它的兩個子結點都是黑色的
    5. 對與每一個結點,從該結點到其全部後代葉結點的簡單路徑上,均包含相同數目的黑色結點

9. B樹和B+ 樹

  • B樹是專門爲磁盤或其餘直接存取的輔助存儲設備而設計的一種平衡搜索樹(非二叉樹),B樹相似紅黑樹,可是不一樣之處在於B樹的結點能夠有不少孩子,從數個到數千個
  • B+樹:B樹的一個常見的變種;
相關文章
相關標籤/搜索