RT

目录

  1. 插入排序及其解决思路
  2. 学习如何分析算法
  3. 渐近记号
  4. 设计分治算法
  5. 分析分治算法
  6. 递归和迭代
  7. 补充问题:

插入排序及其解决思路

算法的作用自然不用多说,无论是在校学生,还是已经工作多年,只要想在计算机这条道路走得更远,算法都是必不可少的。

就像编程语言中的“Hello World!”程序一般,学习算法一开始学的便是排序算法。排序问题在日常生活中也是很常见的,说得专业点:

输入是:n个数的一个序列$$
输出是:这n个数的一个全新的序列$$,其特征是$ a_1^, \leq a2^, \leq … \leq a{n-1}^, \leq a_n^,$

举个例子,在本科阶段学校往往要求做的实验中大多是“学生管理系统”、“信息管理系统”、“图书管理系统”这些。就比如“学生管理系统”中的分数,每当往里面添加一个新的分数时,便会和其他的进行比较从而得到由高到低或由低到高的排序。

我本人是不太喜欢做这种管理系统的…… 再举个比较有意思的例子。

大家肯定都玩过扑克牌,撇开部分人不说,相信大部分童鞋都热衷于将牌按序号排好。那么这其中就蕴含着算法的思想:

  • 1)手中的扑克牌有2种可能:没有扑克牌(为空)或者有扑克牌且已排好序。
  • 2)从桌上抓起一张牌后,从左往右(从右往左)依次进行比较,最终选择一个合适的位置插入。

简单的说,插入排序的精髓在于“逐个比较”。

在列出代码之前,先来看看下面的第一张图,我画的不是太好,就是有没有经过排序的 “8,7,4,2,3,9”几个数字,根据上面的描述,将排序过程描述为:

  • 1)将第二个数字“7”和“8”比较,发现7更小,于是将“8”赋值到“7”所在的位置,然后将7赋值给“8”所在的位置。
  • 2)将”4“移到”7“所在的位置,”7“和”8“后移一位。
  • 3)同样的步骤,将”2“和”3“移到”4“的前面。
  • 4)”9“比前面的数字都大,故不移动。

这里写图片描述

仅仅是这样的描述还是不够的,我们需要更加专业一点。

  • 1)设置一个循环,从第二个数字开始(索引为1)不断与前面的数字相比。
  • 2)每次循环开始时作为比较的数的索引为j,设置temp为其值。(因为在前面也看到了,将”8“赋值到”7“的位置时,如果不将”7“保存起来,那么便丢失了这个数字。)
  • 3)取得j的前一位i。
  • 4)只要i仍在数组中,并且索引为i处的值大于temp,就将i后一位的值设为i处的值,同时将i减1。
  • 5)在i不在数组中或i处的值不必temp大时结束第四部的循环,然后将i后一位的值设置为temp。

将上面这部分描述翻译为insertion_sort函数,下面是完整的测试程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
// C Plus Plus
// A是数组,j是遍历整个数组的索引,temp是每次取出来作比较的值
void insertion_sort() {
for(int j = 1; j < n; j++) {
int temp = A[j];
int i = j-1;
while (i >= 0 && A[i] > temp) {
A[i+1] = A[i];
i = i-1;
}
A[i+1] = temp;
}
}
updated on 2016/09/24
1
2
3
4
5
6
7
8
9
10
11
12
13
// Java
public int[] insertion(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int tmp = nums[i];
int prev = i - 1;
while (prev >= 0 && nums[prev] > tmp) {
nums[prev + 1] = nums[prev];
prev -= 1;
}
nums[prev + 1] = tmp;
}
return nums;
}

下面是能够帮助我们理解算法的正确性的循环不变式的三条性质:

初始化:循环第一次迭代之前,它为真。
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
终止:在循环终止时,不变式能够提供一个有助于证明算法正确性的性质。

就比如上面排序的例子,终止意味着在最后一次迭代时,由传入数组元素构成的子数组元素都已排好序,因此此时子数组就等同与原数组,于是循环终止。

学习如何分析算法

继续分析排序算法,我们知道排序10000个数肯定要比排序10个数所花费的时间更长,但除了输入的项数外就没有其他的影响因素吗?当然有,比如说输入的序列的已被排序的程度,如果是“23456781”这个序列,我们仅仅需要将1放到首位就好,而输入是”87654321“,我们就需要将7到1依次与其前面的数字进行比较。

关于算法的分析也有两个定义:

1)输入规模,当考虑的是排序算法时,那么规模就指的是项数;如果考虑的是图算法,那么规模就是顶点数和边数。
2)运行时间,名义上来说就是算法执行的时间,但实际上我们在分析一个算法时考量的算法执行的操作数或步数。

下面我们通过前面排序算法的伪代码来分析它的运行时间。

1
2
3
4
5
6
7
8
9
INSERTION-SORT(A)
1 for j = 2 to A.length // 代价c1,次数n
2 temp=A[j]; // 代价c2,次数n-1
3 // 将A[j]插入到已排序的A[1..j-1] // 代价0,次数n-1
4 i=j-1; // 代价c4,次数n-1
5 while i>0 and A[i]>temp // 代价c5
6 A[i+1]=A[i]; // 代价c6
7 i=i-1; // 代价c7
8 A[i+1]=temp; // 代价c8,次数n-1

代价为c1处的次数为n应该比较好理解对吧,从j=1到j=n一共有n步,j=n也应该包括在内,因为这是算法终止的情况。而j=n时,程序直接终止了,所以在代价c2、c3、c7处次数都为n-1。

那么在while循环中呢,代价为c4的时候次数为多少呢,很显然应该是$\sum_{j=2}^{n} tj$,而c5和c6在while循环里总有一次它不会去执行,因此次数为$\sum{j=2}^{n} (t_j-1)$。

将代价和次数相乘,便得到了该算法所需的总时间:
$T(n)=c_1n+c_2(n-1)+c4(n-1)+c5\sum{j=2}^{n} t_j+c6\sum{j=2}^{n} (t_j-1)+c7\sum{j=2}^{n} (t_j-1)+c_8(n-1)$

除此之外我们还可以来对算法进行最好和最坏情况的分析:
1)在最好情况下,也就是整个输入数组其实都是排好序的,那么它根本没法进入while循环,也就是说当i取初值j-1时,有$A[i]\leq temp$,从而对$j=2,3,4…n$都有$t_j=1$。

那么算法的总时间也就可以算出来了:
$T(n)=(c_1+c_2+c_4+c_5+c_8)n-(c_2+c_4+c_5+c_8)$

2)在最坏情况下,也就是数组是逆向排好序的,那么就需要将$A[j]$与已排好序的数组$A[1…j_1]$中的每个元素进行比较,从而对$j=2,3,4…n$都有$t_j=j$。

那么算法的总时间也就可以算出来了:
$T(n)=(\frac{c_5}{2}+\frac{c_6}{2}+\frac{c_7}{2})n^2+(c_1+c_2+c_4+\frac{c_5}{2}-\frac{c_6}{2}-\frac{c_7}{2}+c_8)n-(c_2+c_4+c_5+c_8)$

渐近记号

$\Theta$

在上面我已经求出了该排序算法的运行总时间,但我们可以对其做进一步的简化以及抽象,我们只考虑最坏情况,因为在实际中它更有意义。将运行时间表示为$an^2+bn+c$,其中的$a、b、c$都依赖于$c_i$。

让我们来做进一步的抽象,通过只考虑运行时间的增长率和增长量级。因为当程序足够大时,低阶项便不再重要了,甚至连高阶项的系数也可以忽略不计。于是,我们记插入排序在最坏情况下所需要的运行时间为$\Theta(n^2)$。

现在是时候给出$\Theta$的定义了:
$\Theta(g(n))={f(n):存在正数常量c_1、c_2和n_0,使得对所有的n\geq n_0,有0 \leq c_1g(n) \leq f(n) \leq c_2g(n)}$

也就是说在跨过$n_0$之后,$f(n)$就一直处于$c_1g(n)$和$c_2g(n)$之间,其中$c_1g(n)$是下界,$c_2g(n)$是上界。

$O$和$\Omega$

$O$和$\Theta$相比,前者就只是后者的一半——只有渐近上界,而没有渐近下界。那么它的定义为:
$O(g(n))={f(n):存在正数常量c和n_0,使得对所有的n\geq n_0,有0 \leq f(n) \leq cg(n)}$

$\Omega$和$\Theta$相比,前者就只是后者的一半——只有渐近下界,而没有渐近下界。那么它的定义为:
$\Omega(g(n))={f(n):存在正数常量c和n_0,使得对所有的n\geq n_0,有0 \leq f(n)cg(n) \leq cg(n) }$

设计分治算法

前面的排序问题使用的方法叫做增量法,即在排序子数组$A[1…j-1]$后,将单个元素$A[j]$插入到子数组的适当位置,然后参数排序好的数组$A[1…j]$,它的精髓在于“逐个比较”。

现在我们再来介绍一种新的方法,叫做分治法,它同样也是鼎鼎大名。它的精髓在于“一分为二“,而驱动这个算法的这是递归。

分治算法在每层递归中都有三个步骤:

  • 1)分解原问题为若干子问题,这些子问题是缩小版的原问题。(抽象的讲,将一个已经切成楔形的大块西瓜可以再切成多个小的楔形西瓜。)
  • 2)解决这些子问题,递归地求解各个子问题。然后,若问题的规模足够小,则直接求解。(继续举例子,要吃完一大块西瓜,可以不断的吃小部分,当西瓜块足够小时,可以一口干掉。)
  • 3)合并这些子问题的解成原问题的解。(吃完的那些小块西瓜加在一起就是刚才那一块很大的西瓜了。)

虽然西瓜的例子能够体现分治算法的思想,但用前面的扑克牌来演示则更加合适,毕竟它有数字呀。来来来,想象一下,桌上正有两堆牌,且分别都已经排号顺序,可是呢我们需要这两堆牌合并起来并且排序好。

那么怎么操作呢?很简单,一句话就能说清楚:不断从两堆牌的顶上选取较小的一张,然后放到新的扑克牌堆中。

首先我们将扑克牌定义成数组$A$,$p$和$q$以及$r$都是数组的下标,且$p \leq q < r$,两段已排序好的子数组是$A[p..q]$和$A[q+1..r]$,我们需要做的是将其排序为$A[p..r]$。下面的伪代码便实现了这个思想:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MERGE(A,p,q,r)
1 n1 = (q - p) + 1 = q - p + 1
2 n2 = (r - (q + 1)) +1 = r - q
3 let L[1..n1+1] and R[1..n2+1] be new arrays
4 for i = 1 to n1
5 L[i] = A[p + i -1]
6 for j = 1 to n2
7 R[j] = A[q + j]
8 L[n1 + 1] = #
9 R[n2 + 1] = #
10 i = 1;
11 j = 1;
12 for k = p to r
13 if L[i] 小于等于 R[j]
14 A[k] = L[i];
15 i = i + 1;
16 else
17 A[k] = R[j]
18 j = j + 1;

上面的”# “号就是传说中的哨兵牌,每当显露一张哨兵牌时,它不可能为较小的值,除非两个堆都已显露出哨兵牌。但是出现这种情况就意味着算法结束,所有非哨兵牌都已被放置到输出堆。

1
2
3
4
5
6
………………p q r………………
………………3 6 8 9 2 5 7 8………………
k
L 3 6 8 9 # R 2 5 7 8 #
i j

比较”3“和”2“,发现2更小,于是将2放到A数组的首位,并且将$j$移后一位。

1
2
3
4
5
6
………………p q r………………
………………2 6 8 9 2 5 7 8………………
k
L 3 6 8 9 # R 2 5 7 8 #
i j

比较”3“和”5“,发现3更小,于是将3放到数组A的首位,并且将$i$移后一位。

1
2
3
4
5
6
………………p q r………………
………………2 3 8 9 2 5 7 8………………
k
L 3 6 8 9 # R 2 5 7 8 #
i j

以此类推,最终A数组就成排好了序……

1
2
3
4
5
6
………………p q r………………
………………2 3 5 6 7 8 8 9………………
k
L 3 6 8 9 # R 2 5 7 8 #
i j

将上面的思想以及伪代码写成如下程序,大家可以参考参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <iostream>
#include <cstdio>
using namespace std;
#define MAX_N 1000
int A[MAX_N];
int L[MAX_N/2];
int R[MAX_N/2];
int n,p,q,r;
void merge();
int main()
{
printf("数组长度:\n");
scanf("%d",&n);
printf("数组内容:\n");
for(int i=0;i<n;i++)
{
scanf("%d",&A[i]);
}
printf("输入p q r\n");\
scanf("%d %d %d",&p,&q,&r);
merge();
for(int i=0;i<n;i++)
{
printf("%d ",A[i]);
}
return 0;
}
void merge()
{
int n1=q-p+1;
int n2=r-q;
for(int i=0;i<n1;i++)
L[i]=A[p+i];
for(int j=0;j<n2;j++)
R[j]=A[q+j+1];
L[n1]=100;
R[n2]=100;
for(int k=p,i=0,j=0;k<=r;k++)
{
if(L[i]<=R[j])
{
A[k]=L[i];
i=i+1;
}
else
{
A[k]=R[j];
j=j+1;
}
}
}

下面没有用图示而是用了代码显示块来显示,应该也能看的吧?就是不断的归并,最终合成一个完整的排好序的数组。

1
2
3
4
5
6
7
…………………………2 2 5 6 7 8 8 9…………………………
……………………………………归并……………………………………
…………………2 5 6 8………………2 7 8 9…………………
……………………归并…………………………归并……………………
……………2 6…………5 8…………2 9…………7 8……………
……………归并…………归并………归并……………归并……………
…………6……2………8……5………2……9………8……7…………

要完成上面这个程序,我们可以利用前面写好的merge函数呢,下面是伪代码实现。

1
2
3
4
5
6
MERGE-SORT(A,p,r)
1 if p < r
2 q = 小于或等于(p+r)/2的最大整数
3 MERGE-SORT(A,p,q)
4 MERGE-SORT(A,q+1,r)
5 MERGE(A,p,q,r)

然后为了完成这其中$q$的参数传递,将它们设置为全局变量已经不合适了,具体的程序可以参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <iostream>
#include <cstdio>
using namespace std;
#define MAX_N 1000
int A[MAX_N];
int L[MAX_N/2];
int R[MAX_N/2];
void merge(int A[],int p,int q,int r);
void merge_sort(int A[],int p,int r);
int main()
{
int n, p, q, r;
printf("数组长度:\n");
cin >> n;
printf("数组内容:\n");
for (int i = 0;i < n;i++)
{
cin >> A[i];
}
printf("输入p r\n");\
cin >> p >> r;
merge_sort(A, p, r);
for (int i = 0;i < n;i++)
{
printf("%d ", A[i]);
}
return 0;
}
void merge_sort(int A[],int p,int r)
{
if(p<r)
{
int q=(p+r)/2;
merge_sort(A,p,q);
merge_sort(A,q+1,r);
merge(A,p,q,r);
}
}
void merge(int A[], int p, int q, int r)
{
int n1 = q - p + 1;
int n2 = r - q;
for (int i = 0;i<n1;i++)
L[i] = A[p + i];
for (int j = 0;j<n2;j++)
R[j] = A[q + j + 1];
L[n1] = 100;
R[n2] = 100;
for (int k = p, i = 0, j = 0;k<=r;k++)
{
if (L[i] <= R[j])
{
A[k] = L[i];
i = i + 1;
}
else
{
A[k] = R[j];
j = j + 1;
}
}
}

分析分治算法

当我们的输入足够小时,比如对于某个常量c,$n \leq c$,则直接求解需要常量时间,并写作$\Theta(1)$。

对于复杂的问题,我们将其分解成$a$个子问题,每个子问题的规模是原问题的$1/b$.对于这规模为$n/b$的子问题,累计需要$T(n/b)$的时间,所以需要$aT(n/b)$的时间来求解这$a$个子问题。而这其中分解的过程也需要消耗一定的时间,记作$D(n)$,合并这些子问题也需要一定的时间,记作$C(n)$。于是又得到了一个递归式:

当$n \leq c$时,$T(n)=\Theta(1)$

其他情况时,$T(n)=aT(n/b)+D(n)+C(n)$

下面来通过分治模式在每层递归时都有的三个步骤来分析归并排序算法,我们所考虑的是n个数的最坏情况下的运行时间。同样的,归并排序一个元素需要常量时间,当n>1时有如下3个步骤:

分解:分解步骤仅仅计算子数组的中间位置,需要常量时间,因此$D(n)=\Theta(1)$
解决:我们递归地求解两个规模均为$n/2$的子问题,将贡献$2T(n/2)$的运行时间。
合并:一个具有n个元素的子数组上过程MERGE需要$\Theta(n)$的时间,所以$C(n)=\Theta(n)$。

因此$D(n)+C(n)$便等于$\Theta(n)$和$\Theta(n)$相加,可是结果依然为$\Theta(n)$。接着可以得到归并排序的最坏情况运行时间的递归式了。

当$n=1$时,$T(n)=\Theta(1)$

当$n>1$时,$T(n)=2T(n/2)+\Theta(n)$

我们对上式稍作变换如下:

当$n=1$时,$T(n)=c$

当$n>1$时,$T(n)=2T(n/2)+cn$

这个式子可以不断的做递归,最后形成一个递归树的。树的第一层是$cn$,第二层是$cn/2$,第三层是$cn/4$,直到最后一层为$c$。第二层有2棵子树,第三层有4棵子树,直到最后一程有$n$棵子树,因此每层的代价总共为$cn$。而整个树共有$\lg n+1$层,因此总代价为$cn\lg n+cn$。

忽略低阶项和系数项,最终记为$\Theta(n\lg n)$。

递归和迭代

这两个概念也许很多童鞋依旧是老虎老鼠傻傻分不清楚,下面通过求解斐波那契数来看看它们俩的关系吧。

斐波那契数的定义:
$$ f_0 = 0 $$
$$ f_1 = 1 $$
$$ fi = f{i-1}+f_{i-2} (i > 1) $$

递归:

1
2
3
4
5
6
7
8
9
10
11
12
(factorial 6)
(* 6 (factorial 5))
(* 6 (* 5 (factorial 4)))
(* 6 (* 5 (* 4 (factorial 3))))
(* 6 (* 5 (* 4 (* 3 (factorial 2)))))
(* 6 (* 5 (* 4 (* 3 (2 (factorial 1))))))
(* 6 (* 5 (* 4 (* 3 (* 2 1)))))
(* 6 (* 5 (* 4 (* 3 2))))
(* 6 (* 5 (* 4 6)))
(* 6 (* 5 24))
(* 6 120)
720

迭代:

1
2
3
4
5
6
7
8
9
(factorial 6)
(factorial 1 1 6)
(factorial 1 2 6)
(factorial 2 3 6)
(factorial 6 4 6)
(factorial 24 5 6)
(factorial 120 6 6)
(factorial 720 7 6)
720

递归的核心在于:不断地回到起点
迭代的核心在于:不断地更新参数

在下面的代码中:

递归的核心是sum的运算,sum不断的累乘,虽然运算的数值不同,但形式和意义一样。

而迭代的核心是product和counter的不断更新。如上表中,product就是factorial的前2个参数不断的累乘更新成第一个参数;而第二个参数则是counter,其不断的加1来更新自己。

1
2
product <- counter * product
counter <- counter + 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
int factorialRecursive(int n);
int factorialIteration(int product, int counter, int max_count);
int main()
{
int n;
cout<<"Enter an integer:"<<endl;
cin>>n;
cout<<factorialRecursive(n)<<endl;
cout<<factorialIteration(1,1,n)<<endl;
return 0;
}
int factorialRecursive(int n)
{
int sum=1;
if(n==1)
sum*=1;
else
sum=n*factorialRecursive(n-1);
return sum;
}
int factorialIteration(int product, int counter, int max_count)
{
int sum=1;
if(counter>max_count)
sum*=product;
else
factorialIteration((counter*product),(counter+1),max_count);
}
updated on 2016/09/24
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Java
public int factorialRecursive(int n) {
int sum = 1;
if (n == 1) sum *= 1;
else sum = n * factorialRecursive(n - 1);
return sum;
}
public int factorialIteration(int n) {
return factorialIterationCore(1, 1, n);
}
public int factorialIterationCore(int product, int counter, int maxCount) {
if (counter > maxCount)
return product;
else
return factorialIterationCore(counter * product, counter + 1, maxCount);
}

补充问题:

关于上面的factorialIteration函数,今天收到一份邮件,我也通过再次分析学到了很多,这里罗列一下。


第一个问题:

首先来看相对简单的问题,该童鞋在函数内以两种不同方式加上another_sum=2却有着不同的结果。

1
2
3
4
5
6
7
8
9
int factorialIteration(int product, int counter, int max_count) {
int sum = 1;
int another_sum = 2;
if(counter > max_count) {
sum *= product;
another_sum *= product;
} else
factorialIteration(counter*product, counter + 1, max_count);
}
1
2
3
4
5
6
7
8
9
int factorialIteration(int product, int counter, int max_count) {
int sum = 1;
int another_sum = 2;
if(counter > max_count) {
another_sum *= product;
sum *= product;
} else
factorialIteration(counter*product, counter + 1, max_count);
}

因为这个函数声明的是int型的返回类型,但没有用return语句,所以C++自动将其运行的最后一行语句作为了返回语句。所以这两个函数类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int factorialIteration(int product, int counter, int max_count)
{
int sum=1;
int another_sum=2;
if(counter>max_count)
{
sum*=product;
return another_sum*=product;
}
else
factorialIteration((counter*product),(counter+1),max_count);
}
int factorialIteration(int product, int counter, int max_count)
{
int sum=1;
int another_sum=2;
if(counter>max_count)
{
another_sum*=product;
return sum*=product;
}
else
factorialIteration((counter*product),(counter+1),max_count);
}

然而我在CodeBlocks中写的代码不用return是可以的,但在Visual Studio中却是会报错的。

有了这个发现,我原来的代码也可以这样来写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;
int factorialRecursive(int n);
int factorialIteration(int product, int counter, int max_count);
int main()
{
int n;
cout<<"Enter an integer:"<<endl;
cin>>n;
cout<<factorialRecursive(n)<<endl;
cout<<factorialIteration(1,1,n)<<endl;
return 0;
}
int factorialRecursive(int n)
{
int sum=1;
if(n==1)
sum*=1;
else
sum=n*factorialRecursive(n-1);
// return sum; // 去掉这里的return语句
}
int factorialIteration(int product, int counter, int max_count)
{
int sum=1;
if(counter>max_count)
return sum*=product; // 在这里加上return语句
else
factorialIteration((counter*product),(counter+1),max_count);
}

现在来看另一个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
int test(int n);
int sum;
int main()
{
cout<<test(1)<<endl;
return 0;
}
int test(int n)
{
sum = 1;
sum += n;
if (sum < 5)
test(n+1);
}

如果设sum为全局变量,那么会在test函数中每一次调用sum=1时都将sum重新赋值为1。整个程序最后输出为5。这个应该没有什么悬念吧?

如果设sum给test内的局部变量,则会在每一次执行int sum=1语句时都会创建一个新的sum对象,它的存放地址和之前的sum并不相同。然后整个程序最后输出意外的是4。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
int test(int n);
int main()
{
cout<<test(1)<<endl;
return 0;
}
int test(int n)
{
int sum = 1;
sum += n;
if (sum < 5)
return test(n+1);
// return sum; 此处有这一行代码命名为程序1,没有这行代码命名为程序2
}

程序1的输出是5,程序2的输出是4。具体函数执行过程如下:

第一步,调用test(1):

1
2
3
int sum=1
sum=2
return test(2)

第二步,调用test(2):

1
2
3
int sum=1
sum=3
return test(3)

第三步,调用test(3):

1
2
3
int sum=1
sum=4
return test(4)

第四步,调用test(4):

1
2
int sum=1
sum=5

执行到第四步的时候,由于sum以及不比5小了,所以程序1没有进入if语句而是执行下一句return sum,所以输出为1。

而如果是程序2,也就是没有return sum语句,那么程序在执行完第四步后就会返回到第三步,最终调用(return) sum=4,输出4。


第三个问题:

该童鞋还提到了尾递归,这里我就来说说我的理解,如有问题欢迎大家直接评论或邮件给我。

上面代码中的递归函数factorialRecursive应该没问题的吧。

上面的代码我给其命名为迭代。

1
2
3
4
5
6
7
8
int factorialIteration(int product, int counter, int max_count)
{
int sum=1;
if(counter>max_count)
sum*=product;
else
factorialIteration((counter*product),(counter+1),max_count);
}

通过在main函数中调用如下代码来执行该函数:

1
cout<<factorialIteration(1,1,n)<<endl;

当然,也可以另外写一个函数如下:

1
2
3
4
int factorialIter(int n)
{
return factorialIteration(1,1,n);
}

并通过在main函数中直接调用该函数来做计算:

1
cout<<factorialIter(n)<<endl;

函数factorialIteration中的max_count我们称其为“循环不变量”,也就是对于整个运算过程而言这个变量是不变的。为了让大家更加印象深刻,将前面出现过的东西再来复制一遍:

1
2
3
4
5
6
7
8
9
(factorial 6)
(factorial 1 1 6)
(factorial 1 2 6)
(factorial 2 3 6)
(factorial 6 4 6)
(factorial 24 5 6)
(factorial 120 6 6)
(factorial 720 7 6)
720

从第二行开始的factorial的第三个参数”6“就是循环不变量。

尾递归:

在计算机科学中,尾调用是一个作为过程最后一步的子例程调用执行。如果尾调用可能在以后的调用链中再调用这同一个子例程,那么这个子例程就被称为是尾递归,它是递归的一个特殊情况。尾递归非常有用,在实现中也容易处理。尾调用可以不通过在调用堆栈中添加新的栈帧而实现。

传统上,尾部调用消除是可选的。然而,在函数式编程语言中,尾调用消除往往由语言标准作为保障,这种保证允许使用递归,在特定情况下的尾递归,来代替循环。在这种情况下,尽管用它作为一种优化是不正确的(尽管它可能是习惯用法)。在尾递归中,当一个函数调用它自身这种特殊情况下,可能调用消除比传统的尾调用更加合适。

迭代:

迭代是一个重复过程,它的目的是接近既定的目标或结果。每次重复的过程也称为”迭代“,作为迭代的结果都将作为下一次迭代的起点。

迭代在计算中是指的计算机程序中的重复的语句块。它可以表示两个专业术语,同义重复,以及描述一种具有可变状态重复的具体形式。然后令人费解的是,它也可以表示通过显式重复结构的任何重复,而不管其可变性。

在第一个意义上,递归是迭代的一个例子,但通常用”递归“来标记,而不作为”迭代“的例子。

在第二个意义上,(更加狭义地)迭代描述了一种编程风格。这与一个有着更有声明性方法的递归有着鲜明的对比。

第三个意义上,使用while或for循环,以及使用map或fold的函数也被视为迭代。

(以上定义部分摘自英文维基百科)

关于递归和尾递归在函数式编程中的应用也可以看这里:【Scheme归纳】3 比较do, let, loop


本站地址:http://nomasp.com/

欢迎交流,转载请联系本人,多谢🙏