查找算法:顺序查找、二分查找、哈希查找、二叉搜索树

查找,说白了就是在一堆数据里找到你想要的那个。我做了这么多年开发,发现很多性能问题其实都出在查找上。你想想看,一个系统如果查找慢了,整个体验就崩了。今天咱们就把四种最核心的查找算法捋一遍。

顺序查找——最笨但最可靠

顺序查找的思路简单到令人发指:从头到尾,一个一个比。我刚开始学编程时,第一个写的查找代码就是这个。

// 顺序查找
int seq_search(int arr[], int n, int key) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == key) return i;
    }
    return -1;
}

时间复杂度O(n),空间复杂度O(1)。没什么技巧,就是硬找。

我的经验:顺序查找最适合数据量小(比如几十个元素)或者数据无序的场景。我曾经在一个嵌入式项目里,数据总共就20个,用二分查找还得先排序,反而更慢。

二分查找——快,但有前提

二分查找的前提是数据必须有序。它的核心思想是:每次砍掉一半。

// 二分查找(迭代版)
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;
}

时间复杂度O(log n),空间复杂度O(1)。

注意:mid的计算用 left + (right - left) / 2 而不是 (left + right) / 2。为什么?因为left+right可能溢出。我曾经在面试题里见过这个坑,很多人没注意。

二分查找还有递归版本,不过我个人更推荐迭代版。递归虽然代码短,但函数调用有开销,而且容易栈溢出。

// 二分查找(递归版)
int binary_search_rec(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_rec(arr, mid + 1, right, key);
    else 
        return binary_search_rec(arr, left, mid - 1, key);
}

哈希查找——用空间换时间

哈希查找的核心是:通过一个哈希函数,直接把key映射到存储位置。理想情况下,时间复杂度O(1)。

但现实很骨感——哈希冲突是绕不开的问题。常用的解决方式有两种:

  • 链地址法:每个哈希槽挂一个链表
  • 开放地址法:冲突了就往后找空位
// 简单的哈希表(链地址法)
#define TABLE_SIZE 100

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

Node* hash_table[TABLE_SIZE];

int hash_func(int key) {
    return key % TABLE_SIZE;
}

void insert(int key, int value) {
    int index = hash_func(key);
    Node* new_node = (Node*)malloc(sizeof(Node));
    new_node->key = key;
    new_node->value = value;
    new_node->next = hash_table[index];
    hash_table[index] = new_node;
}

int search(int key) {
    int index = hash_func(key);
    Node* cur = hash_table[index];
    while (cur) {
        if (cur->key == key) return cur->value;
        cur = cur->next;
    }
    return -1;
}

核心要点:哈希函数的设计决定了查找效率。一个好的哈希函数应该让数据分布均匀,减少冲突。我常用的技巧是取模时用质数,比如用 10007 而不是 10000。

二叉搜索树——动态数据的利器

如果数据是动态插入删除的,二分查找就不太行了。这时候二叉搜索树(BST)就派上用场了。

BST的性质很简单:左子树所有节点小于根,右子树所有节点大于根。

// 二叉搜索树节点
typedef struct BSTNode {
    int data;
    struct BSTNode* left;
    struct BSTNode* right;
} BSTNode;

// 插入
BSTNode* insert(BSTNode* root, int val) {
    if (root == NULL) {
        BSTNode* node = (BSTNode*)malloc(sizeof(BSTNode));
        node->data = val;
        node->left = node->right = NULL;
        return node;
    }
    if (val < root->data)
        root->left = insert(root->left, val);
    else if (val > root->data)
        root->right = insert(root->right, val);
    return root;
}

// 查找
BSTNode* search(BSTNode* root, int key) {
    if (root == NULL || root->data == key)
        return root;
    if (key < root->data)
        return search(root->left, key);
    else
        return search(root->right, key);
}

平均时间复杂度O(log n),最坏情况O(n)——比如数据已经有序,BST就退化成链表了。

避坑指南:我曾经在一个项目里直接用BST存用户ID,结果用户注册是按顺序来的,树直接变成了一条线,查找慢得离谱。后来换成了平衡二叉树(AVL或红黑树)才解决问题。

四种算法对比

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 数据要求
顺序查找 O(n) O(n) O(1)
二分查找 O(log n) O(log n) O(1) 有序
哈希查找 O(1) O(n) O(n) 需哈希函数
二叉搜索树 O(log n) O(n) O(n) 可比较大小

知识体系总览

下面这张图把四种查找算法的关系梳理清楚了,你可以对照着看:

查找算法 顺序查找 O(n) 无要求 二分查找 O(log n) 需有序 哈希查找 O(1) 需哈希函数 二叉搜索树 O(log n) 动态数据 选择建议 • 数据量小(<100)且无序 → 顺序查找 • 数据量大且有序(静态)→ 二分查找 • 数据量大且需快速查找 → 哈希查找 • 数据动态插入删除 → 二叉搜索树(建议平衡树)

我的建议:实际开发中,别死磕一种算法。我经常是先用哈希表做快速查找,再用BST做范围查询,两者配合着用。记住,没有最好的算法,只有最合适的场景。

嗯,四种查找算法就讲到这里。代码我都跑过,可以直接用。如果你在实际项目中遇到查找性能问题,不妨回头看看这四种思路,总有一款适合你。

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