Swift的二分法查找实践

Swift的二分法查找实践

在这篇教程中我们会使用计算机科学里一个基础的算法:二分法查找
binary search.我们也会实现一个二分法查找的项目:快速自动匹配.

介绍

在一个数组里查找某个元素,最直接的方法是直接循环这个数组, 然后比较看是否等于我们要查找的元素.这种方法叫做linear search, 实现方法如下:

func linearSearch(array: [Int], key: Int) -> Bool {
    for item in array {
        if item == key {
            return true
        }
    }
    return false
}

在大多数实际的项目中顺序查找速度已经满足需求了. 但是如果数据量很大或者比较操作十分耗时, 那么这时候顺序查找性能就会急剧下降.

另一个方法是使用二分法查找. 二分法查找只适用于有序的数据, 所以我们需要将数组排序或者维护它的有序状态来保证二分法查找可以正确运行.

二分法查找是通过将数组从中间位置分开两半, 查找只会发生在其中的一部分.如果当前的元素比查找元素大, 则查找会在前半部分继续.如果当前元素比查找元素小,则查找在后半部分继续.
如果当前元素等于查找元素,则完成查找.

查找操作只有当找到元素了或者数组长度变成1.

为了让二分查找更加的直观, 可以看按字母排序的电话簿.假如你现在想找Nash John的电话号码,比较快捷的方法是从电话簿的中间开始找.你只需要找电话簿的一半,因为Nash John只会出现在当前页的之前或者之后.二分法查找跟这个方法差不多, 除了一直将数据对半分, 知道找到元素.

下图展示了怎样在已下的有序数组里面找到36:

let array = [1, 2, 3, 5, 5, 7, 10, 36, 50]





二分查找法比线性查找法高效是因为二分查找法每一次查找都会将搜索范围缩减一半.当然, 在我们长度为9的数组来说没有很明显的效果, 这里线性查找最多需要9次找到目标, 而二分查找法最多需要4次.但是如果一个数组有1000个元素,线性查找最多需要1000次查找, 而二分查找法则最多需要10次.(查找数组)

即使数组大小为10亿,二分查找法也最多需要30次查找就能够找到元素.

假设数组大小是K, 那么线性查找需要K次查找, 而二分查找需要[Log₂(K)]次.

如果Log₂(K) = n, 那么 2^n = K.

Swift 实现

直到现在为止,我们只从理论的角度来讲二分查找法,让我们来看看怎么用Swift来实现这个算法.这个算法实现为一个方法, 入参是一个数组, 一个目标元素(target), 根据查找结果返回 一个bool值.

我们会维护两个index值('leftright), 代表着查找数组的范围.初始值是数组的第一个元素和最后一个元素的index.

下一步我们会重复从数组中取得中间的元素与target作比较.我们会在一个while循环循环这些操作, 直到数组大小为1,也就是left <= right.

我们会从每一步的循环中计算中间的index(leftright的均值),然后取得元素的值.

现在我们会遇到3种情况:

  1. 找到target, 完成查找.返回true.
  2. 当前元素的值小于target.我们设置leftmid + 1然后让查找在数组的后半部分继续.
  3. 当前元素的值大于target.我们设置rightmid - 1然后让查找在数据的前半部分继续.

如果在退出循环,那么target肯定不在数组中, 那么这时候返回false.

func binarySearch(array: [Int], target: Int) -> Bool {

    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value == target) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }

    return false
}

以上的方法只适用整型数组, 但我们可以很容易地将改方法扩展为Comparable型的数组.

func binarySearch<T : Comparable>(array: [T], target: T) -> Bool {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value == target) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }
    return false 
    }

查找前缀

我们泛型版本的二分查找法可以用在搜索字符串的数组.我们可以进一步扩展该方法为在字符串数组中查找前缀等于target字符串的元素.如果一个字符串以target字符串作为前缀,那么我们就说target字符串就是该元素的前缀.

实现这个功能, 我们只需要在比较元素的时候用hasPrefix方法.

func binarySearchPrefix(array: [String], target: String) -> Bool {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (value.hasPrefix(target)) {
            return true
        }

        if (value < target) {
            left = mid + 1
        }

        if (value > target) {
            right = mid - 1
        }
    }
    return false
}

有可能数组里前缀是target字符串的不止一个.我们可以实现一个方法返回数组中第一个前缀为target元素的下标而不是返回true/false. 相似的, 也可以实现一个返回满足条件的最后一个元素的下标.我们会用这些方法来实现自动匹配
功能.

这里的技巧就是只有当出现已下某一种情况的时候我们才返回下标:

  1. leftright的值相等, 并且当前值的前缀等于target.
  2. 当前值的前缀等于target并且当前值的前一个元素的前缀不等于target(适用于binarySearchFirst方法).我们需要注意不要进行数组的越界操作.
  3. binarySearchLast方法中,我们使用了相同的方法,只是检测当前值的下一个元素的前缀不等于target.

如果数组中没有符号条件的字符串,返回-1.

binarySearchFirst :

func binarySearchFirst(array: [String], target: String) -> Int {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (left == right && value.hasPrefix(target)) {
            return left
        }

        if value.hasPrefix(target) {
            if mid > 0 {
                if !array[mid - 1].hasPrefix(target) {
                    return mid
                }
            }
            right = mid - 1
        } else if (value < target) {
            left = mid + 1
        } else if (value > target) {
            right = mid - 1
        }
    }

    return -1
}

binarySearchLast:

func binarySearchLast(array: [String], target: String) -> Int {
    var left = 0
    var right = array.count - 1

    while (left <= right) {
        let mid = (left + right) / 2
        let value = array[mid]

        if (left == right && value.hasPrefix(target)) {
            return left
        }

        if value.hasPrefix(target) {
            if mid < array.count - 1 {
                if !array[mid + 1].hasPrefix(target) {
                    return mid
                }
            }

            left = mid + 1
        } else if (value < target) {
            left = mid + 1
        } else if (value > target) {
            right = mid - 1
        }
    }

    return -1
}

自动匹配

假设你现在要建一个浏览器, 当用户输入url的时候需要有自动匹配.一个用户可能会有成千上万的url历史纪录,这时如果用线性查找的话自动匹配会有有延迟导致用户体验不好.

线性查找

3.0.gif

二分查找

3.1.gif

这个项目的UI代码相当的简单直接: 一个textField用来让用户输入URL, 下面还有一个tableView来显示自动匹配
.tabeView显示的数据是来自于SearchManager.这个类包含已下的方法:

  • func updateFilter(filter: String),用来更新过滤URLs的字符串.
  • func filteredURLAtIndex(index: Int) -> String返回过滤后URLs列表中的第index个URL.
  • func filteredURLCount() -> Int返回满足过滤条件URLs的数量.

UI代码


class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var textField: UITextField!

var searchManager = SearchManager()

override func viewDidLoad() {
    super.viewDidLoad()


}

@IBAction func textFieldDidChange(sender: AnyObject) {
    searchManager.updateFilter(textField.text)
    tableView.reloadData()
}


func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell = UITableViewCell()

    cell.textLabel!.text = searchManager.filteredURLAtIndex(indexPath.row)

    return cell
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return searchManager.filteredURLCount()
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    self.textField.text = searchManager.filteredURLAtIndex(indexPath.row)
}

}


Search manager

Search manager的作用是初始化之后维护一个从文件中读取的URL列表.另外还有两个需要维护的数组下标: filterStart:第一满足过滤条件URL的下标. filterEnd:最后一个满足过滤条件URL的下标.

下面是SearchManager线性查找的实现.

class SearchManager {

    private var urls: [String]
    private var filterStart: Int
    private var filterEnd: Int

    init() {
        let path = NSBundle.mainBundle().pathForResource("urls", ofType: "txt")!

        let content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)!

        urls = content.componentsSeparatedByString("\n")

        filterStart = 0
        filterEnd = urls.count - 1
    }

    func filteredURLCount() -> Int {
        if filterStart == -1 {
            return 0
        }

        return filterEnd - filterStart + 1
    }

    func filteredURLAtIndex(index: Int) -> String {
        return urls[filterStart + index]
    }



    func updateFilter(filter: String) {

        if filter == "" {
            filterStart = 0
            filterEnd = urls.count - 1
            return
        }

        var foundFirst = false

        var index = 0
        for url in urls {
            if url.hasPrefix(filter) {
                if !foundFirst {
                    filterStart = index
                    foundFirst = true
                }

                filterEnd = index

            }
            index++
        }

        if !foundFirst {
            filterStart = -1
        }
    }
}

SearchManager二分查找法是比较容易实现.需要注意的是我们修改SearchManager的内部实现的时候是不需要修改UI代码的.

class SearchManager {

    private var urls: [String]
    private var filterStart: Int
    private var filterEnd: Int

    init() {
        let path = NSBundle.mainBundle().pathForResource("urls", ofType: "txt")!

        let content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)!

        urls = content.componentsSeparatedByString("\n")

        filterStart = 0
        filterEnd = urls.count - 1
    }

    func filteredURLCount() -> Int {
        if filterStart == -1 {
            return 0
        }

        return filterEnd - filterStart + 1
    }

    func filteredURLAtIndex(index: Int) -> String {
        return urls[filterStart + index]
    }

    func binarySearchLast(array: [String], target: String) -> Int {
        var left = 0
        var right = array.count - 1

        while (left <= right) {
            let mid = (left + right) / 2
            let value = array[mid]

            if (left == right && value.hasPrefix(target)) {
                return left
            }

            if value.hasPrefix(target) {
                if mid < array.count - 1 {
                    if !array[mid + 1].hasPrefix(target) {
                        return mid
                    }
                }

                left = mid + 1
            } else if (value < target) {
                left = mid + 1
            } else if (value > target) {
                right = mid - 1
            }
        }

        return -1
    }

    func binarySearchFirst(array: [String], target: String) -> Int {
        var left = 0
        var right = array.count - 1

        while (left <= right) {
            let mid = (left + right) / 2
            let value = array[mid]

            if (left == right && value.hasPrefix(target)) {
                return left
            }

            if value.hasPrefix(target) {
                if mid > 0 {
                    if !array[mid - 1].hasPrefix(target) {
                        return mid
                    }
                }
                right = mid - 1
            } else if (value < target) {
                left = mid + 1
            } else if (value > target) {
                right = mid - 1
            }
        }

        return -1
    }


    func updateFilter(filter: String) {

        if filter == "" {
            filterStart = 0
            filterEnd = urls.count - 1
            return
        }

        filterStart = binarySearchFirst(urls, target: filter)
        filterEnd = binarySearchLast(urls, target: filter)
    }

}

项目下载

Extra Credits

当自动匹配是根据前缀来实现的时候,二分查找非常适合.当你需要查找数组中是否有满足包含某个子字符串的时候,这种情况下二分查找就不适合了.

如果你对更高级的字符串匹配算法感兴趣, 也可以了解下面的:

如果你想做一个模糊匹配:


原文: Binary Search and Applications

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 依稀还记得大学时,一寝室的室友讨论过将来老公的身高问题,还拿来了小矮凳比划了一下,想着怎么也必须找一个一拥抱刚好能...
    摇啊摇摇到外婆桥阅读 242评论 0 1