在”旭说数据结构之树的简介中“介绍了树的基本知识。这一篇介绍树中的一个重要结构,二叉树。
1.简述二叉树
二叉树既然是树中的一个重要结构,它就在拥有树的全部特征的同时,还拥有自己的独特性。既然名字中带二,就说明最多可以分两个叉,即二叉树的度为2,同时二叉树还规定子树有左右之分,左边的叫做左子树,右边的叫做右子树。规定了左右之分后,3个结点的二叉树就有5种状态了:
定义 | 解释 |
---|---|
完全二叉树 | 设二叉树的高度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。 |
满二叉树 | 除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树。 |
平衡二叉树 | 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 |
2.二叉树的性质
”旭说数据结构之树的简介中“介绍了树的四个性质,这四个性质用于二叉树可得:
1 非空树的结点总数
n
= 所有结点的度之和 +1
即,
n 个结点的非空二叉树有 n−1 个分支2 度为 m 的非空树的第i层最多有
mi−1 个结点即,非空二叉树的第 i 层最多有
2i−1 个结点3 深度为 k 的
m 叉树最多有 mk−1m−1 个结点即,深度为 k 的二叉树最多有
2k−1 个结点4 具有 n 个结点的
m 叉树的最小深度为 logm(n(m−1)+1) 向上取整(向下取整的运算称为Floor,用数学符号⌊⌋表示;向上取整的运算称为Ceiling,用数学符号⌈⌉表示。)。即,具有 n 个结点的二叉树的最小深度为
⌈log2(n+1)⌉ 。还有些比较特殊的性质:
5. 二叉树的终端结点的个数 = 二叉树中度为2 的结点个数 +1 设二叉树的结点总个数为 n ,度为2的个数为
n2 ,终端结点的个数为 n0 ,度为1的结点的个数为 n1 ,则 n=n2+n1+n0 ,
由性质1, n=2∗n2+1∗n1+0∗n0+1=2n2+n1+1 ,所以这两个式子相减得到 n0=n2+16.具有n个结点的完全二叉树的深度为 ⌈log2(n+1)⌉ 。(与性质4同意义)
7.对一颗有n个结点的完全二叉树按层从上往下,每层从左到右进行编号,则编号为i的结点具有如下性质:
i=1 该结点为根结点 i>1 该结点的父结点编号为 ⌊i/2⌋ 2i≤n 该结点的左孩子为 2i 2i+1≤n 该结点的右孩子为 2i+1 3.存储结构
3.1链式存储结构
(下面的代码中利用了之前定义栈,队列)
线性表的链式存储结构中,每个结点有一个_next指针指向下一个结点,在二叉树的链式存储结构中,每个结点应该有两个指针,分别指向左孩子和右孩子。于是我们可以这样定义结点类:3.1.1结点定义
template<typename DataType>class BinaryNode{ public: BinaryNode():_lChild(NULL),_rChild(NULL) { } BinaryNode(DataType data):_lChild(NULL),_rChild(NULL) { _data = data; } private: DataType _data;//数据 BinaryNode* _lChild;//指向左孩子的指针 BinaryNode* _rChild;//指向右孩子的指针 friend BinaryTree<DataType>; };
二叉树类中应该有一个指向根结点的指针作为成员变量,这样我们才能操作这个二叉树。在main函数里,我们想创建一个二叉树,然后对它进行一些操作,比如我们可以写下面的代码:
3.1.2main函数中的测试代码
BinaryTree<char>* binaryTree = new BinaryTree<char>; char preOrder[] = {'A','B','D','G','H','C','E','F'}; char inOrder[] = {'B','G','D','H','A','E','C','F'}; //我们用二叉树的前序和中序来构建一个二叉树 binaryTree-> createBinaryTreeWithPreAndInOrder(preOrder,inOrder,sizeof(inOrder)/sizeof(*inOrder)); //层序遍历 binaryTree->layerOrder(); cout<<endl; //前序遍历 binaryTree->preOrder();//非递归 cout<<endl; binaryTree->preOrderWithRecursion();//递归 cout<<endl; //中 binaryTree->inOrder();//非递归 cout<<endl; binaryTree->inOrderWithRecursion();//递归 cout<<endl; //后 binaryTree->postOrder();//非递归 cout<<endl; binaryTree->postOrderWithRecursion();//递归 cout<<endl; cout<<"二叉树深度"<<binaryTree->getHeight(); cout<<endl;
3.1.3利用前序和中序来构造二叉树:
//前序,中序构造二叉树 void createBinaryTreeWithPreAndInOrder1(DataType *preDatas,DataType* inDatas,int length,BinaryNode<DataType>**root){ if (preDatas == NULL || inDatas==NULL)return; if (*root==NULL) { *root = new BinaryNode<DataType>(*preDatas); } //这时根结点已经有了 //接着要通过中序找到左右子树 DataType* inTemp = inDatas; int leftLength = 0; int rightLength = 0; while(*preDatas!=*inTemp){ //当中序中的值不等于根结点的值时;如果第一次就相等了,说明左子树的长度为0 leftLength++; inTemp++; } rightLength = length - leftLength - 1; if (leftLength>0) {//递归构建左子树 createBinaryTreeWithPreAndInOrder1(preDatas+1, inDatas,leftLength,&((*root)->_lChild)); } if (rightLength>0) {//递归构建右子树 createBinaryTreeWithPreAndInOrder1(preDatas+1+leftLength, inDatas+leftLength+1,rightLength,&((*root)->_rChild)); } }
3.1.4前中后序遍历的递归写法
前中后序遍历的递归写法很简单,
void preOrderWithRecursion1(BinaryNode<DataType>*root) { if (root == NULL)return; cout<<root->_data;//前序遍历,先根结点,然后左右子树 preOrderWithRecursion1(root->_lChild); preOrderWithRecursion1(root->_rChild); } void inOrderWithRecursion1(BinaryNode<DataType>* root){ if (root == NULL)return ; inOrderWithRecursion1(root->_lChild);//中序遍历,先左子树,后根结点,再右子树 cout<<root->_data; inOrderWithRecursion1(root->_rChild); } void postOrderWithRecursion1(BinaryNode<DataType>* root) { if (root == NULL)return ; postOrderWithRecursion1(root->_lChild);//后序遍历,先左子树,后右子树,再根结点 postOrderWithRecursion1(root->_rChild); cout<<root->_data; }
3.1.5非递归写法—详解后序遍历的非递归写法
下面主要说说非递归写法:
非递归写法可以利用一个栈来实现,前序和中序相对容易实现,主要来说说后序遍历的非递归写法:
先描述一下二叉树的后序遍历:
![]()
用栈来实现后序遍历:
![]()
后序遍历的非递归实现代码:
//后序:左右根 //后序的非递归算法比前序和中序的要fuz //需要拿个数组来记录每个结点的右子树是否被访问过了,只有右孩子被访问过了或者没有右孩子才能打印这个结点的值 void postOrder1(BinaryNode<DataType>*root) { Stack<BinaryNode<DataType>*> * stack = new Stack<BinaryNode<DataType>*>; BinaryNode<DataType>* p = root; bool rightHasBeenVisited[20];//用来记录入栈的结点的右子树是否被访问过 while(p)//从根结点一路向左,把左孩子都入栈,并且设置他们的右子树都没有被访问过 { stack->push(p);//把子树根结点入栈 p=p->_lChild; rightHasBeenVisited[stack->count()] = false; } while (!stack->isEmpty())//当栈不为空时 { //获取栈顶元素,并未出栈 BinaryNode<DataType>*pp = stack->getTopData(); //如果该栈顶结点没有右孩子,或者它的右子树已经被访问过了, //则可以打印这个结点了 if (pp->_rChild == NULL||rightHasBeenVisited[stack->count()]) { cout<<pp->_data; stack->pop();//出栈 } else//有右孩子,或者右子树没有被访问过的话,就来访问它 { rightHasBeenVisited[stack->count()]=true; //从该结点的右孩子开始,一路向左,把左孩子都入栈, //并且设置他们的右子树都没有被访问过 //和最开始我们从根结点一路向左,把左孩子入栈一样。 //这样栈中又多了结点的右子树信息, //下次循环while (!stack->isEmpty())开始的时候, //拿到的栈顶元素就是这个右子树中的结点。 if (pp->_rChild) { pp=pp->_rChild; while(pp){ stack->push (pp); pp=pp->_lChild; rightHasBeenVisited[stack->count()]=false; } } } } }
3.1.6二叉树的层序遍历
//层序遍历,用队列来实现 void layerOrder1(BinaryNode<DataType>* root) { if (root == NULL)return; //层序遍历要使用队列 Queue<BinaryNode<DataType>*>* queue = new Queue<BinaryNode<DataType>*>; //先把根结点入队 queue->addDataToQueue(root); BinaryNode<DataType>* p = NULL; //当队列不空时 while(queue->count()){ //取出队首元素,第一次取出的是根结点,打印根结点,再把根结点的左右孩子入队列, //下一次取出的队首元素就是root的左孩子,然后把它的左右孩子入队列 //每次都是打印这个结点,然后把它的左右孩子入队,最终所有的结点都会入队列, p=queue->deleteDataFromQueue();//取出队首元素并删除 cout<<p->_data; if (p->_lChild) { queue->addDataToQueue(p->_lChild); } if (p->_rChild) { queue->addDataToQueue(p->_rChild); } } }
3.1.7计算二叉树的高度
//计算二叉树的高度 int getHeight1(BinaryNode<DataType>*root) { //结点栈 Stack<BinaryNode<DataType>*> * nodeStack = new Stack<BinaryNode<DataType>*>; //记录入栈结点深度的栈 Stack<int>* deepStack = new Stack<int>; BinaryNode<DataType>* p = root; int currentHeight =0 ;//记录遍历到的当前高度 int maxHeight = 0;//记录最大高度 while(p)//从根结点一路向左,把左孩子都入栈,并把其对应的高度入栈 { nodeStack->push(p);//把子树根结点入栈 p=p->_lChild; currentHeight++;//入栈一个,currentHeight就加1 deepStack->push(currentHeight);//把入栈结点的currentHeight也入栈 } while (!nodeStack->isEmpty())//当栈不为空时 { BinaryNode<DataType>*pp = nodeStack->pop();//出栈 currentHeight = deepStack->pop(); //如果该栈顶结点没有右孩子,说明是叶子结点,这条路到头了, //如果这条路的currentHeight大于maxHeight,则设置 //maxHeight = currentHeight if (pp->_rChild == NULL) { if (currentHeight > maxHeight) { maxHeight = currentHeight; } } else//有右子树,把右子树一路向左入栈 { pp=pp->_rChild; while(pp){ nodeStack->push (pp); pp=pp->_lChild; currentHeight++; deepStack->push(currentHeight); } } } return maxHeight; }
3.1.8 较为完整的代码
Queue.h中的代码
template<typename DataType>class Queue { public: Queue(int capacity = 10) { _capacity = capacity; _datas = new DataType[capacity]; _front = 0; _rear = 0; _count =0; } ~Queue() { delete [] _datas; } bool addDataToQueue(DataType data)//入队操作 { //队满返回false if ((_rear+1)%_capacity == _front)return false; _datas[_rear] = data; _rear = (_rear+1)%_capacity; _count++; return true; } DataType deleteDataFromQueue()//出队操作private: { if (_rear==_front)exit(-1); DataType temp = _datas[_front]; _front = (_front+1)%_capacity; _count--; return temp; } int count(){ return _count; } int _capacity; int _front; int _rear; DataType* _datas; int _count; };
Stack.h中的代码:
template<typename DataType> class Stack { public: Stack(int capacity = 10) { _top = -1; _capacity = capacity; _datas = new DataType[capacity]; } ~Stack() { delete [] _datas; } bool push(DataType data) { //如果栈满了,返回false if (_top + 1 == _capacity )return false; _top++; _datas[_top]=data; return true; } DataType getTopData() { //如果栈为空,则退出 if (_top == -1)exit(1); return _datas[_top]; } DataType pop() { //如果栈为空,不能进行出栈操作,则退出 if (_top == -1)exit(1); return _datas[_top--]; } bool isEmpty(){ if (_top == -1)return true; return false; } bool isFull(){ if (_top + 1 == _capacity)return true; return false; } int count(){ return _top+1; } private: DataType* _datas; int _capacity;//栈的容量 int _top;//标志栈顶 };
BinaryTree.h中的代码
#include <iostream> #include "Stack.h" #include "Queue.h" using namespace std; template<typename DataType>class BinaryTree; template<typename DataType>class BinaryNode{ public: BinaryNode():_lChild(NULL),_rChild(NULL) { } BinaryNode(DataType data):_lChild(NULL),_rChild(NULL) { _data = data; } private: DataType _data;//数据 BinaryNode* _lChild;//指向左孩子的指针 BinaryNode* _rChild;//指向右孩子的指针 friend BinaryTree<DataType>; }; template<typename DataType>class BinaryTree{ public: BinaryTree() { _root = NULL; } //前序,中序构造二叉树 void createBinaryTreeWithPreAndInOrder(DataType *preDatas,DataType* inDatas,int length){ createBinaryTreeWithPreAndInOrder1(preDatas,inDatas,length,&_root); } //非递归前序遍历 void preOrder(){ cout<<"非递归前序遍历"; preOrder1(_root); } void inOrder(){ cout<<"非递归中序遍历"; inOrder1(_root); } void postOrder(){ cout<<"非递归后序遍历"; postOrder1(_root); } //层序遍历 void layerOrder() { cout<<"层序遍历 "; layerOrder1(_root); } //递归遍历 void preOrderWithRecursion() { cout<<"递归前序遍历 "; preOrderWithRecursion1(_root); } void inOrderWithRecursion(){ cout<<"递归中序遍历 "; inOrderWithRecursion1(_root); } void postOrderWithRecursion() { cout<<"递归后续遍历 "; postOrderWithRecursion1(_root); } //返回二叉树深度 int getHeight(){ return getHeight1(_root); } //找到一个结点的父结点 BinaryNode<DataType>* getParentNode(BinaryNode<DataType>* child) { if (child == NULL||child == _root)return NULL; //利用层序遍历的思路,使用队列 Queue<BinaryNode<DataType>*>* queue = new Queue<BinaryNode<DataType>*>; //先把根结点入队 queue->addDataToQueue(_root); BinaryNode<DataType>* p = NULL; //当队列不空时 while(queue->count()){ //取出队首元素,第一次取出的是根结点,打印根结点,再把根结点的左右孩子入队列, //下一次取出的队首元素就是root的左孩子,然后把它的左右孩子入队列 //每次都是打印这个结点,然后把它的左右孩子入队,最终所有的结点都会入队列, p=queue->deleteDataFromQueue(); if (p->_lChild) { if (p->_lChild == child) { return p; } queue->addDataToQueue(p->_lChild); } if (p->_rChild) { if (p->_rChild == child) { return p; } queue->addDataToQueue(p->_rChild); } } } //--------------------------------具体实现---------------------------------------------------------------------------------- //前序,中序构造二叉树 void createBinaryTreeWithPreAndInOrder1(DataType *preDatas,DataType* inDatas,int length,BinaryNode<DataType>**root) { if (preDatas == NULL || inDatas==NULL)return; if (*root==NULL) { *root = new BinaryNode<DataType>(*preDatas); } //这时根结点已经有了 //接着要通过中序找到左右子树 DataType* inTemp = inDatas; int leftLength = 0; int rightLength = 0; while(*preDatas!=*inTemp){//当中序中的值不等于根结点的值时;如果第一次就相等了,说明左子树的长度为0 leftLength++; inTemp++; } rightLength = length - leftLength - 1; if (leftLength>0) { createBinaryTreeWithPreAndInOrder1(preDatas+1,inDatas,leftLength,&((*root)->_lChild)); } if (rightLength>0) { createBinaryTreeWithPreAndInOrder1(preDatas+1+leftLength,inDatas+leftLength+1,rightLength,&((*root)->_rChild)); } } ~BinaryTree() { delete _root; } //层序遍历 void layerOrder1(BinaryNode<DataType>* root) { if (root == NULL)return; //层序遍历要使用队列 Queue<BinaryNode<DataType>*>* queue = new Queue<BinaryNode<DataType>*>; //先把根结点入队 queue->addDataToQueue(root); BinaryNode<DataType>* p = NULL; //当队列不空时 while(queue->count()){ //取出队首元素,第一次取出的是根结点,打印根结点,再把根结点的左右孩子入队列, //下一次取出的队首元素就是root的左孩子,然后把它的左右孩子入队列 //每次都是打印这个结点,然后把它的左右孩子入队,最终所有的结点都会入队列, p=queue->deleteDataFromQueue();//取出队首元素并删除 cout<<p->_data; if (p->_lChild) { queue->addDataToQueue(p->_lChild); } if (p->_rChild) { queue->addDataToQueue(p->_rChild); } } } //遍历的递归写法-------------------start----------------------------- void preOrderWithRecursion1(BinaryNode<DataType>*root) { if (root == NULL)return; cout<<root->_data;//前序遍历,先根结点,然后左右子树 preOrderWithRecursion1(root->_lChild); preOrderWithRecursion1(root->_rChild); } void inOrderWithRecursion1(BinaryNode<DataType>* root){ if (root == NULL)return ; inOrderWithRecursion1(root->_lChild);//中序遍历,先左子树,后根结点,再右子树 cout<<root->_data; inOrderWithRecursion1(root->_rChild); } void postOrderWithRecursion1(BinaryNode<DataType>* root) { if (root == NULL)return ; postOrderWithRecursion1(root->_lChild);//后序遍历,先左子树,后右子树,再根结点 postOrderWithRecursion1(root->_rChild); cout<<root->_data; } //遍历的递归写法----------------end-------------------------------- //遍历的非递归写法--------------start-------------------------------- //用栈来实现非递归 //前序:根左右 //读取root后把root入栈,因为root的左子树搞完之后要搞root的右子树 void preOrder1(BinaryNode<DataType>*root) { Stack<BinaryNode<DataType>*> * stack = new Stack<BinaryNode<DataType>*>; BinaryNode<DataType>* p = root; while (p!=NULL || !stack->isEmpty()) { while(p!=NULL) { cout<<p->_data;//打印根结点data stack->push(p);//把子树根结点入栈 p=p->_lChild; } //执行到这,这个结点左孩子为空了,开始搞右子树 if (p==NULL) { p = stack->pop();//拿到这个左孩子为空的结点,去找他的右孩子 p = p->_rChild; } } } //中序:左根右 void inOrder1(BinaryNode<DataType>*root) { Stack<BinaryNode<DataType>*> * stack = new Stack<BinaryNode<DataType>*>; BinaryNode<DataType>* p = root; while (p!=NULL || !stack->isEmpty()) { while(p!=NULL) { //cout<<p->_data;//打印根结点data stack->push(p);//把根结点入栈 p=p->_lChild; } //执行到这,这个结点左孩子为空了,开始搞右子树 if (p==NULL) { p = stack->pop();//拿到这个左孩子为空的结点,去找他的右孩子 cout<<p->_data; p = p->_rChild; } } } //后序:左右根 //后序的非递归算法比前序和中序的要fuz //需要拿个数组来记录每个结点的右子树是否被访问过了,只有右孩子被访问过了或者没有右孩子才能打印这个结点的值 void postOrder1(BinaryNode<DataType>*root) { Stack<BinaryNode<DataType>*> * stack = new Stack<BinaryNode<DataType>*>; BinaryNode<DataType>* p = root; bool rightHasBeenVisited[20];//用来记录入栈的结点的右子树是否被访问过 while(p)//从根结点一路向左,把左孩子都入栈,并且设置他们的右子树都没有被访问过 { stack->push(p);//把子树根结点入栈 p=p->_lChild; rightHasBeenVisited[stack->count()] = false; } while (!stack->isEmpty())//当栈不为空时 { BinaryNode<DataType>*pp = stack->getTopData();//获取栈顶元素,并未出栈 //如果该栈顶结点没有右孩子,或者它的右子树已经被访问过了,则可以打印这个结点了 if (pp->_rChild == NULL||rightHasBeenVisited[stack->count()]) { cout<<pp->_data; stack->pop();//出栈 } else//有右孩子,或者右子树没有被访问过的话,就来访问它 { rightHasBeenVisited[stack->count()]=true; //从该结点的右孩子开始,一路向左,把左孩子都入栈,并且设置他们的右子树都没有被访问过 //和最开始我们从根结点一路向左,把左孩子入栈一样。这样栈中又多了结点的右子树信息, //下次循环while (!stack->isEmpty())开始的时候,拿到的栈顶元素就是这个右子树中的结点。 if (pp->_rChild) { pp=pp->_rChild; while(pp){ stack->push (pp); pp=pp->_lChild; rightHasBeenVisited[stack->count()]=false; } } } } } //遍历的非递归写法--------------end-------------------------------- //计算二叉树的高度 int getHeight1(BinaryNode<DataType>*root) { Stack<BinaryNode<DataType>*> * nodeStack = new Stack<BinaryNode<DataType>*>; Stack<int>* deepStack = new Stack<int>; BinaryNode<DataType>* p = root; int currentHeight =0 ;//记录遍历到的当前高度 int maxHeight = 0;//记录最大高度 while(p)//从根结点一路向左,把左孩子都入栈,并把其对应的高度入栈 { nodeStack->push(p);//把子树根结点入栈 p=p->_lChild; currentHeight++;//入栈一个,currentHeight就加1 deepStack->push(currentHeight);//把入栈结点的currentHeight也入栈 } while (!nodeStack->isEmpty())//当栈不为空时 { BinaryNode<DataType>*pp = nodeStack->pop();//出栈 currentHeight = deepStack->pop(); //如果该栈顶结点没有右孩子,说明是叶子结点,这条路到头了 if (pp->_rChild == NULL) { if (currentHeight > maxHeight) { maxHeight = currentHeight; } } else//有右子树,把右子树一路向左入栈 { pp=pp->_rChild; while(pp){ nodeStack->push (pp); pp=pp->_lChild; currentHeight++; deepStack->push(currentHeight); } } } return maxHeight; } private: BinaryNode<DataType>*_root;//根结点 };
3.2顺序存储结构
利用上述的性质7,考虑,我们拿到一个二叉树,给它填补成一个完全二叉树,把逻辑上的编号对应到相应的数组位置即可。
![]()
但是这种存储结构一般会造成空间的浪费,对于有些树浪费还比较严重,比如下面这种二叉树结构,树中只有三个元素,却得多用4个存储空间。