数据结构之树 | 青训营笔记

136 阅读7分钟

今天在做项目的时候,碰到了与树有关的数据结构,因此前来温习:

关键词:树 二叉树 遍历二叉树 线索二叉树 排序二叉树 平衡二叉树 树的存储 森林 森林树与二叉树的转换 并查集 哈夫曼树

5.1 树:

1.定义:

n= 0 为空树

n>=1 有且只有一个根节点,且其余节点为互不相交的有限集合,且每一个集合本身又是一棵树

2.树的高度与深度区别:深度是从根节点向下,高度是从叶节点向上。

3.树的基本性质:(对照 二叉树的性质)

① 节点数 = 所以叶子节点的度数 +1

② 度数 为m的数在第 i层 至多有 mi−1mi−1

③ 高度 为h 的 m叉树至多 有(mh−1)(m−1)(mh−1)(m−1)个结点

5.2 二叉树

1.定义:

n=0,空二叉树

n>=1 ,有且只有一个根节点和两个互不相交的集合,分别为左子树和右子树

2.与度数为2的区别:

二叉树可以为空树,度数为2的树得至少3个结点

度数为2的树无严格的左右之分,比如只有一个孩子的树,二叉树必须确定是左孩子还是右孩子

3.满二叉树与完全二叉树

4.二叉树性质:

① 节点数 = 所以叶子节点的度数 +1

② 在第 i层 至多有 2i−12i−1

③ 高度 为h 的 2叉树至多 有(2h−1)(2h−1)个结点

④具有n个结点的完全二叉树 深度为 Llog2nJ+1Llog2nJ+1

5.二叉树的存储结构:

①顺序存储:自上而下,从左到右,适合完全二叉树与满二叉树

②二叉链表

③ 三叉链表(线索链表)

6.二叉树的应用:

采用了二分的思想,遍历、搜索、平衡二叉树,公式计算采用了后序遍历二叉树

5.3 遍历二叉树,线索二叉树

5.3.1 遍历二叉树

1.遍历的本质:将二叉树线性化的过程 ,转化成线性序列

2.先序中序后序遍历:根 左 右 左 根 右 左 右 根

:100: 代码的实现:

有递归与非递归两种算法,非递归实现的思路:

中序: 左中右

前序:中左右

后序:左右中

先序遍历建立二叉链表,计算二叉树深度,复制二叉树,统计结点个数:

后序遍历代码实现:p148 9

//p是结点S是栈
while(){
   
   if (p){
      S.push(p);
      p = p->lchild;
   }
   else {
      if(p->rchild &&  p->rchild != r){
         p = p->rchild ;
      }
      else {
          visit(p);
          r = S.pop();
          p = NULL ; 
      }
   }
}

3.由遍历序列 构造二叉树:

先序遍历和中序遍历 可以唯一确定一棵二叉树

后序遍历和中序遍历 也可以唯一确定二叉树

原因是什么? 对于每一棵树可以确定根节点,以及左子树与右子树,这样递归确定最终的二叉树

5.3.2 线索二叉树

实质:将二叉链表的空指针指向前驱或者后驱

结点的格式:

lchildLTagdataRTagrchild
左孩子指针是(1)否(0)左孩子为空节点数据是(1)否(0)右孩子为空右孩子指针

包括三种,前序中序后序线索化树 视频:www.bilibili.com/video/BV1D3…

:100: 代码实现:p136

void InTread ( &p , &pre){
    
    if(p!=NULL){
        //左
    	InTread(p->lchild,pre);
        //中
    	//设置前驱节点
    	if(p->lchild==NULL){
    		p->lchild = pre ;
    		p->ltag = 1 ;
    	}
    	//设置后继节点 pre!=NULL
    	if(pre!=NULL && pre->rchild==NULL){
    	     pre->rchild = p ;
    	     pre->rtag =1 ;
    	}
    	pre = p ;//p节点访问完毕,需要访问下一节点,那么pre需要跟上
        //右
    	InTread(p->rchild,pre);
    }


}
void InTread ( &p , &pre)

 if(p!=NULL)
    InTread(p->lchild,pre);
    if(p->lchild == NULL){
        p->lchild == pre ;
        p->ltag = 1 ;       
    }
 
  if(pre != NULL && pre->rchild ==NULL){
       //右指针的赋值是在下一个指针 时线索化,此时 该节点为pre
        pre -> rchild = p;
        pre -> rtag = 1 ;
            
    }
     pre = p ;   
     InTread(p->rchild,pre);


tag是用来区分是孩子还是前驱或者后缀

5.4 排序二叉树 与平衡二叉树

二叉排序树不是平衡二叉树,它的值满足: 左子树> 根>右子树

难点在于:二叉树的删除

有以下三种情况,最值得注意的是最后一种:

平衡二叉树:

是基于二叉排序树,删除操作与二叉排序树基本一样,不一样在最后一种情况,调整策略是看左右子树谁更高就用哪边的树来填补

平衡二叉树的插入:

 
// 定义平衡二叉树的结点结构
typedef struct BiTNode
{
    int data;
    int bf;             // 平衡因子
    BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

① LL调整:

A是* p ,B 是L,则用代码表示为:

void R_Rotate( BiTree *p )
{
    BiTree L;
    L = (*p)->lchild;
    (*p)->lchild = L->rchild;
    L->rchild = *p;
    *p = L;
}

② RR调整:

// 左旋处理,即RR调整
void L_Rotate( BiTree *p )
{
    BiTree R;
    R = (*p)->rchild;
    (*p)->rchild = R->lchild;
    R->lchild = *p;
    *p = R;
}

③ LR 调整:

说出这种调整过程:根据情况进行判断,将LR拆成LL,RR两个技术动作。先对左子树RR调整,后对T LL调整,调整过程如下:

// 左平衡旋转处理,包括 LL 和 LR 调整
void LeftBalance( BiTree *T )
{
    BiTree L, Lr;
    L = (*T)->lchild;
    switch( L->bf )
    {
    case 1:        // 新结点插入在T的左孩子的左子树上,为LL型,作右旋处理即LL调整
        (*T)->bf = L->bf = 0;
        R_Rotate( T );
        break;
    case -1:        // 新结点插入在T的左孩子的右子树上,为LR型,作双旋处理
        Lr = L->rchild;
        switch( Lr->bf )
        {
        case 1:
            (*T)->bf = -1;
            L->bf = 0;
            break;
        case 0:
            (*T)->bf = L->bf = 0;
            break;
        case -1:
            (*T)->bf = 0;
            L->bf = 1;
            break;
        }
        Lr->bf = 0;
        L_Rotate( &(*T)->lchild );      // 先对T的左子树进行左旋处理即RR调整
        R_Rotate( T );                  // 再对T进行右旋处理即LL调整
    }
}


④ RL调整:

对T的右子树进行RR调整,后对T LL调整

// 右平衡旋转处理,包括 RR 和 RL 调整
void RightBalance( BiTree *T )
{
    BiTree R, Rl;
    R = (*T)->rchild;
    switch( R->bf )
    {
    case -1:        // 新结点插入在T的右孩子的右子树上,为RR型,作左旋处理即RR调整
        (*T)->bf = R->bf = 0;
        L_Rotate( T );
        break;
    case 1:        // 新结点插入在T的右孩子的左子树上,为RL型,作双旋处理
        Rl = R->lchild;
        switch( Rl->bf )
        {
        case 1:
            (*T)->bf = 0;
            R->bf = -1;
            break;
        case 0:
            (*T)->bf = R->bf = 0;
            break;
        case -1:
            (*T)->bf = 1;
            R->bf = 0;
            break;
        }
        Rl->bf = 0;
        R_Rotate( &(*T)->rchild );      // 先对T的左子树进行左旋即RR调整
        L_Rotate( T );                  // 再对T进行右旋即LL调整
    }

}

删除的办法

5.5 树森林

5.6 树的存储

双亲表示法:给每一个结点标号,然后将孩子指向双亲

说明:需要连续的存储空间,能确定唯一的双亲,但确定孩子需要遍历整棵树

孩子表示法:给每一个结点标号,然后将孩子结点用单链表连接起来

说明:寻找子女很方便,但是双亲就很麻烦

孩子兄弟表示法:又称为二叉树表示法 左孩子右兄弟

每个节点包括:节点值,指向第一个节点的指针,指向下一个兄弟结点的指针

5.7 树的应用

5.7.1 哈夫曼树

将节点值排序,每一次选择当前最小的两个值合并,合并后的值也要与当前未选择的叶节点的值进行比较选择最小的两个值合并。(若存在值相同,则另外构造)

哈夫曼编码:左0右1

5.7.2 并查集

5.8 红黑树

是平衡二叉树的一种,zhuanlan.zhihu.com/p/370703859

红黑树的特性: (1)每个节点或者是黑色,或者是红色。 (2)根节点是黑色。 (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!] (4)如果一个节点是红色的,则它的子节点必须是黑色的。 (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

红黑树的应用比较广泛,主要是用它来存储有序的数据,一般用于内存的排序,时间复杂度较低,为O(logn)