redisread / HUGO_blog

存储Hugo博客项目

Home Page:http://hugo.jiahongw.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

数据结构

redisread opened this issue · comments

  1. 最近公共祖先
  2. 红黑树

常见的二叉树

以下的二叉树采用的结构都为链式结构

typedef  struct BiTNode    /* 结点结构 */
{
    int data;    /* 结点数据 */
    struct BiTNode *lchild, *rchild; /* 左右孩子指针 */
} BiTNode, *BiTree;

1. 二叉排序树

二叉排序树又称“二叉查找树”、“二叉搜索树”。

定义

或者是一棵空树,或者是具有下列性质的二叉树:

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

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

  3. 它的左、右子树也分别为二叉排序树。

二叉查找树

中序遍历二叉排序树可得到一个依据关键字的有序序列,一个无序序列可以通过构造一棵二叉排序树变成一个有序序列,构造树的过程即是对无序序列进行排序的过程。每次插入的新的结点都是二叉排序树上新的叶子结点,在进行插入操作时,不必移动其它结点,只需改动某个结点的指针,由空变为非空即可。搜索、插入、删除的时间复杂度等于树高,期望O(logn),最坏O(n)(数列有序,树退化成线性表,如右斜树)。

查找算法

查找过程:

1.若b是空树,则搜索失败,否则:

2.若x等于b的根节点的数据域之值,则查找成功;否则:

3.若x小于b的根节点的数据域之值,则搜索左子树;否则:

4.查找右子树。

5.若查找不成功, 则指针 p 指向查找路径上访问的最后一个结点并返回FALSE

插入算法

插入过程:

  1. 先调用查找操作将要插入的关键字进行比较

  2. 如果在原有的二叉排序树中没有要插入的关键字,则将关键字与查找的结点p(在查找操作中返回的结点)的值进行比较

  3. 若p为空,则插入关键字赋值给该节点;

  4. 若小于结点p的值,则插入关键字作为结点p的左子树;

  5. 若大于结点p的值,则插入关键字作为结点p的右子树;

每次需要插入的节点都为叶子节点。

删除算法

删去一个结点,分三种情况讨论:

  1. 若*p结点为叶子结点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则只需修改其双亲结点的指针即可。

  2. p结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点f的左子树(当p是左子树)或右子树(当p是右子树)即可,作此修改也不破坏二叉排序树的特性。

  3. p结点的左子树和右 子树均不空。在删去p之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整。比较好的做法是,找到*p的**直接前驱(或直接后继)s,用s来替换结点p,然后再删除结点s。(依靠中序遍历在p节点下进行遍历得到的最后一个数即为替换的节点)

删除算法

性能分析

  • 最好的情况是二叉排序树的形态和折半查找的判定树相同,其平均查找长度和logn成正比(O(log2(n)))。
  • 最坏情况下,当先后插入的关键字有序时,构成的二叉排序树为一棵斜树,树的深度为n,其平均查找长度为(n + 1) / 2。也就是时间复杂度为O(n),等同于顺序查找。

虽然二叉排序树的最坏效率是O(n),但它支持动态查找。最好是把它构建成一棵平衡的二叉排序树(平衡二叉树),这些平衡二叉树可以使树高为O(logn),如AVL、红黑树等。

2. 平衡二叉树(AVL)

定义

它或者是一颗空树,或者具有以下性质的二叉树:它的左子树和右子树的深度之差的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。

平衡因子(bf):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1;

在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树

查找、插入和删除在平均和最坏情况下的时间复杂度都是$O(log(n))$。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。

查找操作

平衡二叉树的查找基本与二叉查找树相同。

插入操作

在平衡二叉树中插入结点与二叉查找树最大的不同在于要随时保证插入后整棵二叉树是平衡的。那么调整不平衡树的基本方法就是: 旋转 。

首先,还需要明白的一个概念就是:

最小不平衡子树的根结点:也就是当你进行插入操作时,找到该需要插入结点的位置并插入后,从该结点起向上寻找(回溯),第一个不平衡的结点即平衡因子bf变为-2或2的节点。

那究竟是如何“转”的呢?

其实,可以换一种思路思考,不让它叫“旋转”!而叫——>“两个结点的变换”

下面分情况分析四种旋转方式

左左

即在x的左孩子a的左孩子c上插入一个结点y(该结点也可以是c,如图①),即y可以是c,也可以是c的左孩子(如图②),也可以是c的右孩子(不在画出)

这种左左插入方式有一个规律:不平衡子树的左子树深度比右子树深度大2.

左左旋转

图①②插入的节点都为y,此时向上回溯第一个不平衡的子树根节点为x,那么将x节点及其右子树(图①为NULL,图②为b)一起绕着x的左子树根节点(即a)右旋(即顺时针旋转),然后将a的右子树作为x的左子树,假如a的右子树为空则不必插入。那么这样旋转最后将不平衡子树变为平衡。

右右

即在x的右孩子a的右孩子c上插入一个结点y(该结点也可以是c,如图①),即y可以是c,也可以是c的右孩子(如图②),也可以是c的左孩子(不在画出)

这种右右插入方式有一个规律:不平衡子树的左子树深度比右子树深度小2.

右右旋转

图①②插入的节点都为y,此时向上回溯找到第一个不平衡子树的节点为x,需要将节点x及其左子树(图①为NULL,图二为b)绕着x右子树(两图都为a为根节点的子树)进行左旋(逆时针旋转),然后将其右子树(a)的左节点作为x的右节点,这样使得不平衡子树又再度平衡。

左右

即在x的左孩子a的右孩子c上插入一个结点y(该结点也可以是c,如图①),即y可以是c,也可以是c的右孩子(如图②),也可以是c的左孩子(不在画出)

这种左右插入的规律就是:不平衡子树的左子树高度比右子树大2且左子树的右子树比左子树的左子树深度深。

左右旋转

向上回溯的第一个不平衡子树为x,先对x的左子树左旋(旋转中心为c),再对x的左子树进行右旋(旋转中心为c)。(旋转中心为左子树的右节点)

如果是图①,旋转中心为y

右左

即在x的右孩子a的左孩子c上插入一个结点y(该结点也可以是c,如图①),即y可以是c,也可以是c的右孩子(如图②),也可以是c的左孩子(不在画出)

这种右左插入的规律就是:不平衡子树的右子树高度比左子树大2且右子树的左子树比右子树的右子树深度深。

右左旋转

向上回溯的第一个不平衡子树为x,先对x的右子树右旋(旋转中心为c),再对x的右子树进行左旋(旋转中心为c)。(旋转中心为左子树的右节点)

如果是图①,旋转中心为y

AVL树的操作汇总:

操作汇总

删除操作

删除类似插入的操作。删除时少一个结点,也就是该结点所在的子树深度可能会减小,而插入时多一个结点,该结点所在的子树深度可能会增加,所以递归删除一个结点时,回溯时找到最小不平衡子树的根结点时,要向相反的方向去找属于哪种情况;

如图y为要删除的节点

删除操作

图①:y结点删除后,回溯到x结点从bf=-1变为bf=-2;则需从相反方向即从x的右孩子的方向向下检查属于哪种情况,显然第一个方向为1:右;第二个方向看a的bf的值——若为1时,那就相当于插入时‘右左’的情况;若为-1时,那就相当于插入时‘右右’的情况;可现在a的bf既不是1也不是-1而是0,这就是删除的特殊情况了!我们不妨试试对他进行类似于插入时的‘右右’操作,看怎么样~ 如上图,经过变换后该子树平衡了!但是因子的修改就跟插入时的‘右右’不一样了!此时变为:x的bf=-1,a的bf=1;所以我们不妨就把a的bf=0也归纳为删除的‘右右’或‘左左’(如图②,不再敖述)操作;

那么删除时因子的改变需在插入时因子的改变中添加上:

左左:前a:bf=0 后x:bf=1,a:bf=-1; 右右:前a:bf=0 后x:bf=-1,a:bf=1;其他不变!

可以想象,其实是很简单的道理:除了特殊情况其他都与插入的情况一模一样,说白了就是把深度大的子树(根结点的其中一个)向深度小子树贡献一个深度,那么这样一来,该子树(对于根结点所领导的树)的深度是不是比原来的小1了?!所以要继续向上一个一个进行检索,直到根结点为止!

代码实现

https://blog.csdn.net/nightwizard2030/article/details/72874715

性能分析

优势

平衡二叉树的优势在于不会出现普通二叉查找树的最差情况。其查找的时间复杂度为$O(logN)$。

缺陷

  1. 为了保证高度平衡,动态插入和删除的代价也随之增加.
  2. 所有二叉查找树结构的查找代价都与树高是紧密相关的,能否通过减少树高来进一步降低查找代价呢。

应用场景

应用:windows对进程地址空间的管理用到了AVL树。

3. 红黑树

也被称为"对称二叉B树"。

定义

红黑树(red-black tree) 是一棵满足下述性质的二叉查找树:

  1. 每一个结点要么是红色,要么是黑色。

  2. 根结点是黑色的。

  3. 所有叶子结点都是黑色的(实际上都是Null指针,下图用NIL表示)。叶子结点不包含任何关键字信息,所有查询关键字都在非终结点上。

  4. 每个红色结点的两个子节点必须是黑色的。换句话说:从每个叶子到根的所有路径上不能有两个连续的红色结点

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

红黑树

几个概念:

黑深度 ——从某个结点x出发(不包括结点x本身)到叶结点(包括叶子结点)的路径上的黑结点个数,称为该结点x的黑深度,记为$bd(x)$,根结点的黑深度就是该红黑树的黑深度。叶子结点的黑深度为0。比如:上图$bd(13)=2, bd(8)=2, bd(1)=1$

内部结点 —— 红黑树的非终结点

外部节点 —— 红黑树的叶子结点

相关原理

  1. 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。
  2. 红黑树的树高$(h)$不大于两倍的红黑树的黑深度$(bd)$,即$h<=2bd$
  3. 一棵拥有n个内部结点(不包括叶子结点)的红黑树的树高$h<=2log(n+1)$

查找操作

因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的查找操作与普通二叉查找树上的查找操作相同.

插入操作

我们首先以二叉查找树的方法增加节点并标记它为红色。下面要进行什么操作取决于其他临近节点的颜色。同人类的家族树中一样,我们将使用术语叔父节点来指一个节点的父节点的兄弟节点

假设新加入的结点为N,父亲结点为P,叔父结点为Ui(叔父结点就是一些列P的兄弟结点),祖父结点G(父亲结点P的父亲)。

情况1. 当前红黑树为空,新结点N位于树的根上,没有父结点。

此时很简单,我们将直接插入一个黑结点N(满足性质2),因为是特殊大的情况,不插入红色而插入黑色节点。

情况2. 新结点N的父结点P是黑色。

在这种情况下,我们插入一个红色结点N(满足性质5)

注意:在情况3,4,5下,我们假定新节点有祖父节点,因为父节点是红色;并且如果它是根,它就应当是黑色。所以新节点总有一个叔父节点,尽管在情形4和5下它可能是叶子。

情况3.如果父节点P和叔父节点U二者都是红色。

如下图,因为新加入的N结点必须为红色,那么我们可以将父结点P(保证性质4),以及N的叔父结点U(保证性质5)重新绘制成黑色。如果此时祖父结点G是根,则结束变化。如果不是根,则祖父结点重绘为红色(保证性质5)。但是,G的父亲也可能是红色的,为了保证性质4。我们把G递归当做新加入的结点N在进行各种情况的重新检查。

情况3

注意:在情形4和5下,我们假定父节点P 是祖父结点G 的左子节点。如果它是右子节点,情形4和情形5中的左和右应当对调。

情况4. 父节点P是红色而叔父节点U是黑色或缺少; 另外,新节点N是其父节点P的右子节点,而父节点P又是祖父结点G的左子节点。

如下图, 在这种情形下,我们进行一次左旋转调换新节点和其父节点的角色(与AVL树的左旋转相同); 这导致某些路径通过它们以前不通过的新节点N或父节点P中的一个,但是这两个节点都是红色的,所以性质5没有失效。但目前情况将违反性质4,所以接着,我们按下面的情况5继续处理以前的父节点P。

情况4

情况5. 父节点P是红色而叔父节点U 是黑色或缺少,新节点N 是其父节点的左子节点,而父节点P又是祖父结点的G的左子节点。

如下图: 在这种情形下,我们进行针对祖父节点P 的一次右旋转; 在旋转产生的树中,以前的父节点P现在是新节点N和以前的祖父节点G 的父节点。我们知道以前的祖父节点G是黑色,否则父节点P就不可能是红色。我们切换以前的父节点P和祖父节点G的颜色,结果的树满足性质4[3]。性质 5[4]也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过祖父节点G ,现在它们都通过以前的父节点P。在各自的情形下,这都是三个节点中唯一的黑色节点。

情况5

删除操作

相较于插入操作,红黑树的删除操作则要更为复杂一些。删除操作首先要确定待删除节点有几个孩子,如果有两个孩子,不能直接删除该节点。而是要先找到该节点的前驱(该节点左子树中最大的节点)或者后继(该节点右子树中最小的节点),然后将前驱或者后继的值复制到要删除的节点中,最后再将前驱或后继删除。由于前驱和后继至多只有一个孩子节点,这样我们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。我们并不关心最终被删除的节点是否是我们开始想要删除的那个节点,只要节点里的值最终被删除就行了,至于树结构如何变化,这个并不重要。

应用场景

工业界最主要使用的二叉搜索平衡树,广泛用在C++的STL中。如map和set都是用红黑树实现的。Java用它来实现TreeMap。著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块。

  • epoll在内核中的实现,用红黑树管理事件块
  • nginx中,用红黑树管理timer等

B树

背景

一个比较实际的问题:就是大量数据存储中,实现查询这样一个实际背景下,平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。那么如何减少树的深度(当然不能减少查询数据量),一个基本的想法就是:

  1. 每个节点存储多个元素 (但元素数量不能无限多,否则查找就退化成了节点内部的线性查找了)。

  2. 摒弃二叉树结构,采用多叉树 (由于节点内元素数量不能无限多,自然子树的数量也就不会无限多了)。

这样我们就提出来了一个新的查找树结构 ——多路查找树。 根据AVL给我们的启发,一颗平衡多路查找树(B~树) 自然可以使得数据的查找效率保证在O(logN)这样的对数级别上。

  1. B-树

    B-树是一种多路搜索树

    性质:

    1. 根结点至少有两个子女;

    2. 每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;

    3. 除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;

    4. 所有的叶子结点都位于同一层。

    用在磁盘文件组织 数据索引和数据库索引。

  2. B+树

    B+树是B-树的变体,也是一种多路搜索树:

    1.其定义基本与B-树同,除了:

    2.非叶子结点的子树指针与关键字个数相同;

    3.非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树

    (B-树是开区间);

    5.为所有叶子结点增加一个链指针;

    6.所有关键字都在叶子结点出现;

    用在磁盘文件组织 数据索引和数据库索引。

    B和B+主要用在文件系统以及数据库中做索引等,比如Mysql:B-Tree Index in MySql

    红黑树和多路查找树都是属于深度有界查找树(depth-bounded tree —DBT)

2-3-4树

2-3-4 树计算机科学中是阶为 4 的B树

2-3-4树

2-3-4 树把数据存储在叫做元素的单独单元中。它们组合成节点,每个节点都是下列之一

  • 2-节点,就是说,它包含 1 个元素和 2 个儿子,
  • 3-节点,就是说,它包含 2 个元素和 3 个儿子,
  • 4-节点,就是说,它包含 3 个元素和 4 个儿子 。

2节点

三节点

四节点

每个儿子都是(可能为空)一个子 2-3-4 树。节点是其中没有父亲的那个节点;它在遍历树的时候充当起点,因为从它可以到达所有的其他节点。叶子节点是有至少一个空儿子的节点。

B树一样,2-3-4 树是有序的:每个元素必须大于或等于它左边的和它的左子树中的任何其他元素。每个儿子因此成为了由它的左和右元素界定的一个区间

2-3-4 树是红黑树的一种等同,这意味着它们是等价的数据结构。换句话说,对于每个 2-3-4 树,都存在着至少一个数据元素是相同次序的红黑树。在 2-3-4 树上的插入和删除操作也等价于在红黑树中的颜色翻转和旋转。这使得它成为理解红黑树背后的逻辑的重要工具。

  1. 字典树·(又称trie 树,单词查找树)

    1.又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。
    典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。

    2.它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。

    3.字典树与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。

    用在统计和排序大量字符串,如自动机。

    trie 树的一个典型应用是前缀匹配,比如下面这个很常见的场景,在我们输入时,搜索引擎会给予提示

    还有比如IP选路,也是前缀匹配,一定程度会用到trie

  2. 后缀树

  3. 广义后缀树


参考:

  1. https://www.cnblogs.com/zhuyf87/archive/2012/11/09/2763113.html 二叉排序树
  2. https://www.cnblogs.com/fornever/archive/2011/11/15/2249492.html 平衡二叉树(解惑)
  3. https://www.iteye.com/blog/hxraid-609949 平衡二叉查找树
  4. https://www.iteye.com/blog/hxraid-611816 红黑树(RBT)
  5. https://www.zhihu.com/question/30527705 树的应用场景

快速排序

int partition(vector<int> &num, int left, int right)
{
    int p = num[left];
    while (left < right)
    {
        while (left < right && num[right] >= p)
            --right;
        num[left] = num[right];
        while (left < right && num[left] <= p)
            ++left;
        num[right] = num[left];
    }
    num[left] = p;
    return left;
}

// 快速排序
void quickSort(vector<int> &num, int left, int right)
{
    if (left < right)
    {
        int p = partition(num, left, right);
        quickSort(num, left, p);
        quickSort(num, p + 1, right);
    }
}

堆排序

void heap(vector<int> &num, int n)
{
    if(n == 1) return;
    for(int i = n / 2 - 1;i >= 0;--i)
    {
        int tmax = i;
        int left = 2 * i + 1,right = 2 * i + 2;
        if(left < n && num[left] > num[tmax]) tmax = left;
        if(right < n && num[right] > num[tmax]) tmax = right;
        if(tmax != i) swap(num[i],num[tmax]);
    }
    swap(num[0],num[n-1]);
    heap(num,n-1);
}