查找算法:顺序查找、二分查找、插值查找、分块查找、哈希查找

查找,说白了就是在一堆数据里找到你想要的那个。我做了十几年嵌入式开发,几乎每个项目都离不开查找。从简单的传感器数据检索,到复杂的协议解析,查找算法的选择直接决定了系统的响应速度。

今天咱们就把五种最常用的查找算法掰开揉碎讲清楚。我会结合自己的项目经验,告诉你什么时候该用哪个,哪些坑千万别踩。

核心要点:没有最好的算法,只有最合适的算法。数据量大小、数据是否有序、内存限制、查找频率,这些因素决定了你的选择。

查找算法知识体系 查找算法 顺序查找 无序数据 O(n) 时间复杂度 二分查找 有序数组 O(log n) 时间复杂度 插值查找 均匀分布有序数据 O(log log n) 平均 分块查找 分块有序 介于 O(1)~O(n) 哈希查找 O(1) 平均 需处理冲突 选型建议 • 数据量小(<100):顺序查找就够用 • 数据量大且有序:二分查找或插值查找 • 数据量大且无序:哈希查找(空间换时间) • 内存受限且数据可分块:分块查找

1. 顺序查找——最朴实的办法

顺序查找,就是从头到尾一个一个比。你想想看,这就像在一堆扑克牌里找红桃A,一张一张翻过去就是了。

我在项目中遇到过最典型的场景:解析一个简单的配置表,数据量不超过50条。这时候用顺序查找完全够用,代码还简单,不容易出bug。

// 顺序查找:从数组第一个元素开始逐个比较
int sequential_search(int arr[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == key) {
            return i;  // 找到了,返回下标
        }
    }
    return -1;  // 没找到
}

我的经验:顺序查找有个优化小技巧——把查找频率高的元素往前放。我在做菜单系统时,把最常用的功能放在列表前面,平均查找时间直接降了一半。

注意:顺序查找的时间复杂度是O(n)。数据量超过1000时,响应时间就开始变得明显了。我曾经在一个项目里用顺序查找查2000条记录,结果按键按下去要等半秒才有反应——这显然不行。

2. 二分查找——有序数据的利器

二分查找,说白了就是「猜数字」游戏。每次取中间值,比目标大就往左找,比目标小就往右找。前提是——数据必须有序。

我记得刚入行时,有个老工程师跟我说:「排序花的时间,会在查找时加倍还给你。」这话一点不假。

// 二分查找:要求数组已排序(升序)
int binary_search(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) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return -1;
}

关键点:mid = left + (right - left) / 2 比 (left + right) / 2 更安全。为什么?因为 left + right 可能溢出。我在32位单片机上吃过这个亏,当时查了半天才发现是整数溢出了。

二分查找的时间复杂度是O(log n)。1000条数据最多查10次,100万条数据最多查20次。效率提升非常明显。

3. 插值查找——更聪明的二分

插值查找是二分查找的升级版。二分查找每次都取中间点,而插值查找会根据目标值的大小,估算一个更可能的位置。

举个例子:你在字典里找「张」字,肯定不会从中间翻起,而是从后面翻。这就是插值查找的思路。

// 插值查找:适用于均匀分布的有序数组
int interpolation_search(int arr[], int n, int key) {
    int left = 0, right = n - 1;
    
    while (left <= right && key >= arr[left] && key <= arr[right]) {
        // 根据比例估算位置
        int pos = left + (key - arr[left]) * (right - left) / (arr[right] - arr[left]);
        
        if (arr[pos] == key) {
            return pos;
        } else if (arr[pos] < key) {
            left = pos + 1;
        } else {
            right = pos - 1;
        }
    }
    return -1;
}

避坑指南:我曾经在一个温度传感器数据查找中用插值查找,结果数据分布不均匀(大部分集中在20-30度,偶尔有极端值),插值查找反而比二分查找还慢。所以,数据分布不均匀时,老老实实用二分查找。

4. 分块查找——折中方案

分块查找,就是把数据分成若干块,块内无序,块间有序。查找时先确定目标在哪个块,再在块内顺序查找。

我个人的习惯是:当数据量在几百到几千之间,内存又有限制时,分块查找是个很好的折中方案。它不需要全部排序,维护成本低。

// 分块查找的索引表结构
typedef struct {
    int max_key;  // 块内最大关键字
    int start;    // 块起始位置
    int length;   // 块长度
} Block;

// 分块查找
int block_search(int arr[], Block blocks[], int block_num, int key) {
    // 第一步:在索引表中确定块
    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;
    
    // 第二步:在块内顺序查找
    Block *b = &blocks[block_idx];
    for (int i = b->start; i < b->start + b->length; i++) {
        if (arr[i] == key) return i;
    }
    return -1;
}

我的经验:分块的大小怎么定?我一般取 sqrt(n) 左右。比如1000条数据,分成32块,每块约31条。这样索引表查找和块内查找的复杂度比较均衡。

5. 哈希查找——快到极致

哈希查找,核心思想是「通过一个函数,直接把关键字映射到存储位置」。理想情况下,时间复杂度是O(1)——一次定位,无需比较。

但现实没那么完美。不同的关键字可能映射到同一个位置,这就是「冲突」。处理冲突是哈希表设计的核心。

5.1 哈希函数设计

常用的哈希函数有:

  • 直接定址法:H(key) = key,最简单,但要求key连续且范围小
  • 除留余数法:H(key) = key % p,p一般取质数
  • 平方取中法:先平方,再取中间几位

5.2 冲突处理方法

我项目中用得最多的是链地址法。为什么?因为嵌入式系统里,你不知道数据量会增长到什么程度,链地址法扩展性好。

// 链地址法:每个哈希槽位是一个链表
#define TABLE_SIZE 100

typedef struct Node {
    int key;
    int value;
    struct Node *next;
} Node;

Node* hash_table[TABLE_SIZE];

// 哈希函数
int hash(int key) {
    return key % TABLE_SIZE;
}

// 插入
void insert(int key, int value) {
    int idx = hash(key);
    Node *new_node = (Node*)malloc(sizeof(Node));
    new_node->key = key;
    new_node->value = value;
    new_node->next = hash_table[idx];
    hash_table[idx] = new_node;
}

// 查找
int search(int key) {
    int idx = hash(key);
    Node *cur = hash_table[idx];
    while (cur) {
        if (cur->key == key) return cur->value;
        cur = cur->next;
    }
    return -1;  // 没找到
}

重要提醒:哈希表的装载因子(元素个数/表大小)超过0.75时,冲突会急剧增加。我一般控制在0.5-0.7之间。超过0.7就考虑扩容,否则查找性能会从O(1)退化到O(n)。

我曾经踩过的坑:在一个通信协议解析项目中,我用哈希表存储消息ID到处理函数的映射。当时偷懒用了简单的取模哈希,结果消息ID分布不均匀,大部分冲突集中在几个槽位上。后来改用「除留余数法+取质数模数」,冲突率从30%降到了5%以下。

6. 五种算法对比总结

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 数据要求
顺序查找 O(n) O(n) O(1)
二分查找 O(log n) O(log n) O(1) 有序
插值查找 O(log log n) O(n) O(1) 有序且均匀分布
分块查找 O(sqrt(n)) O(n) O(sqrt(n)) 分块有序
哈希查找 O(1) O(n) O(n) 需设计哈希函数

最后说一句:别为了用算法而用算法。我见过有人为了炫技,在10个元素的数组上用二分查找——排序的时间都比查找本身长。合适的才是最好的。

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