二叉树是树形结构的一个重要类型。许多实际问题抽象出来的数据结构都是二叉树的形式,而且二叉树的存储结构及其算法都比较为简单,因此二叉树使用极为广泛。
二叉树是n(n>=0)个节点的有限集合,它可以是空集(n=0),也可以是由一个根节点及两棵互不相交、分别称作这个根的左子树和右子树的二叉树组成。
就会出现像这样的面试题:(牛人就可以忽略,但初学者要是耐心的看完,肯定可以学到东西的...)
用C++实现一个二叉排序树,完成创建节点、插入节点、删除节点、查找节点等功能。
解析:二叉排序树又称二叉查找数。其定义为:二叉排序树是空树,或者是满足如下性质的二叉树。
(1)若它的左子树非空,则左子树上所有的节点的值均小于根节点的值。
(2)若它的右子树非空,则右子树上所有的节点的值均大于根节点的值。
(3)左、右子树本身又各是一棵二叉排序树。
一个简单的二叉排序树如下图:
对于二叉排序树模型首先需要定义节点类以及二叉排序树类。对于节点类来说,每个节点有向下的两个指针,而且每个节点都只有一个父节点;对于二叉排序树类来说,只需要有树的根节点,就可以进行操作了,因此它们的数据结构如下:
#include <iostream>
#include <stack>
#include <queue>
using namespace std;
//节点类定义
class Node
{
public:
int data; //节点数据
Node *parent; //父节点
Node *left; //左节点
Node *right; //有节点
int tag; //为后序遍历所用
public:
Node() : data(-1), parent(NULL), left(NULL), right(NULL), tag(0){};
Node(int num) : data(num), parent(NULL), left(NULL), right(NULL), tag(0){};
};
//二叉排序树类定义----------------------------------------
class Tree
{
public:
Tree(int num[], int len); //插入num数组的前len个数据
void insertNode(int data); //插入节点,递归方法
void insertNode1(int data); //插入节点,非递归方法
Node *searchNode(int data); //查找节点
void deleteNode(int data); //删除节点及其子树
void InOrderTree(); //中序遍历,递归
void InOrderTreeUnRec(); //中序遍历,非递归
void PreOrderTree(); //先序遍历,递归
void PreOrderTreeUnRec(); //先序遍历,非递归
void PostOrderTree(); //后序遍历,递归
void PostOrderTreeUnRec(); //后序遍历,非递归
void LevelOrderTree(); //层次遍历, 队列实现
bool IsSortedTree(Tree tree); //判断是否为二叉排序树
private:
void insertNode(Node *current, int data); //递归插入方法
Node *searchNode(Node *current, int data); //递归查找方法
void deleteNode(Node *current); //递归删除方法
void InOrderTree(Node *current); //中序遍历,递归
void PreOrderTree(Node *current); //先序遍历,递归
void PostOrderTree(Node *current); //后序遍历,递归
private:
Node *root; //二叉排序树的根节点
};
接下来具体说明各个方法的实现。
构造函数中创建二叉排序树的方法。这里首先生成根节点,然后循环调用插入节点对二叉树进行插入操作。
//插入num数组的前len个数据--------------------------------------
Tree::Tree(int num[], int len)
{
root = new Node(num[0]);
for(int i=1; i<len; i++)
{
//insertNode(num[i]);
insertNode1(num[i]);
}
}
插入节点操作,通常在二叉树的遍历中可以选择递归与非递归方法。insertNode1()使用非递归方法插入节点,其代码如下:
//插入数据为参数data的节点,非递归方法------------------
void Tree::insertNode1(int data)
{
Node *p, *par;
Node *newNode = new Node(data); //创建节点
p = par = root;
while(NULL != p) //查找插入到哪个节点下面
{
par = p; //保存节点
if(data > p->data) //如果data大于当前节点的data
{ //下一步到左子节点,否则进行到右子节点
p = p->right;
}
else if(data < p->data)
{
p = p->left;
}
else if(data == p->data) //不能插入重复节点
{
delete newNode;
return;
}
}
newNode->parent = par; //保存父节点
if(par->data > newNode->data) //把新节点插入在目标节点的正确位置
{
par->left = newNode;
}
else
{
par->right = newNode;
}
}
下面insertNode(int data)使用递归方法插入节点,它的内部调用了private成员函数isnertNode(Node *current, int data),代码如下:
//插入数据为参数data的节点,调用递归插入方法----------------
void Tree::insertNode(int data)
{
if(NULL != root)
{
insertNode(root, data); //调用私有的递归插入方法
}
}
//递归插入方法
void Tree::insertNode(Node *current, int data)
{
//如果data小于当前节点数据,在当前节点的左子树插入
if(data < current->data)
{
if(NULL == current->left) //如果左节点不存在,则插入到左子树
{
current->left = new Node(data);
current->left->parent = current;
}
else
{
insertNode(current->left, data); //对左子树进行递归调用
}
}
else if(data > current->data) //如果data大于当前节点数据,在当前节点的右子树插入
{
if(NULL == current->right) //如果有节点不存在,则插入到有节点
{
current->right = new Node(data);
current->right->parent = current;
}
else
{
insertNode(current->right, data); //对右子树进行递归调用
}
}
return; //data等于当前节点数据时,不插入
}
查找节点也使用了递归,代码如下:
//查找节点--------------------------------------
Node * Tree::searchNode(int data)
{
Node *current = NULL;
if(NULL != root)
{ //调用私有递归方法
current = searchNode(root, data);
}
return current;
}
//递归查找方法
Node *Tree::searchNode(Node *current, int data)
{
if(data < current->data) //左子树
{
if(NULL == current->left) //不存在,返回NULL
return NULL;
return searchNode(current->left, data); //递归左子树
}
else if(data > current->data) //右子树
{
if(NULL == current->right)
return NULL;
return searchNode(current->right, data); //递归右子树
}
return current; //如果相等返回current
}
删除节点的操作,代码如下:
//删除数据为data的节点及其子树----------------------------
void Tree::deleteNode(int data)
{
Node *current = NULL;
current = searchNode(data); //查找节点
if(NULL != current)
{
deleteNode(current); //调用私有删除节点及其子树
}
}
//删除current节点及其子树的所有节点
void Tree::deleteNode(Node *current)
{
if(NULL != current->left) //删除左子树
{
deleteNode(current->left);
}
else if(NULL != current->right) //删除右子树
{
deleteNode(current->right);
}
//如果current是根节点,把root置空
if(NULL == current->parent)
{
delete current;
root = NULL;
return;
}
//将current父亲节点的相应指针置空
if(current->parent->data > current->data)
{
//current为其父节点的左子节点
current->parent->left = NULL;
}
else //current为parNode的右子节点
{
current->parent->right = NULL;
}
delete current; //最后删除此节点
}
public成员函数deleteNode()删除数据为data的节点及其子树,它首先调用了searchNode()查找数据等于data的节点,如果找到节点,则调用private成员函数deleteNode()函数删除节点及其子树。
private成员函数也是使用递归方法进行删除操作的,它的步骤如下:
(1)如果current左子树存在,则递归删除current左子树。
(2)如果current右子树存在,则递归删除current右子树。
(3)最后删除current节点,次数如果current是根节点,需要把root置空,否则把其父节点相应的指针置空。
接下来是实现二叉树的中序遍历,分别使用递归和非递归的方法。
解析:中序遍历的递归算法定义为,若二叉树非空,则一次执行如下操作。
(1)遍历左子树
(2)访问根节点
(3)遍历右子树
中序遍历的递归算法程序代码如下:
//递归,中序遍历------------------------------
void Tree::InOrderTree()
{
if(NULL == root)
{
return ;
}
InOrderTree(root); //调用私有函数
}
void Tree::InOrderTree(Node *current)
{
if(NULL != current)
{
InOrderTree(current->left); //遍历左子树
cout<<current->data<<" "; //打印节点数据
InOrderTree(current->right);//遍历右子树
}
}
对于中序遍历非递归算法,可以采用栈(stack)来临时存储节点。方法如下:
(1)先将根节点入栈,遍历左子树。
(2)遍历完左子树返回时,栈顶元素应为根节点,此时出栈,并打印节点数据。
(3)再中序遍历右子树
代码如下:
//非递归,中序遍历---栈实现--------------------------------
void Tree::InOrderTreeUnRec()
{
stack<Node *> s; //定义栈
Node *p = root;
while(NULL != p || !s.empty())
{
while(NULL != p) //遍历左子树
{
s.push(p);
p = p->left;
}
if(!s.empty())
{
p = s.top(); //得到栈顶内容
s.pop();
cout<<p->data<<" "; //打印
p = p->right; //指向右子节点,
} //下一次循环时就会有中序遍历右子树
}
}
下面是先序遍历的实现。
解析:若二叉树非空,则先序遍历的递归算法,依次执行如下操作。
(1)访问根节点数据
(2)遍历左子树
(3)遍历右子树
先序遍历代码如下:
// 递归,先序遍历-------------------------------
void Tree::PreOrderTree()
{
if(NULL == root)
{
return;
}
PreOrderTree(root); //调用私有函数
}
void Tree::PreOrderTree(Node *current)
{
if(NULL != current)
{
cout<<current->data<<" "; //打印
PreOrderTree(current->left); //遍历左子树
PreOrderTree(current->right); //遍历右子树
}
}
对于先序遍历的非递归算法,使用栈来临时存储节点,方法如下:
(1)打印根节点数据
(2)把根节点的right入栈,遍历左子树
(3)遍历完左子树返回时,栈顶元素应为right,出栈,遍历以该指针为根的子树。
//先序遍历,非递归,栈实现-----------------------------
void Tree::PreOrderTreeUnRec()
{
stack<Node *> s;
Node *p = root;
while(NULL != p || !s.empty())
{
while(NULL != p) //遍历左子树
{
cout<<p->data<<" "; //打印
s.push(p); //把遍历的节点全部压栈
p = p->left;
}
if(!s.empty())
{
p = s.top(); //得到栈顶内容
s.pop(); //出栈
p = p->right; //指向右子节点,
} //下一次循环时就会先序遍历左子树
}
}
接下来是后序遍历二叉树:
解析:若二叉树非空,则后序遍历的递归算法依次执行如下操作,
(1)遍历左子树
(2)遍历右子树
(3)访问根节点
后序遍历的程序代码如下:
//后序遍历,递归-------------------------------
void Tree::PostOrderTree()
{
if(NULL == root)
{
return;
}
PostOrderTree(root); //调用私有函数
}
void Tree::PostOrderTree(Node *current)
{
if(NULL != current)
{
PostOrderTree(current->left); //遍历左子树
PostOrderTree(current->right); //遍历右子树
cout<<current->data<<" "; //打印
}
}
后序遍历非递归算法,可采用标记法,节点入栈时,配一个标志tag一同入栈(tag为0表示遍历左子树前段现场保护,tag为1表示遍历右子树前段现场保护)。首先将tag为0入栈,遍历左子树;返回后,修改栈顶tag为1,遍历右子树;最后访问根节点。程序代码如下:
//后序遍历,非递归----栈实现---------------------------
void Tree::PostOrderTreeUnRec()
{
stack<Node *> s;
Node *p = root;
while(NULL != p || !s.empty())
{
while(NULL != p)
{
s.push(p); //压栈
p = p->left; //遍历左子树
}
if(!s.empty())
{
p = s.top(); //得到栈顶元素
if(p->tag) //tag为1时
{
cout<<p->data<<" "; //打印
s.pop(); //退栈
p = NULL; //第二次访问标志其右子树已经遍历
}
else
{
p->tag = 1; //修改tag为1
p = p->right; //指向有节点,下次遍历其左子树
}
}
}
}
接下来是层次遍历二叉树的算法:
解析:很难直接使用节点的指针(left、right、parent)来实现,但是借助队列就可以轻松的实现。下面是执行步骤:
(1)A入队。
(2)A出队,同时A的子节点B、C入队(此时队列有:B、C)。
(3)B出队,同时B的子节点D、E入队(此时队列有C、D、E)。
(4)C出队,同时C的子节点F、G入队(此时队列有D、E、F、G)。
显然这样就能使出队列的顺序满足层次遍历。代码如下:
//层次遍历-----队列实现------------------------------------
void Tree::LevelOrderTree()
{
queue<Node *> q; //定义队列q
Node *ptr = root;
q.push(root); //根节点入队
while(!q.empty())
{
ptr = q.front(); //得到队头节点
q.pop(); //出队
cout<<ptr->data<<" "; //打印当前节点数据
if(NULL != ptr->left) //当前节点存在左节点,则有节点入队
{
q.push(ptr->left);
}
if(NULL != ptr->right) //当前节点存在有节点,则有节点入队
{
q.push(ptr->right);
}
}
}
最后一个重要的问题是判断给定二叉树是否为二叉排序树。
解析:上面已经编写过中序遍历的算法,使用中序遍历的结果就是二叉排序树的排序树出,因此可以使用中序遍历来实现判定二叉树是否为二叉排序树。程序代码如下:
//判断是否为二叉排序树--利用中序遍历------------------------------
bool Tree::IsSortedTree(Tree tree)
{
int lastvalue = 0; //保存当前值
stack<Node *> s;
Node *p = tree.root;
while(NULL != p || !s.empty())
{
while(NULL != p)
{
s.push(p); //遍历左子树
p = p->left;
}
if(!s.empty())
{
p = s.top(); //得到栈顶元素
s.pop();
if(0 == lastvalue || lastvalue < p->data)
{
//如果第一次弹出或者lastvalue小于当前节点值
lastvalue = p->data;
}
else if(lastvalue >= p->data)
{
//如果lastvalue大雨当前节点值,说明不是二叉排序树
return false;
}
p = p->right; //指向右子节点,
} //下一次循环时就会中序遍历右子树
}
return true; //到这里说明节点数据是升序排列,返回true
}
下面给出主函数的测试:
int main()
{
//中序遍历
int num[] = {5, 3, 7, 2, 4, 6, 8, 1};
Tree tree(num, 8);
cout<<"InOrder: ";
tree.InOrderTree();
cout<<"\nInOrder: ";
tree.InOrderTreeUnRec();
cout<<endl<<endl;
//先序遍历
Tree tree1(num, 8);
cout<<"PerOrder: ";
tree1.PreOrderTree();
cout<<"\nPreOrder: ";
tree1.PreOrderTreeUnRec();
cout<<endl<<endl;
//后序遍历
Tree tree2(num, 8);
cout<<"PostOrder: ";
tree2.PostOrderTree();
cout<<"\nPostOrder: ";
tree2.PostOrderTreeUnRec();
cout<<endl<<endl;
//层次遍历
Tree tree3(num, 8);
cout<<"LevelOrder: ";
tree3.LevelOrderTree();
cout<<endl<<endl;
//判断是否为二叉排序树
Tree tree4(num, 8);
cout<<"InOrder: ";
tree4.InOrderTreeUnRec(); //中序遍历非递归
cout<<"\nIsSortedTree: "<<tree4.IsSortedTree(tree4)<<endl;
Node *node = tree4.searchNode(4);
node->data = 1; //动手把节点数据改成1
cout<<"InOrder: ";
tree4.InOrderTreeUnRec(); //中序遍历非递归
cout<<"\nIsSortedTree: "<<tree4.IsSortedTree(tree4)<<endl<<endl;
return 0;
}
执行结果:
InOrder: 1 2 3 4 5 6 7 8
InOrder: 1 2 3 4 5 6 7 8
PerOrder: 5 3 2 1 4 7 6 8
PreOrder: 5 3 2 1 4 7 6 8
PostOrder: 1 2 4 3 6 8 7 5
PostOrder: 1 2 4 3 6 8 7 5
LevelOrder: 5 3 7 2 4 6 8 1
InOrder: 1 2 3 4 5 6 7 8
IsSortedTree: 1
InOrder: 1 2 3 1 5 6 7 8
IsSortedTree: 0
Press any key to continue