1 min read

算法图解笔记-递归和分治

递归(recursion)思想的最好例子是斐波那契数和阶乘,在这里就不详述了。分而治之(divide and conquer, D&C)是一种经典的递归式问题解决方案。

写递归程序的两个要素:基线条件递归条件。如果不设置基线条件,递归会陷入死循环。如果归纳不出递归条件,就无法执行递归。

1.利用递归来求和

recur_sum = function(list_v) {
  n <- length(list_v)
  if (n < 2)
    return(list_v[n]) #基线条件
  return(list_v[1] + recur_sum(list_v[2:n]))  # 递归条件
}

现在测试一下:

recur_sum(1:10)
## [1] 55
recur_sum(1)
## [1] 1
recur_sum(10)
## [1] 10
recur_sum(1:2000)
## [1] 2001000

递归理解起来,很好理解。但是执行起来,确实比较低效的。因为每次都要调用函数体,复制向量,占用内存开销非常大。递归会记录过程,通常保存在栈(stack)中,是一种经典的后进先出的策略。

2. 快速排序

快速排序的思想非常朴素,譬如对5,2,3,8,9,6,1数列进行排序,步骤如下:

  • 1)随机选择1个数值作为基准值,譬如5;
  • 2)将<5的数放在左边,2,3,1
  • 3)将>5的数放在右边,8,9,6
  • 4)利用递归,对左手边和右手边的数列分别实行步骤1),2),3);
  • 5)基线条件为,如果数列只有1个元素,返回该值。

这实际上也是一种分而治之的思想,把对大数列的排序,划分为对多个小数列的排序。

试着写出程序如下:

quick_sort = function(list_v) {
  n <- length(list_v)
  if (n < 2)
    return(list_v[n])
  pivot <- list_v[1]  #设置基准值
  less <- list_v[2:n][list_v[2:n] <= pivot] #小于基准值的数列
  greater <- list_v[2:n][list_v[2:n] > pivot] #大于基准值的数列
  return(c(quick_sort(less), pivot, quick_sort(greater)))
}

测试一下,成功。

quick_sort(c(5,2,3,8,3,9,6,1,1,6))
##  [1] 1 1 2 3 3 5 6 6 8 9
quick_sort(sample(1:100,30))
##  [1]   8   9  14  17  19  28  32  35  36  38  42  43  47  51  56  57  59  67  73
## [20]  74  75  76  82  84  85  87  90  92  98 100

快速排序的运行时间为\(O(n\log_{2}^{n})\)。如果你每次都选到中间值作为基准值,那么栈的高度是\(\log_{2}^{n}\),每一个栈层,进行的比较次数都是n次,因此最好情况是\(n\log_{2}^{n}\)

如果基准值选择的不好,每次都选到最小值或者最大值,那么就会导致栈的高度为n层,每一个栈层,进行的比较次数仍然为n次,那么最差情况为\(O(n^{2})\)