Swift的二分法查找实践

Swift的二分法查找实践

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

Swift的二分法查找实践_第1张图片

介绍

在一个数组里查找某个元素,最直接的方法是直接循环这个数组, 然后比较看是否等于我们要查找的元素.这种方法叫做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(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

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

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

  • Tries
  • Suffix arrays
  • Suffix trees

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

  • Levenshtein Distance
  • Levenshtein Automaton
  • BK Trees

原文: Binary Search and Applications

你可能感兴趣的:(Swift的二分法查找实践)