递归和非递归

递归和非递归

exdoubled Lv3

能采用递归解决的问题通常具有以下特征

规模为的问题可以被分解为一些规模较小的问题,然后从这些小问题的解可以方便地构造出大问题的解 而这些小问题可以同样的方式继续分解成规模更小的问题,当问题分解到足够小的时候,能直接得到解

递归树、剪枝、记忆化不再赘述

分治思想

把一个规模比较大的问题分解为一个或若干规模比较小的问题,分别对这些较小的问题求解,再综合它们的结果,从而得到原问题的解。这些比较小的问题的求解方法与原来问题的求解方法一样

  • 分解
  • 解决
  • 合并

时间复杂度分析

设一个分治算法将规模问题分解为个规模的子问题,合并子问题需要的计算量为

点击查看证明

容易知道

时有

时,

由于

根据对数性质

时,

Strassen 矩阵乘法

算法思想

将两个矩阵按中点分块(假设为偶数):

每个子块为

传统的分块乘法需要 8 次子矩阵乘法:

Strassen 的关键是构造 7 个“辅助乘积”(每个都是矩阵的乘积),然后用这些乘积组合出。一种常见的构造为:

然后恢复结果子块为:

这样每一层递归只需要次子矩阵乘法,另外外加若干次矩阵加减(加减的时间量级为

复杂度分析

设 Strassen 在大小的矩阵上耗时为。每次将问题分为 7 个子问题(规模为)并进行矩阵加减(合并开销为),

应用上面的定理:

因此有递推式:

目前最新进展已经到

Matrix multiplication - Wikipedia

递归与非递归

递归缺点:

  • 有栈空间溢出的风险
  • 堆栈空间以及函数调用造成较大的时间与空间开销

但是这不代表代码执行效率低,相反,有时递归比非递归快,两者实际是相当的

Hanoi 塔的非递归实现

写出 Hanoi(n,A,B,C) 的递归树,进行树的先序遍历,仿照系统栈的方式实现递归

非递归实现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
struct node{
int n, tag;
char a, b, c;
node(){}
node (int _n , char _a , char _b , char _c , int _tag = 0){
n = _n, a = _a, b = _b, c = _c, tag = _tag;
}
};
void set_value(int& n, char& a, char& b, char& c, int nn , char aa , char bb , char cc){
n = nn, a = aa, b = bb, c = cc;
}

void move_disk (char from, char to){
cout << "Move top disk from "<< from << " to "<< to << endl;
}


void hanoi(int N, char a, char b, char c){
stack<node> S;
bool done = false;
while(!done){
while(n > 1){
S.push(node(n, a,b, c));
set_value(N, a,b, c, N - 1, a, c, b);
}
move_dist(a, c);
if(!s.empty()){
node s = S.top();
S.pop();
set_value(N, a, b, c, s.n, s.a, s.b, s.c);
move_disk(a, c);
set_value(N, a, b, c, n - 1, b, a, c);
}
else done = true;
}
}

其他的实现方式

和递归树不等价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void hanoi2(int N, char a, char b,char c){
stack<node> S;
S.push(node(N, a,b, c));
while(!S.empty()){
node s = S.top(); S.pop();
N = s.n, a = s.a, b = s.b, c = s.c;
if(N == 1){
move_disk(a, c);
}else{
S.push(node(N-l,b, a, c));
S.push(node(l, a,b,c));
S.push(node(N-l, a,c, b));
}
}
}

定义三根柱子为,用数组表示:

步移动的 起点柱子 用函数表示,第步移动的 终点柱子 用函数表示

**起点柱子函数 **

的二进制表示与的按位与,可以得到当前移动的盘子编号对应的柱子位置的模式

终点柱子函数

经过二进制运算可以得到该盘子移动后的目标柱子编号

  1. 汉诺塔问题的盘子移动顺序存在规律:

    • 个盘子每隔步移动一次;
    • 最小盘子移动规律最简单,次小盘子次之。
  2. 利用位运算:

    • 可以计算第步移动的盘子编号;
    • 可以确定该盘子目标位置的循环规律。
  3. 最终公式:

1
2
3
4
5
6
7
8
9
10
11
inline int f(int x){return(x&x - 1)%3;}
inline int g(int x){freturn((x|x - 1) + 1)%3;}

void hanoi3(int N, char a, char b, char c){
int n = (1<<N)- 1;
char dict[3];
dict[0] = a,dict[1] = b, dict[3]= c;
for(int i = 1; i <= n; i++){
cout << dict[F(i)]<<" "<< dict[G(i)]<< endl;
}
}
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
_hanoi:                         # void hanoi(int N, char a, char b, char c)
push rbp
mov rbp, rsp
sub rsp, 32
mov DWORD PTR [rbp-20], edi # N
mov BYTE PTR [rbp-21], sil # a
mov BYTE PTR [rbp-22], dl # b
mov BYTE PTR [rbp-23], cl # c

cmp DWORD PTR [rbp-20], 1
jne .Lrecursion

movzx edx, BYTE PTR [rbp-23] # to
movzx eax, BYTE PTR [rbp-21] # from
mov esi, eax
mov edi, edx
call _move_disk
jmp .Lreturn

.Lrecursion:
mov eax, DWORD PTR [rbp-20]
lea edi, [rax-1] # n-1
movzx esi, BYTE PTR [rbp-21] # a
movzx edx, BYTE PTR [rbp-23] # c
movzx ecx, BYTE PTR [rbp-22] # b
call _hanoi # hanoi(n-1, a, c, b)

movzx edx, BYTE PTR [rbp-23]
movzx eax, BYTE PTR [rbp-21]
mov esi, eax
mov edi, edx
call _move_disk # move_disk(a, c)

mov eax, DWORD PTR [rbp-20]
lea edi, [rax-1]
movzx esi, BYTE PTR [rbp-22]
movzx edx, BYTE PTR [rbp-21]
movzx ecx, BYTE PTR [rbp-23]
call _hanoi # hanoi(n-1, b, a, c)

.Lreturn:
leave
ret
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
void hanoi4(int N, char a, char b, char c) {
stack<node> S;
A:
if (N == 1) {
move_disk(a, c);
goto B;
} else {
S.push(node(N, a, b, c, 1));
set_value(N, a, b, c, N - 1, a, c, b);
goto A;
}

B:
if (!S.empty()) {
node s = S.top(); S.pop();
set_value(N, a, b, c, s.n, s.a, s.b, s.c);
switch (s.tag) {
case 1: {
move_disk(a, c);
S.push(node(N, a, b, c, 2));
set_value(N, a, b, c, N - 1, b, a, c);
goto A;
}
case 2: {
goto B;
}
}
}
}

递归消除

尾递归

递归调用处于递归过程的最后一条语句的情况

1
2
3
4
int f(int n){
if(n == 1) return n;
else return n*f(n-1)
}

改为

1
2
3
4
5
int f(int n){
int f = 1;
for(int i = 2; i <= n; ++i) f = n*f;
return f;
}

线性递归

线性递归函数的最后一步不是递归操作,而是其他操作

1
2
3
4
5
6
7
8
void example(int n){
S1(n);
if(Eval(n)){
example(n-1);
S2(n);
}
else S3(n);
}

根据系统栈的调用,改为

1
2
3
4
5
6
7
8
9
10
11
12
void example(int n){
int k = n+1;
do{
k--;
S1(k);
} while(Eval(k));
S3(k);
while(k < n){
k++;
S2(k);
}
}

Hanoi 塔的拓展

  • Title: 递归和非递归
  • Author: exdoubled
  • Created at : 2025-10-15 17:00:00
  • Updated at : 2025-10-22 10:37:54
  • Link: https://github.com/exdoubled/exdoubled.github.io.git/sjjg/sjjg04/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments