导读:本期聚焦于长沙GEO公司创作的《如何用指针实现数组的二分查找 指针版本的经典算法实现》,敬请观看详情。在有序数组中查找一个元素,常规做法是直接用下标访问,但换用指针实现时,边界控制往往容易出错。指针版本的二分查找核心在于用两个指针分别指向数组首元素和尾元素之后的地址,通过比较中间位置元素与目标值的大小,不断收缩搜索区间。本文从指针运算的角度拆解二分查找的每一步执行流程,给出完整的C语言实现,分析指针移动时的越界风险与空区间判断,并对比下标版本与指针版本在可读性、性能、适用场景上的差异。理解指针偏移和地址比较的细节之后,遇到链表或非连续存储结构时也能快速迁移这一算法思路。

二分查找要求数据有序,这一点不会因为用指针还是下标而改变。用指针实现时,最常见的思路是维护一个左指针和一个右指针,左指针初始指向数组首元素,右指针初始指向数组尾元素的下一个位置,形成一个左闭右开的区间。每次计算中间位置时,不能简单地把两个指针相加再除以二,因为两个指针相加没有定义,必须借助数组首地址的偏移量来计算。例如中间指针可以用数组首地址加上左右偏移量差值的一半得到,这样既满足指针运算规则,又能准确定位到中间元素。

如何用指针实现数组的二分查找 指针版本的经典算法实现

边界处理是二分查找最容易出错的地方。采用左闭右开区间时,只要左指针小于右指针,区间内就至少有一个元素,循环条件写成 left < right 即可。当目标值大于中间元素时,左指针移动到中间指针的下一个位置;当目标值小于中间元素时,右指针直接移动到中间指针的位置;当目标值等于中间元素时,立即返回中间指针。这个移动规则保证了每次迭代后区间长度严格缩小,不会出现死循环,也避免了漏掉第一个或最后一个元素的情况。

指针版本的代码实现与逐步解析

下面给出一段完整的C语言实现,数组假设为升序排列。函数接收数组首地址、数组长度和目标值,返回指向目标值所在位置的指针,如果没有找到则返回NULL。

#include <stdio.h>

int *binary_search_pointer(int *arr, int n, int target) {
    int *left = arr;
    int *right = arr + n;   /* 左闭右开区间,right指向末尾元素之后 */
    while (left < right) {
        int *mid = arr + ((right - left) / 2);  /* 借助首地址计算中间指针 */
        if (*mid == target) {
            return mid;
        } else if (*mid < target) {
            left = mid + 1;   /* 目标在右半部分 */
        } else {
            right = mid;      /* 目标在左半部分 */
        }
    }
    return NULL;
}

int main(void) {
    int a[] = {2, 5, 8, 12, 19, 23, 37, 45, 58};
    int n = sizeof(a) / sizeof(a[0]);
    int target = 19;
    int *pos = binary_search_pointer(a, n, target);
    if (pos != NULL) {
        printf("找到了,索引为: %ld\n", pos - a);
    } else {
        printf("未找到\n");
    }
    return 0;
}

代码中 right = arr + n 是合法的,因为数组元素个数为 n,最后一个元素的地址是 arr + n - 1,arr + n 刚好是尾后地址,只用于比较,不进行解引用。计算中间指针时使用 arr + ((right - left) / 2),而不是 (left + right) / 2,因为两个指针相加属于未定义行为,必须先算出偏移量再和首地址相加。偏移量 (right - left) / 2 是整数除法,结果向下取整,保证了中间指针不会超出右边界。

当数组长度为奇数或偶数时,这个中间位置的取法略有不同。比如长度5,初始 left 指向索引0,right 指向索引5,偏移量差为5,除以2得2,arr + 2 即索引2。长度6时,偏移量差为6,除以2得3,arr + 3 即索引3。无论哪种情况,中间位置都落在区间内部,不会等于 right,因此解引用是安全的。循环内部每轮都会根据比较结果调整 left 或 right,并且调整幅度至少为1个元素,循环变量单调变化,保证了终止性。

指针版本与下标版本的差异与易错点

下标版本的二分查找通常用整型变量 left 和 right 记录边界,中间位置写成 mid = left + (right - left) / 2。指针版本只是把这些整型下标替换成了地址运算,逻辑完全一致,但是更容易出现两个问题:一是在计算中间指针时写成 (left + right) / 2,直接导致编译错误或未定义行为;二是在移动左指针时写成 left = mid + 1 没有问题,但如果写成 left = mid++ 就会出错,因为 mid++ 返回原值再自增,赋值之后 left 仍然指向原来的 mid,造成区间几乎不缩小,可能死循环。

另一个常见错误是把循环条件写成 left <= right,同时又采用左闭右开区间。这样当区间为空时,left 可能等于 right,需要额外判断是否越界,容易造成解引用无效地址。左闭右闭区间是另一种写法,right 初始指向最后一个元素,循环条件写成 left <= right 也可以,但移动规则要相应调整。本文采用的左闭右开写法在C++标准库中的 std::lower_bound 等函数中被广泛使用,理解和掌握这种边界约定能减少出错。

从性能角度看,指针版本和下标版本在编译优化后几乎没有差别,因为下标访问在底层也是通过基址加偏移量实现的。指针版本的优势在于更贴近内存模型,方便在函数中直接返回元素地址,调用者可以通过返回的指针计算出索引,也可以直接修改指向的元素。在需要操作数组片段或者处理动态内存时,指针版本往往能让代码更简洁。但指针版本的可读性稍差一些,尤其是对指针运算不熟悉的读者,需要多花时间理解每一步地址变化。

边界条件验证与测试用例设计

为了验证指针版二分查找的正确性,应当覆盖多种边界情况:空数组、单元素数组、目标值位于首元素、目标值位于末元素、目标值不在数组中但落在中间区间、目标值小于所有元素、目标值大于所有元素、数组长度为偶数和奇数。空数组时 n 为0,right = arr + 0,left = arr,循环条件 left < right 不成立,直接返回 NULL,不会发生任何解引用。单元素数组长度为1,right = arr + 1,区间内只有一个地址,mid 就是那个元素,比较后立即返回或者区间清空。

可以使用以下测试框架进行验证。这里单独写一段测试代码,不依赖任何第三方库,把每个测试用例作为一次函数调用,打印输出实际结果和期望结果。

#include <stdio.h>
#include <assert.h>

/* 复用上面的 binary_search_pointer 函数 */
int *binary_search_pointer(int *arr, int n, int target);

void test_case(int *arr, int n, int target, int expected) {
    int *p = binary_search_pointer(arr, n, target);
    int actual = (p == NULL) ? -1 : (int)(p - arr);
    printf("target=%d, expected=%d, actual=%d\n", target, expected, actual);
    assert(actual == expected);
}

int main(void) {
    int a[] = {1, 3, 5, 7, 9, 11};
    int n = sizeof(a) / sizeof(a[0]);
    test_case(a, n, 1, 0);       /* 首元素 */
    test_case(a, n, 11, 5);      /* 末元素 */
    test_case(a, n, 5, 2);       /* 中间元素 */
    test_case(a, n, 4, -1);      /* 不在数组中 */
    test_case(a, n, 0, -1);      /* 小于所有 */
    test_case(a, n, 15, -1);     /* 大于所有 */
    test_case(NULL, 0, 3, -1);   /* 空数组,注意n为0时不能解引用,函数内部arr可能为NULL但不会使用,因为循环不进入 */
    return 0;
}

注意测试空数组时传递 NULL 作为首地址,函数内部在 n 为0时不会解引用任何地址,因此安全。但为了严谨,实际工程中如果允许 NULL 传入,函数开头可以增加 if (arr == NULL && n > 0) return NULL; 的判断。上述测试代码引入了 assert 宏,需要包含 assert.h,可以在调试阶段快速发现错误。

通过边界测试可以确认指针移动没有越界,尤其是最后一次迭代中 left 和 right 相邻的情况。例如数组 {1,3},查找目标2时,初始 left 指向1,right 指向尾后地址,mid 指向1,目标大于1,left 移动为 mid + 1 即指向3,此时 left < right,区间只剩元素3,mid 再次指向3,目标小于3,right 移动为 mid,此时 left == right,循环结束,返回 NULL,整个过程没有解引用 right 指向的尾后地址。这种逐步推演对理解指针版本很有帮助。

指针二分查找在其他数据场景中的迁移

指针版本二分查找的底层思想可以迁移到其他随机访问结构上,比如 vector 的迭代器、自定义数组类或者内存池中的连续块。只要能够通过首地址加偏移量得到中间地址,并且支持解引用和比较操作,就可以套用相同的代码框架。例如C++中可以直接使用迭代器实现,代码结构几乎一样,只是类型从 int* 换成 std::vector<int>::iterator,计算中间迭代器时不能用首地址加偏移量,因为迭代器没有全局首地址,但可以用 left + (right - left) / 2 来计算,因为随机访问迭代器的加法和减法都是合法的。

对于链表这类非连续存储的结构,指针版本的传统二分查找无法直接使用,因为无法在 O(1) 时间内通过两个指针的差值计算中间位置。此时需要先遍历链表得到长度,再从头节点移动一半步数到达中间节点,整体时间复杂度不变但常数因子变大。不过链表通常利用跳表或者自平衡二叉搜索树来实现高效查找,普通二分查找并不适用。理解了数组指针版本的限制之后,就能清楚地区分哪些数据结构适合二分查找,哪些不适合。

在嵌入式开发中,数组可能存储在 flash 或特定内存区域,指针版本可以直接操作内存地址,省去计算索引的额外开销,但需要注意指针类型的大小和地址对齐要求。如果数组元素是结构体,指针运算会按照结构体大小步进,计算中间指针时同样用首地址加上偏移量乘以类型大小,C语言会自动处理 sizeof 的乘除,无需人工干预。这也是指针版本比手动索引更不易出错的一个原因。

总结来说,用指针实现数组二分查找的关键在于三点:第一,坚持左闭右开区间,循环条件用左指针小于右指针;第二,中间指针通过首地址加偏移量差值的一半计算,不能直接相加两个指针;第三,根据比较结果只移动一侧指针,每次移动确保跳过已比较的元素。掌握这些细节后,指针版二分查找可以成为编写底层算法库时的标准工具之一。

指针二分查找数组修改时间:2026-10-05 23:16:59

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1005/66191.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。