【c++】构建一棵简单的二叉树

  转载自:

      http://blog.csdn.net/LLZK_/article/details/52829525

  

本文主要讲了如何使用c++来构建一个二叉树类,以及一些功能算法的实现。文中大部分函数的思想都是递归,其中赋值运算符重载有传统写法和现代写法两个版本,层序遍历是非递归,前、中、后序遍历有递归和非递归两个版本。

1、构造函数(递归)

2、拷贝构造函数(递归)

3、析构函数(递归)

4、赋值运算符重载(传统/现代)

5、前中后序遍历(递归/非递归)

6、层序遍历(非递归)

7、查找第k层结点个数(递归)

8、精确查找值为x的结点,并返回当前结点的指针(递归)

9、查找叶子结点个数(递归)

10、查找结点总个数(递归)

11、计算树的深度(递归)

 

树的结点类型,每个结点都需要有一个指向右孩子的指针,一个指向左孩子的指针,以及一个数据。(当然,如果你想构造三叉树的话,也可以增加一个指向父节点的指针。)

这里我写出了结点的构造函数,方便我们在创建树的时候使用。

注意:这棵树我使用了模板

[cpp] view plain copy
 
  1. template<typename T>  
  2. struct BinaryTreeNode  
  3. {  
  4.     BinaryTreeNode<T>* _left;//左孩子  
  5.     BinaryTreeNode<T>* _right;//右孩子  
  6.     T _data;//数据  
  7.     BinaryTreeNode(T data = T())//结点自己的构造函数,T()为一个匿名对象。  
  8.         :_left(NULL)//初始化为空  
  9.         , _right(NULL)  
  10.         , _data(data)  
  11.     {}  
  12. };  
 
下面代码中会经常用到BinaryTreeNode<T>   所以将其重命名为Node
[cpp] view plain copy
 
  1. typedef BinaryTreeNode<T> Node;  



当我们向写二叉树类的时候,直接给类设定一个根结点,以这个根结点为基础,构建二叉树。
[cpp] view plain copy
 
  1. class BinaryTree  
  2. {  
  3.  public:  
  4.  private:  
  5.     BinaryTreeNode<T>* _root;//根节点  
  6. };  


1、构造函数
     BinaryTree(const T* a, size_t size,int index, const T& invalid)
构造函数有4个参数,T类型的指针a,传参时传一个数组,负责传入数据。size保存数组a 的大小,index记录下标,invalid表示非法值。
因为我们需要用到递归,所以在这个函数内部我们需要再封装一个递归函数_MakeTree(),并让它的返回值为一个Node*类型的指针。
[cpp] view plain copy
 
  1. BinaryTree(const T* a, size_t size,int index, const T& invalid)  
  2. {  
  3.     _root = _MakeTree(a,size,index,invalid);  
  4. }  
 
我们先来观察一个树:
你会看到上面有许多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函数实现:
[cpp] view plain copy
 
  1. Node* _MakeTree(const T* a, size_t size, int& index, const T& invalid)  
  2. {  
  3.     Node *root = NULL;  
  4.     if (index < size && a[index] != invalid)  
  5.     {  
  6.         root = new Node(invalid);  
  7.         root->_data = a[index];  
  8.         root->_left = _MakeTree(a, size, ++index, invalid);  
  9.         root->_right = _MakeTree(a, size, ++index, invalid);  
  10.     }  
  11.     return root;  
  12. }  

2、拷贝构造函数
同上,同样实现一个递归函数,返回值仍为Node*
[cpp] view plain copy
 
  1. BinaryTree(const BinaryTree<T>& t)  
  2. {  
  3.     _root = CopyTree(t._root);  
  4. }  
 
递归:同样为前序,从根节点开始,先访问当前结点,判断,若当前结点为空,则返回一个空。若当前结点不为空,则拷贝当期结点。然后递归进入当前结点的左子树,同样进行之前的步骤。左子树处理完之后递归处理右子树。然后返回当前结点(每一层的根节点)。
 
注意:上文提到的当前结点,在每一层的递归中值都是不一样的。每递归一层,当前结点就会变成传入的参数root。包括下文
 
CopyTree实现代码:
[cpp] view plain copy
 
  1. Node* CopyTree(const BinaryTreeNode<T>* _root)3、  
  2. {  
  3.     if (_root == NULL)  
  4.     {  
  5.         return NULL;  
  6.     }  
  7.     Node* root = new Node(_root->_data);  
  8.     root->_left = CopyTree(_root->_left);  
  9.     root->_right = CopyTree(_root->_right);  
  10.     return root;  
  11. }  

3、析构函数
同上,但是析构函数不需要返回值。
[cpp] view plain copy
 
  1. ~BinaryTree()  
  2. {  
  3.     Destroy(_root);  
  4. }  
 
递归的道理都与上面两个相同,这里直接给出代码:
[cpp] view plain copy
 
  1. void Destroy( Node* _root)  
  2. {  
  3.     Node* tmp = _root;  
  4.     if (tmp == NULL)//如果根结点为空,则不需要delete,直接return。  
  5.          return;  
  6.     Destroy(tmp->_left);  
  7.     Destroy(tmp->_right);  
  8.     delete tmp;  
  9.     tmp = NULL;  
  10. }  
 
4、赋值运算符重载(=)
 
当我们写任何赋值运算符重载时,都会有两种写法
 
①传统写法,一个元素一个元素的拷贝,传参时传的是const的引用。
这样赋值有个麻烦的地方,我们知道,当给一个结点赋值的时候,这个结点原本的内容,空间就要被销毁掉。就是说我们既要new又要delete。当这个树有1个亿结点时,我们岂不是要重复1亿次?有没有一种更简单的方法呢?
 
②现代写法(建议使用,很巧妙),调用swap函数,交换两个树的root。传参时用的值传递。
[cpp] view plain copy
 
  1. BinaryTree<T>& operator=(BinaryTree<T> t)  
  2. {  
  3.     if (this != &t)//自赋值的优化  
  4.     {  
  5.         std::swap(_root, t._root);  
  6.     }  
  7.     return *this;  
  8. }  
我们都知道,值传递传的是一份临时拷贝(t为一份临时拷贝),临时拷贝的特性就是,它的存活周期只限于这个函数,当函数调用完毕时,它会自动销毁,而我们也恰恰利用了这个特性。当我们运行std::swap(_root,t._root)这个语句的时候,临时拷贝t的_root 和 本树的(this)_root发生了交换。_root原本的值被赋给了临时拷贝t._root,t._root的值被赋给了_root。当我们执行完这个程序的时候t自动销毁,帮我们完成了销毁_root原本内容的工作。我们即完成了赋值,又省去了一大部分工作,一举两得。
 
 
5、前中后序遍历(递归)

①前序
这个我就不再多说了,构造,拷贝构造,析构,都是利用前序遍历的道理来做的。当前结点-->左子树-->右子树。
 
代码:
[cpp] view plain copy
 
  1. void PrevOrder()  
  2. {  
  3.     _PrevOrder(_root);  
  4.     cout << endl;  
  5. }  

_PrevOrder()
[cpp] view plain copy
 
  1. void _PrevOrder(Node* _root)  
  2. {  
  3.     Node* tmp = _root;  
  4.     if (tmp == NULL)  
  5.     {  
  6.         return;  
  7.     }  
  8.     cout << tmp->_data << " ";  
  9.     _PrevOrder(tmp->_left);  
  10.     _PrevOrder(tmp->_right);  
  11. }  
 
②中序
 

判断当前结点是否为空,为空的话,不处理,直接返回。先递归访问当前结点左子树,当左子树处理完毕,再依次返回处理当前结点,再递归访问当前结点右子树。

[cpp] view plain copy
 
  1. void InOrder()  
  2. {  
  3.     _InOrder(_root);  
  4.     cout << endl;  
  5. }  
 
_InOrder()
[cpp] view plain copy
 
  1. void _InOrder(Node* _root)  
  2. {  
  3.     Node* tmp = _root;  
  4.     if (tmp == NULL)  
  5.     {  
  6.         return;  
  7.     }  
  8.     _InOrder(tmp->_left);  
  9.     cout << tmp->_data << " ";  
  10.     _InOrder(tmp->_right);  
  11. }  
 

③后序
判断当前结点是否为空,为空的话,不处理,直接返回。不为空的话,先递归访问当前结点节点的左子树,再递归访问当前结点根节点的右子树,最后访问当前结点。
[cpp] view plain copy
 
  1. void PostOrder()  
  2. {  
  3.     _PostOrder(_root);  
  4.     cout << endl;  
  5. }  
 
_PostOrder()
[cpp] view plain copy
 
  1. void _PostOrder(Node* _root)  
  2. {  
  3.     Node* tmp = _root;  
  4.     if (tmp == NULL)  
  5.     {  
  6.         return;  
  7.     }  
  8.     _PostOrder(tmp->_left);  
  9.     _PostOrder(tmp->_right);  
  10.     cout << tmp->_data << " ";  
  11. }  
 
6、前中后序非递归。
这里我告诉大家一个真理,任何的递归都可以用栈来替换实现。这里我就用一个辅助栈来实现前中后序的非递归。
 
①前序
 
 
 
        
 
代码:
 
[cpp] view plain copy
 
  1. void PrevOrder_NonR()  
  2. {  
  3.     Node* cur = _root;  
  4.     stack<Node*> s;  
  5.     if (cur == NULL)  
  6.     {  
  7.         return;  
  8.     }  
  9.     while (cur || !s.empty())  
  10.     {  
  11.         while (cur)  
  12.         {  
  13.             s.push(cur);  
  14.             cout << cur->_data << " ";  
  15.             cur = cur->_left;  
  16.         }  
  17.         Node* top = s.top();  
  18.         s.pop();  
  19.         cur = top->_right;  
  20.     }  
  21.     cout << endl;  
  22. }  


中序和后序的道理与上相同,只是当前节点的输出做了小小的改动。下面直接贴出代码
 
②中序(非递归)
[cpp] view plain copy
 
  1. void InOrder_NonR()  
  2. {  
  3.     Node* cur = _root;  
  4.     stack<Node*> s;  
  5.     if (cur == NULL)  
  6.     {  
  7.         return;  
  8.     }  
  9.     while (cur || !s.empty())  
  10.     {  
  11.         while (cur)  
  12.         {  
  13.             s.push(cur);  
  14.                 cur = cur->_left;  
  15.             }  
  16.         Node* top = s.top();  
  17.         cout << top->_data << " ";  
  18.         s.pop();  
  19.         cur = top->_right;  
  20.     }  
  21.     cout << endl;  
  22. }  
 
③后序(非递归)
后序的非递归较上面两个多了一个变量,prev,它记录了上一次访问的节点。因为后序是最后访问当前节点的,当我们访问一个节点,我们不知道这个节点的右树是否被访问过。所以我们需要记录一下上一个访问的节点。以便访问右树时做判断。
[cpp] view plain copy
 
  1. void PostOrder_NonR()  
  2. {  
  3.     Node* cur = _root;  
  4.     Node* prev = NULL;  
  5.     stack<Node*> s;  
  6.     while (cur || s.empty())  
  7.     {  
  8.         while (cur)  
  9.         {  
  10.             s.push(cur);  
  11.             cur = cur->_left;  
  12.         }  
  13.         Node* top = s.top();  
  14.   
  15.         //如果右树为空或者右树已经访问过,则访问当前结点,并出栈  
  16.         //如果右树不为空并且没有访问过,则访问右树  
  17.         if (top->_right == NULL || prev == top->_right)  
  18.         {  
  19.             cout << top->_data << " ";  
  20.             prev = top;  
  21.             s.pop();//返回父节点  
  22.         }  
  23.         else  
  24.         {  
  25.             cur = top->_right;  
  26.         }  
  27.     }  
  28.     cout << endl;  
  29. }  


7、层序遍历
 
顾名思义。层序遍历就是按层来访问一颗树,一次访问一层。这里我们用到了一个队列。                      
 
代码:
[cpp] view plain copy
 
  1. void _LevelOrder(Node* _root)  
  2. {  
  3.     Node *tmp = _root;  
  4.         queue<Node*> q;  
  5.     q.push(tmp);  
  6.     while (!q.empty())  
  7.     {  
  8.         Node* top = q.front();  
  9.         q.pop();  
  10.         cout << top->_data << " ";  
  11.         if (top->_left)  
  12.         {  
  13.             q.push(top->_left);  
  14.         }  
  15.         if (top->_right)  
  16.         {  
  17.             q.push(top->_right);  
  18.         }  
  19.     }  
  20. }  


8、查找第k层节点个数
 这个问题,我们只需要查找第k-1层有多少个孩子就行了。
 
同样为递归,前序。
 
[cpp] view plain copy
 
  1. size_t FindKlevel(size_t k)  
  2. {  
  3.     return _FindKlevel(_root,k);  
  4. }  

_FindKlevel()
[cpp] view plain copy
 
  1. size_t _FindKlevel(Node* _root,size_t k)  
  2. {  
  3.     Node *cur = _root;  
  4.     if (cur == NULL || k < 0)  
  5.     {  
  6.         return 0;  
  7.     }  
  8.     if (k == 1)  
  9.     {  
  10.         return 1;  
  11.     }  
  12.     size_t left = _FindKlevel(cur->_left, k-1);  
  13.     size_t right = _FindKlevel(cur->_right, k-1);  
  14.   
  15.     return  left + right;  
  16. }  
 
9、精确查找值为x的结点
 
前序递归查找,如果根节点为空,返回NULL,如果当前节点等于x,返回当前节点的指针。如果当前节点不等于x,则递归进入左子树查找,若左子树没有,则递归进入右子树查找,若这棵树中没有x,返回NULL。
[cpp] view plain copy
 
  1. Node* Find(const T& x)  
  2. {  
  3.     return _Find(_root,x);  
  4. }  
 
_Find()
[cpp] view plain copy
 
  1. Node* _Find(Node* _root,const T& x)  
  2. {  
  3.     Node* ret = NULL;  
  4.     Node* cur = _root;  
  5.     if (cur == NULL)  
  6.     {  
  7.         return NULL;  
  8.     }  
  9.     if (cur->_data == x)  
  10.     {  
  11.         ret = cur;  
  12.     }  
  13.     else  
  14.     {  
  15.         ret = _Find(cur->_left,x);  
  16.         if (ret == NULL)  
  17.         {  
  18.         ret = _Find(cur->_right,x);  
  19.         }  
  20.     }  
  21.     return ret;  
  22. }  

10、查找结点总个数。
 
结点总个数 = 当前节点个数+左子树节点个数+右子树节点个数。
前序递归查找,若当前节点为空,返回,不为空则加1,--->递归左子树----->递归右子树。
[cpp] view plain copy
 
  1. size_t Size()  
  2. {  
  3.     return _Size(_root);  
  4. }  
_Size()
[cpp] view plain copy
 
  1. size_t _Size( BinaryTreeNode<T>* _root )  
  2. {  
  3.     size_t ret = 0;  
  4.     if (_root == NULL)  
  5.     {  
  6.         return ret;  
  7.     }  
  8.     ret++;  
  9.     ret += _Size(_root->_left);  
  10.     ret += _Size(_root->_right);  
  11. }  
 
11、计算树的深度
树的深度取左子树深度+1和右子树深度+1的最大值。(+1为根节点的深度)
 
[cpp] view plain copy
 
  1. size_t Depth()  
  2. {  
  3.     return _Depth(_root);  
  4. }  
 
_Depth()
[cpp] view plain copy
 
  1. size_t _Depth(Node* _root)  
  2. {  
  3.     if (_root == NULL)  
  4.     {  
  5.         return 0;  
  6.     }  
  7.     int left = _Depth(_root->_left) + 1;  
  8.     int right = _Depth(_root->_right) + 1;  
  9.     return left > right ? left : right;  
  10. }  


posted @   博客园—哆啦A梦  阅读(8287)  评论(1编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示