基本数据结构 - 二叉树
Posted AlanTu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基本数据结构 - 二叉树相关的知识,希望对你有一定的参考价值。
摘要
书中第10章10.4小节介绍了有根树,简单介绍了二叉树和分支数目无限制的有根树的存储结构,而没有关于二叉树的遍历过程。为此对二叉树做个简单的总结,介绍一下二叉树基本概念、性质、二叉树的存储结构和遍历过程,主要包括先根遍历、中根遍历、后根遍历和层次遍历。
1、二叉树的定义
二叉树(Binary Tree)是一种特殊的树型结构,每个节点至多有两棵子树,且二叉树的子树有左右之分,次序不能颠倒。
由定义可知,二叉树中不存在度(结点拥有的子树数目)大于2的节点。二叉树形状如下下图所示:
2、二叉树的性质
(1)在二叉树中的第i层上至多有2^(i-1)个结点(i>=1)。备注:^表示此方
(2)深度为k的二叉树至多有2^k-1个节点(k>=1)。
(3)对任何一棵二叉树T,如果其终端结点数目为n0,度为2的节点数目为n2,则n0=n2+1。
满二叉树:深度为k且具有2^k-1个结点的二叉树。即满二叉树中的每一层上的结点数都是最大的结点数。
完全二叉树:深度为k具有n个结点的二叉树,当且仅当每一个结点与深度为k的满二叉树中的编号从1至n的结点一一对应。
可以得到一般结论:满二叉树和完全二叉树是两种特殊形态的二叉树,满二叉树肯定是完全二叉树,但完全二叉树不不一定是满二叉树。
举例如下图是所示:
(4)具有n个节点的完全二叉树的深度为log2n + 1。
3、二叉树的存储结构
可以采用顺序存储数组和链式存储二叉链表两种方法来存储二叉树。经常使用的二叉链表方法,因为其非常灵活,方便二叉树的操作。二叉树的二叉链表存储结构如下所示:
//二叉树的数据结构
typedef struct binary_tree_node
{
int elem;
struct binary_tree_node *left;
struct binary_tree_node *right;
}binary_tree_node,*binary_tree;
举例说明二叉链表存储过程,如下图所示:
从图中可以看出:在还有n个结点的二叉链表中有n+1个空链域。
4、遍历二叉树
遍历二叉树是按照指定的路径方式访问书中每个结点一次,且仅访问一次。由二叉树的定义,我们知道二叉数是由根结点、左子树和右子树三部分构成的。通常遍历二叉树是从左向右进行,因此可以得到如下最基本的三种遍历方法:
(1)先根遍历(先序遍历):如果二叉树为空,进行空操作;否则,先访问根节点,然后先根遍历左子树,最后先根遍历右子树。采用递归形式实现代码如下:
//前序遍历递归版
void preorder_traverse_recursive(binary_tree root)
{
if(root!=NULL)
{
cout<<root->elem<<endl;
preorder_traverse_recursive(root->left);
preorder_traverse_recursive(root->right);
}
}
具体过程如下图所示:
(2)中根遍历(中序遍历):如果二叉树为空,进行空操作;否则,先中根遍历左子树,然后访问根结点,最后中根遍历右子树。递归过程实现代码如下:
//中序遍历递归版
void inorder_traverse_recursive(binary_tree root)
{
if(root!=NULL)
{
inorder_traverse_recursive(root->left);
cout<<root->elem<<endl;
inorder_traverse_recursive(root->right);
}
}
具体过程如下图所示:
(3)后根遍历(后序遍历):如果二叉树为空,进行空操作;否则,先后根遍历左子树,然后后根遍历右子树,最后访问根结点。递归实现代码如下:
//后序遍历递归版
void postorder_traverse_recursive(binary_tree root)
{
if(root!=NULL)
{
postorder_traverse_recursive(root->left);
postorder_traverse_recursive(root->right);
cout<<root->elem<<endl;
}
}
具体过程如下图所示:
写一个完整的程序练习二叉树的三种遍历,采用递归形式创建二叉树,然后以递归的形式遍历二叉树,后面会接着讨论如何使用非递归形式实现这三种遍历,程序采用C++语言实现,完整程序如下:
#include<iostream> #include<cstdlib> using namespace std; //二叉树的数据结构 typedef struct binary_tree_node { int elem; struct binary_tree_node *left; struct binary_tree_node *right; }binary_tree_node,*binary_tree; //前序遍历递归版 void preorder_traverse_recursive(binary_tree root) { if(root!=NULL) { cout<<root->elem<<endl; preorder_traverse_recursive(root->left); preorder_traverse_recursive(root->right); } } //中序遍历递归版 void inorder_traverse_recursive(binary_tree root) { if(root!=NULL) { inorder_traverse_recursive(root->left); cout<<root->elem<<endl; inorder_traverse_recursive(root->right); } } //后序遍历递归版 void postorder_traverse_recursive(binary_tree root) { if(root!=NULL) { postorder_traverse_recursive(root->left); postorder_traverse_recursive(root->right); cout<<root->elem<<endl; } } //初始化二叉树 void init_binary_tree(binary_tree *root) { *root=NULL; } //方法一:创建二叉树,注意传入的参数是二级指针,否则不能运行正确结果 /*void create_binary_tree(binary_tree *root) { int elem; cout<<"Enter the node value (0 is end): "; cin>>elem; if(elem==0) *root=NULL; else { *root=(binary_tree)malloc(sizeof(binary_tree_node)); if(*root==NULL) { cout<<"malloc error"<<endl; exit(1); } (*root)->elem=elem; cout<<"creating the left child node"<<endl; create_binary_tree(&((*root)->left)); cout<<"creating the right child node"<<endl; create_binary_tree(&((*root)->right)); } }*/ //方法二 binary_tree create_binary_tree() { int elem; binary_tree root; cout<<"Enter the node value (0 is end): "; cin>>elem; if(elem==0) root=NULL; else { //在堆空间分配的内存,可以返回局部对象 root=(binary_tree)malloc(sizeof(binary_tree_node)); if(root==NULL) { cout<<"malloc error"<<endl; exit(1); } (root)->elem=elem; cout<<"creating the left child node"<<endl; root->left=create_binary_tree(); cout<<"creating the right child node"<<endl; root->right=create_binary_tree(); } return root; } int main() { binary_tree root; init_binary_tree(&root); root=create_binary_tree(); preorder_traverse_recursive(root); inorder_traverse_recursive(root); postorder_traverse_recursive(root); exit(0); }
运行结果如下:
其中,两种创建方式以及为什么要传递二级指针,可以参考http://tieba.baidu.com/p/228434573#
现在来讨论一下如何采用非递归实现这以上三种遍历。将递归形式转换为非递归形式,引入了额外的辅助结构栈。另外在讨论一次二叉树的层次遍历,可以借助队列进行实现。具体讨论如下:
先序遍历,其实过程很简单:一直往左走 root->left->left->left...->null,由于是先序遍历,因此一遇到节点,便需要立即访问;由于一直走到最左边后,需要逐步返回到父节点访问右节点,因此必须有一个措施能够对节点序列回溯。有两个办法:
1.用栈记忆:在访问途中将依次遇到的节点保存下来。由于节点出现次序与恢复次序是反序的,因此是一个先进后出结构,需要用栈。
使用栈记忆的实现有两个版本。第一个版本是模拟递归的实现效果,跟LX讨论的,第二个版本是直接模拟递归。
2.节点增加指向父节点的指针:通过指向父节点的指针来回溯(后来发现还要需要增加一个访问标志,来指示节点是否已经被访问,不知道可不可以不用标志直接实现回溯?想了一下,如果不用这个标志位,回溯的过程会繁琐很多。暂时没有更好的办法。
先序遍历C++代码:非递归版本,用栈实现,版本1
//非递归先序遍历,使用栈实现,版本一
void preorder_traverse(binary_tree root)
{
stack<binary_tree> s;
if(root!=NULL)
{
s.push(root);
binary_tree tmpnode;
while(!s.empty())
{
tmpnode=s.top();
cout<<tmpnode->elem<<endl; // 先访问根节点,然后根节点就无需入栈了
s.pop();
// 先push的是右节点,再是左节点
if(tmpnode->right)
s.push(tmpnode->right);
if(tmpnode->left)
s.push(tmpnode->left);
}
}
}
每次将节点压入栈,然后弹出,压右子树,再压入左子树,在遍历过程中,遍历序列的右节点依次被存入栈,左节点逐次被访问。同一时刻,栈中元素为m-1个右节点和1个最左节点,最高为h。所以空间也为O(h);每个节点同样被压栈一次,弹栈一次,访问一次,时间复杂度O(n)。
先序遍历代码:非递归版本,用栈实现,版本2
//非递归实现先序遍历,使用栈实现,版本二
void preorder_traverse(binary_tree root)
{
stack<binary_tree> s;
while(root||!s.empty())
{
if(root)
{
cout<<root->elem<<endl;// 先序就体现在这里了,先访问,再入栈
s.push(root);
root=root->left;// 依次访问左子树
}
else
{
root=s.top();// 回溯至父亲节点
s.pop();
root=root->right;
}
}
}
每次都将遇到的节点压入栈,当左子树遍历完毕后才从栈中弹出最后一个访问的节点,访问其右子树。在同一层中,不可能同时有两个节点压入栈,因此栈的大小空间为O(h),h为二叉树高度。时间方面,每个节点都被压入栈一次,弹出栈一次,访问一次,复杂度为O(n)。
(2)中根遍历非递归实现
中根遍历要求顺序是左根右,借助栈s实现。先将根root入栈,接着从根root开始查找最左的子孩子结点直到为空为止,再将左子树节点出栈遍历,然后判断该左子树的右子树节点入栈。循环此过程,直到栈为空为止。采用C++中模板库stack实现先根遍历如下:
//非递归实现中序遍历,使用栈实现
void inorder_traverse(binary_tree root)
{
stack<binary_tree> s;
while(root||!s.empty())
{
if(root)
{
s.push(root);
root=root->left;
}
else
{
root=s.top();
cout<<root->elem<<endl;
s.pop();
root=root->right;
}
}
}
根据上面的先序遍历,可以类似的构造出中序遍历。仔细想一下,只有第2种方法改过来时最方便的。需要的改动仅仅调换一下节点访问的次序,先序是先访问,再入栈;而中序则是先入栈,弹栈后再访问。
(3)后根遍历递归实现
后根遍历要求访问顺序是左右根,采用辅助栈实现时,需要一个标记,判断结点是否访问了,因为右子树是通过跟结点的信息得到的。实现过程是先将根结点及其左子树入栈,并初始标记为0,表示没有访问,然后通过判断栈是否为空和标记的值是否为1来判断是否访问元素。
参考:http://www.cnblogs.com/hicjiajia/archive/2010/08/27/1810055.html
采用C++模板库stack具体实现程序如下:
void postorder_traverse(binary_tree root)
{
if(NULL != root)
{
stack<binary_tree_node*> s;
binary_tree_node *ptmpnode;
int flags[100];
ptmpnode = root;
while(NULL != ptmpnode || !s.empty())
{
//将结点左子树结点入栈
while(NULL != ptmpnode)
{
s.push(ptmpnode);
flags[s.size()] = 0; //标记未访问
ptmpnode=ptmpnode->left;
}
//输出访问的结点
while(!s.empty() && flags[s.size()] == 1)
{
ptmpnode = s.top();
s.pop();
cout<<ptmpnode->elem<<" ";
}
//从右子树开始遍历
if(!s.empty())
{
ptmpnode = s.top();
flags[s.size()] = 1; //登记访问了
ptmpnode = ptmpnode->right;
}
else
break;
}
}
}
一种更简单的实现,第二种思路:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,则可以直接访问它;或者P存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。
//非递归后序遍历,使用栈实现
void postorder_traverse(binary_tree root)
{
binary_tree curr; //当前结点
binary_tree pre=NULL; //上一次访问的结点
stack<binary_tree> s;
if(root)
s.push(root);
while(!s.empty())
{
curr=s.top();
//如果当前结点没有孩子结点或者孩子节点都已被访问过
if((curr->left==NULL&&curr->right==NULL)||(pre!=NULL&&(pre==curr->left||pre==curr->right)))
{
cout<<curr->elem<<endl;
s.pop();
pre=curr;
}
else
{
if(curr->right)
s.push(curr->right);
if(curr->left)
s.push(curr->left);
}
}
}
对于判断条件if((curr->left==NULL&&curr->right==NULL)||(pre!=NULL&&(pre==curr->left||pre==curr->right)))的第二部分,开始我没了解,后来想想好像是当前访问结点之前的访问结点只有两种情况,如果当前访问结点存在右子树,则上一次的访问结点pre只能是右子树,否则,就是左子树了。所以,如果满足条件pre!=NULL&&pre==curr->left,说明当前结点不存在右子树,所以不需要判断右子树是否被访问了。否则就是左右子树都被访问了。
(4)层次遍历实现
层次遍历要求从根向下、从左向右进行访问,可以采用队列实现。先将根入队,然后队列进程出队操作访问结点p,再将结点p的左子树和右子树结点入队,循环执行此过程直到队列为空。出队顺序即是层次遍历结果。采用C++的模板库queue实现如下:
//层次遍历
void levelorder_traverse(binary_tree root)
{
queue<binary_tree> q;
binary_tree tmpnode;
if(root)
q.push(root);
while(!q.empty())
{
tmpnode=q.front();
cout<<tmpnode->elem<<" ";
q.pop();
if(tmpnode->left)
q.push(tmpnode->left);
if(tmpnode->right)
q.push(tmpnode->right);
}
}
综合上面的分析过程写个完整的程序测试二叉树遍历的非递归实现,采用C++语言,借助stack和queue实现,完整程序如下所示:
#include<iostream>
#include<stack>
#include<queue>
#include<cstdlib>
using namespace std;
typedef struct binary_tree_node
{
int elem;
struct binary_tree_node *left;
struct binary_tree_node *right;
}binary_tree_node,*binary_tree;
//非递归先序遍历,使用栈实现,版本一
/*void preorder_traverse(binary_tree root)
{
stack<binary_tree> s;
if(root!=NULL)
{
s.push(root);
binary_tree tmpnode;
while(!s.empty())
{
tmpnode=s.top();
cout<<tmpnode->elem<<" ";
s.pop();
if(tmpnode->right)
s.push(tmpnode->right);
if(tmpnode->left)
s.push(tmpnode->left);
}
}
}*/
//非递归实现先序遍历,使用栈实现,版本二
void preorder_traverse(binary_tree root)
{
stack<binary_tree> s;
while(root||!s.empty())
{
if(root)
{
cout<<root->elem<<" ";
s.push(root);
root=root->left;
}
else
{
root=s.top();
s.pop();
root=root->right;
}
}
}
//非递归实现中序遍历,使用栈实现
void inorder_traverse(binary_tree root)
{
stack<binary_tree> s;
while(root||!s.empty())
{
if(root)
{
s.push(root);
root=root->left;
}
else
{
root=s.top();
cout<<root->elem<<" ";
s.pop();
root=root->right;
}
}
}
//非递归后序遍历,使用栈实现
void postorder_traverse(binary_tree root)
{
binary_tree curr;//当前结点
binary_tree pre=NULL; //前一次访问的结点
stack<binary_tree> s;
if(root)
s.push(root);
while(!s.empty())
{
curr=s.top();
if((curr->left==NULL&&curr->right==NULL)||(pre!=NULL&&(pre==curr->left||pre==curr->right)))
{
cout<<curr->elem<<" ";
s.pop();
pre=curr;
}
else
{
if(curr->right)
s.push(curr->right);
if(curr->left)
s.push(curr->left);
}
}
}
//层次遍历
void levelorder_traverse(binary_tree root)
{
queue<binary_tree> q;
binary_tree tmpnode;
if(root)
q.push(root);
while(!q.empty())
{
tmpnode=q.front();
cout<<tmpnode->elem<<" ";
q.pop();
if(tmpnode->left)
q.push(tmpnode->left);
if(tmpnode->right)
q.push(tmpnode->right);
}
}
//初始化二叉树
void init_binary_tree(binary_tree *root)
{
*root=NULL;
}
//方法一:创建二叉树,注意传入的参数是二级指针,否则不能运行正确结果
/*void create_binary_tree(binary_tree *root)
{
int elem;
cout<<"Enter the node value (0 is end): ";
cin>>elem;
if(elem==0)
*root=NULL;
else
{
*root=(binary_tree)malloc(sizeof(binary_tree_node));
if(*root==NULL)
{
cout<<"malloc error"<<endl;
exit(1);
}
(*root)->elem=elem;
cout<<"creating the left child node"<<endl;
create_binary_tree(&((*root)->left));
cout<<"creating the right child node"<<endl;
create_binary_tree(&((*root)->right));
}
}*/
//方法二
binary_tree create_binary_tree()
{
int elem;
binary_tree root;
cout<<"Enter the node value (0 is end): ";
cin>>elem;
if(elem==0)
root=NULL;
else
{
//在堆空间分配的内存,可以返回局部对象
root=(binary_tree)malloc(sizeof(binary_tree_node));
if(root==NULL)
{
cout<<"malloc error"<<endl;
exit(1);
}
(root)->elem=elem;
cout<<"creating the left child node"<<endl;
root->left=create_binary_tree();
cout<<"creating the right child node"<<endl;
root->right=create_binary_tree();
}
return root;
}
int main()
{
binary_tree root;
init_binary_tree(&root);
root=create_binary_tree();
cout<<"先序遍历:";
preorder_traverse(root);
cout<<endl;
cout<<"中序遍历: ";
inorder_traverse(root);
cout<<endl;
cout<<"后序遍历: ";
postorder_traverse(root);
cout<<endl;
cout<<"层次遍历: ";
levelorder_traverse(root);
cout<<endl;
exit(0);
}
运行结果:
二叉树的非递归遍历
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有前序、中序以及后序三种遍历方法。因为树的定义本身就是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。而对于树的遍历若采用非递归的方法,就要采用栈去模拟实现。在三种遍历中,前序和中序遍历的非递归算法都很容易实现,非递归后序遍历实现起来相对来说要难一点。
一.前序遍历
前序遍历按照“根结点-左孩子-右孩子”的顺序进行访问。
1.递归实现
void preOrder1(BinTree *root) //递归前序遍历
{
if(root!=NULL)
{
cout<<root->data<<" ";
preOrder1(root->lchild);
preOrder1(root->rchild);
}
}
2.非递归实现
根据前序遍历访问的顺序,优先访问根结点,然后再分别访问左孩子和右孩子。即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问其左子树时,再访问它的右子树。因此其处理过程如下:
对于任一结点P:
1)访问结点P,并将结点P入栈;
2)判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
3)直到P为NULL并且栈为空,则遍历结束。
void preOrder2(BinTree *root) //非递归前序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
cout<<p->data<<" ";
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
s.pop();
p=p->rchild;
}
}
}
二.中序遍历
中序遍历按照“左孩子-根结点-右孩子”的顺序进行访问。
1.递归实现
void inOrder1(BinTree *root) //递归中序遍历
{
if(root!=NULL)
{
inOrder1(root->lchild);
cout<<root->data<<" ";
inOrder1(root->rchild);
}
}
2.非递归实现
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才进行访问,然后按相同的规则访问其右子树。因此其处理过程如下:
对于任一结点P,
1)若其左孩子不为空,则将P入栈并将P的左孩子置为当前的P,然后对当前结点P再进行相同的处理;
2)若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的P置为栈顶结点的右孩子;
3)直到P为NULL并且栈为空则遍历结束
void inOrder2(BinTree *root) //非递归中序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
cout<<p->data<<" ";
s.pop();
p=p->rchild;
}
}
}
三.后序遍历
后序遍历按照“左孩子-右孩子-根结点”的顺序进行访问。
1.递归实现
void postOrder1(BinTree *root) //递归后序遍历
{
if(root!=NULL)
{
postOrder1(root->lchild);
postOrder1(root->rchild);
cout<<root->data<<" ";
}
}
2.非递归实现
后序遍历的非递归实现是三种遍历方式中最难的一种。因为在后序遍历中,要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难题。下面介绍两种思路。
第一种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
void postOrder2(BinTree *root) //非递归后序遍历
{
stack<BTNode*> s;
BinTree *p=root;
BTNode *temp;
while(p!=NULL||!s.empty())
{
while(p!=NULL) //沿左子树一直往下搜索,直至出现没有左子树的结点
{
BTNode *以上是关于基本数据结构 - 二叉树的主要内容,如果未能解决你的问题,请参考以下文章