Swift算法-二分搜索Binary Search

声明:算法和数据结构的文章均是作者从github上翻译过来,为方便大家阅读。如果英语阅读能力强的朋友,可以直接到swift算法俱乐部查看所有原文,以便快速学习。作者同时也在学习中,欢迎交流

目的:快速找到数组中的某一元素。

假如你有一个数组,包含数百个数字,然后你需要从中找出某一个数字所在的位置,在大多数情况下,Swift自带的indexOf()函数可以快速帮你解决这种问题。过程如下:

let numbers = [11, 59, 3, 2, 53, 17, 31, 7, 19, 67, 47, 13, 37, 61, 29, 43, 5, 41, 23]

numbers.indexOf(43)  // returns 15

Swift自带的 indexOf函数是属于线性搜索,其代码为:

func linearSearch<T: Equatable>(_ a: [T], _ key: T) -> Int? {
    for i in 0 ..< a.count {
        if a[i] == key {
            return i
        }
    }
    return nil
}

我们可以用以下方法运行代码:
linearSearch(numbers, 43) // returns 15

虽然很简便,但是由于是线性搜索,意味着我们需要从数组的第一位开始搜索整个数组,在最糟糕的情况下,可能你需要搜寻完整个数组才发现数组里面没有你需要寻找的数字。所以说,线性搜索算法跟数组的大小相关性很大,数组越大,意味着消耗的时间越久。

分治法

对于数组大的情况下,最经典的算法当属二分搜索算法。它的核心就是不断的将数组一分为二,直到找到我们寻找的数字。

比如说,一个大小为n的数组,使用线性搜索算法的效率为O(n),而二分搜索算法的效率为O(log n)。更详细一点来说,当数组大小为1000000的时候,二分搜索算法只花了20步就能找到结果。因为log_2(1000000) = 19.9。而当数组大小上升到十亿级别的时候,二分搜索却只需要30步就能找到答案。

不过,使用二分搜索必须有个前提:这个必须数组是按照一定顺序排列的。

二分搜索的工作原理:

1.将数组一分为二,并决定你需要寻找的元素是在数组的左边还是右边。
2.由于我们的数组是已经排序好的数组,所以你只需要将寻找的元素和得到中间数字进行大小比较。
3.确定好寻找的元素在哪边后,在新的数组里面继续一分为二,然后比较大小。
4.不断重复上述过程,直到我们找到这个元素在数组中的位置,或者当数组无法继续一分为二的时候,判断数组中不存在需要寻找的元素。

代码

以下为递归版的二分搜索算法:

func binarySearch<T: Comparable>(_ a: [T], key: T, range: Range<Int>) -> Int? {
    if range.lowerBound >= range.upperBound {
        // 如果我们进入这个函数,则说明要寻找的元素不在数组里
        return nil

    } else {
        // 计算数组一分为二的位置
        let midIndex = range.lowerBound + (range.upperBound - range.lowerBound) / 2

        // 判断寻找的元素是否在数组左侧
        if a[midIndex] > key {
            return binarySearch(a, key: key, range: range.lowerBound ..< midIndex)

        // 判断寻找的元素是否在数组右侧
        } else if a[midIndex] < key {
            return binarySearch(a, key: key, range: midIndex + 1 ..< range.upperBound)

        // 如果进入这个函数内部,则说明我们已经在数组里找到这个元素!
        } else {
            return midIndex
        }
    }
}

我们可以在playground里进行测试:

let numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]

binarySearch(numbers, key: 43, range: 0 ..< numbers.count)  // 得到 13

必须注意这里的numbers是已经排序好的!

虽然说二分算法是不断的进行数组拆分,但是这里我们并不是真的去创建两个新的数组,而是使用Swfit中的Range对象。最初的时候,range包含数组的所有元素的位置,0..<numbers.count,随着拆分不断进行,range越变越小。

示例解析

我们有一组已经排序好的数组:

[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67 ]

在这个数组中,我们需要寻找的元素为43。首先我们需要确定数组中间值来将数组一分为二:

let midIndex = range.lowerBound + (range.upperBound - range.lowerBound)/2

最初的时候,这里的范围是lowerBound = 0upperBound = 19。这里我们可以得到midIndex = 0 + (19 - 0)/2 = 9.5 = 9(向下取整)。如图,我们的中间数组为29,其左右两个数组位数大小相等。

[ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
                                  *

接下来,二分算法将会决定我们接下去会使用哪一边的数组。相关代码为:

if a[midIndex] > key {
    // 使用左边部分
} else if a[midIndex] < key {
    // 使用右边部分
} else {
    return midIndex
}

由于29比43小,所以我们可以确定左侧的数组不存在43。我们继续在右侧数组中寻找43。此时新的范围为midIndex + 1range.upperBound

[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43, 47, 53, 59, 61, 67 ]

此时新的中间值为midIndex = 10 + (19 - 10)/2 = 14

[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43, 47, 53, 59, 61, 67 ]
                                                 *

新的中间值对应的数字为47<43,所以我们继续在左侧数组进行拆分。

[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43 | x, x, x, x, x ]

此时新的中间值为:

[ x, x, x, x, x, x, x, x, x, x | 31, 37, 41, 43 | x, x, x, x, x ]
                                     *

37<43。在右侧数组继续拆分:

[ x, x, x, x, x, x, x, x, x, x | x, x | 41, 43 | x, x, x, x, x ]
                                        *

再一次,41<43,继续拆分:

[ x, x, x, x, x, x, x, x, x, x | x, x | x | 43 | x, x, x, x, x ]
                                            *

终于,我们找到了43,此时的中间值为13,即43在数组的位置为13。整个过程看起来好像很冗长,但其实我们只花了4个步骤就得到了答案。与log_2(19) = 4.23相近。而如果是线性搜索,我们需要花14个步骤才能找到43!

迭代vs递归

通常情况下二分搜索属于递归过程,因为不断运用了同样的逻辑去缩小数组。但是这不意味着我们不能使用迭代的方法去实现二分搜索。同时,递归算法转化成迭代算法实现通常会提高运行效率,因为它只用了一个循环而不是多个嵌套循环。

以下为迭代版本的二分搜索算法:

func binarySearch<T: Comparable>(_ a: [T], key: T) -> Int? {
    var lowerBound = 0
    var upperBound = a.count
    while lowerBound < upperBound {
        let midIndex = lowerBound + (upperBound - lowerBound) / 2
        if a[midIndex] == key {
            return midIndex
        } else if a[midIndex] < key {
            lowerBound = midIndex + 1
        } else {
            upperBound = midIndex
        }
    }
    return nil
}

代码本身会比递归版本的更简单,其主要的不同就是对while循环的使用。我们同样可以在playground对代码进行测试:

let numbers = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67]

binarySearch(numbers, key: 43)  // 得到13

结语

看到这里,是否会感觉需要将数组先进行排序会很复杂?其实不然,虽然排序也需要花时间,但是,排序加上二分搜索的过程往往会比直接进行线性查找来得更快速。 特别是当你不是单单只想寻找一个元素的情况下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容