【数据结构与算法】第四章:树
第四章:树
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 ;一条树的高等于根节点的高;
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 二叉树的遍历与应用
void PreOrderTraversal( BinTree BT){ if( BT){ printf("%d", BT->Element); PreOrderTraversal( BT->Left); PreOrderTraversal( BT->Right); } }
void InOrderTraversal( BinTree BT){ if( BT){ InOrderTraversal( BT->Left); printf("%d", BT->Element); InOrderTraversal( BT->Right); } }
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 + $
最后一个 “ * ” 被读入
4.3 查找树ADT——二叉查找树
查找是二叉树应用中一个重要的内容.如今咱们假设树中的每个节点都被指定一个关键字值,为方便起见,咱们假定这个值是整数而且全部的值互异.
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; }
内部路径长(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 $
对于AVL树,插入操做可能致使平衡性改变(假设删除操做为懒惰删除),因此在插入操做以后,咱们须要进行旋转来恢复AVL树的特性. 在插入以后,只有那些从插入点到根节点的路径上的节点的平衡性可能被改变,由于只有这些节点的子树可能发生变化.咱们只须要旋转高度最小的失去平衡的节点.
假设必须从新平衡的节点叫作 $ \alpha $ .考虑到任意节点最多只有两个儿子,所以当高度出现不平衡的时候, $ \alpha $ 的两棵子树的高度差为2. 一共有上图中的四种状况:
节点 $ k2 $ 不知足AVL树的平衡特性,子树 $ X $ 比子树 $ Z $ 深两层.为使树恢复平衡,咱们要把 $ X $ 上移一层,并同时把 $ Z $ 下移一层. 方法:抓住 $ k1 $ 节点,让它成为根节点,考虑到二叉搜索树的性质, $ k2 $ 成为了 $ k1 $ 的右儿子, $ X $ 和 $ Z $ 依旧分别是 $ k1 $ 的左儿子和 $ k2 $ 的右儿子.子树 $ Y $ 包含原树中介于 $ k1 $ 和 $ k2 $ 之间的那些节点,能够放置到新树 $ k_2 $ 的左儿子的位置上. 这样,全部对位置的要求获得知足.
4.4.2 双旋转
在这其中状况下,单旋转并不能解决平衡遭到破坏的问题,缘由在与子树 $ Y $ 过深.另外一种解决方案以下.
咱们假设子树 $ Y $ 具备一个根 $ k2 $ 和两个子树 $ B $ 和 $ C $. 为了平衡 $ k3 $ 不能做根,考虑到 $ k3 $ 和 $ k1 $ 之间的旋转不能解决问题,所以咱们须要将 $ k2 $ 做为新的根,这就使得 $ k1 $ 成为它的左儿子,$ k_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) $ 时间.
4.5.1 一个简单的想法
对于下面的树,咱们考虑对 $ k1 $ 进行一次 Find 操做以后发生的状况.
其中虚线是访问的路径.首先咱们在 $ k1 $ 和它的父节点之间进行一次单旋转.获得如下结果
而后再次事项3次旋转达到树根
这个想法并非很好,由于虽然 $ k1 $ 的访问获得了简化,但同时 $ k3 $ 的访问却与 $ k_1 $ 以前的深度是同样的.
4.5.2 展开
这条路径上的最后一个旋转:若是 $ X $ 的父节点是树根,那么咱们只须要旋转树根和 $ X $.
其余状况:存在两种状况以及对称的情形须要考虑.假设 $ X $ 有父亲 $ P $ 和祖父 $ G $.
第一种状况:之字形(zig-zag), $ X $ 是右儿子的形式, $ P $ 是左儿子的形式(反之亦然).这种状况下,咱们执行一次相似于AVL树的双旋转.
第二种状况:一字形(zig-zig), $ X $ 与 $ P $ 是左儿子或者右儿子.这种状况下,咱们执行以下图所示的旋转.
4.6 B-树
B-树(B-tree):一种不是二叉树的经常使用查找树.
树的根或者是一片树叶,或者其儿子数介于 2 和 M 之间.
除根意外,全部非树叶节点在儿子树介于 $ \lceil \frac{M}{2} \rceil $ 和 M 之间.
全部的数据都存储在树叶上.在每个内部节点上皆含有指向该节点各儿子的指针 $ P1 $, $ P2 $,...,$ PM $ 和分别储存在子树 $ P2 $,...,$ PM $ 中发现的最小关键字的值 $ k1 $, $ k2 $,...,$ k{m-1} $ .固然,其中可能有些指针是NULL,而对应的$ k_i $是未定义的.
对于每个节点,其子树$ P1 $ 中全部关键字都小于子树 $ P2 $的关键字.
树叶包含全部实际数据,这些数据或者是关键字自己,或者是指向含有这些关键字的记录的指针.
4阶B-树更流行的称呼是 2-3-4树.
下面咱们将经过2-3树的特殊状况来描述B-树的操做.
咱们用椭圆画出内部节点(非树叶),每一个节点包含有两个数据.椭圆中的短横线 "-" 表示内部节点的第二个信息,它代表该节点只有两个儿子.
树叶用方框表示,框内含有关键字,这些关键字是有序的.
为了执行一次 Find 操做,咱们从根开始并依据要查找的关键字与储存在节点上的两个(也有多是一个)值之间的关系肯定最多三个方向中的一个方向.
不过,因为一片树叶只能容纳 2-3 个关键字,所以上面的作法并不老是可行的.若是咱们试图把数字 1 插入其中,咱们会发现 1 所属的节点已经满了.将这个数字 1 放入该节点使得这个树叶有四个关键字,这是不被容许的.解决办法即是,构造两个节点,每一个节点有两个关键字,同时调整他们父节点的信息,以下图.
然而这个方法也不总仍是可行的.当咱们试图插入 19 时候,依据以上办法,咱们将获得以下所示的树.
上图中的树的一个内部节点含有四个儿子,这是不被容许的,咱们只容许每一个节点最多含有 3 个儿子.解决办法很简单,咱们只须要将这个节点分为两个几点,每一个节点含有两个儿子便可.
固然,以上方法可能会致使某个父节点拥有四个儿子,咱们只须要在通向根的路径上已知分下去,直到根节点处.例如,插入数字 28 :
固然,还有其余插入操做.
3.咱们能够经过查找要被删除的关键字并将它除去而完成删除操做.