C++及数据结构复习笔记(十三)(二叉树)

2.4 二叉树

       树属于半线性结构。

2.4.1 二叉树及其表示

       从图论的角度看,树等价于联通无环图。沿每个节点v到根r的唯一通路所经过的边的数目,称为v的深度depth,记作depth(v)。根节点的深度为0,属于第0层。在每一层上,v的祖先至多有一个。v的孩子的总数称为v的度,无孩子的节点称为叶节点,包括根在内的其余节点均为内部节点。树T中所有节点深度的最大值称为该树的高度height。空树高度为-1,单节点的树的高度为0。在二叉树中,每个节点的度数不能超过2。特别地,不含一度节点的二叉树称为真二叉树、

       若每个节点的孩子数均不超过k个的有根树,称作k叉树。在多叉树中,根节点以外的任意节点有且仅有一个父节点。

2.4.2 编码树

       前缀无歧义编码:只要各字符的编码串互不为前缀,则即便出现无法解码的错误,也绝对不致歧义。前缀无歧义编码简称PFC码,此类编码算法可以明确地将二进制编码串分割为一系列与各原始字符相对应的片段,从而实现无歧义解码。

       任一编码方案都可描述为一颗二叉树:从根节点出发,每次向左都对应一个0比特位,向右对应一个1比特位。从根节点到每个节点的唯一通路可以为各节点v赋予一个互异的二进制串,称作根通路串,记作rps(v)。

2.4.3 二叉树的实现

       二叉树节点BinNode模版类中需要注意的代码:

#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) lChild; BinNodePosi(T) rChild;
//父节点及左右孩子节点
……
};

       BinNode节点由多个成员变量组成,具体如图2.3所示。其中data数据类型由模版变量T指定,用于存放各节点所对应的数值对象,lChild,rChild和parent均为指针类型,分别指向左右孩子以及父节点的位置。

图 2.3 BinNode模板类的逻辑结构

       在BinNode模板类各接口以及后续相关算法的实现中,需要频繁检查和判断二叉树节点的状态与性质,有时还需要定位与之相关的特定节点,将二叉树节点常用功能以宏定义的形式整理如下:

/*******************************************************
    *BinNode状态与性质的判断
********************************************************
#define IsRoot(x) (!((x).parent))  //判断x是否为根节点
#define IsLChild(x) (!IsRoot(x)&&(&(x)==(x).parent->lChild))  //判断x是否为左孩子节点
#define IsRChild(x) (!IsRoot(x)&&(&(x)==(x).parent->rChild))  //判断x是否为右孩子节点
#define HasParent(x) (!IsRoot(x))//判断x是否是根节点,若非根节点则有父节点。即判断x是否有父节点
#define HasLChild(x) ((x).lChild)
#define HasRChild(x) ((x).rChild)
#define HasChild(x) (HasLChild(x)||HasRChild(x))//判断x是否有孩子节点
#define HasBothChild(x) (HasLChild(x)&&HasRChild(x))//判断x是否同时有2个孩子节点
#define IsLeaf(x) (!HasChild(x)) //判断x是否为叶子节点

2.4.4 二叉树节点操作接口

一、插入孩子节点

       首先创建新节点,然后将当前节点作为新节点的父亲,并令新节点作为当前节点的左孩子。这里假定在插入新节点前,当前节点尚无左孩子。

template <typename T>  //将e作为当前节点的左孩子,插入二叉树
BinNodePosi(T) BinNode<T>::insertAsLC(T const &e)
{return lChild=new BinNode(e,this);}
template <typename T>
BinNodePosi(T) BinNode<T>::insertAsRC(T const &e)
{return rChild=new BinNode(e,this);}

2.4.5 二叉树BinTree类模板

       在BinNode模版类的基础上,可定义二叉树BinTree模版类,关键的代码如下:

#include”BinNode.h”
template <typename T> 
class BinTree  //二叉树模版类
{
  protected:
    BinNodePosi(T) _root;//根节点
    virtual int updateHeight(BinNodePosi(T) x);//更新节点x的高度
    void updateHeightAbove(BinNode(T) x);//更新节点x及其祖先的高度
    ……
  public:
BinNodePosi(T) attachAsLC(BinNodePosi(T) x,BinTree<T>* &T);//T作为x左子树插入
int remove(BinNodePosi(T) x);//删除以位置x处节点为根的子树,返回子树的规模
};

一、高度更新

       二叉树任意节点的高度,都等于其孩子节点的最大高度加1,因此当某一节点的孩子或后代有所增减,其高度都有必要及时更新。一旦有节点加入或离开,就更新其所有祖先的高度。

       在每一节点v处,只需找到左、右孩子的高度并取之间的最大者,再计入当前节点本身,就得到了v的高度。接下来再从v沿parent指针逆行向上,以此更新各祖先的高度记录。

template<typename T>
int BinTree<T>::updateHeight(BinNodePosi(T) x)//更新节点x的高度
{
  return x->height=1+max(stature(x->lChild), stature(x->lChild));
  // stature(x->lChild)指的是x的左孩子的节点的高度,stature(p)是节点p的高度
}
template <typename T>
void BinTree<T>::updateHeightAbove(BinNodePosi(T) x)//更新x及其祖先的高度
{
  while(x)
{updateHeight(x);x=x->parent;}
}

二、节点插入

       二叉树节点可以通过3种方式插入到二叉树中。insertAsRoot()接口用于将节点插入空树中,为此需要创建一个新节点并存入指定的数据,再令其作为根节点,同时更新全树规模。

template <typename T>
BinNodePosi(T) BinTree<T>::insertAsRoot(T const &e)//将e作为根节点插入空的二叉树
{_size=1;return _root=new BinNode<T>(e);}
template <typename T>
BinNodePosi(T) BinTree<T>::insertAsLC(BinNodePosi(T) x,T const &e)// 将e作为x的二左孩子
{
_size++;
x->insertAsLC(e);//函数重载,将e作为当前节点的左孩子,插入二叉树
updateHeightabove(x);//更新x及其祖先的高度
return x->lChild;;
}
BinNodePosi(T) BinTree<T>::insertAsRC(BinNodePosi(T) x,T const &e)// 将e作为x的二左孩子
{
_size++;
x->insertAsRC(e);//函数重载,将e作为当前节点的右孩子,插入二叉树
updateHeightabove(x);//更新x及其祖先的高度
return x->rChild;;
}

三、子树接入

       任意二叉树均可作为另一二叉树指定节点的左子树或右子树,植入其中。

template <typename T>//二叉树子树介入算法:将S当作节点x的左子树接入,S本身置空
BinNodePosi(T) BinTree<T>::attachAsLC(BinNodePosi(T) x,BinTree<T>* &S)
{//x->lChild==NILL
  if (x->lChild=S->_root) x->lChild->parent=x;//接入
  _size=_size+S->_size;
  updateHeightAbove(x);//更新全树规模及x所有祖先的高度
  S->_root=NULL;S->_size=0;release(S); S=NULL;return x;//释放原树,返回接入位置}
template <typename T>
BinNodePosi(T) BinTree<T>::attachAsRC(BinNodePosi(T) x,BinTree<T>* &S)
{//x->rChild==NILL
  if (x->rChild=S->_root) x->rChild->parent=x;//接入
  _size=_size+S->_size;
  updateHeightAbove(x);//更新全树规模及x所有祖先的高度
  S->_root=NULL;S->_size=0;release(S); S=NULL;return x;//释放原树,返回接入位置}

       若节点x的右孩子为空,则attachAsRC()接口首先将待植入的二叉树S的根节点作为x的右孩子,同时令x作为该节点的父亲。然后更新全树规模以及节点x所有祖先的高度。最后清空一些不在接入节点之外的信息。

四、子树删除

       子树删除与子树接入的过程正好相反,不同之处在于需要将被删除的子树中的节点逐一释放并归还系统。

template <typename T>
int BinTree<T>::remove(BinNodePosi(T) x)
//删除二叉树中位置x处的节点及其后代,返回被删除节点的数值
{
  FromParentTo(*x)=NULL;//切断来自父节点的指针
  updateHeightAbove(x->parent);//更新祖先高度
  int n=removeAt(x);_size=_size-n;return n;//删除子树x,更新规模,返回删除节点总数
}
template <typename T>
static int removeAt(BinNodePosi(T) x)
//删除二叉树中位置x处节点及其后代,返回被删除节点的数值
{
  if(!x) return 0;
  int n=1+removeAt(x->lChild)+removeAt(x->rChild);//递归释放左右子树
  release(x->data);release(x);return n;//返回删除节点总数
}

2.4.6 遍历

       对二叉树的访问可抽象为如下形式:按照事先约定的某种规则或次序,对节点各访问一次而且仅一次,二叉树的这类访问称为遍历。

一、递归式遍历

       按照惯例左兄弟优先于右兄弟,若将节点及孩子分别记作V,L和R,则有VLR,LVR和LRV三种访问次序。分别称为先序遍历,中序遍历和后序遍历。

先序遍历

template <typename T,typename VST>

//元素类型,操作器

void travPre_R(BinNodePosi(T) x, VST& visit)

{

  if(!x) return;

  visit(x->data);

  travPre_R(x->lChild,visit);

  travPre_R(x->rChild,visit);

}

遍历树x,若x为空树,则直接退出,效果相当于递归基。若x非空,则优先访问根节点x;然后以此深入左子树和右子树,递归地进行遍历。

中序遍历

不再赘述

后序遍历

template <typename T, typename VST>

void travPre_R(BinNodePosi(T) x, VST& visit)

{

  if (!x) return;

  travPre_R(x->lChild); travPre_R(x->rChild);

  visit(x->data);

}

二、层次遍历

       在层次遍历中,确定节点访问次序的原则为“先上后下,先左后右”。即先访问树根,左孩子、右孩子、左孩子的左孩子,左孩子的右孩子,右孩子的左孩子,右孩子的右孩子……。

       当然,有根性和有序性是层次遍历序列得以明确定义的基础。正因为确定了树根,各节点才拥有深度这一指标

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.dequeue(x->lChild);//左孩子入队
if (HasRChild(*x)) Q.dequeue(x->rChild);//右孩子入队
  }
}

2.4.7 Huffman编码

.4.7 Huffman编码

一、PFC及其编解码

       PFC编码树的构造:首先由每一字符分别地构造一棵单节点二叉树,并把它们视作一个森林。此后反复从森林中取出2棵树并将其合二为一。若字符集的个数记为|Σ|,即初始森林中有|Σ|棵树,则经过|Σ|-1次迭代后可以合成一个完整的PFC编码树。接收方通过在编码树中反复从根节点出发做相应的漫游,依次完成对信息文本中各字符的解码。

       同类编码解码算法统一测试入口如下:

#include”../BinTree/BinTree.h”//用二叉树实现PFC树
typedef BinTree<char> PFCTree;//PFC树
#include”../Vector/Vector.h”//用向量实现PFC森林
typedef Vector<PFCTree*> PFCForest;//PFC森林
#include”../Bitmap/Bitmap.h”//用位图结构实现二进制编码串
#include”../Skiplist/Skipliist.h”//引入Skiplist式词典结构实现
typedef Skiplist<char, char *> PFCTable;
//PFC编码表,词条格式为:(key=字符,value=编码串)
int main(int argc, char *argv[])//PFC编解码算法统一测试入口
{
  PFDForest* initForest();//初始化PFC森林
  PFCTree* tree=generateTree(forest); release(forest);//生成PFC编码树
  PFCTable* table=generateTable(tree);//将PFC编码树转化为编码表
  for (int i=1;i<argc;i++)//对于命令输入的每一个明文串
  {
Bitmap codeString;//二进制编码串
int n=encode(table,codeString,argv[i]);//生成编码
decode(tree,codeString,n);//利用编码树,对长度为n的二进制编码串进行解码
  }
  release(table);release(tree);return 0;
}

       这里使用向量实现PFC森林,其中各元素分别对应于一颗编码树,使用跳转表式词典结构实现编码表,其中的词条各以某一待编码字符为关键码,以对应的编码串为数据项。使用位图Bitmap实现各字符的二进制编码串。各功能部分的具体实现不再赘述。

       通过上述分析可知,字符x的编码长度rps(x)就是其对应叶节点的深度depth(v(x))。于是各字符的平均码长就是编码树T中各叶节点的平均深度ald(T),计算公式如下,其中|Σ| 是字符集中字符个数(树中叶节点)的数量。

                                                         

       同一字符集的所有编码方案中,平均编码长度最小者称为最优方案,对应的ald()也最小,也称最优二叉树编码,简称最优编码树。

       最优编码树的性质:首先,最优二叉编码树必为真二叉树,不含一度节点的二叉树称为真二叉树。二是叶节点位置的深度之差不能超过1。由这2个性质可知,最优编码树中的叶节点只能出现于最低2层,其特例就是完全二叉树。于是我们可以得到构造最优编码树的一般方法:创建一棵规模为2|Σ|-1 的完全二叉树T,再让Σ中的任意字符随机分配给T中的|Σ|个叶节点。

三、Huffman编码树

       上述最优编码树算法在实际应用中价值并不大,因为多数应用所涉及的字符集 中,个字符出现的频率极少相等或相近。这种情况下,应该考虑权值。本节取自博客,讲的比较简单,只能一定程度上帮助理解。具体的请参照书《数据结构C++语言版》。

       定义:给定n个权值作为n个叶子节点,若树的带权路径长度最小,则称这棵树为哈夫曼树。从根节点到L层节点的路径长度为L-1。节点的带权路径长度为:从根节点到该节点之间路径长度与该节点的权的乘积。树的带权路径长度规定为所有叶子节点带权路径长度之和

假设有n个权值,则构造出的哈夫曼树有n个叶子节点,n个权值分别设为w1,w2,w3……wn。 哈夫曼树的构造规则为:

①   将w1到wn看成是有n棵树的森林(每棵树只有一个节点)。

②   在森林中选择出根节点权值最小的2棵树进行合并,作为一颗新树的左右子树,且新树根节点的权值是左右子树根节点权值之和。

③   从森林中删去所选的2棵树,并将新树加入森林。

④   重复步骤2和3,直到森林中只剩一棵树,该树即为所求的哈夫曼树。

       哈夫曼编码树只是最优带权编码树中的一棵。

四、哈夫曼树的数据结构的选取与设计

struct HuffChar   //Huffman字符
{
  char ch;int weight;//字符,权重(频率)
  HuffChar(char c=’^’,int w=0):ch(c),weight(w){};//带默认参数的构造函数初始化表
  bool operator < (HuffChar const & hc) {return weight>hc.weight;}
  bool operator == (HuffChar const & hc) {return weight==hc.weight;}
}
#define HuffTree BinTree<HuffChar> //Huffman树由BinTree派生,节点类型为HuffChar
#include”../List/List.h”//用List实现
typedef List<HuffTree*> HuffForest;//Huffman森林
#include”../Bitmap/Bitmap.h”
typedef Bitmap HuffCode;//Huffman二进制编码
五、字符出现频率的统计
int* statistics(char* sample_text_file)//统计字符出现频率
{
  int* freq=new int[N_CHAR];//使用数组freq记录各字符出现次数
  memset(freq,0,sizeof(int) *N_CHAR);//清零
  FILE* fp=fopen(sample_text_file,”r”); char ch;
  while(0<fscanf(fp,”%c”,&ch))
if (ch>=0x20) freq[ch-0x20]++;//累计对应出现的次数
  fclose(fp);return freq;
}
六、编码
int encode(HuffTable* table,Bitmap* codeString,char* s)
//按编码表对Botmap串作Huffman编码
{
  int n=0;//待返回的编码串总长n
  for (size_t m=strlen(s),i=0;i<m;i++)//对于一个明文中的每个字符
  {
char **pCharCode=table->get(s[i]);//取出其对应的编码串
if (!pCharCode) pCharCode=table->get(s[i]+’A’-‘a’);//小写字母转大写字母
if (!pCharCode) pCharCode=table->get(‘ ‘);//无法识别的字符转为空格
printf(“%s”,*pCharCode);//输出当前字符编码
for (size_t_m=strlen(*pCharCode),j=0;j<m;j++)//将当前字符的编码接入编码串
  ‘1’==*(*pCharCode+j)? codeString->set(n++):codeString->clear(n++);
  }
  printf(“\n”);return n;
}

猜你喜欢

转载自blog.csdn.net/lao_tan/article/details/81054466