数据结构-查找-静态

259 阅读7分钟

代码开场

int SeqSearch(int *a, int n, int key) {
    for (int i = 1; i <=n; i++) {
        if (a[i] == key) {
            return i;
        }
    }
    return 0;
}

这是一个很简单的一个查找代码了,很基础,没有什么难度。时间复杂度:O(n)。查找过程是从数组中第一个记录开始,逐个进行记录的关键字和给定值比较,如果某个记录与key相等,查找成果;如果直接到最后一个,则数组中没有记录,查找失败。

1、顺序表查找

上面的代码的查找算法,我们通常叫做顺序表查找算法,又叫做线性查找。

1.1 优化

上面的代码看似简单,其实还可以进行优化。优化的点在于:每次循环时都需要对i是否越界进行判断。我们可以设置一个哨兵:

int SeqSearch2(int *a, int n, int key) {
    int i = n;
    a[0] = key;//把数组下标0位置当做哨兵
    while (a[i] != key) {//循环从数组尾部开始
        i--;
    }
    return i;//如果返回0,说明没有找到
}

从数组尾部开始查找,把0位置设置为key,也就是哨兵。如果a[i]中有key,返回i;如果没有返回0,也就是返回哨兵的位置。

这个时间复杂度和普通版一样,都是O(n),很多朋友可能会问,这也没有啥优化的啊?其实在数据量比较多的时候,这种方式会提高很大的效率。这也是一个很好的编程技巧哦!当然,哨兵不一定要在数组开始的地方,也可以放在末尾。

2、有序表查找

之前的例子对数组是否是有序没有要求。如果数组有序,在查找中是很有帮助的。

2.1 折半查找

折半查找又称二分查找。基本思想:有序表中,取中间值作为比较对象。若key等于中间值,则找到了;若小于中间值,则在以中间值进行分割的左半区域进行查找;若大于中间值,则在右半区域进行查找。如图:

图中其实是一种小游戏:我在纸上写一个100以内的数字,你最快能用几次猜出来?

代码
int BinarySearch(int *a, int n, int key) {
    int low = 1;
    int high = n;
    
    while (low <= high) {
        int mid = (low + high) / 2;
        if (key < a[mid]) {
            high = mid - 1;
        } else if (key > a[mid]) {
            low = mid + 1;
        } else {
            return mid;
        }
    }
    
    return 0;
}

代码依然很简单,但是效率很高。时间复杂度O(log n)。这个可远远的好于顺序表查找,这个就是有序的好处。

对于静态查找,一次排序后再无变化,这样的算法是非常好的。但对于频繁执行插入或者删除操作的数据来说,维护有序排序也是一个不小的工作量,这种情况下就不建议使用了。

现在有一个问题,为什么一定要折半呢?如果在一个很大的有序数组中,存放0~10000的数据,我只要查找5,我们自然会想到从头开始是查找是比较快的方法。折半查找还是有优化的空间的。我们继续学习插值查找

2.2 插值查找

折半查找中获取中间值代码

int mid = (low + high) / 2;

进行一个简单的转换

int mid = low + (high-low) / 2;

mid等于最低下标+最高下标与最低下标差值的一半。我们的目的是修改这个一半(二分之一,1/2),改进公式:

例如:

a[11]={0,1,16,24,35,45,65,78,88,94,99}
low=1
high=10
key=16

如果用折半查找,需要四次。如果用公式公式计算:

对2.377取整,mid=2,直接就定位到了结果。代码与折半查找只需要修改mid赋值的地方。

代码
int InterpolationSearch(int *a, int n, int key) {
    int low = 1;
    int high = n;
    
    while (low <= high) {
        //公式
        int mid = low + (high - low) * (key - a[low]) / (a[high] -a[low]);
        if (key < a[mid]) {
            high = mid - 1;
        } else if (key > a[mid]) {
            low = mid + 1;
        } else {
            return mid;
        }
    }
    
    return 0;
}

通过上面的例子可以看出,插值查找对于数据是平均分配的情况下,要由于折半查找很多。但是如果遇到分布不均匀的,例如:{0,1,2,2000,2001,......,9999997,9999998}。这样的数据,就不合适了。

2.3 斐波那锲查找

斐波那锲查找是利用黄金分割点原理来实现的。

代码
int F[100];
int Fibonacci_Search(int *a, int n, int key) {
    //初始最低下标
    int low = 1;
    //初始最高下标
    int high = n;
    //根据数组个数n,获取斐波那契个数组下标。k对应的值,就是在a数组中个数
    int k = 0;
    while (n > F[k]-1) {
        k++;
    }
    
    //对数组个数按照斐波那契的值补齐,用最后一个数组元素值补齐
    for (int i = n; i < F[k]-1; i++) {
        a[i] = a[n];
    }
    
    while (low <= high) {
        //获取中间点的值
        int mid = low + F[k-1]-1;
        if (key < a[mid]) {//主键小于中间值
            high = mid-1;//调整最高下标
            k = k-1;//调整k,用于不断缩小范围
        } else if (key > a[mid]) {//主键大于中间值
            low = mid + 1;//调整最低下标
            k = k-2;//调整k
        } else {
            if (mid <= n) {//正常范围
                return mid;
            } else {//说明是补齐的部分,直接返回n
                return n;
            }
        }
    }
    return 0;
}
解读
  1. 程序开始,a={0,1,16,24,35,47,59,62,73,88,99},n=10,key=59,F={0,1,1,2,3,5,8,13,21,34,...}
  2. 数组长度n对应的斐波那契数列中的位置,F[6]< n < F[7],得到k=7
  3. F[k]=F[7]=13,而a的长度n=10,需要对a数组进行扩展,也就是增加a[11],a[12]。我们将a中的最大值,也就是a[10]的值99赋给a[11],a[12]
  4. 和折半类似,开始循环
  5. 获取mid值,mid = low + F[k-1]-1 = 1+F[7-1]-1=1+8-1=8
  6. key=59,a[mid]=a[8]=73,主键小于中间值,调整,high=mid-1=8-1=7和k=k-1=6
  7. 再次循环,mid = low + F[k-1]-1 = 1+F[6-1]-1=1+5-1=5。a[mid]=a[5]=47<key=59,调整,low=mid+1=5+1=6,k=k-2=4。注意此时是k-2。
  8. 再次循环mid = low + F[k-1]-1 = 6+F[4-1]-1=6+2-1=7。a[mid]=a[7]=62>key=59,调整,high=6,k=4-1=3
  9. 再次循环,mid=6+F[3-1]-1=6。此时a[6]=59=key,返回6,程序运行完成。
问题
  • 为什么对数组要进行补充? 如果key=99,此时查找循环第一次,mid=8与上面例子相同。第二次循环,mid=11,a[11]数组越界了。我们是用斐波那锲数列作为分割值,所以a数组的长度要满足分割值对应的长度就得进行补充。这就是为什么要对数组进行补齐的原因。
  • k-1和k-2是什么意思? 斐波那锲数列中的值F[k]=F[k-1]+F[k-2],所以如图:
    当key<a[mid]时,新范围是low~mid-1,此时的范围个数为F[k-1]-1个;当key>a[mid]时,新范围是mid+1~high,此时范围F[k-2]-1个。也就是说,查找的记录在右侧,左侧的数据都不用再查找了。
优缺点

时间复杂度上与折半一样,都是O(log n)。但平均性能上要由于折半查找。会有一种极端的情况,如果key=1,那么市政都处于左侧半区查找,这种情况效率要低于折半查找。

还要一点,在求mid的时候,折半查找进行了加减乘除运算(mid = low + (high-low) / 2),而斐波那契查找只进行了加减法(mid = low + F[k-1]-1),在海量数据查找时,这种细微的差别可能会影响最终的查找效率。