14--查找算法

305 阅读8分钟

关于数据结构与算法的相关知识,请查看文章《数据结构与算法基础知识文章汇总》

image.png

一、查找相关概念

1.关键字

数据元素中某个数据项的值,又称为键值。用它可以表示一个数据元素,是数据元素的标识

2.查找表

同一类型数据构成的集成,如数组、字典、字符串等。

3.查找

根据给定的某个确定的值,在查找表中根据一个确定的关键字找到与这个确定的值相等的数据元素。

4.主关键字

若关键字可以唯一的标识一个数据元素,则我们称这个关键字为主关键字

5.次关键字

可以标识多个元素的关键字,我们称之为次关键字

二、常见查找算法

1.查找算法的分类

查找算法按查找后是否改变查找表的数据元素分为静态查找动态查找

1.1、静态查找

对查找表进行数据元素的查找对查找表进行增、删的操作。常见的静态查找有顺序查找二分查找插值查找斐波拉契查找

1.2、动态查找

在对查找表进行数据元素的查找过程中,对于查找表中存在的数据元素可以进行删除操作。如果没有找到要查找的数据元素,可以执行数据元素的插入操作。动态查找算法主要是二叉搜索树

三、常用静态查找算法

1.顺序查找

顺序查找也称为线性查找,查找过程:从第一个(或最后一个)数据元素数据元素开始遍历,在查找表中逐个对数据元素的关键字或查找条件进行比较,直到找到目标数据元素为止。如果遍历完查找表依旧没有找到符合条件的数据元素,则查找失败,查找表中无满足条件的数据元素。

image.png

代码实现:

/*
1.a是查找表;
2.n是查找表长度;
3.key是关键字或查找条件。
*/
int Sequential_Search(int *a,int n,int key){
    for(int i = 1; i <= n ; i++)
    {
        if(a[i] == key)
        {
            return i;
        }
    }
    return 0;
}

i从1开始是因为这里0的位置作为一个哨兵,开发者可以根据个人喜欢进行算法的设计。

上面的顺序查找存在的优化的空间,如我们将0位置作为哨兵的话,可以实现如下顺序查找算法的设计,以优化掉i<=n的比较:

image.png

int Sequential_Search2(int *a,int n,int key){
    int i;

    //设置a[0]为关键字值,称为'哨兵'
    a[0] = key;

    //循环从数组尾部开始
    i = n;

    while(a[i] != key) {
        i--;
    }

    //返回0,则说明查找失败
    return i;
}

顺序查找的哨兵设计使得查找过程中的的比较只有一次,即a[i] != key

顺序查找适用于任何顺序的查找表,如果查找表的数据元素是有序的,我们可以有更优的查找算法

2.二分查找

二分查找也称为折半查找,它的前提是查找表必须是有序的(通常是从小到大),而且要使用顺序存储

二分查找的基本思想:

  • 1.在查找表中,取中间记录关键字作比较,如果满足条件,则查找成功;
  • 2.如果关键字比中间记录,则下一次的查找区间是中间记录的左半区
  • 3.如果关键字比中间记录,则下一次的查找区间是中间记录的右半区
  • 4.重复1、2、3步骤,直到找到为止,或最终的查找区间为1,并且没有找到目标为止。

image.png

代码实现:

int Binary_Search(int *a,int n,int key){

    int low,high,mid;

    //定义最低下标为记录首位
    low = 1;

    //定义最高下标为记录末位
    high = n;

    while(low <= high) {
        //折半计算
        mid = (low + high) /2;

        if (key < a[mid]) {
            //若key比a[mid] 小,则将最高下标调整到中位下标小一位;
            high = mid-1;
        }else if(key > a[mid]){
             //若key比a[mid] 大,则将最低下标调整到中位下标大一位;
            low = mid+1;
        }else
        {
            //若相等则说明mid即为查找到的位置;
            return mid;
        }
    }
    return 0;
}

二分查找的时间复要度为logN。

二分查找更新mid的公式是: mid = (low + high) / 2;

变换一下这个公式:mid = low + (high - low) * 0.5;

也就是说,查找范围每次都是缩小上一次查找范围的一半,那么我们是否可以根据low、high和mid来更新缩小查找范围的比例呢?

3.插值查找

在二分查找中我们已经知道了,更新mid值的公式如下:

mid = low + (high - low) * 0.5;

也就是说,二分查找每次缩小的查找范围为上一次查找范围的一半

在二分查找的基础上,我们可以对顺序存储的有序查找表,针对查找表中数据分布均匀的情况的查找算法进行优化。如下图:

image.png

对于上图中这样数据分布均匀的查找表,我们可以计算出关键字key在表中可能出现的位置,如1~100的查找表中,要找88,直接先查88%的位置即可快速找到。公式如下:

image.png

举例分析:

image.png

代码实现:

int Interpolation_Search(int *a,int n,int key){
    int low,high,mid;
    low = 1;
    high = n;

    while(low <= high) {
        //插值
        mid = low+ (high-low)*(key-a[low])/(a[high]-a[low]);

        if(key < a[mid]) {
            //若key比a[mid]插值小,则将最高下标调整到插值下标小一位;
            high = mid-1;
        }else if(key > a[mid]){
            //若key比a[mid]插值 大,则将最低下标调整到插值下标大一位;
            low = mid+1;
        }else
            //若相等则说明mid即为查找到的位置;
            return mid;
    }
    return 0;
}

插值查找是一个不断缩小查找范围,并且缩小的比例不像二分查找那样是固定的,而是根据算法不断进行更新

4.斐波拉契查找

image.png

斐波拉契查找是指利用斐波拉契数列的特殊性来进行查找表的查找算法实现。

1.斐波拉契

斐波拉契数列F = [0,1,1,2,3,5,8,13,21,34,55,89...],特征如下:

  • 1.第0和1个元素分别为0和1
  • 2.从第2个元素开始,后面的元素都为前两个元素之和,即F[k] = F[k-1] + F[k-2]

关于斐波拉契数列的详细介绍请查看百度百科--斐波那契数列

2.黄金分割点

image.png 如上图所示,线段AB,在AB之间找到一个点P,使得AP与AB的比值等于PB与AP的比值,即假设AB长为1,AP长为x,则PB长为1-x。有x/1 = (1-x)/x。则称点P黄金分割点

由比例关系:x/1 = (1-x)/x得到一元二次方程x^2 + x - 1 = 0,解一元二次方程得:

image.png

x值约等于0.618,此值即为黄金分割比例

3.斐波拉契数列与黄金分割比的关系

从斐波拉契数列第4个元素3开始有:

    1. 3/5 = 0.6;
    1. 5/8 = 0.625;
    1. 8/13 = 0.615;
    1. 13/21 = 0.619;
    1. 21/34 = 0.6176;
    1. 34/55 = 0.61818;
    1. 55/89 = 0.61797; ...

从上面例举的斐波拉契数列中前后两个元素的比值可以看出,斐波拉契数列前后两个元素的比值越来越接近0.618的黄金比例。利用这个特性,我们将斐波拉契数列应用到查找算法中。

4.斐波拉契查找的实现

结合前所讲的二分查找和插值查找算法的设计思想,假设low和high指向查找范围的前后位置,key指向关键字,创建斐波拉契数列F查找表a

image.png

1.假设a的长度n = 10,key = 99。在F中找到比k大的第一个元素,记录其下标为k。此时k = 7,F[k] = 13

2.由于n = 10,而F[k] = 13,所以数组元素的个数10小于13,需要扩充数组a,使得其a[11]、a[12]的值为a[10] = 99。这样做是为了保证在算法执行的过程中不出现数组越界从而导致crash

image.png

注意:关于k的更新 image.png

F[k] = F[k-1] + F[k-2],得到上图,然后有:

  • a[mid] < key时,有k = k-2;
  • a[mid] > key时,有k = k-1;

1.第一次查找时

image.png

mid的求解是取斐波拉契数列对应黄金分割下标的值,即F[k-1]的值:

  • low = 1;
  • high = 10;
  • mid = low + F[k-1] - 1 = 1 + F[7-1] -1 = 1 + 8 -1 = 8;
  • key = 99,a[mid] = 73,99 > 73;

此时 a[mid] < key,即要查找的值在数组a的mid下标的右半区,更新low 和 k:

  • low = mid + 1;
  • k = k - 2;

2.第二次比较

image.png 通过上面的两次比较我们就找到了99所在的位置为10

再以查找59为例

1.第一次查找

image.png

image.png

2.第二次查找

image.png

3.第三次查找

image.png

4.第四次查找

image.png

最终找到了59的位置是6

斐波拉契小结:

  • mid = low + F[k-1] - 1;
  • a[mid] < key时,有k = k-2;low = mid + 1;
  • a[mid] > key时,有k = k-1;high = mid - 1;

5.算法实现

int F[100]; /* 斐波那契数列 */

int Fibonacci_Search(int *a,int n,int key){

    int low,high,mid,i,k;

    //最低下标为记录的首位;
    low = 1;

    //最高下标为记录的末位;
    high = n;
    k = 0;

    //1.计算n为斐波拉契数列的位置k;
    while(n > F[k]-1) {
        k++;
    }
    
    //2.将数组a不满的位置补全值;
    for(i = n;i < F[k]-1;i++)
    {
         a[i] = a[n];
    }
        
    //3.
    while(low <= high) {
        //计算当前分隔的下标;
        mid = low+F[k-1]-1;

        if(key < a[mid]) {
            //若查找的记录小于当前分隔记录;
            //将最高下标调整到分隔下标mid-1处;
            high = mid-1;

            //斐波拉契数列下标减1位;
            k = k-1;
        }else if(key > a[mid]){
            //若查找的记录大于当前的分隔记录;
            //最低下标调整到分隔下标mid+1处
            low = mid+1;

            //斐波拉契数列下标减2位;
            k = k-2;
        }else{
            if(mid <= n) {
                //若相等则说明,mid即为查找的位置;
                return mid;
            }else
            {
                //若mid>n,说明是补全数值,返回n;
                return n;
            }
        }
    }
    return 0;
}

6.调试代码

//斐波拉契数列计算;
    F[0]=0;

    F[1]=1;

    for(i = 2;i < 100;i++)
    {

        F[i] = F[i-1] + F[i-2];

    }

    result=Fibonacci_Search(arr,10,99);

    printf("斐波拉契查找:%d \n",result);

    result=Fibonacci_Search(arr,10,59);

    printf("斐波拉契查找:%d \n",result);

参考文献查找算法之斐波那契查找

四、动态查找算法

动态查找算法请查看文章二叉树的应用之二叉搜索树

五、总结

  • 1.查找算法分为静态查找动态查找
  • 2.静态查找算法有顺序查找二分查找插值查找斐波拉契查找
  • 3.动态查找主要是二叉搜索树