22、指针与查找算法:二分查找的指针实现、指针与哈希表、指针与二叉搜索树

查找,是编程里最基础也最核心的操作之一。你想想看,我们写程序,本质上就是在处理数据——存进去,再找出来。而指针,恰恰是让查找变得高效的关键工具。

这一章,我会带你用指针的视角,重新审视三种经典的查找结构:二分查找、哈希表、二叉搜索树。说白了,就是看看指针在这些场景下,到底扮演了什么角色。

一、二分查找的指针实现

二分查找,大家都不陌生。但很多人写二分查找,用的是数组下标。我个人习惯用指针来写,因为指针操作更贴近底层,也更灵活。

先看一个标准实现:

int* binary_search(int* arr, int size, int target) {
    int* left = arr;
    int* right = arr + size - 1;
    
    while (left <= right) {
        int* mid = left + (right - left) / 2;
        
        if (*mid == target) {
            return mid;  // 返回指针,而不是下标
        } else if (*mid < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return NULL;  // 没找到
}

注意看,这里返回的是指针。为什么?因为指针携带了地址信息,你可以直接通过它访问元素,还能做指针运算。如果返回下标,你还得再算一次地址。

我的经验:在嵌入式开发中,我经常用这种返回指针的方式。比如在内存映射的寄存器表中查找某个配置项,返回指针后可以直接读写,省去了下标到地址的转换步骤。

这里有个细节:mid = left + (right - left) / 2。为什么不用 (left + right) / 2?因为指针相加是未定义行为!两个指针相减得到的是ptrdiff_t,是合法的;但相加就不行。嗯,这个坑我踩过。

避坑指南:我曾经在项目里写过 mid = (left + right) / 2,编译器没报错,但运行结果时对时错。后来查了C标准才知道,指针相加是未定义行为。从那以后,我写二分查找只用 left + (right - left) / 2

二、指针与哈希表

哈希表,说白了就是「数组 + 链表」的组合。而指针,是连接这两者的桥梁。

看一个简单的哈希表节点定义:

typedef struct HashNode {
    int key;
    int value;
    struct HashNode* next;  // 拉链法,用指针串起来
} HashNode;

typedef struct HashTable {
    HashNode** buckets;  // 指针的指针,指向一个指针数组
    int size;
} HashTable;

这里 buckets 是二级指针。为什么?因为每个桶里存的是链表的头指针,而我们要动态分配这个指针数组。说白了,buckets 指向一个连续的指针数组,每个指针又指向一个链表节点。

插入操作的实现:

void hash_insert(HashTable* table, int key, int value) {
    int index = key % table->size;
    HashNode* node = (HashNode*)malloc(sizeof(HashNode));
    node->key = key;
    node->value = value;
    
    // 头插法,用指针操作
    node->next = table->buckets[index];
    table->buckets[index] = node;
}

查找操作:

int* hash_find(HashTable* table, int key) {
    int index = key % table->size;
    HashNode* cur = table->buckets[index];
    
    while (cur != NULL) {
        if (cur->key == key) {
            return &(cur->value);  // 返回值的指针
        }
        cur = cur->next;  // 指针遍历链表
    }
    
    return NULL;
}

核心要点:哈希表的效率,取决于指针操作的速度。插入时是O(1)的指针操作,查找时是O(1)的哈希计算加上O(n)的链表遍历。但这里的n是单个桶里的节点数,理想情况下接近1。

我在项目中遇到过一个问题:哈希表的扩容。当负载因子超过阈值时,需要重新分配桶数组,重新计算所有节点的哈希值。这时候,二级指针就派上用场了——你可以直接操作 buckets 指针数组里的每个指针,而不需要移动节点本身的数据。

三、指针与二叉搜索树

二叉搜索树(BST),是链表结构的升级版。每个节点有两个指针,分别指向左子树和右子树。

节点定义:

typedef struct BSTNode {
    int data;
    struct BSTNode* left;
    struct BSTNode* right;
} BSTNode;

插入操作,用二级指针实现会更优雅:

void bst_insert(BSTNode** root, int value) {
    while (*root != NULL) {
        if (value < (*root)->data) {
            root = &((*root)->left);
        } else if (value > (*root)->data) {
            root = &((*root)->right);
        } else {
            return;  // 已存在,不插入
        }
    }
    
    *root = (BSTNode*)malloc(sizeof(BSTNode));
    (*root)->data = value;
    (*root)->left = NULL;
    (*root)->right = NULL;
}

注意看,这里用 BSTNode** 二级指针,直接修改了父节点中指向子节点的指针。不需要额外的父节点变量,也不需要递归。我个人觉得这种写法比递归更直观,也更高效。

查找操作:

BSTNode* bst_search(BSTNode* root, int target) {
    while (root != NULL) {
        if (target == root->data) {
            return root;
        } else if (target < root->data) {
            root = root->left;  // 指针向左移动
        } else {
            root = root->right; // 指针向右移动
        }
    }
    return NULL;
}

我的习惯:写BST查找时,我更喜欢用迭代而不是递归。原因很简单——嵌入式环境栈空间有限,递归容易爆栈。而且迭代版本的指针操作,性能上通常比递归好那么一点点。

删除操作稍微复杂些,但核心还是指针操作:

void bst_delete(BSTNode** root, int target) {
    BSTNode* cur = *root;
    BSTNode** parent_link = root;
    
    // 先找到要删除的节点
    while (cur != NULL && cur->data != target) {
        if (target < cur->data) {
            parent_link = &(cur->left);
            cur = cur->left;
        } else {
            parent_link = &(cur->right);
            cur = cur->right;
        }
    }
    
    if (cur == NULL) return;  // 没找到
    
    // 处理三种情况
    if (cur->left == NULL) {
        *parent_link = cur->right;  // 用右子树替代
        free(cur);
    } else if (cur->right == NULL) {
        *parent_link = cur->left;   // 用左子树替代
        free(cur);
    } else {
        // 左右子树都不为空,找右子树的最小节点
        BSTNode** min_parent = &(cur->right);
        BSTNode* min_node = cur->right;
        while (min_node->left != NULL) {
            min_parent = &(min_node->left);
            min_node = min_node->left;
        }
        
        // 用最小节点的值替换当前节点
        cur->data = min_node->data;
        
        // 删除最小节点
        *min_parent = min_node->right;
        free(min_node);
    }
}

避坑指南:我曾经在删除BST节点时,忘记更新父节点的指针,导致内存泄漏和野指针。后来我养成了一个习惯——删除节点前,先想清楚「谁指向它」,然后通过二级指针来修改那个指向关系。

四、三种查找结构的对比

结构 查找时间复杂度 插入时间复杂度 指针使用特点 适用场景
二分查找(有序数组) O(log n) O(n) 指针算术运算 静态数据,频繁查找
哈希表 O(1) 平均 O(1) 平均 二级指针、链表指针 动态数据,快速查找
二叉搜索树 O(log n) 平均 O(log n) 平均 左右子树指针、二级指针 需要有序遍历的数据

你想想看,这三种结构,本质上都是在用指针来组织数据。二分查找用指针做算术运算,哈希表用指针做链式存储,二叉搜索树用指针做树形结构。指针,就是这些数据结构的「骨架」。

总结一下:指针在查找算法中,不只是「指向数据」,更是「组织数据」的工具。掌握指针的灵活运用,你就能写出更高效、更优雅的查找代码。

指针与查找算法知识体系 指针与查找算法 二分查找 哈希表 二叉搜索树 指针算术运算 返回指针而非下标 二级指针 链表指针遍历 动态扩容指针 左右子树指针 二级指针删除 迭代而非递归 核心思想:指针是组织数据的「骨架」 掌握指针 = 掌握数据结构的灵魂

嗯,这一章的内容就到这里。指针与查找算法的结合,说白了就是「用指针来组织数据,用算法来遍历指针」。你写代码的时候,多想想指针在其中的角色,慢慢就会形成自己的风格。

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