数据结构中有很多树的结构,其中包括二叉树、二叉搜索树、2-3 树、红黑树等等。本文中对数据结构中常见的几种树的概念和用途进行了汇总,不求严格精准,但求简单易懂。

1. 二叉树

二叉树是数据结构中一种重要的数据结构,也是树表家族最为基础的结构。

二叉树的定义:二叉树的每个结点至多只有二棵子树 (不存在度大于 2 的结点),二叉树的子树有左右之分,次序不能颠倒。二叉树的第 i 层至多有 2i-1 个结点;深度为 k 的二叉树至多有 2k-1 个结点;对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则 n0=n2+1。

二叉树的示例

/assets/404.png

满二叉树和完全二叉树:

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。

满二叉树的性质:

1) 一颗树深度为 h,最大层数为 k,深度与最大层数相同,k=h;

1) 叶子数为 2h;

2) 第 k 层的结点数是:2k-1;

3) 总结点数是:2k-1,且总节点数一定是奇数。

完全二叉树:若设二叉树的深度为 h,除第 h 层外,其它各层 (1~(h-1) 层) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

注:完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra 算法、Prim 算法等都要用堆才能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。

/assets/404.png

二叉树的性质

1) 在非空二叉树中,第 i 层的结点总数不超过 2i-1, i>=1;

2) 深度为 h 的二叉树最多有 2h-1 个结点 (h>=1),最少有 h 个结点;

3) 对于任意一棵二叉树,如果其叶结点数为 N0,而度数为 2 的结点总数为 N2,则 N0=N2+1;

4) 具有 n 个结点的完全二叉树的深度为 log2(n+1);

5) 有 N 个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:

若 I 为结点编号则 如果 I>1,则其父结点的编号为 I/2;

如果 2I<=N,则其左儿子(即左子树的根结点)的编号为 2I;若 2I>N,则无左儿子;

如果 2I+1<=N,则其右儿子的结点编号为 2I+1;若 2I+1>N,则无右儿子。

6) 给定 N 个节点,能构成 h(N) 种不同的二叉树,其中 h(N) 为卡特兰数的第 N 项,h(n)=C(2*n, n)/(n+1)。

7) 设有 i 个枝点,I 为所有枝点的道路长度总和,J 为叶的道路长度总和 J=I+2i

2. 二叉查找树

二叉查找树定义:又称为是二叉排序树(Binary Sort Tree)或二叉搜索树。二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:

1) 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2) 若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;

3) 左、右子树也分别为二叉排序树;

4) 没有键值相等的节点。

二叉查找树的性质:对二叉查找树进行中序遍历,即可得到有序的数列。

二叉查找树的时间复杂度:它和二分查找一样,插入和查找的时间复杂度均为 O(logn),但是在最坏的情况下仍然会有 O(n) 的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡(比如,我们查找上图(b)中的 “93”,我们需要进行 n 次查找操作)。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是平衡查找树设计的初衷。**

**二叉查找树的高度决定了二叉查找树的查找效率。**

二叉查找树的插入过程如下:

1) 若当前的二叉查找树为空,则插入的元素为根节点;

2) 若插入的元素值小于根节点值,则将元素插入到左子树中;

3) 若插入的元素值不小于根节点值,则将元素插入到右子树中。

二叉查找树的删除,分三种情况进行处理:

1) p 为叶子节点,直接删除该节点,再修改其父节点的指针(注意分是根节点和不是根节点),如图 a;

2) p 为单支节点(即只有左子树或右子树)。让 p 的子树与 p 的父亲节点相连,删除 p 即可(注意分是根节点和不是根节点),如图 b;

3) p 的左子树和右子树均不空。找到 p 的后继 y,因为 y 一定没有左子树,所以可以删除 y,并让 y 的父亲节点成为 y 的右子树的父亲节点,并用 y 的值代替 p 的值;或者方法二是找到 p 的前驱 x,x 一定没有右子树,所以可以删除 x,并让 x 的父亲节点成为 y 的左子树的父亲节点。如图 c。

/assets/404.png

/assets/404.png/assets/404.png

二叉树相关实现源码:

插入操作:

struct node { int val; pnode lchild; pnode rchild; }; pnode BT = NULL; //递归方法插入节点 pnode insert(pnode root, int x) { pnode p = (pnode)malloc(LEN); p->val = x; p->lchild = NULL; p->rchild = NULL; if(root == NULL){ root = p; } else if(x < root->val){ root->lchild = insert(root->lchild, x); } else{ root->rchild = insert(root->rchild, x); } return root; } //非递归方法插入节点 void insert_BST(pnode q, int x) { pnode p = (pnode)malloc(LEN); p->val = x; p->lchild = NULL; p->rchild = NULL; if(q == NULL){ BT = p; return ; } while(q->lchild != p && q->rchild != p){ if(x < q->val){ if(q->lchild){ q = q->lchild; } else{ q->lchild = p; } } else{ if(q->rchild){ q = q->rchild; } else{ q->rchild = p; } } } return; }
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 struct node{    int val;    pnode lchild;    pnode rchild;}; pnode BT = NULL;  // 递归方法插入节点 pnode insert(pnode root, int x){    pnode p = (pnode)malloc(LEN);    p->val = x;    p->lchild = NULL;    p->rchild = NULL;    if(root == NULL){        root = p;        }        else if(x < root->val){        root->lchild = insert(root->lchild, x);        }    else{        root->rchild = insert(root->rchild, x);        }    return root;} // 非递归方法插入节点 void insert_BST(pnode q, int x){    pnode p = (pnode)malloc(LEN);    p->val = x;    p->lchild = NULL;    p->rchild = NULL;    if(q == NULL){        BT = p;        return ;        }            while(q->lchild != p && q->rchild != p){        if(x < q->val){            if(q->lchild){                q = q->lchild;                }                else{                q->lchild = p;            }                }            else{            if(q->rchild){                q = q->rchild;                }                else{                q->rchild = p;                }        }    }    return;}

删除操作:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677 bool delete_BST(pnode p, int x) // 返回一个标志,表示是否找到被删元素 {    bool find = false;    pnode q;    p = BT;    while(p && !find){  // 寻找被删元素         if(x == p->val){  // 找到被删元素             find = true;            }            else if(x < p->val){ // 沿左子树找             q = p;            p = p->lchild;            }        else{   // 沿右子树找             q = p;            p = p->rchild;            }    }    if(p == NULL){   // 没找到         cout « “没有找到” « x « endl;        }        if(p->lchild == NULL && p->rchild == NULL){  //p 为叶子节点         if(p == BT){  //p 为根节点             BT = NULL;            }        else if(q->lchild == p){               q->lchild = NULL;        }                else{            q->rchild = NULL;            }        free(p);  // 释放节点 p     }    else if(p->lchild == NULL   p->rchild == NULL){ //p 为单支子树         if(p == BT){  //p 为根节点             if(p->lchild == NULL){                BT = p->rchild;                }                else{                BT = p->lchild;                }        }            else{            if(q->lchild == p && p->lchild){ //p 是 q 的左子树且 p 有左子树                 q->lchild = p->lchild;    // 将 p 的左子树链接到 q 的左指针上             }                else if(q->lchild == p && p->rchild){                q->lchild = p->rchild;                }            else if(q->rchild == p && p->lchild){                q->rchild = p->lchild;                }            else{                q->rchild = p->rchild;            }        }        free(p);    }    else{ //p 的左右子树均不为空         pnode t = p;        pnode s = p->lchild;  // 从 p 的左子节点开始         while(s->rchild){  // 找到 p 的前驱,即 p 左子树中值最大的节点             t = s;               s = s->rchild;            }        p->val = s->val;   // 把节点 s 的值赋给 p         if(t == p){            p->lchild = s->lchild;            }            else{            t->rchild = s->lchild;            }        free(s);     }    return find;}

查找操作:

12345678910111213141516171819 pnode search_BST(pnode p, int x){    bool solve = false;    while(p && !solve){        if(x == p->val){            solve = true;            }            else if(x < p->val){            p = p->lchild;            }        else{            p = p->rchild;            }    }    if(p == NULL){        cout « “没有找到” « x « endl;        }     return p;}

3. 平衡二叉树

我们知道,对于一般的二叉搜索树(Binary Search Tree),其期望高度(即为一棵平衡树时)为 log2n,其各操作的时间复杂度 O(log2n) 同时也由此而决定。但是,在某些极端的情况下(如在插入的序列是有序的时),二叉搜索树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即 O(n)。我们可以通过随机化建立二叉搜索树来尽量的避免这种情况,但是在进行了多次的操作之后,由于在删除时,我们总是选择将待删除节点的后继代替它本身,这样就会造成总是右边的节点数目减少,以至于树向左偏沉。这同时也会造成树的平衡性受到破坏,提高它的操作的时间复杂度。于是就有了我们下边介绍的平衡二叉树。

平衡二叉树定义:平衡二叉树(Balanced Binary Tree)又被称为 AVL 树(有别于 AVL 算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用算法有红黑树、AVL 树等。在平衡二叉搜索树中,我们可以看到,其高度一般都良好地维持在 O(log2n),大大降低了操作的时间复杂度。

最小二叉平衡树的节点的公式如下:

F(n)=F(n-1)+F(n-2)+1

这个类似于一个递归的数列,可以参考 Fibonacci 数列,1 是根节点,F(n-1) 是左子树的节点数量,F(n-2) 是右子树的节点数量。

3.1 平衡查找树之 AVL 树

有关 AVL 树的具体实现,可以参考 C 小加的博客《一步一步写平衡二叉树(AVL)》

AVL 树定义:AVL 树是最先发明的自平衡二叉查找树。AVL 树得名于它的发明者 G.M. Adelson-Velsky 和 E.M. Landis,他们在 1962 年的论文 “An algorithm for the organization of information” 中发表了它。在 AVL 中任何节点的两个儿子子树的高度最大差别为 1,所以它也被称为高度平衡树,n 个结点的 AVL 树最大深度约 1.44log2n。查找、插入和删除在平均和最坏情况下都是 O(logn)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在 O(logN)。但是频繁旋转会使插入和删除牺牲掉 O(logN) 左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

AVL 树的自平衡操作——旋转:

AVL 树最关键的也是最难的一步操作就是旋转。旋转主要是为了实现 AVL 树在实施了插入和删除操作以后,树重新回到平衡的方法。下面我们重点研究一下 AVL 树的旋转。

对于一个平衡的节点,由于任意节点最多有两个儿子,因此高度不平衡时,此节点的两颗子树的高度差 2. 容易看出,这种不平衡出现在下面四种情况:

/assets/404.png

1) 6 节点的左子树 3 节点高度比右子树 7 节点大 2,左子树 3 节点的左子树 1 节点高度大于右子树 4 节点,这种情况成为左左。

2) 6 节点的左子树 2 节点高度比右子树 7 节点大 2,左子树 2 节点的左子树 1 节点高度小于右子树 4 节点,这种情况成为左右。

3) 2 节点的左子树 1 节点高度比右子树 5 节点小 2,右子树 5 节点的左子树 3 节点高度大于右子树 6 节点,这种情况成为右左。

4) 2 节点的左子树 1 节点高度比右子树 4 节点小 2,右子树 4 节点的左子树 3 节点高度小于右子树 6 节点,这种情况成为右右。

从图 2 中可以可以看出,1 和 4 两种情况是对称的,这两种情况的旋转算法是一致的,只需要经过一次旋转就可以达到目标,我们称之为单旋转。2 和 3 两种情况也是对称的,这两种情况的旋转算法也是一致的,需要进行两次旋转,我们称之为双旋转。

单旋转

单旋转是针对于左左和右右这两种情况的解决方案,这两种情况是对称的,只要解决了左左这种情况,右右就很好办了。图 3 是左左情况的解决方案,节点 k2 不满足平衡特性,因为它的左子树 k1 比右子树 Z 深 2 层,而且 k1 子树中,更深的一层的是 k1 的左子树 X 子树,所以属于左左情况。

/assets/404.png

为使树恢复平衡,我们把 k2 变成这棵树的根节点,因为 k2 大于 k1,把 k2 置于 k1 的右子树上,而原本在 k1 右子树的 Y 大于 k1,小于 k2,就把 Y 置于 k2 的左子树上,这样既满足了二叉查找树的性质,又满足了平衡二叉树的性质。

这样的操作只需要一部分指针改变,结果我们得到另外一颗二叉查找树,它是一棵 AVL 树,因为 X 向上一移动了一层,Y 还停留在原来的层面上,Z 向下移动了一层。整棵树的新高度和之前没有在左子树上插入的高度相同,插入操作使得 X 高度长高了。因此,由于这颗子树高度没有变化,所以通往根节点的路径就不需要继续旋转了。

双旋转

对于左右和右左这两种情况,单旋转不能使它达到一个平衡状态,要经过两次旋转。双旋转是针对于这两种情况的解决方案,同样的,这样两种情况也是对称的,只要解决了左右这种情况,右左就很好办了。图 4 是左右情况的解决方案,节点 k3 不满足平衡特性,因为它的左子树 k1 比右子树 Z 深 2 层,而且 k1 子树中,更深的一层的是 k1 的右子树 k2 子树,所以属于左右情况。

/assets/404.png

为使树恢复平衡,我们需要进行两步,第一步,把 k1 作为根,进行一次右右旋转,旋转之后就变成了左左情况,所以第二步再进行一次左左旋转,最后得到了一棵以 k2 为根的平衡二叉树。

AVL 树实现源码:

| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 | #define BLACK 1#define RED 0 using namespace std; class bst {private:     struct Node {        int value;        bool color;        Node *leftTree, *rightTree, *parent;         Node() {            color = RED;            leftTree = NULL;            rightTree = NULL;            parent = NULL;            value = 0;        }         Node* grandparent() {            if (parent == NULL) {                return NULL;            }            return parent->parent;        }         Node* uncle() {            if (grandparent() == NULL) {                return NULL;            }            if (parent == grandparent()->rightTree)                return grandparent()->leftTree;            else                return grandparent()->rightTree;        }         Node* sibling() {            if (parent->leftTree == this)                return parent->rightTree;            else                return parent->leftTree;        }    };     void rotate_right(Node *p) {        Node *gp = p->grandparent();        Node *fa = p->parent;        Node *y = p->rightTree;         fa->leftTree = y;         if (y != NIL)            y->parent = fa;        p->rightTree = fa;        fa->parent = p;         if (root == fa)            root = p;        p->parent = gp;         if (gp != NULL) {            if (gp->leftTree == fa)                gp->leftTree = p;            else                gp->rightTree = p;        }     }     void rotate_left(Node *p) {        if (p->parent == NULL) {            root = p;            return;        }        Node *gp = p->grandparent();        Node *fa = p->parent;        Node *y = p->leftTree;         fa->rightTree = y;         if (y != NIL)            y->parent = fa;        p->leftTree = fa;        fa->parent = p;         if (root == fa)            root = p;        p->parent = gp;         if (gp != NULL) {            if (gp->leftTree == fa)                gp->leftTree = p;            else                gp->rightTree = p;        }    }     void inorder(Node *p) {        if (p == NIL)            return;         if (p->leftTree)            inorder(p->leftTree);         cout << p->value << " ";                        if (p->rightTree)            inorder(p->rightTree);    }     string outputColor(bool color) {        return color ? "BLACK" : "RED";    }     Node* getSmallestChild(Node *p) {        if (p->leftTree == NIL)            return p;        return getSmallestChild(p->leftTree);    }     bool delete_child(Node *p, int data) {        if (p->value > data) {            if (p->leftTree == NIL) {                return false;            }            return delete_child(p->leftTree, data);        } else if (p->value < data) {            if (p->rightTree == NIL) {                return false;            }            return delete_child(p->rightTree, data);        } else if (p->value == data) {            if (p->rightTree == NIL) {                delete_one_child(p);                return true;            }            Node *smallest = getSmallestChild(p->rightTree);            swap(p->value, smallest->value);            delete_one_child(smallest);             return true;        }    }     void delete_one_child(Node *p) {        Node *child = p->leftTree == NIL ? p->rightTree : p->leftTree;        if (p->parent == NULL && p->leftTree == NIL && p->rightTree == NIL) {            p = NULL;            root = p;            return;        }                if (p->parent == NULL) {            delete  p;            child->parent = NULL;            root = child;            root->color = BLACK;            return;        }                if (p->parent->leftTree == p) {            p->parent->leftTree = child;        } else {            p->parent->rightTree = child;        }        child->parent = p->parent;         if (p->color == BLACK) {            if (child->color == RED) {                child->color = BLACK;            } else                delete_case(child);        }         delete p;    }     void delete_case(Node *p) {        if (p->parent == NULL) {            p->color = BLACK;            return;        }        if (p->sibling()->color == RED) {            p->parent->color = RED;            p->sibling()->color = BLACK;            if (p == p->parent->leftTree)                rotate_left(p->sibling());            else                rotate_right(p->sibling());        }        if (p->parent->color == BLACK && p->sibling()->color == BLACK                && p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) {            p->sibling()->color = RED;            delete_case(p->parent);        } else if (p->parent->color == RED && p->sibling()->color == BLACK                && p->sibling()->leftTree->color == BLACK && p->sibling()->rightTree->color == BLACK) {            p->sibling()->color = RED;            p->parent->color = BLACK;        } else {            if (p->sibling()->color == BLACK) {                if (p == p->parent->leftTree && p->sibling()->leftTree->color == RED                        && p->sibling()->rightTree->color == BLACK) {                    p->sibling()->color = RED;                    p->sibling()->leftTree->color = BLACK;                    rotate_right(p->sibling()->leftTree);                } else if (p == p->parent->rightTree && p->sibling()->leftTree->color == BLACK                        && p->sibling()->rightTree->color == RED) {                    p->sibling()->color = RED;                    p->sibling()->rightTree->color = BLACK;                    rotate_left(p->sibling()->rightTree);                }            }            p->sibling()->color = p->parent->color;            p->parent->color = BLACK;            if (p == p->parent->leftTree) {                p->sibling()->rightTree->color = BLACK;                rotate_left(p->sibling());            } else {                p->sibling()->leftTree->color = BLACK;                rotate_right(p->sibling());            }        }    }     void insert(Node *p, int data) {        if (p->value >= data) {            if (p->leftTree != NIL)                insert(p->leftTree, data);            else {                Node *tmp = new Node();                tmp->value = data;                tmp->leftTree = tmp->rightTree = NIL;                tmp->parent = p;                p->leftTree = tmp;                insert_case(tmp);            }        } else {            if (p->rightTree != NIL)                insert(p->rightTree, data);            else {                Node *tmp = new Node();                tmp->value = data;                tmp->leftTree = tmp->rightTree = NIL;                tmp->parent = p;                p->rightTree = tmp;                insert_case(tmp);            }        }    }     void insert_case(Node *p) {        if (p->parent == NULL) {            root = p;            p->color = BLACK;            return;        }        if (p->parent->color == RED) {            if (p->uncle()->color == RED) {                p->parent->color = p->uncle()->color = BLACK;                p->grandparent()->color = RED;                insert_case(p->grandparent());            } else {                if (p->parent->rightTree == p && p->grandparent()->leftTree == p->parent) {                    rotate_left(p);                    rotate_right(p);                    p->color = BLACK;                    p->leftTree->color = p->rightTree->color = RED;                } else if (p->parent->leftTree == p && p->grandparent()->rightTree == p->parent) {                    rotate_right(p);                    rotate_left(p);                    p->color = BLACK;                    p->leftTree->color = p->rightTree->color = RED;                } else if (p->parent->leftTree == p && p->grandparent()->leftTree == p->parent) {                    p->parent->color = BLACK;                    p->grandparent()->color = RED;                    rotate_right(p->parent);                } else if (p->parent->rightTree == p && p->grandparent()->rightTree == p->parent) {                    p->parent->color = BLACK;                    p->grandparent()->color = RED;                    rotate_left(p->parent);                }            }        }    }     void DeleteTree(Node *p) {        if (!p || p == NIL) {            return;        }        DeleteTree(p->leftTree);        DeleteTree(p->rightTree);        delete p;    }public:     bst() {        NIL = new Node();        NIL->color = BLACK;        root = NULL;    }     ~bst() {        if (root)            DeleteTree(root);        delete NIL;    }     void inorder() {        if (root == NULL)            return;        inorder(root);        cout << endl;    }     void insert(int x) {        if (root == NULL) {            root = new Node();            root->color = BLACK;            root->leftTree = root->rightTree = NIL;            root->value = x;        } else {            insert(root, x);        }    }     bool delete_value(int data) {        return delete_child(root, data);    }private:    Node *root, *NIL;}; | # 4\. B 树 B 树也是一种用于查找的平衡树,但是它不是二叉树。 **B 树的定义:**B 树(B-tree)是一种树状数据结构,能够用来存储排序后的数据。这种数据结构能够让查找数据、循序存取、插入数据及删除的动作,都在对数时间内完成。B 树,概括来说是一个一般化的二叉查找树,可以拥有多于 2 个子节点。与自平衡二叉查找树不同,B - 树为系统最优化大块数据的读和写操作。B-tree 算法减少定位记录时所经历的中间过程,从而加快存取速度。这种数据结构常被应用在数据库和文件系统的实作上。 在 B 树中查找给定关键字的方法是,首先把根结点取来,在根结点所包含的关键字 K1,…,Kn 查找给定的关键字(可用顺序查找或二分查找法),若找到等于给定值的关键字,则查找成功;否则,一定可以确定要查找的关键字在 Ki 与 Ki+1 之间,Pi 为指向子树根节点的指针,此时取指针 Pi 所指的结点继续查找,直至找到,或指针 Pi 为空时查找失败。 B 树作为一种多路搜索树(并不是二叉的): 1) 定义任意非叶子结点最多只有 M 个儿子;且 M>2; 2) 根结点的儿子数为 [2, M]; 3) 除根结点以外的非叶子结点的儿子数为 [M/2, M]; 4) 每个结点存放至少 M/2-1(取上整)和至多 M-1 个关键字;(至少 2 个关键字) 5) 非叶子结点的关键字个数 = 指向儿子的指针个数 - 1; 6) 非叶子结点的关键字:K[1], K[2], …, K[M-1];且 K[i] < K[i+1]; 7) 非叶子结点的指针:P[1], P[2], …, P[M];其中 P[1]指向关键字小于 K[1]的子树,P[M]指向关键字大于 K[M-1]的子树,其它 P[i]指向关键字属于 (K[i-1], K[i]) 的子树; 8) 所有叶子结点位于同一层; 如下图为一个 M=3 的 B 树示例: [![/assets/404.png](/assets/28995560.jpeg)](http://jbcdn2.b0.upaiyun.com/2017/07/0178191b698ab75a98fa1d0bb03cc51f.jpg) B 树创建的示意图: ![/assets/404.png](/assets/28995563.jpeg) # 5\. B + 树 B + 树是 B 树的变体,也是一种多路搜索树: 1) 其定义基本与 B - 树相同,除了: 2) 非叶子结点的子树指针与关键字个数相同; 3) 非叶子结点的子树指针 P[i],指向关键字值属于 [K[i], K[i+1]) 的子树(B - 树是开区间); 4) 为所有叶子结点增加一个链指针; 5) 所有关键字都在叶子结点出现; 下图为 M=3 的 B + 树的示意图: [![/assets/404.png](/assets/28995565.jpeg)](http://jbcdn2.b0.upaiyun.com/2017/07/0972ef809f286cc29cd2d94687b2ef2d.jpg) B + 树的搜索与 B 树也基本相同,区别是 B + 树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找; **B + 的性质:** 1\. 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的; 2\. 不可能在非叶子结点命中; 3\. 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层; 4\. 更适合文件索引系统。 下面为一个 B + 树创建的示意图: ![/assets/404.png](/assets/28995568.jpeg) # 6\. B * 树 B * 树是 B + 树的变体,在 B + 树的非根和非叶子结点再增加指向兄弟的指针,将结点的最低利用率从 1/2 提高到 2/3。 B * 树如下图所示: [![/assets/404.png](/assets/28995569.jpeg)](http://jbcdn2.b0.upaiyun.com/2017/07/eb5835f421e029240105ccb8e80279ee.jpg) B * 树定义了非叶子结点关键字个数至少为 (2/3)*M,即块的最低使用率为 2/3(代替 B + 树的 1/2); B + 树的分裂:当一个结点满时,分配一个新的结点,并将原结点中 1/2 的数据复制到新结点,最后在父结点中增加新结点的指针;B + 树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针; B * 树的分裂:当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制 1/3 的数据到新结点,最后在父结点增加新结点的指针; 所以,B * 树分配新结点的概率比 B + 树要低,空间使用率更高。 # 7\. Trie 树 Tire 树称为字典树,又称单词查找树,Trie 树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。**它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。** **Tire 树的三个基本性质:** 1) 根节点不包含字符,除根节点外每一个节点都只包含一个字符; 2) 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 3) 每个节点的所有子节点包含的字符都不相同。 **Tire 树的应用:** 1) 串的快速检索 给出 N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。在这道题中,我们可以用数组枚举,用哈希,用字典树,先把熟词建一棵树,然后读入文章进行比较,这种方法效率是比较高的。 2) “串” 排序 给定 N 个互不相同的仅由一个单词构成的英文名,让你将他们按字典序从小到大输出。用字典树进行排序,采用数组的方式创建字典树,这棵树的每个结点的所有儿子很显然地按照其字母大小排序。对这棵树进行先序遍历即可。 3) 最长公共前缀 对所有串建立字典树,对于两个串的最长公共前缀的长度即他们所在的结点的公共祖先个数,于是,问题就转化为求公共祖先的问题。