数据结构(二)——查找算法、树

查找算法

  • 二分查找
    略 mid=(lo + hi)/2;

  • 数组

  • 普通链表

  • 内核链表

  • 企业链表

    • 实现例子:
        typedef struct LISTNODE//结点定义
				{
				    struct LISTNODE *next;
				}ListNode;
        typedef struct Person//需要把结点放在最前面(注意,这里不是指针,如果要使用指针,需要改一下定义)
				{
				    ListNode node;
				    char name[20];
				    int age;
				}Person;
        //向指定位置插入结点
				int LinkedList::List_Insert(ListNode *data,int pos)//不必释放空间
				{
				    if(nullptr == data)
				        return -1;
				    if((pos<0)||(pos>length))
				        pos = length;
				    ListNode* pCurrent = &head;
				    while(pos--)
				        pCurrent = pCurrent->next;

				    data->next = pCurrent->next;
				    pCurrent->next = data;

				    length++;

				    return 0;
				}
				//调用方法
				myList->List_Insert((ListNode*)(&p[1]),1);//需要强制转换
    • 后进先出

    • 后缀表达式实现

      • 构造后缀表达式:

        • 循环进行,直至表达式为空,如果是数字则直接输出;

        • ‘(‘直接入栈,’)‘则将’(‘之前的符号全部输出,并弹出’(’;

        • 如果是其它符号,若栈为空则直接入栈,否则如果栈顶元素的优先级大于当前符号则弹出栈顶元素,直至栈顶元素优先级小于该元素或栈为空,这时候将该符号入栈

      • 运算后缀表达式:

        • 循环进行,直至表达式为空,如果是数字则进栈

        • 如果是符号,(这里我们假设是’+'这类符号)则从栈中弹出一个元素,把它放在符号的右边,再从栈中弹出一个元素,把它放在符号的左边,然后运算,之后把结果入栈

    • 二叉树遍历非递归实现

      • 这里我们需要新建一个结构体,它包含指向二叉树的结点的指针,以及一个标志位,初始化时标志位为false

      • 取走二叉树的头结点,把它入栈

      • 从二叉树中取出一个元素,如果是null,则继续取,取出一个结点后,如果标志位为false,则将标志位置为true,这里我们要注意,栈是后进先出的,所以如果我们要的是先序遍历的结果,则应该新建两个结构体对象,一个包含左结点,一个包含右结点,先把右的入栈,再把左的入栈,然后在把取出来的这个入栈;如果标志位为true,则输出该结点中包含的结点信息(模拟一下就懂了,不难的)

      • 当栈为空时,则退出

    • 队列

      • 先进先出
    • hash(散列表)

      • 散列函数

      • 冲突处理

        • 拉链法
          方法是将大小为M的数组中的每个元素指向一条链表,链表中的每个结点都存储了散列值为该元素的索引的键值对。散列最主要的目的在于均匀地将键散布开来,因此在计算散列后键的顺序信息就丢失了。如果你需要快速找到最大或者最小的键,或是查找每个范围内的键,散列表都不是合适的选择。基于拉链表的散列表的实现简单。在键的顺序并不重要的应用中,它可能是最快的(也是使用最广泛的)符号表实现。

        • 线性探测法
          用大小为M的数组保存N个键值对,其中M>N。我们需要依靠数组中的空位解决碰撞冲突。基于这种策略的所有方法被统称为开放地址散列表。开放地址散列表中最简单的方法叫做线性探测法:当碰撞发生时(当一个键的散列值已经被另一个不同的键占用),我们直接检查散列表中的下一个位置(将索引值加1)。这样的线性探测可能会产生三种结果:
          (1)命中,该位置的键和被查找的键相同;
          (2)未命中,键为空(该位置没有键);
          (3)继续查找,该位置的键和被查找的键不同。
          我们用散列函数找到键在数组中的索引,检查其中的键和被查找的键是否相同。如果不同则继续查找(将索引增大,到达数组结尾时返回数组开头),直到找到该键或者遇到一个空元素。

        • 删除操作:
          如何从基于线性探测的散列表中删除一个键?仔细想一想,你会发现直接将该键所在的位置设为null是不行的的,因为这会使得在此位置之后的元素无法被查找。(后面的null之前的元素全部要重新插入)为了保证性能,我们会动态调整数组的大小来保证使用率在1/8到1/2之间,另外,短小的键簇才能保证较高的效率。

搞清楚树的基本概念

  • 度:指的是一个节点拥有子节点的个数。如二叉树的节点的最大度为2。

  • 深度:数的层数,根节点为第一层,依次类推。

  • 叶子节点:度为0的节点,即没有子节点的节点。

  • 树:树中的每一个节点,可以有n(后续节点)个子节点,但是每个节点只有一个前驱节点。

  • 二叉树:除了叶子节点外,每个节点只有两个分支,左子树和右子树,每个节点的最大度数为2;

  • 满二叉树:除了叶结点外每一个结点都有左右子叶且叶结点都处在最底层的二叉树;

  • 完全二叉树:只有最下面的两层结点度小于2,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。也就是说,在满叉树的基础上,我在最底层从右往左删去若干节点,得到的都是完全二叉树。所以说,满二叉树一定是完全二叉树,但是完全二叉树不一定是满二叉树

  • 平衡二叉树:树的左右子树的高度差不超过1的数,空树也是平衡二叉树的一种。平衡二叉树,又称AVL树。它或者是一棵空树,或者是具有下列性质的二叉树:它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度之差之差的绝对值不超过1.

  • 哈夫曼树:带权路径长度达到最小的二叉树,也叫做最优二叉树。不关心树的结构,只要求带权值的路径达到最小值,哈夫曼树可能是完全二叉树也可能是满二叉树。

  • 单词查找树(Trie树,又称字典树):是一种树形结构,是一种哈希树的变种,是一种用于快速检索的多叉树结构。典型应用是用于统计和排序大量的字符串(但不局限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。Tries树的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  • 关于B树

    • 二叉搜索树:二叉树,每个结点只存储一个关键字,等于则命中,小于走左结点,大于走右结点;二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树(使用的是二分查找的思想)

    • 关于B树的,可以看一下这则漫画:https://www.sohu.com/a/154640931_478315

    • B(B-)树:多路搜索树,每个结点存储M/2到M个关键字,非叶子结点存储指向关键字范围的子结点;所有关键字在整颗树中出现,且只出现一次,非叶子结点可以命中;

    • B+树:在B-树基础上,为叶子结点增加链表指针,所有关键字都在叶子结点中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中;

    • B*树:在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3;

    • B树和B+树都是排序树,随机检索性能很好,顺序检索效率差。

B树

  • 关于B树的应用(更常用于文件系统的索引及某一些数据库)

    • 数据库索引为什么使用树结构存储呢?
      A:树的查询效率高,而且可以保持有序

    • 竟然如此,为什么索引没有使用二叉查找树来实现呢?
      A:其实从算法逻辑上来讲,二叉查找树的查找速度和比较次数都是最优的,但是我们不得不考虑一个现实问题:磁盘IO。
      数据库索引是存储在磁盘上的,当数据量比较大的时候,索引的大小可能有几个G甚至更多。
      当我们利用索引查询的时候,能把整个索引全部加载到内存吗?显然不可能。能再的只有逐一加载每一个磁盘页,这里的磁盘页对应着索引树的结点。

      • 如果使用二叉树,最坏的情况下,磁盘IO次数等于索引树的高度。
      • 为了减少磁盘IO次数,我们就需要把原本“瘦高”的树变得“矮胖”。这就是B-树的特征之一。
      • B树是一种多路平衡搜索树,它的每一个节点最多包含k个孩子,k被称为B树的阶。k的大小取决于磁盘页的大小。
  • 下面来具体介绍一下B-树(Balance Tree),一个m阶的B树具有如下几个特征:(叶节点后面还有东西,算深度时得注意+1)
    1.根结点至少有两个子女。

    2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m

    3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m

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

    5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

B+树(要多余B树做比较)

  • 关于B+树的应用(大部分数据库的索引)

    • 什么是B+树?(注意看图)
      A:1.有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

      2.所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

      3.所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

  • 什么是卫星数据?
    A:所谓卫星数据,指的是索引元素所指向的数据记录,比如数据库中的某一行。在B-树中,无论中间节点还是叶子节点都带有卫星数据。

  • 数据库索引为什么使用B+树而不是B树?
    A:1.单一节点存储更多的元素,使得查询的IO次数更少。(B+树中,只有叶子节点有卫星数据,其余节点仅仅是索引,没有任何数据关联,因此磁盘页可以容纳更多的节点元素)

    2.所有查询都要查找到叶子节点,查询性能稳定。(B+树的卫星数据只存在于叶节点)

    3.所有叶子节点形成有序链表,便于范围查询。 (B+树的叶子节点里面的卫星数据是有序的,而且叶子节点与叶子节点之间有链表指针)

红黑树与AVL树

  • 红黑树

    • 二叉查找树存在着它的缺陷,有时候二叉查找树的性能会变成线性,如何解决二叉查找树多次插入新节点而导致的不平衡呢?我们的主角“红黑树”应运而生。

    • 复杂度:O(logN)

    • 红黑树(Red Black Tree)是一种自平衡的二叉查找树。除了符合二叉查找树的基本特性外,它还具有下列的附加特性:
      1.节点是红色或黑色。

      2.根节点是黑色。

      3.每个叶子节点都是黑色的空节点(NIL节点)。

      4.每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

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

    • 红黑树从根到叶子的最长路径不会超过最短路径的2倍(性能上不如AVL树,但调节比AVL简单)

    • 红黑树的调整:(不看图贼难理解,这是重点……)

      • 变色
        与其父节点交换颜色,即其父节点变为红色,而当前节点变为黑色,注意根节点是黑色,就不要把它换为红色,实现不行就得用旋转

      • 旋转

        • 左旋转
          左旋是将某个节点旋转为其右孩子的左孩子(最好看图,旋转后根节点(相对这里的)为黑(变色))
        • 右旋转
          右旋是节点旋转为其左孩子的右孩子(最好看图,旋转后根节点(相对这里的)为黑(变色))
    • 应用

      • set

      • map

  • AVL树(平衡二叉树)

    • 把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多

    • 特性:

      • 具有二叉查找树的全部特性。
      • 每个节点的左子树和右子树的高度差至多等于1。
    • AVL树的调整:(多看图、多练)(重点啊~)
      -单旋转(左-左型、右-右型)(这里的左右看的是失衡节点的父节点的位置)
      主要看失衡节点向上的三个结点,进行左旋转,第二个结点将成为第一、三结点的父节点

      -双旋转(左-右型、右-左型)
      先对失衡节点向上的两个结点进行调整(这两个结点的父、子关系交换),把它变成左-左型或右-右型

    • AVL树的删除操作:

      • 同插入操作一样,删除结点时也有可能破坏平衡性,这就要求我们删除的时候要进行平衡性调整。删除分为以下几种情况:
        • 首先在整个二叉树中搜索要删除的结点,如果没搜索到直接返回不作处理,否则执行以下操作:
          1.要删除的节点是当前根节点T。如果左右子树都非空。在高度较大的子树中实施删除操作。分两种情况:
          (1)、左子树高度大于右子树高度,将左子树中最大的那个元素赋给当前根节点,然后删除左子树中元素值最大的那个节点。

          (2)、左子树高度小于右子树高度,将右子树中最小的那个元素赋给当前根节点,然后删除右子树中元素值最小的那个节点。

          2.如果左右子树中有一个为空,那么直接用那个非空子树或者是NULL替换当前根节点即可。

          3.要删除的节点元素值小于当前根节点T值,在左子树中进行删除。

          4.要删除的节点元素值大于当前根节点T值,在右子树中进行删除。

线索二叉树定义:

  • 通过考察各种二叉链表,不管二叉树的形态如何,空链域的个数总是多过非空链域的个数。准确的说,n各结点的二叉链表共有2n个链域,非空链域为n-1个,但其中的空链域却有n+1个。因此,提出了一种方法,利用原来的空链域存放指针,指向树中其他结点。这种指针称为线索。

  • 二叉树的遍历本质上是将一个复杂的非线性结构转换为线性结构,使每个结点都有了唯一前驱和后继(第一个结点无前驱,最后一个结点无后继)。对于二叉树的一个结点,查找其左右子女是方便的,其前驱后继只有在遍历中得到。为了容易找到前驱和后继,有两种方法。一是在结点结构中增加向前和向后的指针fwd和bkd,这种方法增加了存储开销,不可取;二是利用二叉树的空链指针。现将二叉树的结点结构重新定义如下:
    lchild ltag data rtag rchild

  • 其中:
    ltag=0 时lchild指向左子女;
    ltag=1 时lchild指向前驱;
    rtag=0 时rchild指向右子女;
    rtag=1 时rchild指向后继;

树的一些其它概念

  • 对任何非空二叉树T,若n0 表示叶结点的个数、n2 表示度为2 的非叶结点的个数,那么两者满足关系n0 = n2 + 1。

证明:首先,假设该二叉树有N 个节点,那么它会有多少条边呢?答案是N - 1,这是因为除了根节点,其余的每个节点都有且只有一个父节点,那么这N 个节点恰好为树贡献了N - 1 条边。这是从下往上的思考,而从上往下(从树根到叶节点)的思考,容易得到每个节点的度数和 0n0 + 1n1 + 2n2 即为边的个数。
因此,我们有等式 N - 1 = n1 + 2
n2,把N 用n0 + n1 + n2 替换,得到n0 + n1 + n2 - 1 = n1 + 2*n2,于是有n0 = n2 + 1

你可能感兴趣的:(数据结构)