【c++】構建一棵簡單的二叉樹
阿新 • • 發佈:2019-02-02
本文主要講了如何使用c++來構建一個二叉樹類,以及一些功能演算法的實現。文中大部分函式的思想都是遞迴,其中賦值運算子過載有傳統寫法和現代寫法兩個版本,層序遍歷是非遞迴,前、中、後序遍歷有遞迴和非遞迴兩個版本。
1、建構函式(遞迴)
2、拷貝建構函式(遞迴)
3、解構函式(遞迴)
4、賦值運算子過載(傳統/現代)
5、前中後序遍歷(遞迴/非遞迴)
6、層序遍歷(非遞迴)
7、查詢第k層結點個數(遞迴)
8、精確查詢值為x的結點,並返回當前結點的指標(遞迴)
9、查詢葉子結點個數(遞迴)
10、查詢結點總個數(遞迴)
11、計算樹的深度(遞迴)
樹的結點型別,每個結點都需要有一個指向右孩子的指標,一個指向左孩子的指標,以及一個數據。(當然,如果你想構造三叉樹的話,也可以增加一個指向父節點的指標。)
這裡我寫出了結點的建構函式,方便我們在建立樹的時候使用。
注意:這棵樹我使用了模板
template<typename T>
struct BinaryTreeNode
{
BinaryTreeNode<T>* _left;//左孩子
BinaryTreeNode<T>* _right;//右孩子
T _data;//資料
BinaryTreeNode(T data = T())//結點自己的建構函式,T()為一個匿名物件。
:_left(NULL)//初始化為空
, _right(NULL)
, _data(data)
{}
};
下面程式碼中會經常用到BinaryTreeNode<T> 所以將其重新命名為Nodetypedef BinaryTreeNode<T> Node;
當我們向寫二叉樹類的時候,直接給類設定一個根結點,以這個根結點為基礎,構建二叉樹。
class BinaryTree
{
public:
private:
BinaryTreeNode<T>* _root;//根節點
};
1、建構函式 BinaryTree(const T* a, size_t size,int index, const T& invalid)
建構函式有4個引數,T型別的指標a,傳參時傳一個數組,負責傳入資料。size儲存陣列a 的大小,index記錄下標,invalid表示非法值。
BinaryTree(const T* a, size_t size,int index, const T& invalid)
{
_root = _MakeTree(a,size,index,invalid);
}
我們先來觀察一個樹:
你會看到上面有許多NULL,這些NULL我們就可以理解為非法值invalid。這棵樹的前序遍歷為: 1 2 3 NULL NULL 4 NULL NULL 5 6 最後一個結點不需要非法值,到時候直接建立即可。 與上對應,我們傳陣列時,應該傳的值即為 int a[10] = {1,2,3,'#','#',4,'#','#',5,6}。非法值的值可以隨意設,這裡我設為‘#’,注意,你向以什麼的樣的順序建樹,就以什麼樣的順序傳參,事先要約定好。(這裡我用的是前序) 遞迴:當我們從陣列讀取到一個數據時,我們先要判斷這個值是不是合法,如果合法則new出一個結點並初始化作為當前結點,此時,進入左孩子遞迴函式讀取下一個資料(++index),並把這個函式的返回值鏈到當前結點root的left,同理,將右孩子遞迴函式的返回值鏈到當前結點的right。如果不合法則return,返回上一層函式。最後我們會得到一個根節點,例圖中的1。 _MakeTree函式實現:
Node* _MakeTree(const T* a, size_t size, int& index, const T& invalid)
{
Node *root = NULL;
if (index < size && a[index] != invalid)
{
root = new Node(invalid);
root->_data = a[index];
root->_left = _MakeTree(a, size, ++index, invalid);
root->_right = _MakeTree(a, size, ++index, invalid);
}
return root;
}
2、拷貝建構函式 同上,同樣實現一個遞迴函式,返回值仍為Node*
BinaryTree(const BinaryTree<T>& t)
{
_root = CopyTree(t._root);
}
遞迴:同樣為前序,從根節點開始,先訪問當前結點,判斷,若當前結點為空,則返回一個空。若當前結點不為空,則拷貝當期結點。然後遞迴進入當前結點的左子樹,同樣進行之前的步驟。左子樹處理完之後遞迴處理右子樹。然後返回當前結點(每一層的根節點)。
注意:上文提到的當前結點,在每一層的遞迴中值都是不一樣的。每遞迴一層,當前結點就會變成傳入的引數root。包括下文
CopyTree實現程式碼:
Node* CopyTree(const BinaryTreeNode<T>* _root)3、
{
if (_root == NULL)
{
return NULL;
}
Node* root = new Node(_root->_data);
root->_left = CopyTree(_root->_left);
root->_right = CopyTree(_root->_right);
return root;
}
3、解構函式 同上,但是解構函式不需要返回值。
~BinaryTree()
{
Destroy(_root);
}
遞迴的道理都與上面兩個相同,這裡直接給出程式碼:
void Destroy( Node* _root)
{
Node* tmp = _root;
if (tmp == NULL)//如果根結點為空,則不需要delete,直接return。
return;
Destroy(tmp->_left);
Destroy(tmp->_right);
delete tmp;
tmp = NULL;
}
4、賦值運算子過載(=)
當我們寫任何賦值運算子過載時,都會有兩種寫法
①傳統寫法,一個元素一個元素的拷貝,傳參時傳的是const的引用。
這樣賦值有個麻煩的地方,我們知道,當給一個結點賦值的時候,這個結點原本的內容,空間就要被銷燬掉。就是說我們既要new又要delete。當這個樹有1個億結點時,我們豈不是要重複1億次?有沒有一種更簡單的方法呢?
②現代寫法(建議使用,很巧妙),呼叫swap函式,交換兩個樹的root。傳參時用的值傳遞。
BinaryTree<T>& operator=(BinaryTree<T> t)
{
if (this != &t)//自賦值的優化
{
std::swap(_root, t._root);
}
return *this;
}
我們都知道,值傳遞傳的是一份臨時拷貝(t為一份臨時拷貝),臨時拷貝的特性就是,它的存活週期只限於這個函式,當函式呼叫完畢時,它會自動銷燬,而我們也恰恰利用了這個特性。當我們執行std::swap(_root,t._root)這個語句的時候,臨時拷貝t的_root 和 本樹的(this)_root發生了交換。_root原本的值被賦給了臨時拷貝t._root,t._root的值被賦給了_root。當我們執行完這個程式的時候t自動銷燬,幫我們完成了銷燬_root原本內容的工作。我們即完成了賦值,又省去了一大部分工作,一舉兩得。
5、前中後序遍歷(遞迴)
①前序 這個我就不再多說了,構造,拷貝構造,析構,都是利用前序遍歷的道理來做的。當前結點-->左子樹-->右子樹。 程式碼:
void PrevOrder()
{
_PrevOrder(_root);
cout << endl;
}
_PrevOrder()
void _PrevOrder(Node* _root)
{
Node* tmp = _root;
if (tmp == NULL)
{
return;
}
cout << tmp->_data << " ";
_PrevOrder(tmp->_left);
_PrevOrder(tmp->_right);
}
②中序
判斷當前結點是否為空,為空的話,不處理,直接返回。先遞迴訪問當前結點左子樹,當左子樹處理完畢,再依次返回處理當前結點,再遞迴訪問當前結點右子樹。
void InOrder()
{
_InOrder(_root);
cout << endl;
}
_InOrder()
void _InOrder(Node* _root)
{
Node* tmp = _root;
if (tmp == NULL)
{
return;
}
_InOrder(tmp->_left);
cout << tmp->_data << " ";
_InOrder(tmp->_right);
}
③後序 判斷當前結點是否為空,為空的話,不處理,直接返回。不為空的話,先遞迴訪問當前結點節點的左子樹,再遞迴訪問當前結點根節點的右子樹,最後訪問當前結點。
void PostOrder()
{
_PostOrder(_root);
cout << endl;
}
_PostOrder()void _PostOrder(Node* _root)
{
Node* tmp = _root;
if (tmp == NULL)
{
return;
}
_PostOrder(tmp->_left);
_PostOrder(tmp->_right);
cout << tmp->_data << " ";
}
6、前中後序非遞迴。
這裡我告訴大家一個真理,任何的遞迴都可以用棧來替換實現。這裡我就用一個輔助棧來實現前中後序的非遞迴。
①前序
程式碼:
void PrevOrder_NonR()
{
Node* cur = _root;
stack<Node*> s;
if (cur == NULL)
{
return;
}
while (cur || !s.empty())
{
while (cur)
{
s.push(cur);
cout << cur->_data << " ";
cur = cur->_left;
}
Node* top = s.top();
s.pop();
cur = top->_right;
}
cout << endl;
}
中序和後序的道理與上相同,只是當前節點的輸出做了小小的改動。下面直接貼出程式碼 ②中序(非遞迴)
void InOrder_NonR()
{
Node* cur = _root;
stack<Node*> s;
if (cur == NULL)
{
return;
}
while (cur || !s.empty())
{
while (cur)
{
s.push(cur);
cur = cur->_left;
}
Node* top = s.top();
cout << top->_data << " ";
s.pop();
cur = top->_right;
}
cout << endl;
}
③後序(非遞迴)
後序的非遞迴較上面兩個多了一個變數,prev,它記錄了上一次訪問的節點。因為後序是最後訪問當前節點的,當我們訪問一個節點,我們不知道這個節點的右樹是否被訪問過。所以我們需要記錄一下上一個訪問的節點。以便訪問右樹時做判斷。
void PostOrder_NonR()
{
Node* cur = _root;
Node* prev = NULL;
stack<Node*> s;
while (cur || s.empty())
{
while (cur)
{
s.push(cur);
cur = cur->_left;
}
Node* top = s.top();
//如果右樹為空或者右樹已經訪問過,則訪問當前結點,並出棧
//如果右樹不為空並且沒有訪問過,則訪問右樹
if (top->_right == NULL || prev == top->_right)
{
cout << top->_data << " ";
prev = top;
s.pop();//返回父節點
}
else
{
cur = top->_right;
}
}
cout << endl;
}
7、層序遍歷 顧名思義。層序遍歷就是按層來訪問一顆樹,一次訪問一層。這裡我們用到了一個佇列。
程式碼:
void _LevelOrder(Node* _root)
{
Node *tmp = _root;
queue<Node*> q;
q.push(tmp);
while (!q.empty())
{
Node* top = q.front();
q.pop();
cout << top->_data << " ";
if (top->_left)
{
q.push(top->_left);
}
if (top->_right)
{
q.push(top->_right);
}
}
}
8、查詢第k層節點個數 這個問題,我們只需要查詢第k-1層有多少個孩子就行了。 同樣為遞迴,前序。
size_t FindKlevel(size_t k)
{
return _FindKlevel(_root,k);
}
_FindKlevel()
size_t _FindKlevel(Node* _root,size_t k)
{
Node *cur = _root;
if (cur == NULL || k < 0)
{
return 0;
}
if (k == 1)
{
return 1;
}
size_t left = _FindKlevel(cur->_left, k-1);
size_t right = _FindKlevel(cur->_right, k-1);
return left + right;
}
9、精確查詢值為x的結點
前序遞迴查詢,如果根節點為空,返回NULL,如果當前節點等於x,返回當前節點的指標。如果當前節點不等於x,則遞迴進入左子樹查詢,若左子樹沒有,則遞迴進入右子樹查詢,若這棵樹中沒有x,返回NULL。
Node* Find(const T& x)
{
return _Find(_root,x);
}
_Find()Node* _Find(Node* _root,const T& x)
{
Node* ret = NULL;
Node* cur = _root;
if (cur == NULL)
{
return NULL;
}
if (cur->_data == x)
{
ret = cur;
}
else
{
ret = _Find(cur->_left,x);
if (ret == NULL)
{
ret = _Find(cur->_right,x);
}
}
return ret;
}
10、查詢結點總個數。 結點總個數 = 當前節點個數+左子樹節點個數+右子樹節點個數。 前序遞迴查詢,若當前節點為空,返回,不為空則加1,--->遞迴左子樹----->遞迴右子樹。
size_t Size()
{
return _Size(_root);
}
_Size()
size_t _Size( BinaryTreeNode<T>* _root )
{
size_t ret = 0;
if (_root == NULL)
{
return ret;
}
ret++;
ret += _Size(_root->_left);
ret += _Size(_root->_right);
}
11、計算樹的深度
樹的深度取左子樹深度+1和右子樹深度+1的最大值。(+1為根節點的深度)
size_t Depth()
{
return _Depth(_root);
}
_Depth()
size_t _Depth(Node* _root)
{
if (_root == NULL)
{
return 0;
}
int left = _Depth(_root->_left) + 1;
int right = _Depth(_root->_right) + 1;
return left > right ? left : right;
}
。請大家採用批評的態度來看這篇博文,博主水平也一般。。。。