遞迴建立二叉樹以及一些基本操作
基本內容
- 二叉樹的基本概念和遍歷方式
- 使用遞迴建立一個簡單的二叉樹
- 二叉樹使用遞迴遍歷時的呼叫棧幀
- 實現程式碼
- 一些基本操作的遞迴實現
一、一些基本的東西
首先我們要明確,二叉樹是一種資料結構,相比於之前的順序表和連結串列,二叉樹是一種非線性結構,比較特殊,剛開始入門二叉樹,我們需要了解二叉樹的基本結構框架,知道如何搭建一個簡單的二叉樹,當然在這篇筆記或者部落格裡面提到的基本都是遞迴演算法,要知道一些基本的二叉樹的操作。
之前我們在學習例如順序錶鏈表,棧和佇列,會涉及到增刪查改,但是在二叉樹的基本部分我們不會涉及到增刪查改,因為二叉樹的實際用處不是很大,我們最重要的是需要了解二叉樹的基本結構,為以後的搜尋樹和平衡樹的學習打下基礎。二叉樹的遍歷是目前階段我們需要熟練掌握的內容。
二叉樹的遍歷方式有三種:前序、中序和後序。 在學習二叉樹的時候,把二叉樹分為三部分:根結點,左子樹和右子樹,所謂遍歷方式即訪問這三部分的先後順序。
我對於二叉樹遍歷的方式的理解是這樣的:
前序遍歷:先訪問根結點,再訪問左子樹,最後訪問右子樹。
中序遍歷:先訪問左子樹,再訪問根節點,最後訪問右子樹。
後序遍歷:先訪問左子樹,再訪問右子樹,最後訪問根結點。
以一個簡單的二叉樹為例子
該二叉樹一共有六個結點,將6個整數放置其中。
一個結點裡面分為三個部分,一個部分存放資料,另外兩個存放指向左子樹根結點的指標和指向右子樹根結點的指標。
二、一些基本的程式碼
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<assert.h>
#include<stack>
#include<queue>
using namespace std;
template<class T>
struct BinaryTreeNode
{
T _data;
BinaryTreeNode<T>* _left;
BinaryTreeNode<T>* _right;
BinaryTreeNode(const T& d)
:_data(d)
,_left(NULL)
, _right(NULL)
{
cout << "構造二叉樹結點" << endl;
}
};
template<class T>
class BinaryTree
{
typedef BinaryTreeNode<T> Node;
private:
Node* _root;
protected:
Node* _CreateTree(T* a, size_t n, const T& invalid, size_t& index) //這裡的index由呼叫這個函式的那個函式裡面給傳進來;但是這裡出現了問題,還是因為棧幀的原因,在往根節點回的時候,下層的index並不會影響上層的index從而導致錯 誤解決方案是: &
{
//index = 0; //注意:這裡不能給下標一個初值,遞迴會出現問題
Node* root = NULL;
if (index < n && a[index] != invalid)
{
root = new Node(a[index]);
root->_left = _CreateTree(a, n, invalid, ++index); //出現的問題: 我一開始寫的是index++,但是出現了無限迴圈,注意建立的新的棧幀就會產生新的index
root->_right = _CreateTree(a, n, invalid, ++index);
}
return root;
}
void _PrevOrder(Node* root) //前序遍歷
{
if (root == NULL) //返回條件
return;
cout << root->_data << " ";
_PrevOrder(root->_left);
_PrevOrder(root->_right);
}
void _InOrder(Node* root) //中序遍歷
{
if (root == NULL) //返回條件
return;
_InOrder(root->_left);
cout << root->_data << " ";
_InOrder(root->_right);
}
void _PostOrder(Node* root) //後序遍歷
{
if (root == NULL) //返回條件
return;
_PostOrder(root->_left);
_PostOrder(root->_right);
cout << root->_data << " ";
}
void _LevelOrder(Node* root) // 層序遍歷 使用非遞迴 利用queue的先進先出原則 [*]
{
queue<Node*> q;
if (root) //根結點入隊
q.push(root);
else
return;
while (!q.empty())
{
Node* cur = q.front();
cout << cur->_data << " ";
if (cur->_left)
q.push(cur->_left);
if (cur->_right)
q.push(cur->_right);
q.pop(); //這裡實際上是通過pop讓樹往下走的,完成了類似於遞迴的作用
}
cout << endl;
}
size_t _Size(Node* root) //求結點個數
{
if (root == NULL)
return 0;
if (root->_left == NULL && root->_right == NULL)// 在這個遞迴中的返回條件
return 1;
return _Size(root->_left) + _Size(root->_right) + 1; //這裡記得+1 因為有根結點
}
size_t _LeafSize(Node* root) //求葉子結點個數
{
if (root == NULL)
return 0;
if (root->_left == NULL&&root->_right == NULL)
return 1;
return _LeafSize(root->_left) + _LeafSize(root->_right);
}
size_t _GetKLevel(Node* root, size_t k) //求第K層結點個數
{
if (root == NULL)
return 0;
if (k == 1)
return 1;
if (k > 1)
return _GetKLevel(root->_left, k - 1) + _GetKLevel(root->_right, k - 1); //尾遞迴
else
{
cout << "'k' is wrong" << " ";
return 0;
}
}
size_t _Depth(Node* root) //求二叉樹的深度
{
if (root == NULL)
return 0;
if (root->_left == NULL && root->_right == NULL)
return 1;
size_t leftdepth = _Depth(root->_left); //分兩邊進行比較
size_t rightdepth = _Depth(root->_right);
return leftdepth > rightdepth ? leftdepth + 1 : rightdepth + 1; //不要忘記+1 根結點
}
Node* _Find(Node* root,const T& t) //查詢 [*]
{
/*if (root == NULL)
return NULL;
Node* tmp = _Find(root->_left, t);
if (root->_data == t)
{
return root;
}
_Find(root->_right, t);*/
if (root == NULL)
return NULL;
if (root->_data == t)
return root;
Node* tmp = _Find(root->_left, t);
if (tmp)
return tmp;
return _Find(root->_right, t);
}
Node* _Copy(Node* root) //拷貝 [*]
{
if (root == NULL)
return NULL;
Node* copyroot = new Node(root->_data);
copyroot->_left = _Copy(root->_left);
copyroot->_right = _Copy(root->_right);
return copyroot;
}
void Destory(Node* root)
{
if (root == NULL)
return;
Destory(root->_left);
Destory(root->_right);
delete root; //釋放當前結點
root = NULL;
}
public:
BinaryTree() //無參建構函式
: _root(NULL)
{
}
BinaryTree(T* a, size_t n, const T& invalid) //帶參的建構函式 //注意:無參的遞迴是不能寫在公有的成員函式裡面的
{
size_t index = 0; //注意這裡為什麼我們把index單獨拿出來給初始化0
_root = _CreateTree(a, n, invalid, index);
}
BinaryTree(const BinaryTree<T>& t) //拷貝構造
{
_root = _Copy(t._root);
}
BinaryTree<T>& operator=(const BinaryTree<T>& t) //賦值運算子過載
{
if (_root)
Destory(_root);
_root = _Copy(t._root);
return *this;
}
void PrevOrder() //前序遍歷
{
_PrevOrder(_root);
cout << endl;
}
void InOrder() //中序遍歷
{
_InOrder(_root);
cout << endl;
}
void PostOrder() //後序遍歷
{
_PostOrder(_root);
cout << endl;
}
void LevelOrder() //層序遍歷 (這個用不到遞迴,所以可以不用這樣寫)
{
_LevelOrder(_root);
}
size_t Size() //結點個數
{
return _Size(_root);
}
size_t LeafSize() //葉子結點個數
{
return _LeafSize(_root);
}
size_t GetKLevel(size_t k) //K層結點
{
return _GetKLevel(_root, k);
}
size_t Depth() //樹的深度
{
return _Depth(_root);
}
Node* Find(const T& d) //查詢
{
return _Find(_root, d);
}
~BinaryTree() //析構
{
Destory(_root);
_root = NULL;
cout << "析構" << endl;
}
};
void test() //———————— 測試 ——————————
{
int arr[] = { 1, 2, 3, '#', '#', 4, '#', '#', 5, 6 };
BinaryTree<int> t1(arr,sizeof(arr)/sizeof(arr[0]),'#');
t1.PrevOrder();
t1.InOrder();
t1.PostOrder();
t1.LevelOrder();
cout << t1.Size() << endl;
cout << t1.LeafSize() << endl;
cout<<t1.GetKLevel(3)<<endl;
cout << t1.Find(3) << endl;
BinaryTree<int> t2(t1);
t2.PrevOrder();
t2.InOrder();
t2.PostOrder();
t2.LevelOrder();
cout << t2.Size() << endl;
cout << t2.LeafSize() << endl;
cout << t2.GetKLevel(3) << endl;
cout << t2.Find(3) << endl;
BinaryTree<int> t3;
t3 = t1;
t3.PrevOrder();
t3.InOrder();
t3.PostOrder();
t3.LevelOrder();
}
int main()
{
test();
return 0;
}
在樹的建立程式碼中出現的一個問題:為什麼下標index 要使用引用?
Node* _BinaryTree(T* a, size_t n, const T& invalid, size_t& index)
注意上面的建構函式裡面的index使用了引用
如果不使用引用就會出現問題
原因是什麼呢?我的理解是這樣的:
由於我們使用陣列來建立,index是下標,是需要不斷的往後走的,如果不使用引用,當開闢了棧空間之後新的棧空間的index得上一層棧空間的index沒有影響,這就導致了左子樹建立完畢了回到根節點來建立右子樹的時候index又回退了,因為是根據陣列來建樹的,index就不能回退,所以所以使用引用是為了讓陣列的下標不斷往後走不受棧幀的影響。
三、遞迴建立以及遍歷的呼叫棧幀
二叉樹在建造的時候的棧幀圖:
(圖中的程式碼已經簡化不具有嚴格的正確性只是作為示意)
此二叉樹在遍歷(前序)的時候棧幀示意圖:
三、一些基本的操作介面的遞迴實現
1.遞迴求第K層的葉子結點數:
程式碼:
size_t _KLevelSize(Node* root,const size_t& K) [*]
{
if (root == NULL)
return 0;
if (K == 1)
return 1;
if (K > 1)
return _KLevelSize(root->_left, K - 1) + _KLevelSize(root->_right, K - 1);
else
perror("K is wrong!");
}
呼叫棧幀示意:
2.遞迴求數的深度(左樹與右樹相比)
程式碼:
size_t _Depth(Node* root)
{
if (root == NULL)
return 0;
if (root->_left == NULL&&root->_right == NULL)
return 1;
size_t leftDepth = _Depth(root->_left);
size_t rightDepth = _Depth(root->_right);
return leftDepth > rightDepth? leftDepth + 1: rightDepth+1; //加1是加上根結點
}
3.遞迴查詢 *
注:查詢即一旦找到了就返回(返回到最頂上的棧空間) 結束整個遞迴的過程
程式碼:
Node* _Find(Node* root, const T& d) //查詢[*]
{
if (root == NULL)
return NULL;
if (root->_data == d)
return root;
/*else //這樣寫有問題
{
_Find(root->_left,d);
_Find(root->_right, d);
}*/
Node* tmp = _Find(root->_left,d);
if (tmp != NULL) //注意這個判斷放的位置
return tmp;
_Find(root->_right, d); //子問題思想
}
4.遞迴 析構樹(Destory函式)
注意:這裡要注意對於一棵樹而言,析構或者釋放空間是從底下往上面析構的,也就是需要先析構子節點再析構父親節點,對於每一個子問題而言也是這樣,所以再函式中delete應該放在遞迴呼叫的後面
程式碼:
void Destory(Node* root) //[*]
{
if (root == NULL)
return;
Destory(root->_left);
Destory(root->_right);
delete root; //注意這裡的delete一定要放在最後 相當於後序析構,因為先析構根結點子樹就找不到了,沒辦法析構
}
5.遞迴 拷貝樹(Copy函式)
注意:針對於實現二叉樹的拷貝構造和賦值運算子的過載的時候 使用到了拷貝函式,這裡也需要建立子問題的思想
程式碼:
Node* _Copy(Node* root)
{
Node* newroot = NULL;
if (root == NULL)
return NULL;
newroot = new Node(root->_data);
newroot->_left = _Copy(root->_left);
newroot->_right = _Copy(root->_right);
return newroot;
}
總結:
以上只是遞迴實現
遞迴是用空間換取時間(寫程式碼簡單)的程式設計方式,如果使用非遞迴,編寫的難度就會增加,在後面的部落格會繼續整理有關於非遞迴的方式遍歷樹以及一些非遞迴實現的操作介面