递归相关题目 | Go 语言实现

207 阅读2分钟

暴力递归就是尝试

  1. 把问题转化为规模缩小了的同类问题的子问题
  2. 有明确的不需要继续进行递归的条件(base case)
  3. 有当得到了子问题的结果之后的决策过程
  4. 不记录每一个子问题的解

相关题目

汉诺塔

打印n层汉诺塔从最左边移动到最右边的全部过程

思路:

  1. 先将 1 - (i-1) 的元素移动到 middle
  2. 将 i 元素移动到 right
  3. 将 1- (i-1) 的元素移动到 right
 // Hanoi 汉诺塔问题
 func Hanoi(n int) {
    if n <= 0 {
       return
    }
    HanoiRecur(n, n, "left", "right", "middle")
 }
 ​
 func HanoiRecur(rest, down int, from, to, help string) {
    if rest == 1 {
       fmt.Println("move", down, "from", from, "to", to)
    } else {
       HanoiRecur(rest-1, down-1, from, help, to)
       HanoiRecur(1, down, from, to, help)
       HanoiRecur(rest-1, down-1, help, to, from)
    }
 ​
 }

字符串子序列

打印一个字符串的全部子序列,包括空字符串

思路:从左到右每个字符串都可以选上,或者不选

 // Sequence 打印一个字符串的所有子序列
 func Sequence(str string) {
    bytes := make([]byte, len(str))
    copy(bytes, str)
    SequenceRecur(bytes, 0)
 }
 ​
 func SequenceRecur(bytes []byte, index int) {
    if index == len(bytes) {
       fmt.Println(string(bytes))
       return
    }
    SequenceRecur(bytes, index+1)
    tmp := bytes[index]
    bytes[index] = 0
    SequenceRecur(bytes, index+1)
    bytes[index] = tmp
 }

把 bytes[index] 设置为0相当于不选,因为0代表空字符串,每次不选完成后要还原,所以用 tmp 记录下来

字符串全排列

打印一个字符串的全部排列,要求不要出现重复的排列

 // Permutation 打印一个字符串的全排列,不出现重复
 func Permutation(str string) {
    if len(str) == 0 {
       return
    }
    bytes := make([]byte, len(str))
    copy(bytes, str)
    PermutationRecur(bytes, 0)
 }
 ​
 // PermutationRecur 递归
 // index 是即将要被选择的字符
 // [i..] 范围上的所有字符都可以被选择
 // [0..i-1] 是前面已经选择出来的结果
 func PermutationRecur(bytes []byte, index int) {
    if index == len(bytes) {
       fmt.Println(string(bytes))
       return
    }
    // 使用 map 存储当前位置被选择的字符,如果有重复的就不选,达到去重的目的
    visited := make(map[byte]struct{})
    for j := index; j < len(bytes); j++ {
       if _, ok := visited[bytes[j]]; !ok {
          visited[bytes[j]] = struct{}{}
          // 交换位置即代表 index 位置被选择,然后进行选择下一个位置的字符
          bytes[index], bytes[j] = bytes[j], bytes[index]
          PermutationRecur(bytes, index+1)
          // 还原后以进行下一次对 index 位置的选择
          bytes[index], bytes[j] = bytes[j], bytes[index]
       }
    }
 }

逆序栈

不适用额外数据结构逆序栈

思路:每次取出栈底的元素,然后逆序剩下的元素,再将取出的元素压回栈

 // ReverseStack 逆序栈
 func ReverseStack(stack *Stack) {
    if stack == nil || stack.IsEmpty() {
       return
    }
    last := GetAndRemoveLast(stack)
    ReverseStack(stack)
    stack.Push(last)
 }
 ​
 func GetAndRemoveLast(stack *Stack) int {
    cur := stack.Pop()
    if !stack.IsEmpty() {
       last := GetAndRemoveLast(stack)
       stack.Push(cur)
       return last
    }
    return cur
 }

取纸牌

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A和玩家B都绝顶聪明。请返回最后获胜者的分数。

【举例】 arr=[1,2,100,4]。

开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来玩家 B可以拿走2或4,然后继续轮到玩家A...

如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继续轮到玩家A...

玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1,让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家A拿走。玩家A会获胜,分数为101。所以返回101。

arr=[1,100,2]。 开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜,分数为100。所以返回100。

思路:只需要返回 A 和 B 各自最大分数的最大值

 func WinnerScore(arr []int) int {
    if arr == nil {
       return 0
    }
    f := first(arr, 0, len(arr)-1)
    s := second(arr, 0, len(arr)-1)
    if f > s {
       return f
    }
    return s
 }
 ​
 // first 表示先手拿牌
 func first(arr []int, l, r int) int {
    if l == r {
       return arr[l]
    }
    // 已经先手拿牌了,接下来应该由后手拿牌的人先拿了再拿
    left := arr[l] + second(arr, l+1, r)  // 拿了左边的牌
    right := arr[r] + second(arr, l, r-1) // 拿了右边的牌
    if left > right {
       return left
    }
    return right
 }
 ​
 // second 表示后手拿牌
 func second(arr []int, l, r int) int {
    if l == r {
       return 0
    }
    left := first(arr, l+1, r)  // 表示先手的人把 l 位置的牌拿了
    right := first(arr, l, r-1) // 表示先手的人把 r 位置的牌拿了
    // 因为先手的人肯定把大牌取走了,所以这里返回较小值
    if left < right {
       return left
    }
    return right
 }

最大价值

给定两个长度都为 N 的数组 weights 和 values,weights[i] 和 values[i] 分别代表 i 号物品的重量和价值。给定一个正数 bag,表示一个载重 bag 的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

思路:每个物品都有两种情况,选或者不选,返回组合的最大值即可,注意 weight 和不能超过 bag

 // MaxValue 解决 knapsack 问题
 func MaxValue(weights, values []int, bag int) int {
    if weights == nil || values == nil || len(weights) != len(values) || bag < 1 {
       return 0
    }
    return MaxValueRecur(weights, values, bag, 0, 0, 0)
 }
 ​
 func MaxValueRecur(weights, values []int, bag, weight, value, index int) int {
    if index == len(weights)-1 {
       if weight+weights[index] <= bag {
          return value + values[index]
       }
       return value
    }
    max := 0
    if weights[index]+weight <= bag {
       max = MaxValueRecur(weights, values, bag, weight+weights[index], value+values[index], index+1)
    }
    notChoose := MaxValueRecur(weights, values, bag, weight, value, index+1)
    if max < notChoose {
       max = notChoose
    }
    return max
 }

转换数组为字符串

规定1和A对应、2和B对应、3和C对应... 那么一个数字数组比如 [1,1,1],就可以转化为"AAA"、"KA"和"AK"。给定一个只有数字字符组成的字符串 str,返回有多少种转化结果。

思路:当前数字如果是1或者当前数字为2下一数字小于6的话,就可以将当前数字转化为字符或者将当前数字与下一个数字组合起来转化为同一个字符

 // Convert 返回有多少种将 arr 转化为字符串的转化结果
 func Convert(arr []int) int {
    if arr == nil {
       return 0
    }
    return ConvertRecur(arr, 0)
 }
 ​
 func ConvertRecur(arr []int, index int) int {
    if index >= len(arr)-1 {
       return 1
    }
    res := 0
    if arr[index] == 1 || (arr[index] == 2 && arr[index+1] <= 6) {
       res += ConvertRecur(arr, index+2)
    }
    res += ConvertRecur(arr, index+1)
    return res
 }

N 皇后

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列,也不在同一条斜线上。

给定一个整数n,返回n皇后的摆法有多少种。 n=1,返回1。 n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。n=8,返回92。

思路:逐行判断皇后可以摆放的位置,当前位置是否可以摆放由已经摆放的皇后决定,只需要判断不同列和不同斜线即可,所以每次记录下已经摆放的皇后的位置

使用一个 record 数组记录已经放了的位置,下标代表行,值代表列

 // NQueues n皇后问题
 func NQueues(n int) int {
    if n <= 3 {
       return 0
    }
    // record used to store the number of columns that have been placed
    record := make([]int, n)
    return Num1(record, 0, n)
 }
 ​
 func Num1(record []int, i, n int) int {
    if i == n {
       return 1
    }
    res := 0
    for j := 0; j < n; j++ {
       if isValid(record, i, j) {
          record[i] = j
          res += Num1(record, i+1, n)
       }
    }
    return res
 }
 ​
 func isValid(record []int, i, j int) bool {
    for k := 0; k < i; k++ {
       if record[k] == j || math.Abs(float64(i-k)) == math.Abs(float64(j-record[k])) {
          return false
       }
    }
    return true
 }

使用位运算优化:

大致思路与上面相同,但是不适用数组来记录已经摆放的皇后的位置,使用二进制的数的每一位来代表能否摆放

第一行摆放一个皇后,下一行的同列,左斜线的右斜线都被她限制了不能摆皇后,使用三个二进制的数表示,1表示限制,0表示不限制

在下一行中对他们进行或(|)运算就得到了这一行对下一行的总限制,此时皇后就只能摆放在为0的位置,摆放了过后新的列限制为上一行的列限制将当前位标为1,新的左限制为上一行的左限制将当前位标1后左移一位(<<1),新的右限制为上一行的右限制将当前位标1后右移一位(>>1),依次类推

还有一个问题就是怎么得到我们应该选择哪一位,因为我们很容易就可以得到一个数最右侧的1,我们这时对得到的总限制处理一下(只处理后面n位),将0变成1,1变成0,然后得到新数 position ,为1的位置代表可以放皇后,每次都获取最右侧的1,然后用这个数来减去它,代表我们选择了这一位,然后再递归处理下一行即可,当我们这个 position 为0的时候代表这一行处理完了,就可以返回了

limit 表示有 n 个皇后就将 limit 的后 n 位处理为1

 // NQueues2 n皇后问题,只能处理64位以内的问题
 func NQueues2(n int) int {
    if n <= 3 || n >= 64 {
       return 0
    }
    var limit int64 = (1 << n) - 1
    return Num2(limit, 0, 0, 0)
 }
 ​
 func Num2(limit int64, leftLim, rightLim, colLim int64) int {
    if colLim == limit {
       return 1
    }
    res := 0
    pos := limit & (^(leftLim | rightLim | colLim))
    for pos != 0 {
       mostRight := pos & (^pos + 1)
       pos = pos - mostRight
       res += Num2(limit,
          (leftLim+mostRight)<<1,
          (rightLim+mostRight)>>1,
          colLim+mostRight)
    }
    return res
 }

测试看一下速度提升得怎么样

 func TestNQueues(t *testing.T) {
     start := time.Now()
     fmt.Println(NQueues(14))                      // output: 365596
     fmt.Println(time.Since(start).Milliseconds()) // output: 4432
     start = time.Now()
     fmt.Println(NQueues2(14))                     // output: 365596
     fmt.Println(time.Since(start).Milliseconds()) // output: 144
 }

可以发现优化结果是非常明显的,且使用位运算时只需要几个变量即可完成,不需要额外的数据结构