算法图解之Swift实践【第三章 递归】

129 阅读2分钟

本人已参与「新人创作礼」活动,一起开启掘金创作之路。

递归

递归是一种解决问题的有效方法,在递归过程中,函数将自身作为子例程调用。

递归是算法中一个重要的解法,是一种优雅的问题解决方法。

简单来说,程序调用自身的编程技巧叫递归。递归的思想是把一个大型复杂问题层层转化为一个与原问题规模更小的问题,问题被拆解成子问题后,递归调用继续进行,直到子问题无需进一步递归就可以解决的地步为止。

示例

使用递归计算阶乘

func factorial(n: Int) -> Int {
    if n <= 1 {
       return 1
    } else {
       return n * factorial(n - 1)
    }
}

使用递归实现,代码的可读性更强,但是实际性能与循环相同,并未有所提升。 递归只是让解决方案更加清晰,但实际上,并没有性能上的优势。

基线条件和递归条件

由于递归函数中会调用自己,因此编写这样的函数时很容易出错,进而导致无限循环。我们需要给定判定条件,以使得循环在满足条件后停止。

每个递归函数都有两部分:基线条件(base case)和递归条件(recursive case)。

  • 基线条件: 函数不再调用自己的条件,从而避免无限循环。
  • 递归条件:函数调用自己的条件。

有了基线条件和递归条件,函数才能按照预期的那样运行。

func countdown(i: Int) {
    print(i)
    if i <= 0 { // 基线条件
        return 
    } else { // 递归条件
        countdown(i - 1)
    }
}

栈是一种简单的数据结构,只有压入(插入)和弹出(删除并读取)的操作,遵循先进后出原则。

调用栈

计算机在内部使用被称为调用栈的栈。 调用栈的特点是,函数内调用另一个函数时,当前函数暂停并处于未完成状态。用于存储多个函数的变量的栈,被称为调用栈。

递归调用栈

递归函数也使用调用栈,栈在递归中扮演着重要角色。

练习

3.1 根据下面的调用栈,你可获得哪些信息?

题图3.1

1.首先调用了函数greet,并将参数name的值指定为maggie。

2.接下来,函数greet调用了函数greet2,并将参数name的值指定为maggie。

3.此时函数greet处于未完成(挂起)状态。

4.当前的函数调用为函数greet2。

5.函数greet2执行完毕后,函数greet将接着执行。

3.2 假设你编写了一个递归函数,但不小心导致它没完没了地运行。正如你看到的,对于每次函数调用,计算机都将为其在栈中分配内存。递归函数没完没了地运行时,将给栈带来什么影响?

栈将会没完没了地增大,每个程序可使用的调用栈空间都是有限的,程序用完这些空间后,将因栈溢出而终止。

由于本书的练习题4.1 - 4.4考察的主要内容还是递归,因此将这几题移到递归这一章里一并探讨。

4.1 请编写前述sum函数的代码。

func sum(_ arr: [Int], _ l: Int) -> Int {
    return l == 0 ? 0 : sum(arr, l - 1) + arr[l - 1]
}

4.2 编写一个递归函数来计算列表包含的元素数。

func getItemCount(_ arr: [Int]) -> Int {
    var varArr = arr
    if arr.isEmpty {
        return 0
    }
    varArr.removeFirst()
    return 1 + getItemCount(varArr)
}

4.3 找出列表中最大的数字。

func findMax(_ arr: [Int]) -> Int {
    let arr = arr
    let length = arr.count
    let temp = arr[0]
    
    return findMax(arr, length, temp)
}

func findMax(_ arr: [Int], _ l: Int, _ temp: Int) -> Int {
    var res = temp
    
    if l <= 0 {
        return res
    } else {
        res = arr[l] > res ? arr[l] : res
        return findMax(arr, l - 1, res)
    }
}

小结

  • 递归指的是调用自己的函数。
  • 每个递归函数都有两个条件:基线条件和递归条件。
  • 栈有两种操作:压入和弹出。
  • 所有函数调用都进入调用栈。
  • 调用栈可能很长,这将占用大量的内存。