美文网首页
通过Local function捕获变量共享资源

通过Local function捕获变量共享资源

作者: 醉看红尘这场梦 | 来源:发表于2020-09-08 10:03 被阅读0次

    了解了Swift中local function可以捕获变量的特性之后,除了用捕获的变量保存状态,我们还可以用它来在多次调用之间共享资源,以提高程序的执行效率。在这一节,我们就通过归并排序的实现,来了解这个用法。

    实现原理的简单回顾

    在开始具体的编码前,我们先来回顾下算法的原理。假设,我们有两摞已经排好序的数字pileApileB,它们各自的值,如图所示:

    merge sort

    要把这两摞数字从小到大合并起来,该怎么做呢?

    首先,我们找到pileApileB顶端两个数字中,更小的一个,并把它单独拿出来:

    merge sort

    其次,我们反复执行第一步的过程,不断从两摞数据的顶端拿走更小的一个放到结果里:

    merge sort merge sort

    第三,当其中的一摞数据已经拿光之后,由于之前两摞数据各自都已经是排序好的,因此,我们只要把剩余的一摞数据直接添加在结果最后,就好了:

    merge sort

    这样,自下而上,我们就得到了期望的结果。这就是merge sort中merge部分的含义。

    除此之外,我们还有另外一部分工作。为了对任意一个数组排序,我们要先把它变成两摞已经排序好的数字。怎么做呢?

    首先,我们从中间位置,先对数组进行一次分割。这就就得到了两摞数字,但是它们不一定都是已经排序好的:

    merge sort

    其次,在分割出来的两摞数字中,我们再进行分割:

    merge sort

    这样,对于图中的[1, 6]来说,我们可以把它理解为是两摞已排序好的数字了,因为一个数字是无法排序的(同理[8, 9]也是这样)。至此,我们就不用再继续分割,可以陆续进行merge了:

    1. [1, 6]排序,然后把得到的结果和[2]排序;
    2. [8, 9]排序;
    3. 把步骤1和步骤2的结果进行排序

    这样,就完成整个归并排序的过程了。

    merge sort

    一个有待改进的实现方法

    理解了这个归并排序的设计思想之后,我们就可以动手实现它了。我们先来看一个普通的实现方式。通常归并排序的数组拆分和子数组合并是被分开实现的。我们就通过extension Array来实现这个算法。

    首先,是先拆分后,再归并排序的整体过程:

    extension Array where Element: Comparable {
        mutating func mergeSort(_ begin: Index, _ end: Index) {
            if (end - begin) > 1 {
                let mid = (begin + end) / 2
    
                mergeSort(begin, mid)
                mergeSort(mid, end)
    
                merge(begin, mid, end)
            }
        }
    }
    
    

    在上面的代码里,最核心的逻辑就是只要拆分的数组中的元素个数大于2,就计算中间位置后,继续拆分;否则,就以计算的mid为中线,对[begin, mid)[mid, end)这个数组进行归并排序。

    其次,如何来实现这个merge方法呢?按照一开始我们描述的那样,不断从两摞数据顶端取走最小的一个,取空一个之后,把剩余的另一摞直接加在结果的末尾就好了:

    extension Array where Element: Comparable {
        private mutating func merge(_ begin: Index, _ mid: Index, _ end: Index) {
            // 1\. The result
            var tmp: [Element] = []
    
            // 2\. Fetch the smaller one from the two piles
            var i = begin
            var j = mid
    
            while i != mid && j != end {
                if self[i] < self[j] {
                    tmp.append(self[I])
                    i += 1
                }
                else {
                    tmp.append(self[j])
                    j += 1
                }
            }
    
            // 3\. Append the remaining
            tmp.append(contentsOf: self[i ..< mid])
            tmp.append(contentsOf: self[j ..< end])
    
            // 4\. Update self
            replaceSubrange(begin..<end, with: tmp)
        }
    }
    
    

    因为merge属于mergeSort的执行细节,因此我们把它定义为了private方法。如果你理解了整个算法的思想,上面的代码并不会有任何难度。实现了这两个方法之后,我们就可以用下面的代码试一下:

    var numbers = [1, 2, 6, 9, 8]
    numbers.mergeSort(numbers.startIndex, numbers.endIndex)
    
    

    如果一切正常,你就能看到排序后的结果:[1, 2, 6, 8, 9]了。

    通过捕获局部变量共享资源

    上面这个实现虽然可以正常工作,但是仍有改进的空间。仔细分析merge的执行过程就会发现,每次递归调用时用于保存局部排序结果的tmp都是重新在调用栈中申请的,这其实是一个没必要的行为。由于最多情况下,tmp也就容纳原始数组的所有元素,因此,我们可以利用local function捕获变量的特性,为每一次merge的执行提供一个共享的存储空间:

    mutating func mergeSort(_ begin: Index, _ end: Index) {
        // A shared storage across all recursive calls
        var tmp: [Element] = []
        tmp.reserveCapacity(count)
    
        func merge(_ begin: Int, _ mid: Int, _ end: Int) {
            // 1\. Use the same shared storage
            tmp.removeAll(keepingCapacity: true)
    
            // The same as before
            var i = begin
            var j = mid
            // ...
            // Omit for simplicity
        }
    
        if ((end - begin) > 1) {
            let mid = (begin + end) / 2
    
            mergeSort(begin, mid)
            mergeSort(mid, end)
    
            merge(begin, mid, end)
        }
    }
    
    

    这次,我们在mergeSort创建了一个可以包含原始数组所有元素的临时变量,并merge定义为了mergeSort的local function。在merge的实现里,我们捕获了临时变量tmp,并在每次使用前,把它清空。这样,就不用在每次迭代的时候,重新创建和销毁一个Array了。

    并且,由于tmp仍旧是一个局部变量,当我们对不同的Array排序时,每一个mergeSort调用,都会有一个自己的tmp变量,也不会在多次使用mergeSort的时候带来冲突。

    除此之外,local function还有一个副作用,就是它更深层次的隐藏了merge方法的实现,我们甚至都不用把它定义为private,它完全是mergeSort的私人财产。

    以上,就是除了共享状态之外,通过local function捕获变量的特性,在多次调用之间共享资源的用法。当然,是否值得这样使用,你还是要在性能,代码简洁性和可维护性上取得一个平衡,无论如何,作为一个有效的提升性能的途径,你应该知道捕获变量的这种用法。

    相关文章

      网友评论

          本文标题:通过Local function捕获变量共享资源

          本文链接:https://www.haomeiwen.com/subject/zuunehtx.html