文章

栈模拟递归的通用解法

我们知道,递归的过程可以用栈来模拟。但对于一些困难的问题,我们好像很难用栈去写。比如说汉诺塔问题。再比如说:

1
2
3
4
5
6
int f(int i){
    return i <= 1 ? 1 : f(i-1) + g(i-2);
}
int g(int i){
    return i <= 1 ? 1 : f(i+1) + g(i-1);
}

在这些例子中,我们该怎么知道什么时候该入队,什么时候该出队呢?比如在第二个例子里,我们调用了f(i),我们并不能把f(i)立刻出队。即使f(i-1)计算完成,我们还需要等到g(i-2)计算完成后才能出队。
此时我们需要持有一种对计算机、程序的一种相当本质的看法:状态机。程序运行本质上都是一些状态的转换。我们在运行的过程中保存当前的状态pc,然后根据当前不同的状态执行不同的任务。
比如对于第二个例子,我们可以定义此时fg所处于的状态为以下四个状态:

1
2
3
4
5
6
enum State {
    CALL,        // 初始调用 
    CALC_FIRST,  // 计算第一个子表达式
    CALC_SECOND, // 计算第二个子表达式
    RETURN       // 返回结果
};

我们在栈帧中保存以下几个值:

1
2
3
4
5
6
struct StackFrame {
    FuncType func; // 当前执行的函数类型
    int i;         // 输入参数
    State state;   // 当前执行状态
    int result;    // 中间结果存储
};

取出栈顶的元素top,根据不同的状态,执行以下不同的任务:

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
switch (top.state) {
    case CALL:
        // 处理基本情况
        if (top.i <= 1) {
            top.result = 1;
            top.state = RETURN;
        } else {
            // 设置为计算第一个子表达式的状态
            top.state = CALC_FIRST;
            if (top.func == FUNC_F) {
                // f 函数需要计算 f(i-1)
                callStack.push({FUNC_F, top.i - 1, CALL, 0});
            } else {
                // g 函数需要计算 f(i+1)
                callStack.push({FUNC_F, top.i + 1, CALL, 0});
            }
        }
        break;
        
    case CALC_FIRST:
        // 保存第一个子表达式结果
        top.result = finalResult;
        // 设置为计算第二个子表达式的状态
        top.state = CALC_SECOND;
        if (top.func == FUNC_F) {
            // f 函数需要计算 g(i-2)
            callStack.push({FUNC_G, top.i - 2, CALL, 0});
        } else {
            // g 函数需要计算 g(i-1)
            callStack.push({FUNC_G, top.i - 1, CALL, 0});
        }
        break;
        
    case CALC_SECOND:
        // 计算最终结果 = 第一个子表达式结果 + 第二个子表达式结果
        top.result += finalResult;
        top.state = RETURN;
        break;
        
    case RETURN:
        // 保存当前函数的计算结果
        finalResult = top.result;
        // 完成计算,弹出栈帧
        callStack.pop();
        break;
}

在其中我们用finalResult变量来传递上一个弹出的栈帧所返回的值,将该值传递给调用它的函数.通过保存状态,我们就知道何时该函数该做什么事情。 以下是完整的代码:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <iostream>
#include <stack>

// 定义函数类型
enum FuncType {
    FUNC_F, // f函数
    FUNC_G  // g函数
};

// 定义计算状态
enum State {
    CALL,        // 初始调用
    CALC_FIRST,  // 计算第一个子表达式
    CALC_SECOND, // 计算第二个子表达式
    RETURN       // 返回结果
};

// 栈帧结构,存储每个调用的状态
struct StackFrame {
    FuncType func; // 当前执行的函数类型
    int i;         // 输入参数
    State state;   // 当前执行状态
    int result;    // 中间结果存储
};

// 统一的非递归计算函数
int calculate(FuncType initial_func, int initial_i) {
    std::stack<StackFrame> callStack;
    
    // 将初始调用推入栈中
    callStack.push({initial_func, initial_i, CALL, 0});
    
    int finalResult = 0;
    
    while (!callStack.empty()) {
        // 引用栈顶元素以便修改
        StackFrame& top = callStack.top();
        
        switch (top.state) {
            case CALL:
                // 处理基本情况
                if (top.i <= 1) {
                    top.result = 1;
                    top.state = RETURN;
                } else {
                    // 设置为计算第一个子表达式的状态
                    top.state = CALC_FIRST;
                    if (top.func == FUNC_F) {
                        // f 函数需要计算 f(i-1)
                        callStack.push({FUNC_F, top.i - 1, CALL, 0});
                    } else {
                        // g 函数需要计算 f(i+1)
                        callStack.push({FUNC_F, top.i + 1, CALL, 0});
                    }
                }
                break;
                
            case CALC_FIRST:
                // 保存第一个子表达式结果
                top.result = finalResult;
                // 设置为计算第二个子表达式的状态
                top.state = CALC_SECOND;
                if (top.func == FUNC_F) {
                    // f 函数需要计算 g(i-2)
                    callStack.push({FUNC_G, top.i - 2, CALL, 0});
                } else {
                    // g 函数需要计算 g(i-1)
                    callStack.push({FUNC_G, top.i - 1, CALL, 0});
                }
                break;
                
            case CALC_SECOND:
                // 计算最终结果 = 第一个子表达式结果 + 第二个子表达式结果
                top.result += finalResult;
                top.state = RETURN;
                break;
                
            case RETURN:
                // 保存当前函数的计算结果
                finalResult = top.result;
                // 完成计算,弹出栈帧
                callStack.pop();
                break;
        }
    }
    
    return finalResult;
}

// f 函数的非递归实现
int f_non_recursive(int i) {
    return calculate(FUNC_F, i);
}

// g 函数的非递归实现
int g_non_recursive(int i) {
    return calculate(FUNC_G, i);
}

int main() {
    int n = 5;
    std::cout << "f(" << n << ") = " << f_non_recursive(n) << std::endl;
    std::cout << "g(" << n << ") = " << g_non_recursive(n) << std::endl;
    return 0;
}

汉诺塔问题其实也类似。
这是汉诺塔问题的递归写法:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include<iostream>
#include<stack>
using namespace std;

// 定义操作状态枚举,用于模拟递归过程中的不同阶段
enum Operation{
    MOVE_N_1_TO_AUX,  // 将n-1个盘子从源柱移动到辅助柱
    MOVE_N_TO_TAR,    // 将第n个盘子(最大的)从源柱移动到目标柱
    MOVE_AUX_TO_TAR,  // 将n-1个盘子从辅助柱移动到目标柱
    RETURN           // 当前操作完成,返回结果
};

// 定义栈帧结构体,存储每次"函数调用"的状态和参数
struct StackFrame{
    int n;           // 当前要移动的盘子数量
    char source;     // 源柱子
    char auxiliary;  // 辅助柱子
    char target;     // 目标柱子
    Operation state; // 当前执行状态
    int result;      // 存储移动次数结果
};

// 非递归实现汉诺塔问题,返回值为移动次数
int Hanoi(int n, char source, char auxiliary, char target){
    // 创建调用栈,用于模拟递归过程
    stack<StackFrame> callStack;
    
    // 将初始调用推入栈中,初始状态为移动n-1个盘子到辅助柱
    callStack.push({n, source, auxiliary, target, MOVE_N_1_TO_AUX, 0});
    
    // 存储最终结果(总移动次数)
    int finalResult = 0;
    
    // 当栈不为空时,继续处理
    while(!callStack.empty()){
        // 引用栈顶元素以便修改
        StackFrame &top = callStack.top();
        
        // 根据当前状态执行相应操作
        switch (top.state){
            case MOVE_N_1_TO_AUX:{
                // 基本情况:只有一个盘子时,直接移动到目标柱
                if(top.n == 1){
                    cout << top.source << "->" << top.target << endl;
                    top.result = 1;  // 记录移动一次
                    top.state = RETURN;  // 设置状态为返回
                }else{
                    // 将n-1个盘子从源柱移到辅助柱(先记录下一步操作)
                    top.state = MOVE_N_TO_TAR;  // 更新当前栈帧的下一状态
                    
                    // 创建新的栈帧处理子问题:将n-1个盘子从source移到auxiliary,以target为辅助
                    callStack.push({top.n - 1, top.source, top.target, top.auxiliary, MOVE_N_1_TO_AUX, 0});
                }
                break;
            }
            case MOVE_N_TO_TAR:{
                // 将第一子问题的结果加到当前结果中
                top.result += finalResult;
                
                // 更新状态为移动辅助柱上的盘子到目标柱
                top.state = MOVE_AUX_TO_TAR;
                
                // 移动最大的盘子从源柱到目标柱
                cout << top.source << "->" << top.target << endl;
                top.result++;  // 记录这次移动
                
                // 创建新的栈帧处理第二子问题:将n-1个盘子从auxiliary移到source,以target为辅助
                callStack.push({top.n - 1, top.auxiliary, top.source, top.target, MOVE_N_1_TO_AUX, 0});
                break;
            }
            case MOVE_AUX_TO_TAR:{
                // 将第二子问题的结果加到当前结果中
                top.result += finalResult;
                
                // 所有操作完成,设置状态为返回
                top.state = RETURN;
                break;
            }
            case RETURN:{
                // 保存当前栈帧的结果
                finalResult = top.result;
                
                // 弹出已完成的栈帧
                callStack.pop();
            }
        }
    }
    
    // 返回汉诺塔问题的总移动次数
    return finalResult;
}

int main(){
    // 设置盘子数量
    int n = 3;
    
    // 调用Hanoi函数,设定三根柱子为A、B、C
    int result = Hanoi(n, 'A', 'B', 'C');
    
    // 输出移动总次数
    cout << "count:" << result;
    return 0;
}
本文由作者按照 CC BY 4.0 进行授权