树与二叉树

1.相关概念

1.树的基本概念

1.空树:结点树为0的树

2.非空树:

1)有且仅有一个根节点

2)没有后继节点的节点称为“叶子结点”

3)有后继节点的节点称为“非叶子结点”

3.除了根节点外,任何一个节点都有且仅有一个前驱;每个节点可以有0个或多个后继

4.路径:只能从上往下;路径长度,路径经过的节点个数

2.节点之间的关系描述

1.节点关系

1)祖先节点

2)子孙节点

3)双亲节点(父节点)

4)孩子节点

5)兄弟节点

6)堂兄弟节点(在同一层)

2.属性

1)节点的层次(深度)——从上往下数

2)节点的高度——从下往上数

3)树的高度(深度)——总共多少层

4)节点的度——有几个孩子(分支)

5)树的度——各个节点的度的最大值

3.有序树&无序树

1.有序树——从逻辑上看,树中节点的各子树从左至右是有次序的,不能互换

2.无序树——逻辑上看,树中节点的各子树从左至右是无次序的,可以互换

4.森林

森林是m(m > 0)颗互不相交的树的集合

注意:森林和树的相互转化问题

2.树的常见性质

1.节点数 = 总度数 + 1

2.度为m的树和m叉树

图片说明

3.度为m的树第i层至多有m^(i - 1)个节点(i>=1)

图片说明

4.高度为h的m叉树至多有多少个节点

图片说明

图片说明

图片说明

3.二叉树

1.基本概念

图片说明

图片说明

2.满二叉树&完全二叉树

图片说明

3.二叉排序树

图片说明

4.平衡二叉树

左右子树的深度之差不能超过1

图片说明

5.二叉树的性质

图片说明

图片说明

图片说明

完全二叉树:

图片说明
图片说明
图片说明

6.树的存储结构

1.双亲表示法:每个节点中保存指向双亲的“指针”==>适合用顺序存储结构(数组),链式结构无法访问叶节点
图片说明

2.孩子表示法:顺序+链式:找孩子方便,找双亲不方便
图片说明

3.孩子兄弟表示法:链式,左指针指向做孩子,右指针指向兄弟

图片说明

7.树和二叉树的转化

图片说明

8.森林和二叉树的转换

将森林中的每棵树转换为二叉树,然后将每颗树的根节点当做兄弟节点

图片说明

二叉树转为森林,则右子树中的节点是每颗树的根节点

图片说明

9.树和森林的遍历

1.树的遍历

1)先根遍历

根节点->左子树先跟遍历->右子树先根遍历

树的先根遍历与树转化为二叉树后的先序遍历结果一致

2)后根遍历

左子树先跟遍历->右子树先根遍历->根节点

树的后根遍历序列与这棵树的相应二叉树的中序序列相同

3)层序遍历

和二叉树的层序遍历类似

2.森林的遍历

1)先序遍历:依次对每个树进行先序序列;与森林转化为二叉树的先序遍历结果一致

2)中序遍历:等同于依次对各个树进行后根遍历与森林转化为二叉树的中序遍历结果一致

3.总结

图片说明

4.二叉树的存储结构

1.顺序存储

#define MaxSize 100
struct TreeNode{
  ElemType value;        //节点中的数据元素
  bool isEmpty;        //节点是否为空
};
//定义一个长度为MaxSize的数组t,按照从上至下,从左至右的顺序依次存储完全二叉树中的各个节点
TreeNode t[MaxSize];

void Init(){
  for(int i = 0; i < MaxSize; ++i){
    t[i].isEmpty = true;
  }
}

图片说明

图片说明

2.链式存储

图片说明

代码:

struct ElemType{
  int value;
};

typedef struct BiTNode{
  ElemType data;
  struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

BiTree root = NULL;

//插入根节点
root = (BiTree)malloc(sizeof(BiTNode));
root->data = {1};
root->lchild = NULL;
root->rchild = NULL;
//新节点p做为根节点的左子树
BiTNode *p = (BiTNode*)malloc(sizeof(BiTNode));
p->data = {2};
p->lchild = NULL;
p->rchild = NULL;
root->lchild = p;

5.二叉树的遍历

遍历:按照某种次序访问所有节点

1.先序遍历

根左右

void PreOrder(BiTree T){
  if(T != NULL){
    visit(T);
    PreOrder(T->lchild);
    PreOrder(T->rchild);
  }
}

2.中序遍历

左根右

void InOrder(BiTree T){
  if(T != NULL){
    InOrder(T->lchild);
    visit(T);
    InOrder(T->rchild);
  }
}

3.后序遍历

左右根

void PostOrder(BiTree T){
  if(T != NULL){
    PostOrder(T->lchild);
    PostOrder(T->rchild);
    visit(T);
  }
}

4.遍历算法的应用

1.求树的深度

int treeDepth(BiTree T){
  if(T == NULL){
    return 0;
  }
  int l = treeDepth(T->lchild);
  int r = treeDepth(T->rchild);
  return l > r ? l + 1: r + 1;
}

5.层序遍历

设置辅助队列

void level(BiTree T){
  if(T == NULL){
    return;
  }
  queue<BiTree> q;
  q.push(T);
  while(!q.empty()){
    BiTree* p = q.top;
    q.pop();
    visit(p);
    if(p->lchild != NULL){
      q.push(p->lchild);
    }
    if(p->rchild != NULL){
      q.push(p->rchild);
    }
  }
}

6.构建二叉树

注意:如果只给出一颗二叉树的 前/中/后/层 序遍历中的一种,不能唯一确定一颗二叉树

以下三种可以唯一确定一个颗二叉树

1.前序 + 中序遍历序列

2.后序 + 中序遍历序列

3.层序 + 中序遍历序列

注意:层序遍历的特点是:先是根节点 -》左子树的根—》右子树的根

图片说明

6.线索二叉树

1.线索二叉树的作用

普通二叉树的遍历,只能从根节点开始,无法从某个节点开始遍历其他节点,找前驱、后继很不方便,遍历操作必须从根开始

线索二叉树:方便从一个指定节点出发,找到其前驱、后继;方便遍历

图片说明

2.中序线索化

二叉树的线索化:中序线索化、先序线索化、后序线索化

typedef struct ThreadNode{
  ElemType data;
  struct ThreadNode *lchild,*rchild;
  int ltag,rtag;
}ThreadNode,*ThreadTree;

//全局变量
ThreadNode *pre = NULL;

void CreateInThread(ThreadTree T){
  pre = NULLL;
  if(T != NULL){
    InThread(T);
    //修改最后一个节点的tag位
    if(pre->rchild == NULL){
      pre->rtag = 1;
    }
  }
}

//中序遍历二叉树,一边遍历一边线索化
void InThread(ThreadTree T){
  if(T != NULL){
    InThread(T->lchild);
    visit(T);
    InThread(T->rchild);
  }
}

//线索:q指针修改左指针,pre修改右指针
void visit(ThreadNode *q){
  //左指针为空,指向前驱q->lchild = pre
  if(q->lchild == NULL){
    q->lchild = pre;
    q->ltag = 1;
  }
  //右指针为空,指向后继pre->rchild = q
  if(pre != NULL && pre->rchild == NULL){
    pre->rchild = q;
    pre->rtag = 1;
  }
  pre = q;
}

3.先序线索化

//全局变量
ThreadNode *pre = NULL;

void CreateInThread(ThreadTree T){
  pre = NULLL;
  if(T != NULL){
    InThread(T);
    //修改最后一个节点的tag位
    if(pre->rchild == NULL){
      pre->rtag = 1;
    }
  }
}

//中序遍历二叉树,一边遍历一边线索化
void PreThread(ThreadTree T){
  if(T != NULL){
    visit(T);
    //只用ltag=0即左指针指向孩子时,才访问,否则不访问
    //不设置该条件会导致死循环
    if(T->ltah == 0)
        InThread(T->lchild);
    InThread(T->rchild);
  }
}

//线索:q指针修改左指针,pre修改右指针
void visit(ThreadNode *q){
  //左指针为空,指向前驱q->lchild = pre
  if(q->lchild == NULL){
    q->lchild = pre;
    q->ltag = 1;
  }
  //右指针为空,指向后继pre->rchild = q
  if(pre != NULL && pre->rchild == NULL){
    pre->rchild = q;
    pre->rtag = 1;
  }
  pre = q;
}

4.后序线索化

//全局变量
ThreadNode *pre = NULL;

void CreateInThread(ThreadTree T){
  pre = NULLL;
  if(T != NULL){
    InThread(T);
    //修改最后一个节点的tag位
    if(pre->rchild == NULL){
      pre->rtag = 1;
    }
  }
}

void PostThread(ThreadTree T){
  if(T != NULL){
    PostThread(T->lchild);
    PostThread(T->rchild);
    visit(T);
  }
}

void visit(ThreadNode *q){
  if(q->lchild == NULL){
    q->lchild = pre;
    q->ltag = 1;
  }
  if(pre != NULL && pre->rchild == NULL){
    pre->rchild = q;
    pre->rtag = 1;
  }
  pre = q;
}

注意:

1、线索化,最重要的是为了方便找当前节点的前驱和后继,因此在线索化过程中,如果发现当前指针q的左子树为空,则指向前驱节点,如果发现前驱节点pre的右子树为空,则pre的后继一定是q

2、线序需要在visit中特殊判断,否则会导致死循环

5.线索二叉树找前驱/后继

1、中序线索二叉树找前驱/后继

代码多看几遍!!!!

中序中,根节点后继一定是右子树中最左下的节点,根节点的前驱一定是左子树最右下的节点

//找到以p为根的子树中,第一个被中序遍历的节点
ThreadNode *FirstNode(ThreadNode *p){
  //没有被线索化
  while(p->ltag == 0) p = p->lchild;
  return p;
}
//在中序线索二叉树中找到p的后继节点
ThreadNode* NextNode(ThreadNode* p){
  //如果没有被线索化,右子树中最左下节点
  if(p->trag == 0) return FirstNode(p->rchild);
  else return p->rchild;
}

//找到以p为根的子树中,最后一个被中序遍历的节点
ThreadNode* LastNode(ThreadNode *p){
  while(p->rtag == 0) p = p->rchild;
  return p;
}

//在中序线索二叉树中找到p的前驱节点
ThreadNode* PreNode(ThreadNode *p){
  //如果没有被线索化,左子树中最右下的节点
  if(p->ltag == 0) return LastNode(p->lchild);
  else return p->lchild;
}

//对中序线索二叉树进行中序遍历,利用线索实现非递归算法
void Inorder(ThreadNode *T){
  for(ThreadNode* p = FirstNode(T); p != NULL; p = NextNode(p))
    visit(p);
}

//对中序线索二叉树进行逆中序遍历,利用线索实现非递归算法
void RevInorder(ThreadNode *T){
  for(ThreadNode *p = LastNode(T); p != NULL; p = PreNode(p))
    visit(p);
}

2、先序线索二叉树找前驱/后继

在先序中,左右子树中的节点只可能是根的后继不可能是前驱,因此是无法找到当前节点的前驱和后继的,只能用土办法从根节点开始遍历

如果二叉树中有父节点指针,且该父节点存在,则可以找到前驱:如果该节点是父节点的右孩子,则前驱是父节点的左子树中先序遍历的最后一个节点

图片说明

3、后序线索二叉树找前驱/后继

在后序遍历中,左右子树只可能是当前节点的前驱不可能是后继,除非从根节点开始遍历;

如果二叉树中有父节点指针,则可以找到后继:

图片说明

6.总结

图片说明

7.二叉排序树BST

1.定义

左子树节点值 < 根节点值 < 右子树节点值

进行中序遍历,可以得到一个递增的有序序列

typedef struct BSTNode{
  int key;
  struct BSTNode *lchild, *rchild;
}BSTNode, *BSTree;

2.二叉排序树的查找

//非递归版本  最坏空间复杂度 O(1)
BSTNode *BST_Search(BSTree T, int key){
  while(T != NULL && key != T->key){
    if(key < T->key){
      T = T->lchild);
    }
    else{
      T = T->rchild;
    }
  }
  return T;
}

//递归版本        最坏空间复杂度 O(N)
BSTNode *BST_Search2(BSTree T, int key){
  if(T == NULL){
    return NULL;
  }
  if(key == T->key){
    return T;
  }
  if(key < T->key){
    BST_Search2(T->lchild, key);
  }
  else{
    BST_Search2(T->rchild, key);
  }
}

查找效率分析

查找成功时的平均查找长度

ASL = 每层的层数 * 每层的节点数 加起来

查找失败时的平均查找长度:

ASL = 查找失败时指针可能停留的位置所在的层数 加起来 除以 可能停留位置的个数

图片说明

3.二叉排序树的插入

插入操作不能破坏平衡二叉树的性质;注意二叉排序树中不允许有两个值相等的节点

bool BST_Insert(BSTree T, int key){
  if(T == NULL){
    T = (BSTree)malloc(sizeof(BSTNode));
    T->key = key;
    T->lchild = NULL;
    T->rchild = NULL;
    return true;
  }
  else if(k == T->key){
    return false;            //值已经存在,插入失败
  }
  else if(key < T->key){
    BST_Insert(T->lchild, key);
  }
  else{
    BST_Insert(T->rchild, key);
  }
}

//构建一颗二叉排序树
str = [5,7,3,6,8,1,2];
BSTree T = NULL;

void Creat_BST(BSTree &T, int str[], int n){
  T = NULL;
  int i = 0;
  while(i < n){
    BST_Insert(T, str[i]);
    i++;
  }
}

4.二叉树的删除

//叶子节点:直接删除
//只有左子树:左子树替换要删除的节点
//只有右子树:右子树直接疾患要删除的节点
//既有左子树,又有右子树:
//            1)找后继:找到中序遍历的下一个节点,替换当前节点
//            2)找前驱:找到中序遍历的前一个节点,替换当前节点
//            替换后删除前驱或者后继

8.平衡二叉树AVL

1.定义

树上的任一节点的左子树和右子树的高度差不超过1

节点的平衡因子 = 左子树高 - 右子树高

2.插入操作

插入操作类似BST,但是为了保持平衡因子,所以需要调整

3.插入新节点后调整问题

调整策略 备注
LL 在A的左孩子的左子树中插入导致不平衡
RR 在A的右孩子的右子树中插入导致不平衡
LR 在A的左孩子的右子树中插入导致不平衡
RL 在A的右孩子的左子树中插入导致不平衡
LL

图片说明

RR

图片说明

LR:

图片说明

图片说明

RL
图片说明
图片说明

4.查找效率

最坏情况下,查找一个关键字最多需要对比n次,即查找操作的时间复杂度不可能超过O(h)
图片说明

9.哈夫曼树

1.结点的权

有某种显示含义的数值(如:表示节点的重要性)

2.节点带权路径长度

从树的根到该节点的路径长度(经过的边数)与该节点上权值乘积

图片说明

3.树的带权路径长度

树中所有叶节点的带权路径长度之和(WPL)

4.哈夫曼树

1.定义

在含有n个带权叶节点的二叉树中,其中带权路劲长度WPL最小的二叉树称为哈夫曼树,也称为最优二叉树

2.构造

图片说明

5.哈夫曼编码

可变长度编码——允许对不同字符用不等长的二进制位表示

没有一个编码另一个编码的前缀,则称这样的编码为前缀编码

注意:因为哈夫曼树不唯一,所以哈夫曼编码不唯一