二叉樹原理及編程詳解(完全二叉樹|堆排序|遍歷|重建)

目錄

一、二叉樹基礎概念

1.1 基礎概念:

1.2 二叉樹的性質

二、二叉樹的遍歷

2.1 三種遍歷方法

2.2 二叉樹的構建

2.3 重建方法及解析

2.4 重建二叉樹的代碼實現

c++重建二叉樹

python重建二叉樹

三、二叉樹編程彙總

3.1 樹的子結構

c++樹的子結構

3.2 鏡像二叉樹

c++鏡像二叉樹

3.3 按層打印二叉樹

c++按層打印二叉樹

3.4 判斷排序二叉樹後序遍歷

c++判斷排序二叉樹後序遍歷

3.5 二叉樹的深度

c++二叉樹深度

3.6 平衡二叉樹

c++平衡二叉樹

3.7 對稱二叉樹

c++對稱二叉樹

四、堆排序

4.1 大頂堆與小頂堆

4.2 基本思想與步驟

步驟一、構造初始堆

步驟二、頂端與末尾交換

4.3 代碼

創建堆

調整當前非葉子節點

完整代碼


一、二叉樹基礎概念

1.1 基礎概念:

  • 子樹:非空二叉樹節點數N>1時候,除了根節點之外的結點可以分爲互不相交的有限集合,每個集合本身又是一顆二叉樹。
  • 結點的度:結點擁有子樹的數量。
  • 葉子結點,也稱爲終端結點:度爲0的結點
  • 非終端結點,也稱爲分支結點:度不爲0的結點
  • 滿二叉樹:深度爲k的二叉樹具有 2^k-1 個結點
  • 完全二叉樹(堆):完全二叉樹是效率很高的數據結構。對於深度爲K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度爲K的滿二叉樹中編號從1至n的結點一 一對應時稱之爲完全二叉樹。(堆即是完全二叉樹的數組對象)

結點 node,不是節點。

1.2 二叉樹的性質

  • 性質一:二叉樹的第i層上至多有2^(i-1)個節點。
  • 性質二:深度爲k的二叉樹,至多有 2^k-1 個節點
  • 性質三:二叉樹若終端結點爲n0,度爲2的節點爲n2,則 n0=n2+1

前兩個容易證明,第三個性質,所有結點爲n,度爲1的結點個數爲n1,度爲2的結點個數n2

n=n0+n1+n2

設置B爲分支的個數,即兩個結點之間的連接情況,B=n1+2×n2,除了根結點之外,所有結點均有分支進入,n=B+1

結合起來即爲: n0=n2+1

  • 性質四:n結點的完全二叉樹的深度爲 floor[ log2(n) ] +1
  • 性質五:當前節點編號爲n,則其左孩子結點爲 2×n+1,右 2×n+2

 

二、二叉樹的遍歷

2.1 三種遍歷方法

前序,中序,後續是根據根節點出現的位置來命名的。

  • 前序:根,左,右
  • 中序:左,根,右
  • 後續:左,右,根

寬度優先遍歷:

一層一層往下遍歷,10,6,14,4,8,12,16

2.2 二叉樹的構建

 設某棵二叉樹的中序遍歷序列爲ABCD,前序遍歷序列爲CABD,則後序遍歷該二叉樹得到序列爲( )?
正確答案: A 

  • BADC
  • BCDA
  • CDAB
  • CBDA

之前感覺有點難,因爲沒弄明白遍歷是怎麼進行的,弄明白遍歷的進行即可看懂。

2.3 重建方法及解析

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。做這種題需要有遞歸的思想,並不難。理論推導:

前序第一個是根節點的數值,必然是1,中序的時候左邊爲1左邊的值,右邊爲1右邊的值。 然後在分開的左子樹和右子樹之中重複進行此步驟。

剩下的步驟遞歸即可完成。

 

2.4 重建二叉樹的代碼實現

輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。

c++重建二叉樹

關於這個,依然是真的vector的操作需要知道。尾部添加變量的函數是push_back.

/**
 * Definition for binary tree
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
        int length=pre.size();
        if(length==0)return NULL;
        //root node
        TreeNode* root=new TreeNode(pre[0]);
        vector<int> left_pre,left_vin,right_pre,right_vin;
        //find root node in vin
        int root_loc_vin;
        for( root_loc_vin=0;root_loc_vin<length;root_loc_vin++){
            if (vin[root_loc_vin]==pre[0])break;
        }
        //split vin,pre to left
        for(int idx_left_vin=0;idx_left_vin<root_loc_vin;idx_left_vin++){
            left_vin.push_back(vin[idx_left_vin]);
            left_pre.push_back(pre[idx_left_vin+1]);
        }
        //split vin,pre to right
        for(int idx_right_vin=root_loc_vin+1;idx_right_vin<length;idx_right_vin++){
            right_vin.push_back(vin[idx_right_vin]);
            right_pre.push_back(pre[idx_right_vin]);
        }
        root->left=reConstructBinaryTree(left_pre,left_vin);
        root->right=reConstructBinaryTree(right_pre,right_vin);
        return root;
    }
};

有幾點需要注意:

指針類型的函數,如果直接返回,需要返回NULL,

創建新的節點的話需要用指針=new struct(構造函數)

vector的push_back()函數,複習前面的insert()函數,刪除用pop_back()函數

python重建二叉樹

# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
class Solution:
    # 返回構造的TreeNode根節點
    def reConstructBinaryTree(self, pre, tin):
        # write code here
        if(len(pre)==0):
            return None
        root=TreeNode(pre[0])
        # find root loc in tin
        idx_root_tin=tin.index(pre[0])
        root.left=self.reConstructBinaryTree(pre[1:idx_root_tin+1],tin[0:idx_root_tin])
        root.right=self.reConstructBinaryTree(pre[idx_root_tin+1:],tin[idx_root_tin+1:])
        return root

需要了解python的語法,可看python項目應用實例(一)常見運算|維度|基本元素|基本語法|函數 中的八

python中的分片與步長 https://www.cnblogs.com/kuqs/p/6541723.html

分片的時候,[開始位置:結束位置+1]這個要搞清楚

三、二叉樹編程彙總

3.1 樹的子結構

輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)

遞歸非常重要,並且可能存在遞歸的嵌套。這種題需要反覆做,反覆體會。

c++樹的子結構

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    bool RootHasSubTree(TreeNode* pRoot1, TreeNode* pRoot2){
        if(pRoot2==NULL)return true;
        if(pRoot1==NULL)return false;
        if(pRoot1->val==pRoot2->val)return RootHasSubTree(pRoot1->left,pRoot2->left)&&RootHasSubTree(pRoot1->right,pRoot2->right);
        else return false;
    }
    bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
    {
        if(pRoot1==NULL||pRoot2==NULL)return false;
        bool result=false;
        if(pRoot1->val==pRoot2->val)result=RootHasSubTree(pRoot1,pRoot2);
        if(!result)result=HasSubtree(pRoot1->left,pRoot2);
        if(!result)result=HasSubtree(pRoot1->right,pRoot2);
        return result;
    }
};

3.2 鏡像二叉樹

對於二叉樹問題,掌握遞歸則基本掌握了二叉樹的解法。

操作給定的二叉樹,將其變換爲源二叉樹的鏡像。(數的子節點也要倒順序)

c++鏡像二叉樹

跟先遞歸調用mirror,再進行左右交換一樣。

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    void Mirror(TreeNode *pRoot) {
        if(pRoot==NULL)return;
        TreeNode * temp=pRoot->left;
        pRoot->left=pRoot->right;
        pRoot->right=temp;
        Mirror(pRoot->left);
        Mirror(pRoot->right);
        return;
    }
};

3.3 按層打印二叉樹

從上往下打印出二叉樹的每個節點,同層節點從左至右打印。

解析:這叫寬度優先遍歷。

c++按層打印二叉樹

需要掌握queue的操作:https://www.cnblogs.com/danielStudy/p/6726491.html

注意queue的用法與stack類似。也是.push()與.pop(),但是第一個元素是.front(),而stack的頂元素爲.top()

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    vector<int> PrintFromTopToBottom(TreeNode* root) {
        vector<int> value_queue;
        if(root==NULL)return value_queue;
        queue<TreeNode*> node_que;
        node_que.push(root);
        while(!node_que.empty()){
            TreeNode* current_node=node_que.front();
            value_queue.push_back(current_node->val);
            if(current_node->left!=NULL)node_que.push(current_node->left);
            if(current_node->right!=NULL)node_que.push(current_node->right);
            node_que.pop();
        }
        return value_queue;
    }
};

3.4 判斷排序二叉樹後序遍歷

輸入一個整數數組,判斷該數組是不是某二叉搜索樹的後序遍歷的結果。如果是則輸出Yes,否則輸出No。假設輸入的數組的任意兩個數字都互不相同。

解析:二叉樹可以用於排序,並且其中有很多知識點。排序二叉樹的值,後序遍歷,則一定是,順序一定是{小值,大值,中間值}

任意一個節點,只要其左邊的值順序一定是{比它小,比它大}

c++判斷排序二叉樹後序遍歷

如果掌握了前面的知識點,則這個可以很快做出來。

class Solution {
public:
    bool VerifySquenceOfBST(vector<int> sequence) {
        int sequence_size=sequence.size();
        if(sequence_size<1)return false;
        for(int seq_idx=sequence_size-1;seq_idx>0;seq_idx--){
            int compare_idx=0;
            while(sequence[compare_idx]<sequence[seq_idx])compare_idx++;
            while(sequence[compare_idx]>sequence[seq_idx])compare_idx++;
            if(compare_idx!=seq_idx)return false;
        }
        return true;
    }
};

3.5 二叉樹的深度

輸入一棵二叉樹,求該樹的深度。從根結點到葉結點依次經過的結點(含根、葉結點)形成樹的一條路徑,最長路徑的長度爲樹的深度。

解析:基本上,二叉樹的題,都需要運用遞歸,思路對的話,代碼量並不大。

c++二叉樹深度

/*
struct TreeNode {
	int val;
	struct TreeNode *left;
	struct TreeNode *right;
	TreeNode(int x) :
			val(x), left(NULL), right(NULL) {
	}
};*/
class Solution {
public:
    int TreeDepth(TreeNode* pRoot)
    {
        if(pRoot==NULL)return 0;
        int left_depth=TreeDepth(pRoot->left);
        int right_depth=TreeDepth(pRoot->right);
        if(left_depth>right_depth)return left_depth+1;
        else return right_depth+1;
    }
};

3.6 平衡二叉樹

輸入一棵二叉樹,判斷該二叉樹是否是平衡二叉樹。

解析:平衡二叉樹的左右節點深度差值不能超過1.

c++平衡二叉樹

直接算出深度即可。abs表示絕對值函數,可以直接用。直接套用上題深度的測試。寫法上可以更高明一點。

class Solution {
public:
    int tree_depth(TreeNode* pRoot){
        if(pRoot==NULL)return 0;
        int left_depth=tree_depth(pRoot->left);
        int right_depth=tree_depth(pRoot->right);
        if(left_depth>right_depth)return left_depth+1;
        else return right_depth+1;
    }
    bool IsBalanced_Solution(TreeNode* pRoot) {
        if(pRoot==NULL)return true;
        if(abs(tree_depth(pRoot->left)-tree_depth(pRoot->right))>1)return false;
        else return true;
    }
};

選擇結構的寫法可以如下:

class Solution {
public:
    int tree_depth(TreeNode* pRoot){
        if(pRoot==NULL)return 0;
        int left_depth=tree_depth(pRoot->left);
        int right_depth=tree_depth(pRoot->right);
        return (left_depth>right_depth)? left_depth+1:right_depth+1;
    }
    bool IsBalanced_Solution(TreeNode* pRoot) {
        if(pRoot==NULL)return true;
        return (abs(tree_depth(pRoot->left)-tree_depth(pRoot->right))>1)? false:true;
    }
};

3.7 對稱二叉樹

請實現一個函數,用來判斷一顆二叉樹是不是對稱的。注意,如果一個二叉樹同此二叉樹的鏡像是同樣的,定義其爲對稱的。

解析:原題給出的函數接口,只有一個節點,bool isSymmetrical(TreeNode* pRoot),並不利於遞歸調用,可以多設一個函數接口實現調用:

c++對稱二叉樹

/*
struct TreeNode {
    int val;
    struct TreeNode *left;
    struct TreeNode *right;
    TreeNode(int x) :
            val(x), left(NULL), right(NULL) {
    }
};
*/
class Solution {
public:
    bool isSymmetrical(TreeNode* pRoot)
    {
        if(pRoot==NULL)return true;
        return isSymmetrical_node(pRoot->left,pRoot->right);
    }
    bool isSymmetrical_node(TreeNode* pLeft,TreeNode* pRight){
        if(pLeft==NULL&&pRight==NULL)return true;
        else if(pLeft==NULL||pRight==NULL)return false;
        if(pLeft->val==pRight->val)
            return isSymmetrical_node(pLeft->right,pRight->left)&&isSymmetrical_node(pLeft->left,pRight->right);
        else return false;
    }
};

 

四、堆排序

堆(英語:heap)是計算機科學中一類特殊的數據結構的統稱。堆通常是一個可以被看做一棵樹的數組對象。若將和此次序列對應的一維數組(即以一維數組作此序列的存儲結構)看成是一個完全二叉樹,則堆的含義表明,完全二叉樹中所有非終端結點的值均不大於(或不小於)其左、右孩子結點的值。由此,若序列{k1,k2,…,kn}是堆,則堆頂元素(或完全二叉樹的根)必爲序列中n個元素的最小值(或最大值)。

樹形選擇排序方法尚有輔助存儲空間較多、和“最大值”進行多餘比較等缺點。爲了彌補,威洛姆斯(J. willioms)在1964年提出了另一種形式的選擇排序——堆排序

https://www.cnblogs.com/chengxiao/p/6129630.html

堆排序是利用這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,它的最壞,最好,平均時間複雜度均爲O(nlogn),它也是不穩定排序。首先簡單瞭解下堆結構。

堆排序運用完全二叉樹的性質,將二叉樹的節點地址編號爲地址,不用構建帶有指針的二叉樹,只用數組即可實現二叉樹。

爲什麼不穩定排序:舉一個反例即可,兩個葉子節點上的數相同,但是葉子節點有可能進行swap,也有可能不進行swap,就可能無法保持穩定。

4.1 大頂堆與小頂堆

排序中的堆與隊列和程序中的堆與棧不一樣,注意區分。

堆是具有以下性質的完全二叉樹:每個結點的值都大於或等於其左右孩子結點的值,稱爲大頂堆;或者每個結點的值都小於或等於其左右孩子結點的值,稱爲小頂堆。如下圖:

注意只需要父節點大於子節點,並不需要子節點之間進行排序。

4.2 基本思想與步驟

堆排序的基本思想是:將待排序序列構造成一個大頂堆,此時,整個序列的最大值就是堆頂的根節點。將其與末尾元素進行交換,此時末尾就爲最大值。然後將剩餘n-1個元素重新構造成一個堆,這樣會得到n個元素的次小值。如此反覆執行,便能得到一個有序序列了

再簡單總結下堆排序的基本思路:

  a.將無需序列構建成一個堆,根據升序降序需求選擇大頂堆或小頂堆;

  b.將堆頂元素與末尾元素交換,將最大元素"沉"到數組末端;

  c.重新調整結構,使其滿足堆定義,然後繼續交換堆頂元素與當前末尾元素,反覆執行調整+交換步驟,直到整個序列有序。

步驟一、構造初始堆

將給定無序序列構造成一個大頂堆(一般升序採用大頂堆,降序採用小頂堆)。容易理解,大頂堆在交換之後最後的元素最大,小頂堆在交換之後最後的元素最小。

  a.假設給定無序序列結構如下

此時我們從最後一個非葉子結點開始(葉結點自然不用調整,第一個非葉子結點 arr.length/2-1=5/2-1=1,也就是下面的6結點),從左至右,從下至上進行調整。(注意關於堆的性質,有一個性質是,第一個非葉子節點的編號=length/2 -1

找到第二個非葉節點4,由於[4,9,8]中9元素最大,4和9交換。(堆之中是三個數字進行比較)

這時,交換導致了子根[4,5,6]結構混亂,繼續調整,[4,5,6]中6最大,交換4和6。

此時,我們就將一個無需序列構造成了一個大頂堆。

步驟二、頂端與末尾交換

頂端與末尾元素交換之後,迭代進行步驟一

將堆頂元素與末尾元素進行交換,使末尾元素最大。然後繼續調整堆,再將堆頂元素與末尾元素交換,得到第二大元素。如此反覆進行交換、重建、交換。

a.將堆頂元素9和末尾元素4進行交換

b.重新調整結構,使其繼續滿足堆定義

c.再將堆頂元素8與末尾元素5進行交換,得到第二大元素8.

後續過程,繼續進行調整,交換,如此反覆進行,最終使得整個序列有序

4.3 代碼

https://www.cnblogs.com/AlgrithmsRookie/p/5896603.html

創建堆

此爲創建大頂堆的代碼,這種遍歷是自底向頂的遍歷

 void make_heap(int *a, int len)
 {
    for(int i =  (len-1)/2; i >= 0; --i)    //遍歷每個 非葉子節點
                  adjust_heap(a, i, len);//不用考慮那麼多, 用面向對象的思鄉去考慮,   
 }                                    //這個函數的作用就是用來使當前節點的子樹符合堆的規律

調整當前非葉子節點

當前非葉子節點構建大頂堆,

void adjust_heap(int* a, int node, int size)
{
        int left = 2*node + 1;
        int right = 2*node + 2;  
        int max = node;
        if( left < size && a[left] > a[max])    
                max = left;
        if( right < size && a[right] > a[max])
                max = right;
        if(max != node)
        {   
                swap( a[max], a[node]);    //交換節點
                adjust_heap(a, max, size);     //遞歸
        }                     
}

最後的遞歸表示,如果node與max的值交換之後,新的a[max]可能比它的兩個兒子小,即管不住它的兩個兒子,因此需要繼續遞歸。

同時,考慮到如果node爲葉子節點,則left和right<size這個判斷就能保證了。

完整代碼

#include <iostream>
using namespace std;
 
void adjust_heap(int* a, int node, int size)
{
        int left = 2*node + 1;
        int right = 2*node + 2;
        int max = node;
        if( left < size && a[left] > a[max])
                max = left;
        if( right < size && a[right] > a[max])
                max = right;
        if(max != node)
        {
                swap( a[max], a[node]);
                adjust_heap(a, max, size);
        }
}
 
void heap_sort(int* a, int len)
{
        for(int i = len/2 -1; i >= 0; --i)
                adjust_heap(a, i, len);
 
        for(int i = len - 1; i >= 0; i--)
        {
                swap(a[0], a[i]);           // 將當前最大的放置到數組末尾
                adjust_heap(a, 0 , i);              // 將未完成排序的部分繼續進行堆排序
        }
}
 
int main()
{
 
        int a[10] = {3, 2, 7, 4, 2, -999, -21, 99, 0, 9  };
        int len= sizeof(a) / sizeof(int);
 
        for(int i = 0; i < len; ++i)
                cout << a[i] << ' ';
        cout << endl;
 
        heap_sort(a, len);
 
        for(int i = 0; i < len; ++i)
                cout << a[i] << ' ';
        cout << endl;
 
        return 0;
}

五、紅黑樹

後續更新

5.1 定義

https://baike.baidu.com/item/%E7%BA%A2%E9%BB%91%E6%A0%91/2413209?fr=aladdin

紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,典型的用途是實現關聯數組。它雖然是複雜的,但它的最壞情況運行時間也是非常良好的,並且在實踐中是高效的: 它可以在O(log n)時間內做查找,插入和刪除,這裏的n 是樹中元素的數目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章