数据结构-王道-查找

目录

查找

查找的基本概念

  1. 查找:在数据集合中寻找满足某种条件的数据元素的过程称为查找。查找的结果一般分为两种:查找成功,即在数据集合中找到了满足条件的数据元素;另一种是查找失败。
  2. 查找表(查找结构):用于查找的数据集合称为查找表,它由同一类型的数据元素(或记录)组成,可以是一个数组或链表等数据类型。对查找表经常进行的操作一般有四种:①查询某个特定的数据元素是否在表中;②检索满足条件的某个特定的数据元素的各种属性;③在查找表中插入一个数据元素;④从查找表总删除某个数据元素。
  3. 静态查找表:如果一个查找表的操作只涉及①和②的操作,则无需动态的修改查找表。与此对应,需要动态的插入或删除的查找表则称为动态查找表。适合静态查找表的查找方式有:顺序查找,折半查找,散列查找等;适合动态查找表的查找方法有:二叉排序树的查找,散列查找等。二叉排序树和B树都是二叉排序树的改进。二叉排序树与二叉平衡树已经在第4章介绍过。
  4. 关键字:数据元素中唯一标识该数据元素的某个数据项的值,使用基于关键字的查找,查找的结果应该是唯一的。比如由一个学生元素构成的数据集合,则学生元素中“学号”这一数据项的值唯一的标识一个学生。
  5. 平均查找长度:在查找过程中,一次查找的长度是指需要比较的关键次数,而平均查找长度则是指所有查找过程中进行关键字比较次数的平均值。其数学定义为\(\sum_{i=1}^{n}P_iC_i\)公式中,n是查找表的长度;\(P_i\)是查找某个数据元素的概率,一般认为每个数据元素的查找概率相等;\(C_i\)是查找到第i个数据元素所需进行的比较次数。平均查找长度是衡量查找算法效率的最主要指标。

线性表顺序查找

       顺序查找的方法就不说了,下面给出的算法,主要是为了说明其中引入的"哨兵"的作用。如果忘了线性表的动态存储结构点击这里

typedef struct//查找表的数据结构
{
    int *data;//元素存储空间基质,建表时按实际长度分配,0号单源留空。
    int TableLen;// 表的长度。
}SSTable;
int Search_Seq(SSTable ST,int key)
{
    ST.data[0]=key;
    for(int i=ST.TableLen;ST.data[i]!=key;i--)
        return i;
}

       在上述算法中,将\(ST.data[0]\)称为"哨兵"。引入目的是使得Search_Seq内的循环不必判断数组是否会越界,因为当满足i==0时,循环一定会跳出。需要说明的是,在程序中引入哨兵这个操作,并不是这个算法独有的。因为引入哨兵而带来的便利,我们可以将哨兵放到其他许多的程序当中去使用。这样可以避免很多不必要的判断语句,从而提高程序的效率。
       对于有n个元素的表,给定值key与表中第i个元素的关键字相等,即定位第i个元素时,需要进行\(n-i+1\)次比较,即\(C_i=n-i+1\)。查找成功时平均长度为:

\(ASL_{成功}=\sum_{i=1}^{n}P_i(n-i+1)\)

       当每个元素的查找概率相等时,即 \(ASL_{成功}=\sum_{i=1}^{n}P_i(n-i+1)=\frac{n+1}{2}\)
       查找不成功时,与表中各关键字的比较次数显然 \(n+1\)次,从而顺序查找不成功的平均查找长度 \(n+1\)
       通常,查找表中记录的查找概率并不相等。若能预先得知每个记录的查找概率,则应先对记录的查找概率进行排序,使表中华记录按照查找概率从小到大重新排列。
       综上所述,顺序查找的缺点是当n较大时,平均查找长度较大,效率低;有点事对数据元素的存储并没有要求,顺序存储或者链式存储皆可。对表中记录的有序性也没有要求,无论记录是否按照关键码有序均可应用。同时还需注意,对线性的链表只能进行顺序查找。

有序表顺序查找

       如果在查找之前就已经知道表是按照关键字有序的,那么当查找失败时可以不用再比较到表的另一端才能返回失败信息了,这样的话能够大大的减少,查找失败的平均查找长度。
       假设表L是按照关键字从小到大排列的,查找的顺序是从前向后查找,带查找元素的关键字为key,当查找到第i个元素的时候发现:第i个元素的关键字小于key,但是第i+1个元素的关键字大于key,所以表中不存在关键字为key的元素。
        在有序表的顺序查找中,查找成功的平均查找长度和一般线性表的顺序查找一样。查找失败时,查找指针一定走到了某个失败的结点。这些失败结点使我们虚构的空节点,实际上是不存在的,所以到达失败结点所查找的长度等于它上面一个圆形结点的所在层数。查找不成功的平均查找长度在相等查找概率的情形下有:

\(ASL_{不成功}=\sum_{j=1}^{n}q_j(l_j-1)=\frac{1+2+\ldots +n}{n+1}=\frac n 2 + \frac n {n+1}\)
式中, \(q_j\)是到达第j个失败结点的概率,在相等的查找概率情形下,为 \(\frac1 {n+1}\);所以可以看出当查找不成功是,该算法比一般的顺序查找算法好一点。

折半查找

       折半查找:和二分查找的原理是差不多的。它仅适用于有序的顺序表。基本思路是:首先将给定值key与表中间位置元素比较,如果相等,则查找成功,返回元素位置;如果不等,则需差汇总啊的元素只能在中间元素以外的前半部分或者后半部分中(例如:在查找表升序排列时,若给定值key大于中间元素的关键字,则所查找的元素只可能在后半部分)。然后在缩小的范围内继续进行同样的查找,如此反复知道找到为止,或者确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。
算法如下:

typedef struct
{
    int *data;
    int MaxSize,length;
}SeqList;
int Binary_Search(SeqList L,int key)
{
    int low=0,high=L.length-1,mid;
    while(low<=high)
    {
        mid=(low+high)/2;
        if(L.data[mid]==key)
            return mid;
        else if(L.data[mid]>key)
            high=mid-1;
        else
            low=mid+1;
    }
    return -1;
}
![Alt text](./1537588914583.png)

上述算法过程,如果形象描述一下就能画出来平衡二叉树。对平衡二叉树(二叉排序树)进行中序遍历可以得到有序的序列。

分块查找

       分块查找:又称为索引顺序查找,吸取了顺序查找和折半查找各自的优点,既有动态结构,有适于快速查找。
       分块查找的基本思想:将查找表分为若干个字块,块内的元素可以无序,但块之间是有序的。即即第一个块中的关键字小于第二个快中的所有记录的关键字,第二个块中的最大关键字小于第三个块中所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块最大关键字和各块中第一个元素的地址,索引表按关键字有序排列。
       分块查找的过程分为两步:第一步 在建立索引表中确定待查记录所在的块,可以顺序查找或者折半查找索引表;第二步在块内顺序查找。

Alt text

       分块查找的平均查找长度为索引查找和块内查找的平均长度之和,设索引查找和块内查找的平均查找长度分别为\(L_1,L_2\),则分块查找的平均长度为:\(ASL=L_1+L_2=[\log_2(b+1)]+\frac{s+1}{2}\)

B树和B+树


B树及其基本操作

       B树,又称为多路平衡查找树,B树中所有结点的孩子结点数的最大值称为B树的阶,通常用m表示。一个m阶B树或者为空树,或者为满足如下特性的m叉树。

  1. 树中每个结点至多有m棵子树(即至多含有m-1个关键字)。
  2. 若根节点不是终端结点,则至少含有两颗子树。
  3. 除根节点之外的所有非叶节点至少有\([\frac m 2]\)棵子树(即至少含有\([\frac m 2 -1]\)个关键字)。
  4. 所有非叶节点的结构如下。
    Alt text

       其中,\(k_i\)为结点的关键字,且满足\(K_1<K_2<K_3<\ldots<K_n\);为指向子树根节点的指针,且指针\(P_{i-1}\)所指子树中所有结点的关键字均小于\(K_i\)\(P_i\)所指子树中所有结点的关键字均大于\(K_i\)\(n(\frac m 2-1\leq n\leq m-1)\)为结点中关键字的个数。

       因为排序二叉树的高度可能会太高,所以发现了平衡二叉树,但是因为在实际应用中的缺陷:数据库分为关系型数据库和非关系型数据库(MangoDB)数据的索引是存储在磁盘上的,当数据库量比较大的时候,索引的大小可能有几个G甚至更多。当我们利用索引查询的时候,如要将整个索引加载大内存中是不可能的,这样对运行内存大小的压力太大,所以能做的就是逐一加载每一个磁盘页,这里的磁盘页对应着索引树的结点。如果我们利用平衡二叉树的话,要进行的磁盘读取次数太多,而现在磁盘读取正是在计算机中拖后腿的存在CPU的计算速度(比较速度)已经很快了,这个时候就有了B树(又称为B-树),想办法降低树的高度,尽量减少内存的读取次数,以加快数据库索引的速度
       下面介绍一下B树,一个m阶的B树具有如下几个特征:

  1. 根节点至少有两个子女。
  2. 每个中间结点都包含k-1个元素和k个孩子,其中\(\frac m 2\leq k \leq m\)
  3. 每一个叶子节点都包含\(k-1\)个元素,其中\(\frac m 2 \leq k \leq m\)
  4. 所有的叶子结点都位于同一层。
  5. 每个结点中的元素从小打到排列,结点当中\(k-1\)个元素正好是k个孩子包含的元素的值域分划。

    Alt text

    这棵树中,咱们看一下\((2,6)\)节点。该节点中有两个元素2和6,又有三个孩子\(1,(3,5),8\)。其中1小于元素2,(3,5)在元素在元素2和6之间,\(6<8<9\)。其中的\(9<11<12\)。最后一个叶子节点\((13,15)\)是大于12的话。

    B树的插入

    ![Alt text](./1537617056831.png)

           节点3,5已经是两元素节点,无法继续增加。父亲节点2,6也是两元素节点同样无法增加,但是根节点是单元素节点,可以升级为两元素节点。于是拆分节点3,5与节点2,6,让根节点9升级为两个元素的节点4,9。节点6独立为根节点的第二个孩子。

    B树的删除

    ![Alt text](./1537617910440.png)

            删除11之后节点12只有一个孩子,不符合B树的归法,因此找出12,13,15三个节点的中位数13,取代12,而12自身下移称为第一个孩子。

    散列(Hash)表

           散列表的基本概念:在前面介绍的线性表和数表的查找中,记录在表中的位置个记录关键字之间不存在确定关系。因此,在这些表中查找记录时需要进行一系列的关键字比较。这一类查找方式是建立在"比较"的基础上,查找的效率取决于比较的次数。
           一个把查找表中关键字映射成该关键字对应的地址的函数,记为\(Hash(key)=Addr\)
           散列函数可能会把两个或者两个以上的不同关键字映射到同一地址,我们称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。一方面设计好的散列函数应该尽量减少这样的冲突;另一方面由于这种冲突总是不可避免的,所以还要设计好处理冲突的方法
           散列表:是根据关键字而进行访问的数据结构。也就是说,散列表建立了关键字和存储地址之间的一种直接映射关系。
           理想情况下,对散列表进行查找的时间复杂度为\(O(1)\),即与表中元素的个数无关。下面将介绍常用的散列函数和处理冲突的方法。

    散列函数的构造方法

           在构造散列函数的时候,必须注意以下几点。
  6. 散列函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖散列表的大小或地址范围。
  7. 散列函数计算出来的地址应该能等概论,均匀的分布在整个地址空间,从而减少冲突的发生。
  8. 散列函数应该尽量简单,能够在较短的时间内就计算出任意关键字对应的散列地址。

    直接定制法

           直接取关键字的某个线性函数值为散列地址,散列函数为:
    \(H(key)=a*key+b\)

    公式中,a和b为常数。这种计算方法最简单,并且不会产生冲突。他适合关键字的分布基本连续的情况,若关键字的分布不连续,空位较多,将造成存储空间的浪费。


除留余数法

       这是一种最简单,最常用的方法,假定散列表表长为m,取一个不大于m但是最接近于m的质数p,利用以下的公式把关键字转换成散列地址。散列函数为:

\(H(key)=key \% p\)

       除留余数法的关键是选好p,使得每一个关键字通过该函数转换后等概率的映射到散列空间上的任一地址,从而尽可能减少冲突的可能性。


数字分析法

       设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布的均匀一些,每种数目出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,则应该选取数码分布较为均匀的若干位作为散列地址。这种方法适合于已知的关键字集合,如果更换了关键字,就需要重新构造新的散列函数。


平方取中法

       顾名思义,取关键字的平方值的中间记为作为散列地址。具体取多少位要看实际情况而定。这种方法得到的散列地址与关键字的每一位都有关系,是的散列地址分布比较均匀。适用于关键字的每一位取值都不够均匀或小于散列地址所需的位数。


折叠法

       将关键字分割成位数相同的几部分(最后一部分的位数可以短一些),然后取这几部分的叠加和作为散列地址,这种方法称为折叠法。关键字位数很多,而且关键字中每一位上数字分布大致均匀时,可以采用折叠法得到散列地址。


       在不同情况下,不同的散列函数会发挥出不同的性能因此不能笼统的说那种散列函数最好。在实际选择中,采用何种构造散列函数的方法取决于关键字集合的情况,但是目标是为了使产生冲突的可能性尽量降低。

处理冲突的方法

       应该注意到,任何设计出来的散列函数都不可能绝对的避免冲突,为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找写一个“空”的Hash地址。
       假设已经选定散列函数H(key),下面用\(H_i\)表示冲突发生之后的第i粗探测的散列地址。
       开放定值法:所谓开放定值法,指的是可存放新表项的空闲地址即向她的同义词表项开放,又向它的非同一次表项开放。其数学递推公式为:

\(H_i=(H(key)+d_i)\%m\)
公式中, \(i=1,2,\ldots,k(k\leq m-1)\);m表示散列表表长; \(d_i\)为增量序列。当取定某一增量序列后,则对应的处理方法是确定的。通常有一下四种取法:

  1. 线性探测法:当\(d_i=1,2,\ldots,m-1\),称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(当探测到表尾地址\(m-1\)时,下一个探测地址是表首地址0),知道找出一个空闲单位(当表为填满时一定能找到一个空闲单位)或查边全表。线性探测法可能使第i个散列地址的同义词存入第i+1个散列地址,这样本应存入第i+1个散列地址的元素就争夺第i+2个散列地址的元素的地址……,从而造成大量元素在相邻的散列地址上“聚集”(或堆积)起来,大大降低了查找效率。

    字符串的模式匹配

    简单的模式匹配算法

           串的模式匹配,是求第一个字符串在第二个字符串中的位置。
           下面给出一种简单的字符串模式匹配算法:从主串S指定的字符开始(一般是第一个)和模式串T的第一个字符比较,若相等,则继续逐个比较后续字符,直到T中的每个字符一次和S中的一个连续的字符序列相等,则称匹配成功;如果比较过程中有某对字符不想等,则从主串S的下一个字符起重新和T的第一个字符比较。如果S中的字符都比完了,仍然没有匹配成功,则称匹配不成功。代码如下:
int Index(SString S,SString T)
{
    int i=1,j=1;
    while(i<=S[0]&&j<=T[0])
        if(S[i]==T[j])
            i++,j++;
        else
            i=i-j+2,j=1;
    if(j>T[0])
        return i-T[0];
    else
        return 0;
}
![Alt text](./1537691063423.png)

简单模式匹配算法的最坏时间复杂度为\(O(m*n)\),n和m分别为主串和模式串的长度。

改进的模式匹配算法-KMP

       KMP算法可以在时间复杂度为\(O(m*n)\)的级数上完成串的模式匹配操作。其改进在于:每当一趟匹配过程中出现字符比较不相等时,不需回溯i指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离之后,继续进行比较
       回顾上图的匹配过程,在第三趟的匹配中,当\(i=7,j=5\)字符比较不等时,又从\(i=4,j=1\)重新开始比较。
       然而通过仔细的观察可以发现\(i=4,5,6 .j=1\)这三次的比较都是不需要进行的。因为从第三趟部分匹配的结果就可以得出,主串中第4,5,6个字符必然是‘b’,‘c’和‘a’。因为模式中第一个字符是a,因此它无需再和这3个字符进行比较,而仅需要将模式向右移动两个字符的位置继续进行\(i=3,j=1\)时的字符比较。由此在整个匹配的过程中,i指针没有回溯,如图所示。

Alt text

       KMP算法的每趟比较过程中让字串向后滑动到一个合适的位置,让这个位置上的字符和主串中的那个字符比较,这个合适的位置与字串本身的结构有关。
       下面来看一种更加一般的情况,假设原始串为S,长度为n,模式串为T,长度为m。目前匹配到如下的划线位置。
Alt text

        \(S_{i-j},S_{i-j+1},\ldots,S_{i-1}\)\(T_0,T_1,\ldots,T_{j-1}\)的部分匹配成功,恰好到 \(S_i\)\(T_j\)的时候匹配失败,如果要保持i不变,同时达到让模式串T相对原始串S右移的话,我们可以想办法更新j的值,找到一个最大的k,满足 \(S_{i-k},S_{i-k+1},\ldots,S_{i-1}=T_0,T_1,\ldots,T_{k-1}\) 使新的j=k,然后让 \(S_i\)\(T_j\)进行匹配 即 \(next[j]\)表示当前模式串匹配到 \(T[j]\)遇到失配时,在模式串中需要重新和主串匹配的位置。换而言之,next数组的求解实际是对每个位置找到最长的公共前缀。所以next数组的定义为:
Alt text

void get_next(char T[],int next[])
{
    int i=1;
    next[1]=0;
    int j=0;
    while(i<=T[0]) // T[0] 存储 字符的长度。
        if(j==0||T[i]==T[j])
            i++,j++,next[i]=j;
        else
            j=next[j];
}
![Alt text](./1537717450972.png)
int kmp(char S[],char T[],int next[],int pos)
{
    int i=pos;
    j=1;
    while(i<=S[0]&&j<=T[0])
        if(j==0||S[i]==T[j])
            i++,j++;
        else
            j=next[j];
    if(j>T[0])
        return i-T[0];
    else
        return 0;
}

猜你喜欢

转载自www.cnblogs.com/A-FM/p/9694675.html