23. 查找算法:顺序查找、有序表查找

查找,说白了就是在一堆数据里找到你想要的那个。我做了这么多年嵌入式开发,几乎每个项目都离不开查找操作。从最简单的传感器数据检索,到复杂的路由表匹配,查找算法的选择直接影响系统响应速度。

今天咱们就把三种最基础的查找算法掰开揉碎讲清楚。顺序查找、折半查找、插值查找、斐波那契查找——嗯,一个一个来。

23.1 顺序查找:最朴素的暴力法

顺序查找的思路简单到令人发指:从第一个元素开始,挨个比较,直到找到目标或者遍历完整个数组。

我曾经在一个数据采集项目中用过这个算法。当时数据量不大,也就几百个采样点,用顺序查找完全够用。但后来数据量涨到几万,系统就开始卡顿了——这就是没提前评估复杂度的教训。

代码实现

// 顺序查找:返回目标索引,未找到返回 -1
int seqSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;
        }
    }
    return -1;
}

复杂度分析

情况 比较次数 时间复杂度
最好情况 1(第一个就是) O(1)
最坏情况 n(最后一个或不存在) O(n)
平均情况 (n+1)/2 O(n)

核心结论:顺序查找的时间复杂度是 O(n)。数据量小的时候无所谓,数据量一大就扛不住了。

小技巧:我习惯在数组末尾加一个"哨兵"(把目标值放在 arr[n] 位置),这样就不用每次循环都判断下标是否越界。实测能省掉大约 15% 的比较开销。

23.2 折半查找:有序数据的利器

折半查找,也叫二分查找。前提条件很明确:数据必须有序。你想想看,如果数据是乱序的,折半查找就没法用。

我记得有一次接手一个老项目,里面用的就是顺序查找,数据量大概 10 万条。我改成折半查找后,查找时间从秒级降到了毫秒级。老板当时还问我是不是换了服务器——其实只是换了个算法。

代码实现

// 折半查找:数组必须有序(升序)
int binarySearch(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;  // 防止溢出
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

注意:计算 mid 时不要写成 (left + right) / 2。当 left 和 right 都接近 INT_MAX 时,相加会溢出。用 left + (right - left) / 2 更安全——这是我在一次代码审查中学到的。

复杂度分析

折半查找每次把查找范围缩小一半。所以查找次数就是 log₂(n) 级别。具体来说:

  • 最好情况:O(1) —— 中间元素就是目标
  • 最坏情况:O(log n) —— 一直找到最底层
  • 平均情况:O(log n)

说白了,10 亿条数据用折半查找,最多只需要比较 30 次。而顺序查找可能要比较 10 亿次。差距就是这么大。

23.3 插值查找:更聪明的折半

折半查找每次都在中间切一刀。但你想过没有——如果数据分布比较均匀,我们其实可以猜得更准。

比如你要在一本 1000 页的字典里找"苹果"这个词。你不会从第 500 页开始翻,而是会从前面几页开始。插值查找就是这个思路。

核心公式

插值查找的 mid 计算公式变成了:

mid = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left])

这个公式说白了就是:根据目标值在数据范围内的"比例位置"来猜测索引。如果数据分布均匀,这个猜测非常准。

代码实现

// 插值查找:适用于数据分布均匀的有序数组
int interpolationSearch(int arr[], int n, int target) {
    int left = 0, right = n - 1;
    while (left <= right && target >= arr[left] && target <= arr[right]) {
        // 防止除零
        if (arr[left] == arr[right]) {
            if (arr[left] == target) return left;
            else return -1;
        }
        int mid = left + (target - arr[left]) * (right - left) / (arr[right] - arr[left]);
        if (arr[mid] == target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

复杂度对比:

  • 数据均匀分布时:O(log log n) —— 比折半查找还快
  • 数据分布极不均匀时:O(n) —— 退化成顺序查找

我个人建议:只有在数据量较大且分布比较均匀的情况下才用插值查找。否则折半查找更稳定。

23.4 斐波那契查找:黄金分割的妙用

斐波那契查找,名字听着挺唬人。其实它和折半查找很像,只是分割点不是中间,而是黄金分割点附近。

为什么会用斐波那契数列?因为斐波那契数列相邻两项的比值趋近于黄金分割比 0.618。用这个比例来分割数组,在某些场景下可以减少磁盘 I/O 次数——我记得在数据库索引设计中有人研究过这个。

核心思想

先构造一个斐波那契数列 F[k],使得 F[k] 刚好大于等于数组长度 n。然后每次用 F[k-1] 作为分割点。

// 斐波那契查找
int fibSearch(int arr[], int n, int target) {
    // 构造斐波那契数列,F[k] >= n
    int fib[100];
    fib[0] = 0; fib[1] = 1;
    int k = 1;
    while (fib[k] < n) {
        k++;
        fib[k] = fib[k-1] + fib[k-2];
    }
    
    // 扩展数组到 fib[k] 长度
    int* temp = (int*)malloc(fib[k] * sizeof(int));
    memcpy(temp, arr, n * sizeof(int));
    for (int i = n; i < fib[k]; i++) {
        temp[i] = arr[n-1];  // 用最后一个元素填充
    }
    
    int left = 0, right = fib[k] - 1;
    while (left <= right) {
        int mid = left + fib[k-1] - 1;
        if (temp[mid] == target) {
            free(temp);
            return (mid < n) ? mid : n-1;
        } else if (temp[mid] < target) {
            left = mid + 1;
            k -= 2;  // 右边部分长度是 F[k-2]
        } else {
            right = mid - 1;
            k -= 1;  // 左边部分长度是 F[k-1]
        }
    }
    free(temp);
    return -1;
}

实际经验:斐波那契查找在实际工程中用得不多。我做了这么多年项目,只用过一次——在一个对内存访问模式有特殊要求的嵌入式系统里。大多数情况下,折半查找已经够用了。

23.5 四种查找算法对比总结

算法 前提条件 时间复杂度 空间复杂度 适用场景
顺序查找 O(n) O(1) 小数据量、无序数据
折半查找 有序 O(log n) O(1) 通用有序查找
插值查找 有序且均匀分布 O(log log n) ~ O(n) O(1) 均匀分布的大数据量
斐波那契查找 有序 O(log n) O(1) 特殊内存访问场景

23.6 知识体系结构图

下面这张图帮你理清四种查找算法的关系与选择逻辑:

查找算法知识体系 查找算法 无序数据 顺序查找 O(n) 有序数据 折半查找 O(log n) 插值查找 O(log log n) 斐波那契查找 O(log n) 选择建议 数据量小(<1000)→ 顺序查找,简单直接 数据量大且有序 → 折半查找,稳定可靠 数据均匀分布 → 插值查找,更快一步

23.7 避坑指南

最后分享几个我踩过的坑:

  • 我曾经在一个项目中直接用折半查找,结果数据是降序排列的——找了半天找不到。记住:折半查找的前提是数组有序,而且你得知道是升序还是降序。
  • 我曾经在插值查找里没处理除零问题,arr[right] - arr[left] 等于 0 时直接崩溃。现在我的代码里都会加这个判断。
  • 我建议初学者先从顺序查找入手,理解透了再学折半查找。别一上来就搞斐波那契查找——那个在实际项目中真的很少用。

查找算法是数据结构的基础,也是面试的高频考点。把这四种算法吃透,你就能应对绝大多数查找场景了。


公众号:蓝海资料掘金营,微信deep3321