【数据结构与算法】第四章:树

【数据结构与算法】第四章:树
第四章:树
4.1 预备知识算法

一棵树是一些节点的集合.一棵树是 $ N $ 个节点和 $ N-1 $ 条边的结合,其中一个节点叫作根.
【数据结构与算法】第四章:树express

  • 空树:这个集合是空集.数据结构

  • 这个集合非空时,一颗树由 根(root) 节点 $ r $ 以及0个或者多个非空的子树 $ T1, T2, ..., T_k $ 组成的.这些子树中的每一棵的根都被来自 $ r $的一条有向的边(edge)所链接.ide

  • 儿子(child):每一颗子树的根称做根 $ r $ 的儿子.spa

  • 父亲(child):$ r $ 是每一颗子树的根的父亲.除去根节点外,每一个节点都有且仅有一个父亲.操作系统

  • 举例
    【数据结构与算法】第四章:树
  • 在上图的树中,节点 $ A $ 是根节点.设计

  • 节点 $ F $ 由一个父亲 $ A $ 和三个儿子 $ K,L,M $.3d

  • 树叶(leaf):没有儿子的节点.例如上图中 $ B,C,H,I,P,Q,K,L,M,N $.指针

  • 兄弟(sibling):就具备相同父亲的节点.例如上图中 $ K,L,M $.code

  • 相似地,能够定义祖父(grandparent)和孙子(grandchild).

  • 路径(path):从节点 $ n1 到 nk $ 的路径定义为节点$ n1, n2, ..., nk $ 的一个序列,其中节点 $ ni $ 是节点 $ n_{i+1} $的父亲, $ 1 \le i < k $ .

  • 路径的长度(length):该边路径上的边的条数,即 $ k-1 $.对于每一个节点自己而言,它有一条到本身自己的长为 0 的路径.注意一棵树从根到每一个节点仅有一条路径.

  • 深度(depth):节点 $ ni $ 的深度为从根节点到 $ ni $ 的惟一路径的长.根节点的深度为 0 .

  • 高(height):从 $ n_i $ 获得一片树叶的最长路径的长度.全部的树叶的高都是 0 ;一条树的高等于根节点的高;

  • 若是存在一条 $ n1 $ 到 $ n2 $ 的路径,那么 $ n1 $ 是 $ n2 $的一位 祖先(ancestor) , $ n2 $ 是 $ n1 $ 的一个 后裔(descendant) .若是$ n1 \neq n2 $ 那么 $ n1 $ 是 $ n2 $的一位 真祖先(proper ancestor),$ n2 $ 是 $ n1 $ 的一个 真后裔(proper descendant).

4.1.1 树的实现
对于一颗通常的树,咱们采起第一儿子/下一兄弟表示法,其节点声明以下:
则对于4.1举例中的一颗树,咱们能够表示为

【数据结构与算法】第四章:树
4.1.1 树的遍历与应用
遍历见二叉树部分. 应用:操做系统的目录结构.
4.2 二叉树

typedefstructTreeNode*PtrToNode;
structTreeNode{
ElementTypeElement;
PtrToNodeFirstChild;
PtrToNodeNextSibling;
}
  • 二叉树:一棵树,其中每一个节点的儿子数最多两个.

【数据结构与算法】第四章:树

其中 $ TL $ 和 $ TR $ 都可能为空 .

4.2.1 二叉树的实现
涉及到树时,咱们不明显地使用NULL指针,由于具备 N 个节点的二叉树都有须要 N + 1 个NULL指针.

typedef struct TreeNode *PtrToNode;
typedef struct PtrToNode Tree;
struct Node{
    ElementType Element;
    Tree Left;
    Tree Right;
};

4.2.2 二叉树的遍历与应用

  • 先序遍历 过程:1. 先访问根节点;2. 先序遍历左子树; 3. 先序遍历右子树. 

【数据结构与算法】第四章:树

void PreOrderTraversal( BinTree BT){
    if( BT){
        printf("%d", BT->Element);
        PreOrderTraversal( BT->Left);
        PreOrderTraversal( BT->Right);
    }
}
  • 中序遍历 过程:1. 中序遍历左子树;2. 访问根节点; 3. 中序遍历右子树. 

【数据结构与算法】第四章:树

void InOrderTraversal( BinTree BT){
    if( BT){
        InOrderTraversal( BT->Left);
        printf("%d", BT->Element);
        InOrderTraversal( BT->Right);
    }
}
  • 后序遍历 过程:1. 后序遍历左子树;2. 后序遍历右子树; 3.访问根节点. 

【数据结构与算法】第四章:树

void PostOrderTraversal( BinTree BT){
   if( BT){
       PostOrderTraversal( BT->Left);
       PostOrderTraversal( BT->Right);
       printf("%d", BT->Element);
   }
}

三种遍历方式,通过的路线相同,只是访问时刻不一样.

  • 还有使用堆栈实现的非递归先序/中序/后序遍历,使用队列实现的层次遍历.

  • 应用

二叉树的遍历主要用途之一时编译器的设计领域.如下为一颗表达式树(expression tree).

【数据结构与算法】第四章:树

  • 先序遍历结果: + + a b c + * d e f g

  • 中序遍历结果: a + b c + d e + f * g

  • 后序遍历结果: a b c + d e f + g * +

  • 构造一颗表达式树对于后缀表达式,若是符号是操做数,那么咱们建立一个单节点树并将一个指向它的指针压入堆栈中;若是符号是操做符,那么咱们从栈中弹出指向两棵树的指针并造成一个新的树. 输入为 $ a b + c d e + $

  • a b 两个符号是操做数,咱们能够建立两颗单节点树并将指向它们的指针压入堆栈中.

【数据结构与算法】第四章:树

  • “ + ” 被读入,两个字符造成的指针被弹出,造成一颗新的树.

【数据结构与算法】第四章:树

  • c d e 被读入

【数据结构与算法】第四章:树

  • “ + ” 被读入

【数据结构与算法】第四章:树

  • “ * ” 被读入

【数据结构与算法】第四章:树

最后一个 “ * ” 被读入

【数据结构与算法】第四章:树
4.3 查找树ADT——二叉查找树
查找是二叉树应用中一个重要的内容.如今咱们假设树中的每个节点都被指定一个关键字值,为方便起见,咱们假定这个值是整数而且全部的值互异.

  • 二叉查找树:对于树中的每个节点 $ X $ ,它的左子树的全部关键字的值小于 $ X $ ,它的右子树的全部关键字的值大于 $ X $.

【数据结构与算法】第四章:树

4.3.0 二叉查找树的声明

struct TreeNode;
typedef struct TreeNode *Position;
typedef struct TreeNode *SearchTree;
struct TreeNode{
    ElementType Element;
    SearchTree Left;
    SearchTree Right;
};

4.3.1 MakeEmpty

SearchTree MakeEmpty( SearchTree T){
    if( T != NULL){
        MakeEmpty( T->Left);
        MakeEmpty( T->Right);
        free( T);
    }
    return NULL;
}

4.3.2 Find

//返回具备关键字X节点的指针
//若是节点不存在就返回NULL
Position Find( ElementType X, SearchTree T){
    if( T == NULL)
        return NULL;
    if( X > T->Element )
        return Find( X, T->Right);
    else if( X < T->Element )
        return Find( X, T->Left);
    else if return T;
}

4.3.3 FindMin 和 FindMax

//FindMin 递归实现
Position FindMin( SearchTree T){
    if( T == NULL)
        return NULL;
    else if( T->Left == NULL)
        return T;
    else 
        return FindMin( T->Left);
}
//FindMax 非递归实现
Position FindMax( SearchTree T){
    if( T != NULL)
        while( T->Right != NULL)
            T = T->Right;
    return T;
}

4.3.4 Insert

SearchTree Insert( ElementType X, SearchTree T){
    if( T == NULL){
        T = ( SearchTree)malloc( sizeof(struct TreeNode));
        if( T == NULL)
            FatalError(" Out of space");
        else{
            T->ELement = X;
            T->Left = T->Right = NULL;
        }
    }
    else if( X < T->Element )
        T->Left = Insert( X, T->Left);
    else if( X > T->Element )
        T->Right = Insert( X, T->Righ

4.3.5 Delete
删除是最困难的操做,考虑如下几种状况.

  • 删除树叶节点,直接删除便可.

  • 删除的节点有一个儿子,将该节点的父亲节点指向这个节点的儿子.以下图所示

【数据结构与算法】第四章:树

  • 删除的节点有两个儿子.通常的策略是找到右子树中最小的一个数据代替须要删除的节点,并同时递归地删除这个最小数据的节点.考虑到右子树中的那个最小的节点不可能含有左儿子,所以删除很容易实现.以下图所示.

【数据结构与算法】第四章:树

SearchTree Delete( ElementType X, SearchTree T){
    Position TmpCell;
    if( T == NULL)
        Error(" Element not found");
    else if( X < T->Element)
        T->Left = Delete( X, T->Left);
    else if( X > T->Element )
        T->Right = Delete( X, T->Right);
    //定位ok
    else if ( T->Right && T->Left){ //删除含有两个儿子的节点
        TmpCell = FindMin( T->Right);
        T->Element = TmpCell->Element;
        T->Right = Delete( TmpCell->Element, T->Right);
    }
    else{                           //删除含有一个儿子或者没有儿子的节点
         TmpCell = T;
         if( T->Left == NULL)
            T = T->Right;
         else if( T->Right == NULL)
            T = T->Left;
        free( TmpCell);
    }
    return T;
}
  • 懒惰删除法:对于将要被删除的数据,咱们只是作一个被删除的记号.
    4.3.6 平均情形分析
  • 内部路径长(internal path length):一颗树全部节点的深度的和. 对于一颗树而言,设 $ D(N) $ 是具备 $ N $个节点的某棵树 $ T $ 的内部路径长,则 $ D(1) = 0 $,假设 $ i $ 为左子树的节点个数,则有 $ D(N) = D(i) + D(N-i-1) + N - 1 $. 若是全部子树的大小都等可能的出现,这个对于二叉查找树而言是成立的,但仍是对于二叉树不成立,那么咱们能够获得 $$ D(N) = \frac{2}{N}[\sum_{j=0}^{N-1}D(j)] + N - 1 $$ 求解获得 $ D(N) = O(N Log N) $ ,所以对于任意一个节点,它的指望深度为 $ O(N Log N) $.
    4.4 AVL平衡树

  • AVL(Adelson-Velskii and Landis):带有平衡条件的二叉查找树.

  • 平衡因子(BF,Balance Factor):左子树的高度 $ hl $,右子树的高度 $ hr $.

  • 平衡条件:左子树与右子树的高度相差的绝对值不大于1.$ | hl - hr | \le 1 $

  • 空树的高度通常定义为-1.所以空树也是AVL树.

【数据结构与算法】第四章:树

  • AVL树的旋转

对于AVL树,插入操做可能致使平衡性改变(假设删除操做为懒惰删除),因此在插入操做以后,咱们须要进行旋转来恢复AVL树的特性. 在插入以后,只有那些从插入点到根节点的路径上的节点的平衡性可能被改变,由于只有这些节点的子树可能发生变化.咱们只须要旋转高度最小的失去平衡的节点.

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树
假设必须从新平衡的节点叫作 $ \alpha $ .考虑到任意节点最多只有两个儿子,所以当高度出现不平衡的时候, $ \alpha $ 的两棵子树的高度差为2. 一共有上图中的四种状况:

  1. 对于 $ \alpha $ 的左儿子的左子树进行一次插入;解决办法:LL旋转.
  2. 对于 $ \alpha $ 的左儿子的右子树进行一次插入;解决办法:LR旋转.
  3. 对于 $ \alpha $ 的右儿子的左子树进行一次插入;解决办法:RL旋转.
  4. 对于 $ \alpha $ 的右儿子的右子树进行一次插入.解决办法:RR旋转.
    4.4.1 单旋转
    • 如何解决情形1
    • 以下图所示,旋转前的图在左边,旋转后的图在右边.其中虚线表示树的各层的深度.

【数据结构与算法】第四章:树

节点 $ k2 $ 不知足AVL树的平衡特性,子树 $ X $ 比子树 $ Z $ 深两层.为使树恢复平衡,咱们要把 $ X $ 上移一层,并同时把 $ Z $ 下移一层. 方法:抓住 $ k1 $ 节点,让它成为根节点,考虑到二叉搜索树的性质, $ k2 $ 成为了 $ k1 $ 的右儿子, $ X $ 和 $ Z $ 依旧分别是 $ k1 $ 的左儿子和 $ k2 $ 的右儿子.子树 $ Y $ 包含原树中介于 $ k1 $ 和 $ k2 $ 之间的那些节点,能够放置到新树 $ k_2 $ 的左儿子的位置上. 这样,全部对位置的要求获得知足.

  • 如何解决情形4
    情形4与情形1是一种对称的状况.

【数据结构与算法】第四章:树

4.4.2 双旋转

  • 如何解决情形2

【数据结构与算法】第四章:树

在这其中状况下,单旋转并不能解决平衡遭到破坏的问题,缘由在与子树 $ Y $ 过深.另外一种解决方案以下.

【数据结构与算法】第四章:树

咱们假设子树 $ Y $ 具备一个根 $ k2 $ 和两个子树 $ B $ 和 $ C $. 为了平衡 $ k3 $ 不能做根,考虑到 $ k3 $ 和 $ k1 $ 之间的旋转不能解决问题,所以咱们须要将 $ k2 $ 做为新的根,这就使得 $ k1 $ 成为它的左儿子,$ k_3 $ 成为它的右儿子,此时彻底肯定了这四棵树的最终位置.

  • 如何解决情形3

【数据结构与算法】第四章:树

4.4.3 AVL插入旋转的实现

static int Height( Position P){
   if( P == NULL)
       return -1;
   else
       return P->Height;
}
AvlTree Insert( ElementType X, AvlTree T){
   if( T == NULL){
       T = ( AvlTree)malloc( sizeof( struct AvlNode));
       if( T == NULL)
           FatalError(" Out of space!");
       else{
           T->Element = X;
           T->Height = 0;
           T->Left = T->Right = NULL;
       }
   }
   else if( X < T->Element){
       T->Left = Insert( X, Left);
       if( Height( T->Left) - Height( T->Right) == 2)
           if( X < T->Left->Element)
               T = SingleRotateWithLeft( T);
           else
               T = DoubleRotateWithLeft( T);
   }
   else if( X > T->Element){
       T->Right = Insert( X, Right);
       if( Height( T->Left) - Height( T->Right) == 2)
           if( X > T->Right->Element)
               T = SingleRotateWithLeft( T);
           else
               T = DoubleRotateWithLeft( T);
   }
   T->Height = Max( Height( T->Left), Height( T->Right)) + 1;
   return T;
}
static Position SingleRotateWithLeft( Position K2){
   Position K1;
   K1 = K2->Left;
   K2->Left = K1->Right;
   K1->Right = K2;
   K2->Height = Max( Height( K2->Left), Height( K2->Right)) + 1;
   K1->Height = Max( Height( K1->Left),  K2->Height) + 1;
   return K1;
}
static Position DoubleRotateWithLeft( Position K3){
   K3->Left = SingleRotateWithLeft( K3->Left);
   return SingleRotateWithLeft( K3);
}

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树

4.5 伸展树

* 伸展树(splay tree):保证从空树开始任意连续 $ M $ 次对树的操做最多花费 $ O(MlogN) $ 时间.

  • 伸展树的基本思想:当一个节点被访问后,它就要通过一系列AVL树的旋转被放到根上.

4.5.1 一个简单的想法
对于下面的树,咱们考虑对 $ k1 $ 进行一次 Find 操做以后发生的状况.

【数据结构与算法】第四章:树

其中虚线是访问的路径.首先咱们在 $ k1 $ 和它的父节点之间进行一次单旋转.获得如下结果

【数据结构与算法】第四章:树

而后再次事项3次旋转达到树根

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树

这个想法并非很好,由于虽然 $ k1 $ 的访问获得了简化,但同时 $ k3 $ 的访问却与 $ k_1 $ 以前的深度是同样的.

4.5.2 展开

  • 展开(Splaying):它的思路与相似与旋转,不过稍微有些改动.咱们依然从底部向上沿着访问路径旋转,另 $ X $ 是访问路径上的一个非根节点,咱们将在这个路径上施行旋转操做.

这条路径上的最后一个旋转:若是 $ X $ 的父节点是树根,那么咱们只须要旋转树根和 $ X $.
其余状况:存在两种状况以及对称的情形须要考虑.假设 $ X $ 有父亲 $ P $ 和祖父 $ G $.
第一种状况:之字形(zig-zag), $ X $ 是右儿子的形式, $ P $ 是左儿子的形式(反之亦然).这种状况下,咱们执行一次相似于AVL树的双旋转.

【数据结构与算法】第四章:树

第二种状况:一字形(zig-zig), $ X $ 与 $ P $ 是左儿子或者右儿子.这种状况下,咱们执行以下图所示的旋转.

【数据结构与算法】第四章:树

4.6 B-树

  • B-树(B-tree):一种不是二叉树的经常使用查找树.

  • 阶为M的B-树的特征
  1. 树的根或者是一片树叶,或者其儿子数介于 2 和 M 之间.

  2. 除根意外,全部非树叶节点在儿子树介于 $ \lceil \frac{M}{2} \rceil $ 和 M 之间.

  3. 全部的树叶都在的相同的深度上.
  • 全部的数据都存储在树叶上.在每个内部节点上皆含有指向该节点各儿子的指针 $ P1 $, $ P2 $,...,$ PM $ 和分别储存在子树 $ P2 $,...,$ PM $ 中发现的最小关键字的值 $ k1 $, $ k2 $,...,$ k{m-1} $ .固然,其中可能有些指针是NULL,而对应的$ k_i $是未定义的.

  • 对于每个节点,其子树$ P1 $ 中全部关键字都小于子树 $ P2 $的关键字.

  • 树叶包含全部实际数据,这些数据或者是关键字自己,或者是指向含有这些关键字的记录的指针.

  • 以下图,4阶B-树的例子

【数据结构与算法】第四章:树

4阶B-树更流行的称呼是 2-3-4树.

下面咱们将经过2-3树的特殊状况来描述B-树的操做.

【数据结构与算法】第四章:树

咱们用椭圆画出内部节点(非树叶),每一个节点包含有两个数据.椭圆中的短横线 "-" 表示内部节点的第二个信息,它代表该节点只有两个儿子.
树叶用方框表示,框内含有关键字,这些关键字是有序的.

  1. 为了执行一次 Find 操做,咱们从根开始并依据要查找的关键字与储存在节点上的两个(也有多是一个)值之间的关系肯定最多三个方向中的一个方向.

  2. 为了执行一次 Insert 操做,咱们首秀按按照执行 Find 操做的步骤,当到达了一片树叶的时候,咱们就找到了要插入 X 的正确位置.例如,插入数字 18.

【数据结构与算法】第四章:树

不过,因为一片树叶只能容纳 2-3 个关键字,所以上面的作法并不老是可行的.若是咱们试图把数字 1 插入其中,咱们会发现 1 所属的节点已经满了.将这个数字 1 放入该节点使得这个树叶有四个关键字,这是不被容许的.解决办法即是,构造两个节点,每一个节点有两个关键字,同时调整他们父节点的信息,以下图.

【数据结构与算法】第四章:树
然而这个方法也不总仍是可行的.当咱们试图插入 19 时候,依据以上办法,咱们将获得以下所示的树.

【数据结构与算法】第四章:树

上图中的树的一个内部节点含有四个儿子,这是不被容许的,咱们只容许每一个节点最多含有 3 个儿子.解决办法很简单,咱们只须要将这个节点分为两个几点,每一个节点含有两个儿子便可.

【数据结构与算法】第四章:树

固然,以上方法可能会致使某个父节点拥有四个儿子,咱们只须要在通向根的路径上已知分下去,直到根节点处.例如,插入数字 28 :

【数据结构与算法】第四章:树

【数据结构与算法】第四章:树

固然,还有其余插入操做.

3.咱们能够经过查找要被删除的关键字并将它除去而完成删除操做.

相关文章
相关标签/搜索