谈谈二分搜索及其变体

栏目: IT资讯 · 发布时间: 5年前

内容简介:在前作讨论二分搜索是一个效率很高的算法。一个良好实现的二分搜索算法,其时间复杂度可以达到 $\Theta(\log n)$,而空间复杂度只有 $O(1)$。特别地,二分搜索算法的描述十分简洁。作为程序员,总是喜欢 clean and powerful 的东西。因此,二分搜索无疑对程序员有巨大的吸引力。按照 Knuth 的说法,「尽管第一个二分搜索算法早在1946年就被发表,但第一个没有bug的二分搜索算法却是在12年后才被发表出来」。

在前作讨论 基于比较的 排序 算法的复杂度下界 时,我们提及了二分搜索算法。

二分搜索是一个效率很高的算法。一个良好实现的二分搜索算法,其时间复杂度可以达到 $\Theta(\log n)$,而空间复杂度只有 $O(1)$。特别地,二分搜索算法的描述十分简洁。作为程序员,总是喜欢 clean and powerful 的东西。因此,二分搜索无疑对 程序员 有巨大的吸引力。

按照 Knuth 的说法,「尽管第一个二分搜索算法早在1946年就被发表,但第一个没有bug的二分搜索算法却是在12年后才被发表出来」。

此篇我们讨论二分搜索算法的原理及其各种变体的实现。

算法描述

二分搜索是针对支持随机访问的有序数据集进行查找操作的算法。最基本的二分搜索,查找的是等于目标元素的元素在数据集中的位置。它的描述十分简单:

  • 折半取中,判断元素与目标元素的大小关系
    • 小于——往前继续折半
    • 大于——往后继续折半
    • 等于——返回

此处要注意二分搜索的适用场景:

  • 依赖顺序表结构
  • 数据本身必须有序
  • 数据量相对比较元素的开销要足够大——不然遍历即可
  • 数据量相对内存空间不能太大——不然顺序表装不下

二分搜索的实现

#include <iterator>
#include <functional>

template <typename IterT,
          typename ValueT = typename std::iterator_traits<IterT>::value_type,
          typename Compare = std::less<ValueT>>
IterT bsearch(IterT first,
              IterT last,
             ValueT target,
            Compare comp = Compare()) {
    IterT result = last;
    while (std::distance(first, last) > 0) {
        IterT mid = first + std::distance(first, last) / 2;
        if (comp(*mid, target)) {
            first = mid + 1;
        } else if (comp(target, *mid)) {
            last = mid;
        } else {  // equal
            result = mid;
            break;
        }
    }
    return result;
}

这一实现有一些技巧值得说一说。

首先,搜索范围是由 firstlast 构成的左闭右开区间。在编程中,坚持使用左闭右开区间,能够避免大多数索引越界的问题。这是个好习惯,值得一说。

其次,这一实现以 mid = low + (high - low) / 2 的方式来确定折半点。与之相对,还有一种写法是 mid = (low + high) / 2 。在数学的角度,这两种写法完全相同。但是在计算机的角度,后者可能涉及到整数的溢出。因此,为了避免溢出,我们应当优先采用实现当中的写法。

最后,这一实现以 while 循环替代递归,节省了函数的递归调用带来的开销。与之搭配,在未能找到目标时,通过调整区间首尾实现折半动作。这种实现方式是处于效率的考量。

二分搜索的变体

单就查找等于目标的元素来说,这一任务还有哈希表和查找树等数据结构能高效地完成。相较二分搜索,它们的限制更少——不需要数据集本身有序,也不需要分配连续的大块内存。如此看来,二分搜索似乎只是看起来美好,实际用途应该不广。

但事实上,二分搜索还有若干变体。这些变体实现的功能,上述这些数据结构通常很难以较低的时间复杂度完成。这些变体才是最能体现二分搜索威力的场景。这里介绍常见的四个变体:

  • 查找支持随机访问的有序数据集中,第一个等于给定值的元素
  • 查找支持随机访问的有序数据集中,最后一个等于给定值的元素
  • 查找支持随机访问的有序数据集中,第一个不小于给定值的元素
  • 查找支持随机访问的有序数据集中,最后一个不大于给定值的元素

这些变体的实现也不难。在上述标准二分搜索的基础上,只需要稍加改造即可。需要关注的核心点,就是在不同条件下,区间的首尾应该如何变化。以下是我以 C++ 实现的这些变体。这份实现里值得一提的地方,在基础款的二分搜索实现中已经提过,便不再赘述。

#include <iterator>
#include <functional>

enum class BsearchPolicy { UNSPECIFIED, FIRST, LAST, FIRST_NOT_LESS, LAST_NOT_GREATER };

template <typename IterT,
          typename ValueT = typename std::iterator_traits<IterT>::value_type,
          typename Compare>
IterT bsearch(IterT first,
              IterT last,
             ValueT target,
            Compare comp,
      BsearchPolicy policy = BsearchPolicy::UNSPECIFIED) {
    IterT result = last;
    while (std::distance(first, last) > 0) {
        IterT mid = first + std::distance(first, last) / 2;
        if (policy == BsearchPolicy::FIRST_NOT_LESS) {
            if (!comp(*mid, target)) {
                if (mid == first or comp(*(mid - 1), target)) {
                    result = mid;
                    break;
                } else {
                    last = mid;
                }
            } else {
                first = mid + 1;
            }
        } else if (policy == BsearchPolicy::LAST_NOT_GREATER) {
            if (comp(target, *mid)) {
                last = mid;
            } else {
                if (std::distance(mid, last) == 1 or comp(target, *(mid + 1))) {
                    result = mid;
                    break;
                } else {
                    first = mid + 1;
                }
            }
        } else {  // policy == UNSPECIFIED or FIRST or LAST
            if (comp(*mid, target)) {
                first = mid + 1;
            } else if (comp(target, *mid)) {
                last = mid;
            } else {  // equal
                if (policy == BsearchPolicy::FIRST) {
                    if (mid == first or comp(*(mid - 1), *mid)) {
                        result = mid;
                        break;
                    } else {
                        last = mid;
                    }
                } else if (policy == BsearchPolicy::LAST) {
                    if (std::distance(mid, last) == 1 or comp(*mid, *(mid + 1))) {
                        result = mid;
                        break;
                    } else {
                        first = mid + 1;
                    }
                } else {
                    result = mid;
                    break;
                }
            }
        }
    }
    return result;
}

template <typename IterT,
          typename ValueT = typename std::iterator_traits<IterT>::value_type,
          typename Compare = std::less<ValueT>>
IterT bsearch(IterT first,
              IterT last,
             ValueT target,
      BsearchPolicy policy = BsearchPolicy::UNSPECIFIED) {
        return bsearch(first, last, target, Compare(), policy);
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

我用微软改变世界

我用微软改变世界

保罗·艾伦 / 吴果锦 / 浙江人民出版社 / 2012-3 / 46.00元

《我用微软改变世界(微软联合创始人保罗•艾伦回忆录)》内容简介:1975年,两个从大学退学的男孩夜以继日地设计一款软件。其中一个男孩就是后来的世界首富比尔盖茨,而另外一个则作为盖茨背后的男人,一直生活在盖茨的阴影里,其实,他的人生经历远比盖茨更为传奇和丰富。 16岁,与比尔盖茨在顶级名校湖畔中学相遇,成为最佳拍档,无数趣事,无数闹腾,高呼“处男万岁”还不够,还得意扬扬把这话刻在碑上留给学弟们......一起来看看 《我用微软改变世界》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换