【数据结构】(树总结)二叉树搜索/查找树

深入理解二叉查找树、B树和B+树 文章目录一、二叉查找树二、B树三、B+树 一、二叉查找树 二叉查找树,树中的任何节点的值大于它的左孩子,且小于它的右孩子。 任意节点的左、右子树也分别为二叉搜索树。 它的搜索时间复杂度为o(logn)。 但是在二叉查找树中,图下这种情况想要搜索值为3的节点,效率非常低。 为了解决二叉查找树的不平衡问题,2-3树孕育而生,2-3树能够很好的实现树的平衡。 2-3树每个节点都包含1个元素和2个孩子(... 阅读详情

 转载来源;https://blog.youkuaiyun.com/abc_12366/article/details/79428739#普通查找树bst

承接之前的『树』,本文将目标特别锁定在『查找树』;这里整理出我遇到的各种形式的查找树,以后可能会不定期更新,以尽可能多的囊括所有种类的查找树;虽然标题为“搜索树”,但是我还是习惯叫“查找树”,以下也将沿袭着一传统

学习『查找树』心里面始终要有一个意识:对于查找树而言『平衡』很重要



普通查找树(BST)

没有任何限制的查找树,只要满足每个结点的左结点比该结点小而右结点比该结点大即可;普通查找树是后面介绍的所有种类查找树(AVL树、伸展树、红黑树……)的超集,普通的查找树,它既可能是平衡的,也可能是不平衡的(平衡与否直接决定了程序执行的效率)——这里给出最坏情况(当所有元素构成单调数列时搜索效率最低,直接退化成单链表)

这里写图片描述

也就是说,如果树中插入的是随机数据则执行效果很好,但如果插入的是有序或者逆序的数据,那么二叉查找树的执行速度就变得很慢(所以才需要平衡)

查找树的初始化(返回根结点)


TreePtr BSTInit(ElementType E)
{
    TreePtr T = malloc(sizeof(TreeNode));
    if(T)
    {
        T->data = E;
        T->left = T->right = NULL;
        return T;
    }
    return NULL;
}

查找树的查找(线性下降,不需要遍历)

这里略作小结:如果要遍历一棵树我们可以采用三种方法(前、中、后),但是如果要遍历一棵查找树通常从树根开始查找(因为查找树的数据结点分布是高度有序的,所以这样效率最高也最简单)

TreeNode BinarySearch(ElementType E, TreePtr T)
{
    while(T)
    {
        if(E == T->data)
        {
            return *T;
        }
        else if(E > T->data)
        {
            T = T->right;
        }
        else{
            T = T->left;
        }
    }
    return NULL;
}

查找树的查找时间通常和它的高度、宽度(扁平度)有关,一棵树越趋于平衡(高度、宽度维持在特定值)查找就越快(也有例外,有的查找树在搜索前事先确定好各个结点的位置——这样的树是静态的,也就不存在平衡一说——典型的例子如哈夫曼树) 
一棵N结点查找树如果它是可变的(可以达到不同的平衡形态),那么理论上它的查找时间复杂度必然介于:O(logN)~O(N)之间

补充几点:

1)如果要按大小顺序查找,采用中序遍历(也很简单) 


如图,查找顺序就是 1->3->5->10->10.5->11->12->13->15->16->17->18->20

2)对结点进行的每次操作(查找、插入、删除、包括修改)既可能让原来平衡的树不在平衡,也可能幸运的继续保持平衡;如果仍保持平衡,有的树(比如AVL树)就不做任何改变,而另外有些树(比如伸展树)不管当前平衡状态而追求的是平均角度看的平衡,在每次操作后都会采取相应改变以调整树的构型——这一点也体现了不同种类树的区别 
附图: 


忽略最下面的“26”号……如果我们把它放到右边“65”的下面作为其右结点,可以预见这棵树仍然是平衡的(我们说在『插入』操作后仍然保持平衡)——而后是否调整重构(怎么调整)就体现了不同种类的查找树的不同特点——从这一点看,我们说正是平衡的调整方案确定了不同的树啊

3)用队列结构对二叉树进行层序遍历(C++实现):


#include <queue>

void LevelTraversal(BSPtr *ptr)
{
    queue<BSPtr*> rel;
    rel.push(ptr);
    while(!rel.isEmpty())
    {
        BSPtr *front = rel.front();
        printf("%d\n", front->data);
        rel.pop();
        if(front->left != null)
            rel.push(frontt->left);
        if(front->right != null)
            rel.push(front->right);
    }
}

另外,学习查找树除了会写代码还要会看图 
这里写图片描述 
这里写图片描述

以上,是两棵不同的二叉查找树!虽然从物理模型角度看它们是等价的,但是在计算机科学中必须要区别对待——对每个结点,画一条经过它的水平线,对于与它相邻的结点(用轻绳连接),如果在这条水平线上面就说它是这个结点的父结点,否则在下面就称为该结点的孩子结点

哈夫曼树(Huffman Tree)

哈夫曼让连接各个结点的路径带权,对于一组给定的数据,总存在唯一的让所有带权路径之和最小的树形结构,我们称这样的树为关于这一组给定数据的哈夫曼树;通常处理的数据数目庞大以至于不可能完全统计,通常使用概率代替频率作为描述特定数据集合的哈夫曼树的权值 
这里写图片描述

可以看出,哈夫曼树并不满足一般查找树“左孩子比父结点大、右孩子比符结点小”的特点(如父结点46,他的两个孩子分别是21、25),从这一点看哈夫曼树不是严格的查找树——但是考虑到它可以查找数据的功能,姑且认为它也是一种特殊的查找树(静态的查找树,只能进行查找通常不做数据的修改、添加等操作)

哈夫曼树的ADT

typedef struct _HuffmanNode
{
    ElementType data;
    int weight;
    struct _HuffmanNode *left;
    struct _HuffmanNode *right;
}HuffmanNode, *HuffmanPtr;

哈夫曼树的构造

查找的是数据,但是比较的是权值 
为了使得到的哈夫曼树的结构尽量唯一,通常规定生成的哈夫曼树中每个结点的左子树根结点的权小于等于右子树根结点的权(下面的代码就是把小的作为左孩子)

/*
 * 函数功能:找出给定数据集合中最小、次小的两个元素
 * E:用于接受数据的数组
 * n:数组元素个数
 * p1:指向最小元素下标
 * p2:指向次小元素下标
*/

static int FindMinNode(ElementType *E[], int n, int *p1, int *p2)
{
    int index;
    int fir_min = sec_min = 0xffff;

    if(E == NULL) return -1;

    for(index=0; index<n index++)
    {
        if(weight != 0)
        {
            if(E[index]->weight < fir_min)
            {
                sec_min = fir_min;
                fir_min = E[index]->weight;
                *p2 = *p1;
                *p1 = index;
            }
            else if(E[index]->weight < sec_min)
            {
                sec_min = E[index]->weight;
                *p2 = index;
            }
        }
    }
    return 0;
}
/* 函数功能:建立哈夫曼树
 * E:给定数据数组
 * n:给定数据个数
 */

HuffmanPtr CreateHuffmanTree(ElementType E[], int n)
{
    int i, j;
    HuffmanPtr b[], q;
    b = malloc(n*sizeof(HuffmanNode));
    for(i=0; i<n; i++)
    {
        b[i] = malloc(sizeof(HuffmanNode));
        b[i]->weight = E[i];
        b[i]->left = b[i]->right = NULL;
    }
    for(i=0; i<n-1; i++) //主体部分,循环n-1次建立Huffman Tree
    {
        for(j=0; j<n; j++) //让k1、k2分别指向第一和第二棵树
        {
            int k1=-1, k2;
            if(b[j] != NULL && k1==-1)
            {
                k1 = j;
                continue;
            }
            if(b[j] != NULL)
            {
                k2 = j;
                break;
            }
        }

        FindMinNode(E, n, &k1, &k2);

        /*由最小权值、次小权值的两个结点建立一棵新树,q指向树根*/
        q = malloc(sizeof(HuffmanNode));
        q->weight = b[k1]->weight + b[k2]->weight;
        q->left = b[k1];
        q->right = b[k2];

        b[k1] = q;
        b[k2] = NULL;
    }
    free(b);
    return q;
}

AVL树(AVL-BST)

一种简单的高度平衡的自平衡查找树;它限制树中任何节点的两个子树的高度最大差别为1;在每次增加、删除元素后,要重新调整AVL树的结构以达到新的平衡(牵一发而动全身)

AVL树的四种失衡类型:1)LL型,2)LR型,3)RL型,4)RR型 
AVL树的四种调整类型:1)右单旋,2)左右双旋,3)右左双旋,4)左单旋

几种旋转的英文叫法:右单旋 => Zig、左单旋 => Zag、左右双旋 => Zag-Zig、右左双旋 => Zig-Zag

这里写图片描述 
这里写图片描述

AVL树的性质

对于每一个结点 
0)高度自平衡 
1)左右子树也都是AVL树 
2)左右子树的高度之差不会超过1

AVL树的ADT

typedef struct _AVLNode
{
    ElementType data;
    int height;
    struct _AVLNode *left;
    struct _AVLNode *right;
}AVLNode, *AVLPtr;

AVL树的插入

/*严格来说,这并不是结点的高度;但是我们约定用这样的二进制数表示它,好处就是简化了信息的复杂度,同样当height的差等于2时旋转——这一点看和一般意义上的height是等价的*/
static int Height(AVLPtr P)
{
    if(P == NULL)
    {
        return -1;
    }
    else{
        return P->height;
    }
}
/*左单旋*/
static AVLPtr SingleRotateWithLeft(AVLPtr K2)
{
    AVLPtr 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), Height(K2->height)) + 1;
    return K1;
}

/*右双旋*/
static AVLPtr DoubleRotateWithLeft(AVLPtr K3)
{
    K3->left = SingleRotateWithRight(K3->left);
    return SingleRotateWithLeft(K3);
}

/*插入函数主体部分,调用上面的函数*/
int AVLInsert(ElementType E, TreePtr T)
{
    if(T == NULL)
    {
        T = malloc(sizeof(TreeNode));
        if(T)
        {
            T->data = E;
            T->left = T->right = NULL;
            T->height = 0;
        }
        else{
            return -1;
        }
    }
    else if(E < T->data)
    {
        T->left = AVLInsert(E, T->left);
        if(Height(T->left) - Height(T->right) == 2)
        {
            if(X < T->left->data)
            {
                T = SingleRotateWithLeft(T);
            }
            else{
                T = DoubleRotateWithLeft(T);
            }
        }
    }
    else if(E > T->data)
    {
        T->right = AVLInsert(E, T->right);
        if(Height(T->right) - Height(T->left) == 2)
        {
            if(X > T->left->data)
            {
                T = SingleRotateWithRight(T);
            }
            else{
                T = DoubleRotateWithRight(T);
            }
        }
    }
    T->height = Max(Height(T->left), Height(T->right));
    return T;
}

树的旋转


树堆(Treap)

树堆的每个结点都有一个在插入时随机赋予的优先级,其结构相当于以随机数据插入的二叉查找树,在平衡被打破时它也要通过旋转建立新的平衡

优先级越小的结点越靠近根结点;从这一点看,树堆和哈夫曼树倒有几分相似(树堆的优先级相当于哈夫曼树的权值,不过二者作用相反——哈夫曼树是权值越大的越靠近树根,而且树堆的每个结点的优先级也不满足等于两个子结点的相加关系->看下图13+14≠6)

这里写图片描述

树堆的性质

树堆的性质总结起来就是,1. 最小堆原则:从树根往下,每个结点的优先级都必须小于其任意孩子结点的优先级(注意不一定是一个比它大一个比它小,那是二叉查找树存储数据的方法)2. 树堆既可以看成一棵二叉查找树,也可以看成是一个堆结构(Treap=Tree+Heap)

树堆的ADT

typedef struct _TreapNode
{
    ElementType data;
    int priority;
    struct _TreapNode *left;
    struct _TreapNode *right;
}TreapNode, *TreapPtr;

树堆的删除

因为树堆既可以看成树,也可以看成堆,所以这里删除结点就有两种不同的思路(分别从「二叉树」和「堆」的角度)

/*使用二叉查找树的方式删除结点*/
if(结点是叶子结点)
{
    直接删除之;
}
else if(结点有一个孩子)
{
    删除该结点,并让它的唯一孩子代替它原来的位置;
}
else
{
    删除该结点,并让其右子树中最小值(或左子树中最大值)代替它原来的位置;
}
/*使用堆的方式删除结点:把要删除的结点旋转到叶子上再直接删除*/

伸展树(Splay Tree)

伸展树(Splay Tree)也叫分裂树,是一种自调整的平衡查找树;它能在O(log N)内完成插入、查找和删除操作;由丹尼尔·斯立特Daniel Sleator和罗伯特·恩卓·塔扬Robert Endre Tarjan在1985年发明;和AVL树不同,它的特点是伸展(本质上是和AVL一样都是二叉查找树,不过在失衡后的调整措施不同)

不会保证树一直是平衡的,但各种操作的平摊时间复杂度是O(logN),因而,从平摊复杂度上看,伸展树也是一种平衡二叉树(专业术语:『摊还』)

伸展树的自调整体现在:每一次对结点的查找、插入、删除操作,就把该结点移到根结点,如果需要删除在这之后进行伸展树不需要记录用于平衡树的冗余信息(结点的高度)

伸展树的伸展

所谓伸展就是把指定结点旋转到树根,可以把伸展看成是特殊的旋转,而旋转是所有平衡查找树的特性(为什么要旋转到树根?因为查找树树的遍历都是从树根开始,这样可以让那些访问频率高的结点优先被访问到)说到优先级,我们说『伸展树的优先级是在人为干涉下动态变化的,而树堆的优先级和外界干涉无关是根据随机数算法随机分配的』;另外,伸展树的优先级(不严谨的说,优先级就是结点距离树根的路径长度)是概念上的,实际上并不存在

伸展树每次Splay前要考察的层数为三层(祖父、父亲、自己),提出Splay要『向上追溯两层而非一层』的是著名的计算机科学家Tarjan在1985年提出的

伸展树的伸展不是被动而是主动的,不论查找、删除、或者插入结点,不论当前状态是否已经平衡都要进行伸展;具体来说,当一个结点被查询时说明该结点被访问的可能性增大故进行伸展使该结点上移,同理可以分析删除、插入操作,总之伸展行为总是主动发生在每一次的访问(查、删、插)操作后

我们说『伸展树』是牺牲时间保护空间的典型代表 —— 不用额外的空间存储结点的高度信息,但是作为替代每次查找、删除、插入操作后都要进行一次伸展

伸展树的伸展类型根据被访问的结点位置分为三种情形:

1)被访问结点是树根的左或右孩子(最简单)


这时候只要进行一次单旋(是左孩子->右旋)

2)被访问结点不是根结点的孩子,且和它的父亲都是各自父亲的左或右结点

这里写图片描述

这里写图片描述

观察构型,应该右右双旋或左左双旋(这里是右右)

3)被访问结点不是根结点的孩子,且和它的父亲分别是各自父亲的左孩子和是右孩子

这里写图片描述

这里写图片描述

观察构型,应该左右双旋或右左双旋(这里是左右)

看不懂?没关系!且听我慢慢道来……

首先对于伸展树而言,它的目标是要『把当前操作结点旋转到树根』,这句话的理解很关键(复习二叉树相关概念),我们知道一个结点到树根的路径是唯一的(『路径』就是指『最短路径』),于是最简单的approach就是『向上回溯,遇到左孩子就右旋,遇到右孩子就左旋』,看上去很完美——BUT!

看图说话(其实也可以看本文的第一张图),what if 『构成树的所有元素可以构成一个单调数列』? 
这里写图片描述
伸展操作(不同于『普通旋转』,『伸展操作』影响的范围或者说被操作结点的位置变化更大,因为人家的目标很明确就是树根)后你旋转得到的新树是这样的:对于单调数列,在『伸展操作』后,就像没有旋转一样,因为O(N)还是O(N)时间复杂度没变! 
这里写图片描述

另外考虑这样的例子: 
这里写图片描述
这里写图片描述
这种奇怪构型的查找树在进行『姑且认为是粗糙版的伸展操作』后,原来的k3结点却被推到了和k1同样深的位置——同样的,单旋转也没能降低这种情况下的时间复杂度

基于上述两个代表性例子——这些旋转的效果是将新结点一直推向树根,使得对该结点的进一步访问很容易(暂时的);不足之处是它把另外一个结点几乎推向和新结点以前同样的深度,而对那个结点的访问又将把另外的结点向深处推进……我们想『看来单旋转是不行了,不妨试试双旋转』

『双旋』也是可以转换成两步单旋的,我们假设被访问结点为x 
这里写图片描述

在旋转之前要明确『目标是什么』:伸展树的目标是把访问结点移动到整棵树的树根

在明确了目标后,自然想到讨论结点和树根的关系

case1)x的父结点是树根 
访问结点9后,只需要单旋一次(这里是左旋)把该结点搞成树根就完事了

这样最简单,想要双旋还搞不了 
这里写图片描述

并且我们说单次旋转操作涉及到要判断的树结点层数不会太多(最多到『祖父』结点,这就是极限了)

case2)之字形 
被访问结点X 介于父节点P 和 祖父节点G 之间,确切地说是 P≤X≤G(在『里面』) 
这时候执行Zig-Zag或Zag-Zig(两次方向相反的单旋)

对于『RL』、『LR』的『之字形』,AVL树也要旋转两次,从结果上看AVL树的Rotate和这里伸展树的Splay是一样的,都是把X旋转到了树根位置(AVL是无意为之,而Splay则是故意为之) 
这里写图片描述 
这里写图片描述

case3)一字型 
被访问结点X 不介于父节点P 和 祖父节点G 之间,确切地说是 X≤P≤G 或 G≤P≤X(在『外面』) 
这之后执行Zig-Zig或Zag-Zag(两次方向相同的单旋,依次把最上面的树根翻下来)

可以看到,和AVL树的一字型Rotate相比,看下图如果是AVL树到第二步把G翻下来就完事了,而Splay则多了一个把P翻下来的过程(实际上是要把X翻上去——Splay的思想就是把操作结点翻到对应子树的树根位置) 
这里写图片描述 
这里写图片描述

总结:以后看到『伸展树』就要想到『伸展』,想到『把当前结点移动到树根位置』,还要想到我上面举的两个例子,要形成反射弧——

通过本章节你学到了什么,你的思维导图:

伸展树 => 伸展,就是把当前结点想办法移动到树根位置 => 尽量不要加深树的深度 => 使用双旋转(就像DNA的双螺旋一样)

另外,伸展树一律使用『双旋转』,除非特殊情况,即被访问结点的父亲是树根,这时候我们也想双旋转但是因为想搞搞不了只得使用单旋转

/*函数功能:实现伸展树的Splay操作
 *函数说明:使用迭代,如果当前结点不是树根就根据情况旋转,当它的父亲是树根时跳出循环(因为时另一种情况),并根据case进行单旋转,其中封装了PushUp函数
 *请参说明:T为树根结点,N时新插入结点
 */
void Splay(SplayPtr *T, SplayPtr *N)
{
    while(T != N && T->left != N && T->right != N)
        PushUp(T, N);
    if(T->left == N) SingleRotateWithRight(T, N);
    else if(T->right == N) SingleRotateWithLeft(T, N);
}
/*函数功能:实现Splay的主体功能,被封装在splay中
 *请参说明:i和j是判断条件的标记,也可以用两个boolean类型代替
 */
void PushUp(SplayPtr T, SplayPtr N)
{
    SplayPtr parent, grandparent;
    int i, j;
    parent = N->parent;
    grandparent = N->parent->parent;
    i = grandparent->left == parent ? 0 : 1;
    j = parent->left == N ? 0 : 1;
    if(i==0 && j==1)
        zig-zig(T, N);  //右右双旋
    else if(i==0 && j==1)
        zag-zig(T, N);  //左右双旋
    else if(i==0 && j==1)
        zig-zag(T, N);  //右左双旋
    else
        zag-zag(T, N);  //左左双旋
}

注意下,每次Splay最多也只是把当前结点移动到它的祖父结点位置——之后如果要移动到整棵树的树根位置,还要继续Splay(如果此时父结点就是树根,那就直接单旋转完事)——反正记着,伸展树的Splay的最终目的是把操作结点移至整棵树的树根位置

伸展树的区间操作

首先想一想,伸展树为什么适合『区间操作』(不是说别的树不可以,只是说它的构型特点比较适合)?因为每次Splay(伸展树的核心操作)都要把相应的结点旋转的树根,这样旋转后该结点的位置就确定了,既然位置确定了又考虑到伸展树作为一棵查找树它所有结点分布应该满足有序的特点,所以对该新树根做一些遍历之类的操作就很简单

所以,与其说『伸展树的区间操作』,不如说『借用Splay函数的区间操作』

给里给出思路:要求区间[s, e]的极值,将结点s-1伸展成树根,再将结点e+1伸展为树根的右孩子,那么结点e+1的左子树就代表了区间[s, e],结点e+1的左孩子的极值域就是所求范围(以上,必须要保结点s-1和结点e+1存在)

结点大小平衡树(SBT)

名字不要和普通查找树弄混了(那是BST)

SBT,即Size Balanced Tree,节点大小平衡树,是一种自平衡二叉查找树,它是由中国广东中山纪念中学的陈启峰发明的;实践中,SBT是所有种类的平衡树中效率较高的一种;SBT的高度是O(logN),Maintain是O(1),所有主要操作都是O(logN);SBT的特点是,它需要专门去维护其大小,从而实现构建平衡二叉树的目的

SBT的自平衡是通过size域实现的

结点大小平衡树最主要的特色或者说核心操作就是『维护子树大小』,这里聪明的读者肯定一眼就看出来为了维护所谓的子树大小必然要有一个附加的『size』域用来存放每个结点的子树大小

SBT的高度是O(logN),Maintain是O(1),所有主要操作都是O(logn)——用陈老师的原话就是『目前为止速度最快的高级二叉搜索树』

SBT的实现结构高度

P.s. Treap、Splay、SBT,号称是『三大查找树』(并无考证,姑且信之吧)

SBT的性质

SBT是一种通过Size域来保持平衡的查找树,它的性质总结起来一句话:对于SBT的每个结点,每棵子树的大小不小于其兄弟的子树大小

写成数学表达式就是:

Size[Right[T]] ≥ Size[Left[Left[T]]], Size[Right[Left[T]]]
Size[Left[T]] ≥ Size[Right[Right[T]]], Size[Left[Right[T]]]

做成PPT也好理解: 
这里写图片描述

SBT的操作集

这里写图片描述

SBT的ADT

因为旋转总是发生在当前结点和其父结点之间,所以要取得这个结点的父亲的引用或者指针(当然,或者转换思路把要传给Rotate函数的参数写成上面的结点就不用加上parent域了,加上parent域是为了便于查找);另一种思路是用一个数组维护整个SBT

/*写法一*/
typedef struct _SBTNode
{
    ElementType key;
    int size;
    struct _SBTNode *left;
    struct _SBTNode *right;
}SBTNode;
/*写法二:用数组维护整棵树*/
typedef struct
{
    int key, left, right, size;
}tree[MAXSIZE]; 

SBT的旋转

SBT的旋转函数最后是要封装到Maintain函数中去的,大体操作和普通的AVL旋转没有区别,不过SBT有SBT的特点:发现SBT每次旋转,包括判断是否要旋转(我是说『Maintain』,就是一系列旋转),波及到的结点都大于2层但是没有层数的上界限制,也就是说可以有3层、4层、5层……这一点要有一个印象,就是『SBT可以很深』

SBT的旋转和AVL的旋转没有区别,无非是多了个size域……

/*S表示旋转前在上面的结点,旋转后翻到下面*/
int RotateWithLeft(SBTNode *S)
{
    SBTNode *tmp = S->right->left;
    S->right->left = S;
    S->right = tmp;
    S->right.size = S.size;
    /*S的size,因为S被翻下来后获得了它原来孩子的孩子,所以加一*/
    S.size = S.left.size + S.right.size;
}

SBT的Maintain函数(核心关键)

当我们删除、插入一个结点到SBT中,SBT的大小会发生改变(从而破坏SBT的两个重要性质),这时候就需要调用Maintain函数对SBT进行修复

1)Maintain(T)用于修复以T为根的SBT

2)调用Maintain(T)的前提是T的子树都已经是SBT了 
看到注意事项2,你要想到两点:1)既然说『调用Maintain的前提是T的子树都是SBT』,那么我们说不看旋转单看Maintain,Maintain操作一定是自下而上进行的,即先Maintain下面的再Maintain上面的,2)虽然话是这么说没错,但是如果把Maintain和Rotate结合起来看的话(事实上,Maintain就是包含Rotate的),由于首先Rotate操作已经让发生问题的结点转到上面去了,我们想要『先Maintain下面的再Maintain上面的』,不过这时候是要接着Rotate操作后对翻下来的结点进行Maintain就好了

要细分的话,Maintain调用有四种情况:分别是1)Size[A]>Size[R],2)Size[B]>Size[R],3)Size[D]>Size[L],4)Size[C]>Size[L];不过二叉树好就好在它比较对称,后两种情况和前两种情况对应为镜像情形,这里以前两种要Maintain的情形分析

Maintain函数其实并不复杂,如果我们忽略那些多余的自子树结点(途中三角形部分),Maintain总是发生在一定范围内(从图中看好像是三四层的『感觉』)

case1:Size[A] > Size[R]

这里写图片描述

0x01)顺时针旋转根结点T(不是以T为转轴,而是拎着T)让T下,L上(R不动)

这里写图片描述

0x02)由于旋转后不确定T是否满足SBT的性质即不能确定是否仍然是SBT,我们对翻下来的结点T进行Maintain操作(熟练后就形成惯性思维了,知道要对翻下来的结点先进行维护),Maintain操作的基础也是Rotate——在Maintain过T后,我们知道又解决掉一层,接下来只要对Maintain发生的最高一层结点也就是L进行维护即可;在这之后,不要再想着是不是要继续上溯对L的父结点进行Maintain,那是完全没有必要的(回头看之前的红色部分),因为我们说『调用Maintain(T)的前提是T的子树都已经是SBT了』,这句话的理解我认为是理解Maintain函数的关键(每一次『操作』包括删除、插入的影响范围都是以操作位置为核心向周围散开的,如果没有特殊要求——比如Splay要求伸展比如要搞到树根位置这就是特殊要求——并不会影响范围不会太大)

这里写图片描述

这里写图片描述

case2:Size[B] > Size[R]

情形二和情形一相比,不同在于『新插入点在Maintain影响范围最高层的左右或右左位置,或者说插入点位于旋转点的内侧(情形一插入点位于旋转点的外侧)』

这里写图片描述

0x01)图中是『LR』类型,所以先旋转L结点划归为『LL』类型后(这不,又是case1了)再旋转T结点(两步合为一步)

这里写图片描述

这里写图片描述

0x02)旋转(Rotate)过后,就是维护(Maintain)了,这里L和T由于在同一层,都是B的孩子,所以谁先Maintain谁后Maintain是没关系的,不过在Maintain(L)& Maintain(T)过后,不要忘了现在处在『影响范围最高层』的B结点,也要Maintain——Maintain和Rotate一样,都有点『自下而上』的感觉(上溯,直到影响范围消失)

这里写图片描述

这里写图片描述

代码部分:

/*函数说明:维护函数,触发条件是size域不满足相关性质;其中前两个if对应图片的case1、case2,后两个就是前两种的镜像而已
 *请参说明:T是旋转结点也是影响范围的最高层位置;flg:方便后面Insert调用,每次插入把平衡操作压入栈中(注意注释前后的语句变化,加上注释的版本方便后面函数的调用)
 *备注:没有使用数组维护整个SBT树,因为如果使用数组的话,key域的类型就定死了,我们使用一个结构体left、right来封装各自的数据域;另外SBT的Maintain有四种情况这个记住
 */
void Maintain(int *T, bool flg)
{
    if(flg)
    {
        if(T->left->left.size > T->right->size)
        {
            RotateWithRight(T);
            //Maintain(T->left);
            //Maintain(T);
        }
        else if(T->left->right->size > T->right->size)
        {
            RotateWithLeft(T->left);
            RotateWithRight(T);
            //Maintain(T->right);
            //Maintain(T->left);
            //Maintain(T);
        }
    }
    else
    {
        else if(T->right->right->size > T->left->size)  //对应case1镜像
        {
            RotateWithLeft(T);
            //Maintain(T->left);
            //Maintain(T);
        }
        else if(T->right->left->size > T->left->size)  //对应case2镜像
        {
            RotateWithRight(T->right);
            RotateWithLeft(T);
            //Maintain(T->left);
            //Maintain(T->right);
            //Maintain(T);
        }
    }
    Maintain(T->left, false);  //#
    Maintain(T->right, true);  //#
    Maintain(T, false);  //#
    Maintain(T, true);  //#
}

略作小结:SBT的平衡是靠Size域维护的,当Size域不满足SBT性质时,要进行Maintain操作;而Maintain操作是包括Rotate操作的,在Rotate后,再要根据具体插入位置判断Maintain的结点次数——整个Maintain函数就是一个判断并递归调用而已

SBT的插入(实现Maintain接口)

Insert函数封装Maintain函数,Maintain函数封装Rotate函数,Rotate函数封装size域的变化……

/*函数说明:实现SBT的结点插入
 *参数T:为根树结点;参数newNode:为新结点;parent:用来保存T变为其孩子之前的值(确切地说是保存T变为NULL之前的值)
 *备注:在实际使用当中,写法是灵活的——比如可以封装一个NewNode函数用来创建新结点,那么这里只要传入它的key域即可
 */
void Insert(SBTPtr *newNode, SBTPtr *T)
{
    if(T == NULL)
    {
        T = newNode;
    }

    SBTPtr *parent = NULL;
    while(T)
    {
        if(newNode->key > T->key)
        {
            parent = T;
            T = T->right
        }
        else if(newNode->key < T->key)
        {
            parent = T;
            T = T->left;
        }
    }
    if(parent->key < newNode->key)
    {
        parent->right = newNode;
    }
    else
    {
        praent->left = newNode;
    }
    Maintain(T, key>=T->size);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

SBT的Select函数

SBT的Select函数返回以某结点为树根的子树包含第k小的值的结点指针 
还是那句话,对于给定的查找树结点,左孩子的值>当前结点的值>右孩子的值

int Select(SBTPtr *T, int k)
{
    int r = T->left->size+1;
    if(r == k)
        return T->key;
    else
        if(r < k)
            return Select(T->right, k-r);
        else
            return Select(T->left, k);
}           
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

SBT的Rank函数

SBT的Rank函数返回某结点的key值排名;Rank和Select互为补充

int Rank(SBTPtr *T, int key)
{
    if(key < T->key)
        return Rank(T->left, key);
    else
        if(key > T->key)
            retrun rank(T->right, key) + T->left->size + 1;
        else
            retrun T->left->size + 1;
}

替罪羊树(Scapegoat Tree)

替罪羊树是一种基于重构的重量级(为什么说它是『重量级』?因为每次都要暴力重构啊,不懂往下看)查找树

替罪羊树的『平衡』是这样定义的:给出定值alpha(0.5≤alpha≤1),对于对树中的每个结点x,如果Size(left(x))≤α·size(x)和Size(right(x))≤α·size(x),则称这棵树满足替罪羊树的平衡;也就是说,结点的两棵子树包含结点数不超过整棵子树的α倍,那么就称这个节点x是α-大小平衡的(∂大小自取)——始终要谨记,不同的树对于『平衡』的定义是不同的,而只有AVL才是真正意义上的高度平衡

替罪羊树不是『旋转机制』,真的它不用旋转(像Splay那样好歹也是基于Rotate的,但是替罪羊树每次的『趋平衡操作』却是『取中点,定树根』)。看到这里,好像维护平衡不用旋转的也只有『替罪羊树』了……

如果在插入、删除结点后,查找树不再满足替罪羊树的平衡,就要『拍平重构』整棵树:先『打散』、『拍平』所有的结点,让后递归地,取中值结点为树根,并对左右子树做同样的操作

不难看出,『拍平』的时间复杂度(我们讲时间复杂度通常是指『最坏时间复杂度』)为O(N)

2-3树(2-3 Tree)

2-3树是一种多路查找树,它包含2-结点和3-结点——多路查找是为了方便磁盘读写的需要(后面有讲,一句就是把待读取信息尽量集中化)

2-结点:等同于一般查找树的结点,即有两个孩子(或者没有孩子),左孩子小右孩子大

3-结点:该结点有三个孩子(或者没有孩子),其中左孩子小于结点中较小元素,中间孩子介于两个孩子之间,右孩子大于结点中较大元素

由于3-结点的加入,2-3树的所有叶子结点在同一层上(矮矮胖胖的特点)

这里写图片描述

可以看到,所谓n-结点就是有n个孩子的结点(对应该结点就有n-1个值域)

2-3树的插入(构造2-3树)

因为比普通查找树,每个结点多了一个可能的值域(每个结点可以有0、1、2个不同的值域,),所以2-3树在插入新结点时,不仅可作为已有结点的孩子,也可以融合到已有的2-结点成为3-结点(其实想一想,我们可以认为普通的二叉查找树是所谓的1-2树,这样我们说知识就成体系了)

1)将新结点插入到2-结点 
原2-结点扩充成为3-结点

2)将新结点插入到3-结点

case1)3-结点没有父结点(即整棵树只有它一个3-结点) 
我们采用『先插入,后分解』的思想,把新结点插入到原来的3-结点使其变成一个4-结点(3个值域,4个孩子),然后把该4-结点分解并合成为一棵新的二叉查找树(即左孩子<当前结点<右孩子)

case2)3-结点有一个2-结点的父结点 
同样先插入,再分解4-结点,将分解后的新树的父节点融入到2-节点的父节点中去

case3)3-结点又一个3-结点的父结点 
还是先插入,再分解4-结点,接着往上融合,由于2-3树的最大结点值域数为2,而4-结点有3个值域,所以当前结点的父结点接着向上融合,直到父结点为2-结点为止;如果向上到根节点都是3-节点,将根节点扩充为4-节点,然后分解为新树,至此,整个树增加一层,仍然保持平衡

如果要构建一棵2-3树,他的理论基础就是『2-3树的插入』,即从零开始不断插入一些新的结点到树里面去;略

小结:2-3树因为加入了3-结点,所以一个结点最多可以有两个值域这不是说真的可以选择性的让某些结点有两个孩子而某些结点有三个孩子,那样的话就达不到矮矮胖胖的效果了——每一个结都要有达到最大值域数的趋势,在达到最大值域数之前,只要有新结点进来都要优先考虑融合——举例:一个2结点,当新结点插入后,融合成为3-结点,优先考虑融合

2-3树的一个著名变种就是『著名的红黑树』(在文章后面)


B树(B Tree)

首先声明,B树和B-树(『-』是连字符)是同一种树的两种不同叫法

B树也是一种多路查找树,B 树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然不能超过磁盘块的大小,根据磁盘驱动的不同一般块的大小在1k~4k左右);这样树的深度降低了,就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据,这样的特点决定了B树(以及它的变体B+树,后面有讲)很适合用在那些可能需要频繁磁盘读写的场景,比如磁盘文件系统、数据库索引查找(为啥?因为『多路』所以每次磁盘可以读取更多数据)

B树的每个节点比原来那些节点多了一个指示该节点元素个数的存储空间

最小度数为d、最大度数(称为B树的阶)为2*d-1、高度为h的B树(一般B树的阶都比较大):

1)最坏情况下(类似于普通二分查找):第一层有1个结点、第二层有2个结点、第三层有2d……第h(h≥3)层有2d^(h-2)个结点 
这里写图片描述

2)最好情况下(尽可能发挥多路径的优势):第一层有1个结点、第二层有2个结点、第三层有(2d-1)^2……第h层(h≥3)有(2d-1)^(h-1个结点)

这里写图片描述

红黑树(Red Black Tree)

它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(Symmetric Binary B-Trees),后来在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的『红黑树』

红黑树背后的逻辑就是2-3树

这里写图片描述

红黑树的性质

红黑树的五条性质是理解红黑树的基础,必须记住(其实用熟了就是水到渠成的事情)

1)结点是红色或黑色

2)树根是黑色

3)每个叶子结点NIL也是黑色的

4)每个红色结点的两个孩子是黑色的(反之不一定成立)

5)从任一结点到每个叶子结点的所有路径包含相同数目的黑色结点

以上,第一条『颜色』可以用字符串写出,也可以用一个二元变量(比如boolean类型)来表示,这不是固定死的;第三条『叶子结点』是指『空的叶子NIL』;第四条也可以说成『从每个叶子到树根的所有路径上不能有两个连续的红色节点』;与其说这五条是『性质』,不如认为这是五条『作为红黑树必须要遵守的约定』

在满足前五条性质的基础上,可以导出红黑树一个最重要的性质:从树根到叶子的最长的可能路径不多于最短的可能路径的两倍长;建议自证。

这里略作解释:由性质四的推论『从每个叶子到树根的所有路径上不能有两个连续的红色节点』,考虑极端情况『最短路径都是黑色结点』,结合性质五让任意结点等于树根则『从树根到叶子交替出现黑色、红色、黑色……』,所以对于一般情况『从树根到叶子的最长路径不多于最短路径的两倍长』

注意性质五,『从任一结点到每个叶子结点的所有路径包含相同数目的黑色结点』,如果我们忽略红色结点,红黑树就和2-3树一样所有的叶子结点都在同一层了——为了做到这一点,我们认为树中所有红色结点不是真正的结点而把它们看成某种『链接』,用来链接相邻的两个黑色结点,既然有两个黑色结点就有两个值域,故可以把红黑树看成是一种2-3树的特殊实现

红黑树的失衡和修正

和所有查找树一样,『插入』、『删除』操作可能会导致树的失衡

红黑树因为加入了『颜色』这个属性,所以除了旋转还可以通过调整结点的颜色来调整平衡

既然失衡(准确说是『五条性质的一条或多条被打破』)发生在『插入』、『删除』操作后,下面就以具体实例进行讲解

红黑树的插入

插入红黑树的新结点必须是红色(硬性规定!),稍微解释下,考虑到性质五,如果插入结点的颜色不确定,就不知道在插入后还是否满足黑色结点数目相等,所以干脆就定死了,要求『新插入的结点必须是红色的』

在插入后,检查如果有违背五条性质任何一条,就通过变色或者旋转以满足五条性质(以重新达到平均意义上的『平衡』)

同样我们分类讨论结点插入,存在以下五种情况:

1)被插入结点是树根 
违背性质二:把该结点变色为黑色即可

2)被插入结点的父结点是黑色 
什么也不要做,这时候满足红黑树的五条性质

3)被插入结点的父结点是红色——这个比较复杂(复杂不怕,分类讨论之)

case1)叔叔结点也是红色 
把父结点和叔结点变为黑色,把祖父结点变为红色,设置祖父结点为当前操作结点(用来判断是否违背五条性质),并递归上述操作 
图片来自网络: 
这里写图片描述 
这里写图片描述 
这里写图片描述

case2)叔叔结点是黑色或缺失,且插入结点是『RL』或『LR』类型 
以当前结点的父结点作为新的当前结点,把插入结点绕着父结点旋转(左孩子右旋,右孩子左旋),并对新的当前结点递归操作

case3)叔叔结点是黑色或缺失,且插入结点是『RR』或『LL』类型 
把父结点变成黑色,把祖父结点变成红色,接着以祖父结点为支点旋转(左孩子右旋,右孩子左旋,注意当前结点没变还是插入结点),并对当前结点递归操作

图从略

小结一下,红黑树的插入:首先按照二叉查找树正常插入,然后对当前结点递归判断是否满足红黑树的五条性质,如果不满足则先进行变色,后进行旋转(这个先后顺序已然成为惯例);另外,注意到红黑树结点总要判断父结点是否满足某些情况,所以可以考虑每个结点增加一个用来存储父结点的域(parent,以及叔叔结点的域uncle),这样查找起来也方便(牺牲空间保护时间);另另外,上述实现翻译成代码免不了有想用递归的冲动,但是不妨使用迭代这样效率更高

插入讲完了,删除类似:首先视作普通二叉查找树进行结点删除,然后先变色,后旋转(不过是多写几个if-else罢了!)


红黑树(Red Black Tree)

它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(Symmetric Binary B-Trees),后来在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的『红黑树』

红黑树背后的逻辑就是2-3树

这里写图片描述

红黑树的性质

红黑树的五条性质是理解红黑树的基础,必须记住(其实用熟了就是水到渠成的事情)

1)结点是红色或黑色

2)树根是黑色

3)每个叶子结点NIL也是黑色的

4)每个红色结点的两个孩子是黑色的(反之不一定成立)

5)从任一结点到每个叶子结点的所有路径包含相同数目的黑色结点

以上,第一条『颜色』可以用字符串写出,也可以用一个二元变量(比如boolean类型)来表示,这不是固定死的;第三条『叶子结点』是指『空的叶子NIL』;第四条也可以说成『从每个叶子到树根的所有路径上不能有两个连续的红色节点』;与其说这五条是『性质』,不如认为这是五条『作为红黑树必须要遵守的约定』

在满足前五条性质的基础上,可以导出红黑树一个最重要的性质:从树根到叶子的最长的可能路径不多于最短的可能路径的两倍长;建议自证。

这里略作解释:由性质四的推论『从每个叶子到树根的所有路径上不能有两个连续的红色节点』,考虑极端情况『最短路径都是黑色结点』,结合性质五让任意结点等于树根则『从树根到叶子交替出现黑色、红色、黑色……』,所以对于一般情况『从树根到叶子的最长路径不多于最短路径的两倍长』

注意性质五,『从任一结点到每个叶子结点的所有路径包含相同数目的黑色结点』,如果我们忽略红色结点,红黑树就和2-3树一样所有的叶子结点都在同一层了——为了做到这一点,我们认为树中所有红色结点不是真正的结点而把它们看成某种『链接』,用来链接相邻的两个黑色结点,既然有两个黑色结点就有两个值域,故可以把红黑树看成是一种2-3树的特殊实现

红黑树的失衡和修正

和所有查找树一样,『插入』、『删除』操作可能会导致树的失衡

红黑树因为加入了『颜色』这个属性,所以除了旋转还可以通过调整结点的颜色来调整平衡

既然失衡(准确说是『五条性质的一条或多条被打破』)发生在『插入』、『删除』操作后,下面就以具体实例进行讲解

红黑树的插入

插入红黑树的新结点必须是红色(硬性规定!),稍微解释下,考虑到性质五,如果插入结点的颜色不确定,就不知道在插入后还是否满足黑色结点数目相等,所以干脆就定死了,要求『新插入的结点必须是红色的』

在插入后,检查如果有违背五条性质任何一条,就通过变色或者旋转以满足五条性质(以重新达到平均意义上的『平衡』)

同样我们分类讨论结点插入,存在以下五种情况:

1)被插入结点是树根 
违背性质二:把该结点变色为黑色即可

2)被插入结点的父结点是黑色 
什么也不要做,这时候满足红黑树的五条性质

3)被插入结点的父结点是红色——这个比较复杂(复杂不怕,分类讨论之)

case1)叔叔结点也是红色 
把父结点和叔结点变为黑色,把祖父结点变为红色,设置祖父结点为当前操作结点(用来判断是否违背五条性质),并递归上述操作 
图片来自网络: 
这里写图片描述 
这里写图片描述 
这里写图片描述

case2)叔叔结点是黑色或缺失,且插入结点是『RL』或『LR』类型 
以当前结点的父结点作为新的当前结点,把插入结点绕着父结点旋转(左孩子右旋,右孩子左旋),并对新的当前结点递归操作

case3)叔叔结点是黑色或缺失,且插入结点是『RR』或『LL』类型 
把父结点变成黑色,把祖父结点变成红色,接着以祖父结点为支点旋转(左孩子右旋,右孩子左旋,注意当前结点没变还是插入结点),并对当前结点递归操作

图从略

小结一下,红黑树的插入:首先按照二叉查找树正常插入,然后对当前结点递归判断是否满足红黑树的五条性质,如果不满足则先进行变色,后进行旋转(这个先后顺序已然成为惯例);另外,注意到红黑树结点总要判断父结点是否满足某些情况,所以可以考虑每个结点增加一个用来存储父结点的域(parent,以及叔叔结点的域uncle),这样查找起来也方便(牺牲空间保护时间);另另外,上述实现翻译成代码免不了有想用递归的冲动,但是不妨使用迭代这样效率更高

插入讲完了,删除类似:首先视作普通二叉查找树进行结点删除,然后先变色,后旋转(不过是多写几个if-else罢了!)


区间树(Interval Tree)

区间树也是一种二叉查找树,是在红黑树基础上进行扩展得到的支持以区间为元素的动态集合,其中每个节点的关键值是区间的左端点

使用区间树,区间内元素的查找和插入都可以在O(lgN)的时间内完成(这里的区间查找的并不是精确查找,而是查找和给定区间重叠的元素)

这里写图片描述

区间树的ADT


/*interval结构体声明*/
typedef struct interval
{
    int high;
    int low;
}Interval;
/*IntervalNode结构体声明*/
typedef struct _intervalNode
{
    int key;
    bool color;
    struct _intervalNode *parent;
    struct _intervalNode *left;
    struct _intervalNode *right;

    struct Interval interval;
    int max;
}IntervalNode;
/*IntervalTree结构体声明*/
typedef struct _intervalTree
{
    Interval *root;
}IntervalTree;

这里Interval(额外信息)被封装在IntervalNode(结点结构)中,而后者又被封装在IntervalTree(树结构)中,层层封装。

这样的做法思路清晰,便于理解,之前我们对AVL树的处理是没有写出树结构,而最后使用时在main函数中定义一个结点指针,它就相当于这里的root指针,后面所有的操作都是在它的基础上进行的,对于AVL我们也可以借鉴这里的做法,把一个root指针封装到声明的树结构中,然后要用时使用树结构就可以了

应用区间树的情形,必须符合区间加法,反例如求众数的相关问题就不能用线段树——所谓满足区间加法、区间减法的意思,如果知道[l, m]和[m+1, r]的信息,可以求出[l, r]的信息,就说[l, m]和[m, r]满足区间加法,如果知道了[1, x]和[1, y]的信息,可以求出[x+1, y]的信息,就说[1, x]和[1, y]满足区间减法(请在脑子里画一个数轴表示区间的图,理解起来很简单)

区间树的基本操作

1)建树

2)查询

3)插入

4)删除

5)更新

区间树的一个特殊形式就是线段树(而且通常说线区间就是指线段树),下面重点讲解线段树

在讲之前,介绍一下我的代码风格:因为是区间所以我不喜欢用left、right来表示(那是表示链表这样的元素之间的关系),我会用low、high来表示相应区间的左边界、右边界,如果是查询的话,因为给定了范围我会用大些字母L、H替代low、high,这样看起来很清爽



线段树(Segment Tree)

线段树是特殊的区间树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点(线段树的结点表示线段区间)

线段树快速查找的理论基础是:当n≥3时,一个[1, n]的线段树可以将[1, n]的任意子区间[L, R]分解为不超过2*[ log2(n-1)]个子区间

在讨论线段树时,[L, R]是值从L到R这(R-L+1)个连续的离散点,而不是真的指一条线段

这里写图片描述

因为线段树是一棵二叉查找树,所以它的操作集中除了建树是O(N),其它的都是O(logN)

线段树的ADT

/*顺序存储(推荐)*/
typedef struct _segmentNode
{
    int low;
    int high;
    int sum;
}SegmentTree[NODESIZE];  //NODESIZE = 4*n(n为需要区间长度,包括两端)

/*链式存储(和顺序存储比较)*/
typedef strut _segmentNode
{
    struct _segmentNode *left;
    struct _segmentNode *right;
    int sum;
}SegmentNode;

怎么说呢,看个人习惯吧——不过使用顺序存储的貌似相对容易实现一点,特别是后面的update等操作的时候;另外都知道顺序存储的查找效率要高些;并且使用数组甚至可以不用存储每个结点的左右孩子(详见后文)

这里上一张基于数组的线段树图(来自网络)

这里写图片描述

(规律)可以看出, 对于线段树而言,一个结点下标为n,则它的左孩子下标为2*n,右孩子下标为2*n+1;又因为它是一步一步构造出来的这一点很清楚,叶子节点只会出现在最下层和次下层,所以一棵线段树一定是一棵完全二叉树(一类查找效率很高的树),在特殊情况下还会成为效率极高的满二叉树

另外如果使用链式存储,为了方便描述,我们通常会给原始数组(之后要把这里面的数据填进线段树中)建立一个数组,以后取用的使用也好找;对于每个结点由于下标唯一对应,我们还可以建立一个描述结点存储的额外信息的数组(如下面的sum数组)

define DATASIZE 100000
int Data[DATASIZE];
int sum[DATASIZE<<2];
int tag[DATASIZE<<2];

线段树的构造及初始化(包括一次整棵树的更新)

线段树的构造(不单指建立树根而是指建立整棵树)实际上是利用了二分法的思想

可以看出,线段树丛树根(从high、low的值看可以表示要讨论的整个区间)往下,每一次的分叉(划分区间),都进行如下操作:

1)由父结点[L, R],求出M=(L+R)/2,这里的除法是『整除』(向下取整,体现离散的点)

2)将父区间划分为两个子区间分别是[L, M]和[M+1, R]

3)对新产生的两个子区间递归进行步骤2和步骤3的操作

4)直到L==R,该结点是树叶结点

/*函数功能:自下而上更新当前结点的数据域
 *请参说明:index就是当前结点的下标
 *备注解释:该函数不直接出现,而是被封装到BuildSegmentTree中;这里偷个懒了,认为每个结点存储了它的左右孩子
 */
 void PushUp(int index){sum[index] = sum[left] + sum[right]}
/*函数功能:从left到righ指定区间构建一棵线段树
 *请参说明:low:当前区间左边界,high:当前区间右边界,index:当前结点编号
 *备注解释:使用顺序存储,好处就是不用维护额外的指针域,这里直接传入原始类型;坏处就是每个结点可以保存的内容只能是int之类的整数类型,这一点要注意;另外看代码,这里我选择当前区间的和sum作为要保存的内容,在时机构造中可以灵活选择(必须是整数类型)
 */
void BuildSegmentTree(int index, int low, int high)
{
    if(high == low)  //比如[2,2],表示已尽达到树叶结点
    {
        sum[index] = Data[low];
        return;
    }
    BuildSegmentTree(index<<1, left, (high+low)>>1);
    BuildSegmentTree(index<<1|1, (high+low)>>1+1, right);
    PushUp(index);
}   

如果待查找区间是2的n次幂的话,那么构造出来的线段时就是一棵满二叉树

线段树用来对编号连续的一些点进行修改或者统计操作,修改和统计的复杂度都是O(logN)

从线段树的分叉可以看出,如果最终要查找的区间长度为n,至少需要为线段树结点分配(2n-1)*结点大小的内存(等号当前仅当该线段树成为一棵满二叉树时成立);鉴于此,在实际工程中往往声明结点个数为4*n(n为区间长度)

线段树的更新(修改原数组元素的值)

当有新的结点被插入到线段树中,或修改了原来的老数据,就要更新数据了

/*函数说明:点更新,让Data[X]+=C
 *请参说明:X:目标结点下标,C:常数,low:当前区间左边界,high:当前区间右边界,index:当前结点下标
 */
void Update(int X, int C, int low, int high, int index)
{
    if(low == high)
    {
        sum[index] += C;
        return;
    }
    int m = (low+high)>>1;
    if(X <= m) Update(X, C, low, m, index<<1)
    else Update(X, C, m+1, high, index<<1|1)
    PushUp(index);
}

线段树的查询(以求和为例)

因为上面写的线段树ADT中保存的数据域是sum,所以这里查询就是求指定区间的sum;在实际应用中注意灵活处理

/*函数功能:查询[L..H]的和
 *请参说明:L:查询区间左边界,H:查询区间右边界,low:当前区间左边界,high:当前区间右边界,index:当前结点下标
 */
void Query(int L, int H, int low, in high, int index)
{
    if(L <= low && H >= high) return sum[index];

    int m = (low+high)>>1;
    int ans = 0;
    if(L <= m) ans += Query(L, H, low, m, index<<1);
    if(H >= m) ans += Query(L, H, m+1, high, index<<1|1);
    return ans;
}
Algorithm:【算法进阶之路】之算法面试刷题集合—树结构算法的简介(二叉树/多路查找树/字典树)、具体结构(FBT/CBT/BST/BBT/Heap/Huffman、B树/B+树/R树、字典树) ​ Algorithm:【算法进阶之路】之算法面试刷题集合—树结构算法的简介(二叉树/多路查找树/字典树)、具体结构(FBT/CBT/BST/BBT/Heap/Huffman、B树/B+树/R树、字典树)及其运算(增删查/遍历/旋转)、代码实现之详细攻略 目录 一、树结构 1.1、二叉树的简介 1.2、多路查找树(特殊的多叉树)——二叉到多叉的思考 1.3、字典树(Trie) 二、二叉树相关的算法题集合 早期—二叉 阅读详情

相关推荐

STM32实现薄膜压力传感器数据采集(标准库和HAL库实现)

薄膜压力传感器采集压力数据

XiaoCaiDaYong的博客 3万+

关于查找树的总结【BST、AVL、红黑树、B树、B+树】

前言 关于各种查找树,可以说是面试、学习绕不开的点,正好最近我也有这种需求,就来进行一波总结与讨论,争取把这些查找树的用法、关联讲清楚 当然,这篇文章就类似于一篇使用指南、手册,如果要对哪一块进行一个深入的探究,那么还是需要再单独去查阅相关的文献与资料,如果和您的预期不符,请及时止损(因为本人很多时候都会抱着极大的期待去点开一篇文章,然而内容和标题都不符合 在这多叨叨一句,我认为知其然还需要知其所以然,探索一个啥知识点,就好就是用来干嘛的,从哪来的,有啥优点又有啥缺点,有缺点怎么解决,其实这些树之间,都是

qq_44197482的博客 1522

抽象代数学习笔记(抽象代数的历史、运算)

1. 抽象代数历史 对于五次及以上的代数方程是否存在根式解这个问题,欧拉、拉格朗日、高斯等数学家都没得到答案,但是他们得出五次及以上的代数方程组不存在根式解的结论。19世纪初,阿贝尔提出五次及以上的代数方程不存在根式解,并找出一类特殊的五次及以上的代数方程可以用根式解表示出来。另外一个年轻的数学家伽罗华...

huangzhpei的博客 3668

数据结构学习笔记——查找算法中的树形查找(B树、B+树)

B树和B+树属于树形查找算法中的一种,主要用于数据库系统、文件系统和磁盘存取等方面,都是用于存储和索引大量的数据,以提高检索效率。例如,在磁盘存储中,通过将数据分散到多个磁盘块中,并使用树形结构来组织这些磁盘块,从而提高了查找速度和查找效率。若设B树中所有结点的孩子结点个数的最大值为m,则该B树是一棵m阶B树,另外B+树则是B树的变形。B+树可以由分块查找推广,所以也称为多级分块查找,即m阶B+树,它是B树的变形,与B树相同,B树和B+树都是平衡的多叉树,都用在文件索引结构和数据库索引中,但B+树更加适用。

小宇y的博客 3199

C#操作MySQL从入门到精通(1)——安装MySQL数据库

我们使用C#操作MySQL数据库第一步要做的就是安装MySQL数据库,本文就是详细讲解如何安装MySQL数据库。

qq_34059233的博客 964

17、激光切割质量影响因素及优化研究

本文研究了激光切割过程中焦点位置、入射角、激光功率、切割速度和气体压力等因素对切割质量的影响,采用响应面方法(RSM)和Box-Behnken设计,基于三种不同喷嘴对SS 304不锈钢进行实验。通过建立切缝宽度比和条纹深度的回归模型,并结合方差分析验证模型有效性,深入分析各参数对切割质量的影响机制。研究发现喷嘴结构差异显著影响参数作用趋势,尤其在切割速度和气体压力响应上表现明显。最终通过多目标优化确定最小激光功率、适中切割速度和最大气体压力为最优组合,显著提升切割质量,为实际生产提供了理论依据和技术支持。未

grape的博客 132

数据结构与算法:树形查找

新星计划:数据结构与算法,学习笔记,创作打卡3!本文主要是对树形查找中的二叉排序树、平衡二叉树、红黑树、B树和B+树的详细介绍!

墨城烟柳ベ旧人殇的博客 2474

基于 STM32 的全自动洗车监控系统设计与实现

本文提出一种基于 STM32F103RCT6 芯片的全自动洗车监控系统方案,通过多传感器融合与智能控制算法,实现车辆检测、洗车流程自动化及状态远程监控。系统集成硬件选型、电路设计、软件流程及通信功能,可广泛应用于智能洗车场景。

weixin_42300449的博客 964

二叉查找树(C++)

二叉查找树是什么呢?(也叫二叉搜索树)就是字面意思,首先是一颗二叉树:一棵树,最多分两个叉。1.1 满二叉树上面那个图就是一颗二叉树,同时也是一颗满二叉树:每一个节点都有两个儿子,左儿子和右儿子。同时啊,还有完全二叉树:一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。1.2 完全二叉树1.3 普通二叉树。

wo_ai_luo_的博客 1538

你的小龙虾 OpenClaw 还很笨?快给它装上这几个核心技能(OpenClaw 必装skills整理)

🦞 刚装小龙虾OpenClaw? 本文分享一些OpenClaw必装技能,从视觉理解到智能学习,让它从“会聊天”变“会干活” 🚀

XiaoqiangClub的博客 871

32、机械臂数字孪生模型创建指南

本文介绍了如何在MATLAB Simscape中为一款3自由度(DOF)微型机械臂创建数字孪生模型。内容涵盖硬件参数测量、Simulink模型搭建流程,包括初始条件设置、基座与上部主体构建、上臂结构设计等关键步骤。通过使用Solid、Revolute Joint和Rigid Transform等模块,实现机械臂多体系统建模,并支持后续运动仿真与物理系统的对比诊断。读者可依据指南逐步完成模型构建,相关资源可通过MATLAB File Exchange或指定网站获取。

qsc9012345的博客 215

数据结构学习笔记——查找算法中的树形查找(二叉排序树)

> 二叉排序树的查找过程与折半查找(二分查找)相似,即折半查找的判定树就是一棵二叉排序树。# 一、二叉排序树的定义二叉排序树也称为二叉查找树,其中,各结点值的关系是:`左子树

小宇y的博客 1万+

桌面运维工程师面试题汇总(高频完整版)

岗位,覆盖:电脑硬件、系统安装与故障、网络、办公软件、安全、AD域、打印机、日常排错、服务意识。

老韩的Linux云计算架构师进阶之路 923

[数据结构与算法] 树结构之二叉排序树、平衡二叉树、多路查找树

树结构之BST和AVL树 BST介绍创建和遍历 删除 AVL树介绍 BST 介绍 需求: 给你一个数列 (7, 3, 10, 12, 5, 1, 9),要求能够高效的完成对数据的查询和添加 解决方案——使用数组 数组未排序, 优点:直接在数组尾添加,速度快。 缺点:查找速度慢. 数组排序,优点:可以使用二分查找,查找速度快,缺点:为了保证数组有序,在添加新数据时,找到插入位...

时间静止 1957

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

数据结构之树

本篇只涉及树的概念范围和应考思路,不涉及具体结构或算法的实现与思考 1.树 在计算器科学中,树(英语:tree)是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点: ①每个节点有零个或多个子节...

lishinho的博客 6万+

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

【算法与数据结构 09】什么是树、二叉树、二叉查找树?

人类都听的懂的《白话数据结构》~

不积跬步,无以至千里! 3719

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

数据结构---树(所有类型的树总结)

文章目录数据结构---树1.二叉树2.二叉查找树-BST3.平衡二叉树-AVL4.红黑树5.哈夫曼树6.B树7.B+树8.R树总结 数据结构—树 本章总结数据结构中一些树的特征和结构。转载自这里 1.二叉树 二叉树:最多有两颗子树的树被称为二叉树。 二叉树又分为斜树,满二叉树,完全二叉树: 斜树:所有节点都只有左子树的二叉树叫做左斜树,所有节点都只有右子树的二叉树叫做右斜树。(本质就是链表) 满二叉树:二叉树中所有非叶子结点的度都是2,且叶子结点都在同一层次上。 完全二叉树:如

小皮皮LL的博客 7738

树、二叉树、二叉查找树、AVL 树及红黑树的深入解析

本文系统阐述了树的基本概念、术语及常见结构,包括树的定义、遍历方式等内容。详细介绍了二叉树的定义、性质、分类,对比了二叉树与度为 2 的树的区别。对二叉查找树进行了代码实现与性能分析,指出其在理想与最坏情况下的性能差异。深入讲解了平衡二叉搜索树(AVL 树)的特点与性能优势。重点探讨了红黑树,从定义、性质、代码实现,到左旋右旋操作的原理及代码展示,还列举了其在数据结构、操作系统、数据库、网络服务器等多领域的应用场景,并对比了二叉搜索树、AVL 树和红黑树在定义特性、平衡策略、操作时间复杂度、适用场景、空间复

m0_73359068的博客 1694

树、二叉树、二叉查找树(二叉搜索树)

树、二叉树、二叉查找树、二叉搜索树

樱木天亥 1649

关于二叉树、二叉查找树、平衡二叉树、红黑树、B树、B+树的区别和特性

二叉树、平衡二叉树、红黑树、B树和B+树详解

WanguideShenghuo的博客 1229

数据结构(一)、二叉树(BT),二叉查找树(BST),平衡二叉树(AVL树)

数据结构中有很多树的结构,其中包括二叉树、二叉搜索树、红黑树等等。本文中对数据结构中的二叉树以及二叉查找树,平衡二叉树AVL进行介绍,如果对你有帮助的话,请点个赞吧~谢谢!

王义凯 的博客 5391

【十二】数据结构之树、二叉树、二叉查找树、平衡二叉查找树AVL、红黑树、B树、B+树简介

树 二叉树 二叉查找树 平衡二叉树 红黑树 B树

Sid小杰的博客 2181

什么是树(Tree),数据结构中一般有哪些树?

树(Tree)的基本概念 树是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。 文章目录二叉树二叉树的遍历方法AVL树红黑树B树B+树 二叉树 每个结点至多拥有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序...

凯哥大数据——刘凯的博客 8454

数据结构-树、二叉树、查找树、平衡树

专栏原创出处:github-源笔记文件 ,github-源码 ,欢迎 Star,转载请附上原文出处链接和本声明。 文章目录1. 树的概念2. 二叉树2.1 二叉树的遍历2.2 二叉树的类型二叉树相关算法参考 1. 树的概念 具有 n(n≥0)n(n\ge 0)n(n≥0) 个节点的有限集称为树。 当 n=0n = 0n=0 时称为空树; 当 n≥1n\ge 1n≥1 时,仅有一个特定的称为根...

2.wa 973

4. 树(二叉树、二叉查找树/二叉排序树/二叉搜索树、平衡二叉树、平衡二叉B树/红黑树)

二叉树是一种常见的树状数据结构,由节点组成,每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树结构,在这种树中,每个节点的值都大于其左子树中的任何节点的值,且小于其右子树中的任何节点的值。这使得二叉查找树具有高效的搜索和插入操作。平衡二叉树(Balanced Binary Tree),也称为 AVL 树(平衡二叉搜索树),是一种特殊的二叉查找树,它的特点是每个节点的左子树和右子树的高度差不超过1。

kuxiaoyang的博客 1243

二叉查找树、平衡二叉树、B树、B+树、红黑树介绍

1、二叉查找树 二叉查找树BST(binary search/sort tree)又叫二叉搜索树或者二叉排序树,它首先是一个二叉树,而且必须满足下面的条件: 1)若左子树不空,则左子树上所有结点的值均小于它的根节点的值(左小) 2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值(右大) 3)左、右子树也分别为二叉排序树 2、平衡二叉树 平衡二叉树(AVL树),又称自平衡二叉查找树。平衡二叉树必定是二叉搜索树,反之则不一定。满足下面的条件: 1、左结点小于根节点,右结点大于根节点.

promisessh的博客 1491
上一篇: 【数据结构】完美二叉树, 完全二叉树和完满二叉树
下一篇: 【机器学习】参数与超参数
HelloZEX
博客等级 码龄11年 486粉丝 264原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值