数据结构与算法笔记之二叉树

鉴于二叉树是数据结构的重点内容之一,并且也是IT行业面试必问的问题之一。因此,我将从以下几个方面来介绍二叉树。
一、基本概念介绍
二、二叉树的实现
三、遍历算法的实现

一、基本概念介绍

在介绍二叉树之前,先介绍一些基本概念。
1、树
树是一种分层结构,而层次化这一东西几乎蕴含在所有事物及其联系当中。从文件系统,一直到地球生态系统乃至人类社会系统,层次化系统以及层次化结构无所不在。
从图论的角度来看,树等价于连通的无环图,因此树是由一组顶点以及联结与期间的若干边组成,如下图所示:
在这里插入图片描述
根就是顶点,从程序的角度又叫节点

2、深度与层次
由树的连通性,每一节点与根之间都有一条路径相连,而根据树的无环性,由根通过每个节点的路径必然唯一。
深度:沿每个节点v到根r的唯一通路所经过边的数目,记为depth(v)。
依据深度排序,可对所有节点做分层归类,特别地,约定根节点的深度为0,属于第0层。
在这里插入图片描述
如上图,v的孩子总数,称为度数(degree),而没有孩子的节点,称为叶结点(leaf),包括根在内的其余结点皆称为内节点(internal node)
v所有的后代及其之间的联合边称为子树(subtree)

3、高度
树中所有节点深度的最大值称作该树的**高度(height)。**单个节点高度为0,空树高度为-1。全树的高度即其根节点r的高度,height(T)=height(v)。

4、二叉树
每个节点度数不超过2,如下图所示:
在这里插入图片描述

小结一下:
1)、除了根节点之外每个节点只有一个父节点,根节点没有父节点。
2)、除了叶节点之外所有节点都有一个或多个子节点,叶节点没有子节点。
3)、父节点和子节点之间用指针链接。

二、二叉树的实现

1、二叉树节点

BinNode模板类

#pragma once

#define BinNodePosi(T) BinNode<T>* //节点位置
#define stature(p) ((p) ? (p)->height : -1) //节点高度(与“空树高度为-1”的约定相统一)
typedef enum { RB_RED, RB_BLACK} RBColor; //节点颜色

template <typename T> struct BinNode { //二叉树节点模板类
// 成员(为简化描述起见统一开放,读者可根据需要进一步封装)
   T data; //数值
   BinNodePosi(T) parent; BinNodePosi(T) lc; BinNodePosi(T) rc; //父节点及左、右孩子
   int height; //高度(通用)
   int npl; //Null Path Length(左式堆,也可直接用height代替)
   RBColor color; //颜色(红黑树)
// 构造函数
   BinNode() :
      parent ( NULL ), lc ( NULL ), rc ( NULL ), height ( 0 ), npl ( 1 ), color ( RB_RED ) { }
   BinNode ( T e, BinNodePosi(T) p = NULL, BinNodePosi(T) lc = NULL, BinNodePosi(T) rc = NULL,
             int h = 0, int l = 1, RBColor c = RB_RED ) :
      data ( e ), parent ( p ), lc ( lc ), rc ( rc ), height ( h ), npl ( l ), color ( c ) { }
// 操作接口
   int size(); //统计当前节点后代总数,亦即以其为根的子树的规模
   BinNodePosi(T) insertAsLC ( T const& ); //作为当前节点的左孩子插入新节点
   BinNodePosi(T) insertAsRC ( T const& ); //作为当前节点的右孩子插入新节点
   BinNodePosi(T) succ(); //取当前节点的直接后继
   template <typename VST> void travLevel ( VST& ); //子树层次遍历
   template <typename VST> void travPre ( VST& ); //子树先序遍历
   template <typename VST> void travIn ( VST& ); //子树中序遍历
   template <typename VST> void travPost ( VST& ); //子树后序遍历
// 比较器、判等器(各列其一,其余自行补充)
   bool operator< ( BinNode const& bn ) { return data < bn.data; } //小于
   bool operator== ( BinNode const& bn ) { return data == bn.data; } //等于
   /*DSA*/
   /*DSA*/BinNodePosi(T) zig(); //顺时针旋转
   /*DSA*/BinNodePosi(T) zag(); //逆时针旋转
};

#include "BinNode_implementation.h"

2、二叉树

BinTree模板类

#pragma once

#include "BinNode.h" //引入二叉树节点类
template <typename T> class BinTree { //二叉树模板类
protected:
   int _size; BinNodePosi(T) _root; //规模、根节点
   virtual int updateHeight ( BinNodePosi(T) x ); //更新节点x的高度
   void updateHeightAbove ( BinNodePosi(T) x ); //更新节点x及其祖先的高度
public:
   BinTree() : _size ( 0 ), _root ( NULL ) { } //构造函数
   ~BinTree() { if ( 0 < _size ) remove ( _root ); } //析构函数
   int size() const { return _size; } //规模
   bool empty() const { return !_root; } //判空
   BinNodePosi(T) root() const { return _root; } //树根
   BinNodePosi(T) insertAsRoot ( T const& e ); //插入根节点
   BinNodePosi(T) insertAsLC ( BinNodePosi(T) x, T const& e ); //e作为x的左孩子(原无)插入
   BinNodePosi(T) insertAsRC ( BinNodePosi(T) x, T const& e ); //e作为x的右孩子(原无)插入
   BinNodePosi(T) attachAsLC ( BinNodePosi(T) x, BinTree<T>* &T ); //T作为x左子树接入
   BinNodePosi(T) attachAsRC ( BinNodePosi(T) x, BinTree<T>* &T ); //T作为x右子树接入
   int remove ( BinNodePosi(T) x ); //删除以位置x处节点为根的子树,返回该子树原先的规模
   BinTree<T>* secede ( BinNodePosi(T) x ); //将子树x从当前树中摘除,并将其转换为一棵独立子树
   template <typename VST> //操作器
   void travLevel ( VST& visit ) { if ( _root ) _root->travLevel ( visit ); } //层次遍历
   template <typename VST> //操作器
   void travPre ( VST& visit ) { if ( _root ) _root->travPre ( visit ); } //先序遍历
   template <typename VST> //操作器
   void travIn ( VST& visit ) { if ( _root ) _root->travIn ( visit ); } //中序遍历
   template <typename VST> //操作器
   void travPost ( VST& visit ) { if ( _root ) _root->travPost ( visit ); } //后序遍历
   bool operator< ( BinTree<T> const& t ) //比较器(其余自行补充)
   { return _root && t._root && lt ( _root, t._root ); }
   bool operator== ( BinTree<T> const& t ) //判等器
   { return _root && t._root && ( _root == t._root ); }
   /*DSA*/
   /*DSA*/void stretchToLPath() { stretchByZag ( _root ); } //借助zag旋转,转化为左向单链
   /*DSA*/void stretchToRPath() { stretchByZig ( _root, _size ); } //借助zig旋转,转化为右向单链
}; //BinTree

#include "BinTree_implementation.h"

三、遍历算法的实现

通常树有以下几种遍历方式:
1、先序遍历
2、中序遍历
3、后序遍历
下面分别介绍3种遍历方式的思想以及实现过程。

1、先序遍历
算法思想:先访问根节点,再访问左子节点,最后访问右子节点。
在这里插入图片描述

算法实现过程:

template <typename T> template <typename VST> //元素类型、操作器
void BinNode<T>::travLevel ( VST& visit )
{ //二叉树层次遍历算法
   Queue<BinNodePosi(T)> Q; //辅助队列
   Q.enqueue ( this ); //根节点入队
   while ( !Q.empty() ) 
   { //在队列再次变空之前,反复迭代
      BinNodePosi(T) x = Q.dequeue(); visit ( x->data ); //取出队首节点并访问之
      if ( HasLChild ( *x ) ) Q.enqueue ( x->lc ); //左孩子入队
      if ( HasRChild ( *x ) ) Q.enqueue ( x->rc ); //右孩子入队
   }
}

2、中序遍历
算法思想:先访问左子节点,再访问根节点,最后访问右子节点。
在这里插入图片描述

算法实现过程:
共五个迭代版本:

void BinNode<T>::travIn ( VST& visit ) { //二叉树中序遍历算法统一入口
   switch ( rand() % 5 ) { //此处暂随机选择以做测试,共五种选择
      case 1: travIn_I1 ( this, visit ); break; //迭代版#1
      case 2: travIn_I2 ( this, visit ); break; //迭代版#2
      case 3: travIn_I3 ( this, visit ); break; //迭代版#3
      case 4: travIn_I4 ( this, visit ); break; //迭代版#4
      default: travIn_R ( this, visit ); break; //递归版
   }
}

下面分别介绍五个版本:

迭代版本1:

template <typename T, typename VST> //元素类型、操作器
void travIn_I1 ( BinNodePosi(T) x, VST& visit ) 
{ //二叉树中序遍历算法(迭代版#1)
   Stack<BinNodePosi(T)> S; //辅助栈
   while ( true ) 
   {
      goAlongLeftBranch ( x, S ); //从当前节点出发,逐批入栈
      if ( S.empty() ) break; //直至所有节点处理完毕
      x = S.pop(); visit ( x->data ); //弹出栈顶节点并访问之
      x = x->rc; //转向右子树
   }
}

迭代版本2:

template <typename T, typename VST> //元素类型、操作器
void travIn_I2 ( BinNodePosi(T) x, VST& visit )
{ //二叉树中序遍历算法(迭代版#2)
   Stack<BinNodePosi(T)> S; //辅助栈
   while ( true )
      if ( x )
	  {
         S.push ( x ); //根节点进栈
         x = x->lc; //深入遍历左子树
      } else if ( !S.empty() ) {
         x = S.pop(); //尚未访问的最低祖先节点退栈
         visit ( x->data ); //访问该祖先节点
         x = x->rc; //遍历祖先的右子树
      } else
         break; //遍历完成
}

迭代版本3:

template <typename T, typename VST> //元素类型、操作器
void travIn_I3 ( BinNodePosi(T) x, VST& visit ) { //二叉树中序遍历算法(迭代版#3,无需辅助栈)
   bool backtrack = false; //前一步是否刚从右子树回溯——省去栈,仅O(1)辅助空间
   while ( true )
      if ( !backtrack && HasLChild ( *x ) ) //若有左子树且不是刚刚回溯,则
         x = x->lc; //深入遍历左子树
      else { //否则——无左子树或刚刚回溯(相当于无左子树)
         visit ( x->data ); //访问该节点
         if ( HasRChild ( *x ) ) { //若其右子树非空,则
            x = x->rc; //深入右子树继续遍历
            backtrack = false; //并关闭回溯标志
         } else { //若右子树空,则
            if ( ! ( x = x->succ() ) ) break; //回溯(含抵达末节点时的退出返回)
            backtrack = true; //并设置回溯标志
         }
      }
}

迭代版本4

template <typename T, typename VST> //元素类型、操作器
void travIn_I4 ( BinNodePosi(T) x, VST& visit ) { //二叉树中序遍历(迭代版#4,无需栈或标志位)
   while ( true )
      if ( HasLChild ( *x ) ) //若有左子树,则
         x = x->lc; //深入遍历左子树
      else { //否则
         visit ( x->data ); //访问当前节点,并
         while ( !HasRChild ( *x ) ) //不断地在无右分支处
            if ( ! ( x = x->succ() ) ) return; //回溯至直接后继(在没有后继的末节点处,直接退出)
            else visit ( x->data ); //访问新的当前节点
         x = x->rc; //(直至有右分支处)转向非空的右子树
      }
}

递归版本

template <typename T, typename VST> //元素类型、操作器
void travIn_R ( BinNodePosi(T) x, VST& visit ) { //二叉树中序遍历算法(递归版)
   if ( !x ) return;
   travIn_R ( x->lc, visit );
   visit ( x->data );
   travIn_R ( x->rc, visit );
}

3、后序遍历
算法思想:先访问左子节点,再访问右子节点,最后访问根节点。
在这里插入图片描述

算法实现过程:
有两种版本:迭代版和递归版

template <typename T> template <typename VST> //元素类型、操作器
void BinNode<T>::travPost ( VST& visit ) { //二叉树后序遍历算法统一入口
   switch ( rand() % 2 ) { //此处暂随机选择以做测试,共两种选择
      case 1: travPost_I ( this, visit ); break; //迭代版
      default: travPost_R ( this, visit ); break; //递归版
   }
}

版本1:迭代版

template <typename T, typename VST>
void travPost_I ( BinNodePosi(T) x, VST& visit ) { //二叉树的后序遍历(迭代版)
   Stack<BinNodePosi(T)> S; //辅助栈
   if ( x ) S.push ( x ); //根节点入栈
   while ( !S.empty() ) {
      if ( S.top() != x->parent ) //若栈顶非当前节点之父(则必为其右兄),此时需
         gotoHLVFL ( S ); //在以其右兄为根之子树中,找到HLVFL(相当于递归深入其中)
      x = S.pop(); visit ( x->data ); //弹出栈顶(即前一节点之后继),并访问之
   }
}

版本2:递归版

template <typename T, typename VST> //元素类型、操作器
void travPost_R ( BinNodePosi(T) x, VST& visit ) { //二叉树后序遍历算法(递归版)
   if ( !x ) return;
   travPost_R ( x->lc, visit );
   travPost_R ( x->rc, visit );
   visit ( x->data );
}

到此,二叉树的基本概念及实现过程介绍完毕,后面将对一些常见的二叉树相关的面试题进行总结。
二叉树面试考点:主要考查应聘者对二叉树的先序遍历、中序遍历、后序遍历的理解程度。只有对二叉树的不同遍历算法有了深刻理解,应聘者才能在遍历序列中划分出左、右子树对应的子序列。

猜你喜欢

转载自blog.csdn.net/tpf930726/article/details/83627915