查找基础:从顺序到折半,再到分块

查找,说白了就是在一堆数据里找到你想要的那个。这事儿听起来简单,但实际做起来门道不少。我做了这么多年嵌入式开发,几乎每个项目都离不开查找操作——从传感器数据检索到配置表匹配,查找算法的好坏直接决定了系统的响应速度。

今天咱们就把查找的三种基本方法捋一遍:顺序查找、折半查找、分块查找。这三种方法各有各的脾气,用对了地方就是利器,用错了就是灾难。

查找算法知识体系 查找基础 顺序查找 折半查找 分块查找 无序/有序均可 时间复杂度 O(n) 实现最简单 必须有序 时间复杂度 O(log n) 效率最高 块内无序 块间有序 介于两者之间 适合索引查找 选择依据:数据规模 × 是否有序 × 查找频率

什么是查找?

查找,就是根据给定的关键字,在一组数据中找到对应的记录。关键字可以是学号、姓名、或者某个传感器的ID。

我习惯把查找分为两类:静态查找和动态查找。静态查找就是数据不变,只查不改;动态查找则允许插入和删除。今天讲的三种方法都属于静态查找范畴。

核心概念: 查找算法的好坏通常用「平均查找长度(ASL)」来衡量。说白了,就是平均要比较多少次才能找到目标。ASL 越小,算法越快。

顺序查找:最笨但最可靠

顺序查找,就是从头到尾一个一个比。你想想看,这方法虽然笨,但有个好处——不挑数据。不管数据是有序还是无序,都能用。

我在项目中遇到过这样的情况:一个嵌入式设备启动时,需要从配置表里查找某个外设的初始化参数。配置表只有几十条记录,而且经常变动,用顺序查找反而最省事——代码简单,不容易出bug。

// 顺序查找 - 最朴素的实现
int seq_search(int arr[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == key) {
            return i;  // 找到了,返回下标
        }
    }
    return -1;  // 没找到
}

这段代码没什么花哨的,就是循环加比较。平均查找长度是 (n+1)/2,也就是说数据量翻倍,查找时间也翻倍。

小技巧: 我习惯在数组末尾放一个「哨兵」——把要查找的值放在最后一个位置。这样就不用每次循环都判断是否越界,能省一点时间。在嵌入式这种资源受限的环境里,这点优化有时候挺管用。
// 带哨兵的顺序查找
int seq_search_sentinel(int arr[], int n, int key) {
    int i = 0;
    arr[n] = key;  // 放置哨兵
    while (arr[i] != key) {
        i++;
    }
    return (i < n) ? i : -1;  // 如果i==n说明没找到
}

折半查找:快,但有条件

折半查找,也叫二分查找。这名字起得好——每次把查找范围折掉一半。效率高得吓人,但前提是数据必须有序。

我记得有一次做车载导航系统,需要从几十万个POI(兴趣点)里快速查找某个地点。数据是按拼音排序的,用折半查找,每次查找只需要比较十几次,比顺序查找快了不知道多少倍。

// 折半查找 - 递归实现
int binary_search_recursive(int arr[], int left, int right, int key) {
    if (left > right) return -1;
    
    int mid = left + (right - left) / 2;  // 防止溢出
    
    if (arr[mid] == key) {
        return mid;
    } else if (arr[mid] > key) {
        return binary_search_recursive(arr, left, mid - 1, key);
    } else {
        return binary_search_recursive(arr, mid + 1, right, key);
    }
}

// 折半查找 - 迭代实现(我更喜欢这个)
int binary_search_iterative(int arr[], int n, int key) {
    int left = 0, right = n - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        
        if (arr[mid] == key) {
            return mid;
        } else if (arr[mid] > key) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return -1;
}
注意: 我曾经踩过一个坑——用 mid = (left + right) / 2 计算中间位置。当 left 和 right 都很大时,加起来可能溢出。用 left + (right - left) / 2 就安全多了。这种细节在嵌入式开发里尤其重要。

折半查找的时间复杂度是 O(log n)。什么意思呢?1000个数据,最多比较10次;100万个数据,最多比较20次。这效率,你品品。

分块查找:折中的智慧

分块查找,也叫索引顺序查找。它把数据分成若干块,块内可以无序,但块与块之间必须有序。说白了,就是第一块的所有数据都比第二块小,第二块比第三块小,以此类推。

我为什么说它折中?因为它结合了顺序查找和折半查找的优点。查找时先在索引表里用折半查找找到目标所在的块,然后在块内用顺序查找。既不用全排好序,又比纯顺序查找快。

// 分块查找的简单实现
typedef struct {
    int max_key;  // 块内最大关键字
    int start;    // 块起始位置
    int length;   // 块长度
} IndexBlock;

int block_search(int arr[], IndexBlock blocks[], int block_num, int key) {
    // 1. 在索引表中查找目标块
    int block_idx = -1;
    for (int i = 0; i < block_num; i++) {
        if (key <= blocks[i].max_key) {
            block_idx = i;
            break;
        }
    }
    if (block_idx == -1) return -1;
    
    // 2. 在目标块内顺序查找
    int start = blocks[block_idx].start;
    int end = start + blocks[block_idx].length;
    for (int i = start; i < end; i++) {
        if (arr[i] == key) {
            return i;
        }
    }
    return -1;
}
实际应用: 我在做文件系统的时候用过这个思路。目录项按文件名首字母分块,A-M一块,N-Z一块。查找时先确定首字母在哪块,再在块内顺序查找。比全量顺序查找快了一倍多。

三种方法怎么选?

我个人的经验是这么选的:

  • 数据量小(几十个以内):用顺序查找。代码简单,维护方便,性能差别不大。
  • 数据量大且有序:用折半查找。这是首选,没有之一。
  • 数据量大但无序,或者需要频繁插入删除:考虑分块查找。块内无序,插入删除方便;块间有序,查找效率有保障。
算法 时间复杂度 数据要求 适用场景
顺序查找 O(n) 无要求 小数据量,或无序数据
折半查找 O(log n) 必须有序 大数据量,静态数据
分块查找 O(log m + k) 块间有序 动态数据,索引查找

最后说一句:别小看查找算法。我见过太多项目因为选错了查找方式,导致系统响应慢得让人抓狂。选对算法,你的代码就跑得快;选错了,再牛的硬件也救不了你。

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