先来给出二叉树的定义
struct BinaryTreeNode
{
BinaryTreeNode(const T& data)
:_data(data)
,_left(NULL)
,_right(NULL)
,_parent(NULL)
{}
T _data;
BinaryTreeNode<T>* _left;
BinaryTreeNode<T>* _right;
//BinaryTreeNode<T>* _parent;
};
接下来有若干问题,通过这些问题加深对二叉树的理解
1. 前序 / 中序 / 后序遍历(非递归)
前序遍历代码
void PreOrder()
{
stack<Node*> s;
Node* cur = _root;
while (cur || !s.empty())
{
//这个根节点和左边访问过了
//因为等会还要访问右路,所以需要压栈
while (cur)
{
cout << cur->_data << " ";
s.push(cur);
cur = cur->_left;
}
//在栈里面的左路节点全部访问了
//右路又是一棵树 对右路用子问题
cur = (s.top())->_right;
s.pop();
}
}
中序遍历代码
void InOrder()
{
stack<Node*> s;//因为是递归所以势必要用到栈
Node* cur = _root;
while (cur||!s.empty())
{
//左路节点全部压栈
while (cur)
{
s.push(cur);
cur = cur->_left;
}
//到这里的一定是左子树遍历完毕
//栈中节点自身和右节点还没有访问
Node* top = s.top();
s.pop();
cout << top->_data << " ";
//对右子树用子问题
cur = top->_right;
}
}
后序遍历代码
void PostOrder()
{
stack<Node*> s;
Node* cur = _root;
Node* prev = NULL;//定义最近被访问的节点
while (cur || !s.empty())
{
//左 右 根
while (cur)
{
s.push(cur);
cur = cur->_left;
}
Node* top = s.top();
if (top->_right == prev||top->_right==NULL)//一个节点被访问的前提是无右子树或者右子树被访问过
{
cout << top->_data << " ";
s.pop();//一定是访问了 才可以pop
prev = top;
}
else//循环子问题
cur = top->_right;
}
}
2. 判断一棵二叉树是否是平衡二叉树
解析:平衡二叉树是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树,并且平衡二叉树也叫二叉搜索平衡树,因此平衡二叉树必然也是一棵二叉搜索树,根据这个思路我们可以写递归算法代码,先判断当前根节树是否是平衡二叉树,在继续遍历左子树和右子树是否是平衡二叉树
size_t Depth()
{
return _Depth(_root);
}
size_t _Depth(Node* root)
{
if (root == NULL)
return 0;
return (_Depth(root->_left) > _Depth(root->_right) ? _Depth(root->_left) + 1 : _Depth(root->_left) + 1);
}
bool Isbalance()
{
return _Isbalance(_root);
}
bool _Isbalance(Node* root)
{
if (root == NULL)
return true;
return (abs(Depth(root->_left) - Depth(root->_right))<= 1)
&& _Isbalance(root->_left) && _Isbalance(root->_right);
}
但是这种写法的效率太低了,因为做了很多的重复计算,接下来我们对这个算法优化一下,优化策略就是要减少重复计算
bool _Isbalance(Node* root, int& depth)
{
//后续遍历 遍历过程求子树高度 (参考牛客网代码)
if (root == NULL){
return true;
}
int ldepth = 0, rdepth = 0;
if (_Isbalance(root->_left, ldepth) && Isbalance(root->_right, rdepth))
{
int dif = abs(left - right);
if (dif > 1)
return false;
depth = (ldepth > rdepth ? ldepth : rdepth) + 1;
return true;
}
return false;
}
从上面可以看出我们每次在当前节点都不算高度,从下面通过引用参数depth往上面传,这样子算高度这个计算次数一下减少了很多
3.求二叉树的镜像
这个用递归很好解决,交换两个孩子节点
Node* Mirror()
{
return _Mirror(_root);
}
void _Mirror(Node* root)
{
if (root == NULL)
return;
swap(root->_left, root->_right);
_Mirror(root->_left);
_Mirror(root->_right);
}
4. 求两个节点的最近公共祖先
在这里分为三种情况我们分别来进行处理,每种情况根据树的类型我们做出最优的解决方案
(1)搜索二叉树 在搜索二叉树,只要两个节点不同一侧,则当前根节点必然就是最近公共祖先节点,这是由搜索二叉树的特性决定的
Node* GetCommonAce(Node* n1, Node* n2)
{
return _GetCommonAce( _root, n1, n2);
}
Node* _GetCommonAce(Node* root, Node* n1, Node* n2)
{
//有时候要先看失败的情况 这样子问题一下就简单许多
//因为当你先判断在两侧的时候有一个问题就是 两个大小不一定
//递归一定要有失败的出口
if (root == NULL || n1 == NULL || n2 == NULL)
return NULL;
//两个都在左边
if (n1->_data < root->_data&&n2->_data < root->_data)
_GetCommonAce(root->_left, n1, n2);
//两个都在右边
else if (n1->_data > root->_data&&n2->_data > root->_data)
_GetCommonAce(root->_right, n1, n2);
//一个在左边一个在右边
else
return root;
}
(2)带父节点指针的二叉树 这个可以转换成两个链表的相交问题
Node* GetCommonAce(Node* n1, Node* n2)
{
//失败也要有返回
if (_root == NULL || n1 == NULL || N2 == NULL)
return NULL;
int d1 = Depth(n1);
int d2 = Depth(n2);
Node* Longlength = NULL;
Node* Shortlength = NULL;
if (d1 > d2)
{
Longlength = n1;
Shortlength = n2;
}
else
{
Longlength = n2;
Longlength = n1;
}
int gap = abs(d1 - d2);
while (gap--)
{
Longlength = Longlength->_parent;
}
//Shortlength->_data != Longlength->_data
//保证了两个节点是父子关系这种情况
while (Shortlength&&Longlength&&Shortlength->_data != Longlength->_data)
{
Longlength = Longlength->_parent;
Shortlength = Shortlength->_parent;
}
if (Shortlength->_data == Longlength->_data)
{
return Shortlength;
}
else
return NULL;
}
(3)普通二叉树
递归方法
Node* GetCommonAce(Node* n1, Node* n2)
{
return _GetCommonAce(_root, n1, n2);
}
Node* _GetCommonAce(Node* root, Node* n1, Node* n2)
{
if (root == n1 || root == n2)
return root;
bool inleft1 = false, inright1 = false, inleft2 = false, inright2 = false;
inleft1 = _Find(root->_left, n1);
if (inleft1 == false)
{
inright1 = _Find(root->_right, n1);
}
inleft2 = _Find(root->_left, n2);
if (inleft2 == false)
{
inright2 = _Find(root->_right, n2);
}
if ((inleft1&&inright2) || (inright1&&inleft2))//不同一侧
return root;
else if (inleft1&&inleft2)//两个节点都在当前根节点的左边
return _GetCommonAce(root->_left, n1, n2);
else if (inright1&&inright2//两个节点都在当前根节点的右边
return _GetCommonAce(root->_right, n1, n2);
else
return NULL;
}
bool _Find(Node* root, Node* x)
{
if (root == NULL)
return false;
if (root == x)
return true;
return _Find(root->_left, x) || _Find(root->_right, x);
}
保存路径方法
Node* GetCommonAce(Node* n1, Node* n2)
{
return _GetCommonAce(_root, n1, n2);
}
Node* _GetCommonAce(Node* root, Node* n1, Node* n2)
{
stack<Node*> s1, s2;
// s1 s2 不一样
Find(n1, s1);
Find(n2, s2);
while (s1.size() != s2.size())
{
if (s1.size() > s2.size())
{
s1.pop();
}
else
{
s2.pop();
}
}
while (!s1.empty() && !s2.empty() && s1.top() != s2.top())
{
s1.pop();
s2.pop();
}
if (s1.top() == s2.top())
return s1.top();
return NULL;
}
bool Find(Node*x, stack<Node*>& paths)
{
return _Find(_root, x, paths);
}
bool _Find(Node* root, Node* x, stack<Node*>& paths)
{
if (root == NULL)
return false;
paths.push(root);
if (root == x)
return true;
bool l = _Find(root->_left, x, paths);
if (l == true)
return true;
bool r = _Find(root->_right, x, paths);
if (r == true)
return true;
paths.pop();//这里说明根左右子树中都没有找到,那么可以pop数据,给递归上层返回false
return false;
}
5. 求二叉树中最远的两个节点的距离
在这里一定要注意的是最远的两个节点不一定是根节点的最左节点和最右节点,有可能是在子树里面是最远的,例如:
在这个里面其实E和I才是距离最远的,而不是E和A的右边空节点的距离
int GetMax()
{
int _max = 0;
_GetMax(_root, _max);
return max;
}
void _GetMax(Node* root, int& _max)
{
if (root == NULL)
return;
int max = Depth(root->_left) + Depth(root->_right);
if (max > _max)
_max = max;
_GetMax(root->_left, _max);
_GetMax(root->_right, _max);
}
优化方法,在上面的递归方法中我们明显可以看到的是做了很多的重复运算,并且通过我们之前写的代码我也可感受到递归代码简单,但是效率不高,对于递归我们通常有两种优化方法,1.空间换时间,2.消除重复运算,在这里我们采用第二种方法消除重复运算、
int GetMax()
{
int _max = 0;
_GetMax(_root, _max);
return max;
}
int _GetMax(Node* root, int& _max)
{
if (root==NULL)
return 0;
//层层望上面返
int leftdepth = _GetMax(root->_left, _max);
int rightdepth = _GetMax(root->_right, _max);
if (leftdepth + rightdepth > _max)
_max = leftdepth + rightdepth;
return (leftdepth > rightdepth ? leftdepth : rightdepth ) + 1;
}
6. 由前序遍历和中序遍历重建二叉树(如:前序序列:1 2 3 4 5 6 - 中序序列:3 2 4 1 6 5)
前序遍历确定根节点,中序遍历确定确定左右树有什么
Node* ReBuildTree(char* pre, char* in, size_t len)
{
if (pre == NULL || in == NULL || len <= 0)
return NULL;
//闭区间
return _ReBuildTree(pre, in, in + len);
}
Node* _ReBuildTree(char*& pre, char* in, char* inend)//中序最后一个
{
if (*pre == '\0')
return NULL;
Node* newRoot = new Node(*pre);
//1.是叶子节点 2.不是叶子节点
if (in == inend)
return newRoot;
char* pos = in;
while (pos != inend)
{
if (*pos == *pre)
break;
pos++;
}
assert(pos <= inend);
//左右子树递归解决 相当于建树
newRoot->_left = _ReBuildTree(++pre, in, pos - 1);
newRoot->_right = _ReBuildTree(++pre, pos + 1, inend);
return newRoot;
}
7. 判断一棵树是否是完全二叉树 运用了层序遍历
我们首先分析一下完全二叉树的叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树,从上图看出5这个节点只有左孩子,它之后的节点没有孩子,我们根据这种特性,可以设置一个标记一个节点有没有孩子,
我们有两个原则:
1.将每一个节点没有左孩子或者右孩子归类一种情况,分开容易混并且没有意义
2.只要有一个节点有孩子节点那么就判断标志位,如果是false,说明该节点前面有节点没有左孩子或者右孩子,违背完全二叉树的定义,返回false,如果是true,反之成立,这里一定要自己想懂没有左孩子或者右孩子归类一种情况是根绝我们这个判断完全二叉树需求所决定的,完美契合需求
//完全二叉树的要求 叶子节点只能在最下面一层和次层
bool IsCompleteBinaryTree()
{
queue<Node*> q;
q.push(_root);
bool tag = true;
while (!q.empty())
{
Node* top = q.front();
q.pop();
if (top->_left)
{
if (tag == false)//说明前一个节点没有孩子 而这个节点有左孩子 则必然不符合完全二叉树的定义
return false;
q.push(top->_left);
}
else
tag = false;//说明没有左孩子
if (top->_right)
{
if (tag == false)
return false;
q.push(top->_right);
}
else
tag = false;
}
return true;
}
8. 将二叉搜索树转换成一个排序的双向链表。就是线索化,要求不能创建任何新的结点,只能调整树中结点指针的指向
void Change()
{
//线索化
stack<Node*> s;
Node* cur = _root;
cur = _root;
Node* prev = NULL;
while (cur || !s.empty())
{
while (cur)
{
s.push(cur);
cur = cur->_left;
}
Node* top = s.top();
s.pop();
top->_left = prev;
if (prev)
{
prev->_right = top;
}
prev = top;
cur = top->_right;
}
}