递归算法改成非递归算法的思路
撰稿日期:2024.9.27
一、C 语言的函数调用栈
C 语言中,函数调用的过程如下所示:
- 局部变量本来就在栈内存中,
caller
函数的返回地址(call
命令的下一条命令地址)入栈 - 将需要传递的参数传入寄存器或栈中(我们这里假设仅使用栈来传参。实际上,传入的参数就可以看作是
callee
的局部变量) callee
执行完毕,调用到ret
,此时,callee
的局部变量也已全部出栈caller
的返回地址出栈给RIP
,此时程序的控制流回到了caller
中
下文叙述中,i
均指 1,2,⋯ ,n1, 2, \cdots, n1,2,⋯,n
二、只会调用一次自身的递归算法
考虑只会调用一次自身的递归算法的结构:
RET_TYPE fun(ARG_TYPE ai0) {
if(should_end(ai0))
return OPERATION_END(ai0);
OPERATION_A(ai0); // 此操作中将产生 ai1
RET_TYPE ret = fun(ai0);
return OPERATION_B(ai0, ai1, ret);
}
可以描述一下其执行行为:
- 对
ai0
执行 A 操作,生成ai1
- 操作中生成的
ai1->bi0
入栈 - 操作 B 的地址入栈
RIP
指针切换到fun
函数的开头- 对
bi0
执行 A 操作,生成bi1
- 操作中生成的
bi1->ci0
传入寄存器或入栈 - 操作 B 的地址入栈
RIP
指针切换到fun
函数的开头- (假设
fun({ci0}
已满足 should_end)ci0
全部出栈 RIP
指针切换到操作 B,此时,栈顶为bi1
- 执行完操作 B 后,
bi0, bi1
和其他局部变量出栈,操作 B 的结果返回 RIP
指针切换到操作 B,此时,栈顶为ai1
(实际上栈顶可能是返回值,但这里为了方便理解,我们将其认为成是ai1
,下同)- 执行完操作 B 后,
ai0, ai1
和其他局部变量出栈,操作 B 的结果返回 - 这个返回的操作 B 就是交给最初的调用者的结果
根据这些行为,我们可以写出利用一个栈结构来将其改造成非递归算法的方法:
RET_TYPE fun(ARG_TYPE a1, ARG_TYPE a2, ..., ARG_TYPE an) {
Stack stack = InitStack();
push(stack, a1, a2, ..., an);
while(!should_end(top(stack))) { // 检查栈顶的参数组是否满足结束递归的条件
s1, s2, ..., sn = top(stack);
OPERATOR_A(s1, s2, ..., sn); // 这个操作会产生下一步递归所需的 b1, b2, ..., bn
push(stack, b1, b2, ..., bn);
}
RET_TYPE ret = OPERATION_END(top(stack));
while(stack.size() > 1) {
e1, e2, ..., en = pop(stack); // callee`s args
r1, r2, ..., rn = top(stack); // caller`s args
ret = OPERATION_B(r1, r2, ..., rn, e1, e2, ..., en, ret);
}
return ret;
}
示例:将递归的快速幂算法改造成非递归的快速幂:
/* b 是底数, e 是指数
* 暂不考虑对 b=1 时的优化 */
int fast_pow(int b, int e) {
if(e == 1)
return b;
int next_exp = e / 2;
int ret = fast_pow(b, next_exp);
if(e % 2 == 0) {
return ret * ret;
}else {
return b * ret * ret;
}
}
将其改造为非递归的算法:
int fast_pow_new(int b, int e) {
PStack stack = InitStack();
push(stack, e);
while(top(stack) != 1) {
int e1 = top(stack);
int e2 = e1 / 2;
push(stack, e2);
}
int ret = b;
while(size(stack) > 1) {
int e1 = pop(stack);
int e2 = top(stack);
if(e2 % 2 == 0) {
ret = ret * ret;
}else {
ret = b * ret * ret;
}
}
return ret;
}
三、只会调用两次自身的递归算法
其结构如下所示:
RET_TYPE fun(ARG_TYPE ai0) {
if(should_end(ai0))
return OPERATION_END(ai0);
OPERATION_A(ai0); // 此过程中将产生 ai1
ret1 = fun(ai1);
OPERATION_B(ai0, ai1, ret1); // 此过程中将产生 ai2
ret2 = fun(ai2);
return OPERATION_C(ai0, ai1, ai2, ret1, ret2);
}
让我们分析一下这个算法的执行过程:
- 对
ai0
执行操作 A,产生了ai1
ai1->bi0
入栈,RIP
切换到fun
函数开头- 对
bi0
执行操作 A,产生了bi1
bi1->ci0
入栈,假设此时ci0
满足了should_end
,那么,ci0
出栈RIP
指针切换到操作 B 的地址,此时,栈顶为bi1
- 对
bi0, bi1
执行操作 B,产生了bi2
bi2->c'i0
入栈,假设此时c'i0
满足了should_end
,那么,c'i0
出栈RIP
指针切换到操作 B 的地址,此时,栈顶为bi2
- 对
bi0, bi1, bi2
执行操作 C,然后它们均出栈
RIP
指针切换到操作 B 的地址,此时,栈顶为ai1
- 对
ai0, ai1
执行操作 B,产生了 `ai2``- ``ai2->b’i0
入栈,
RIP切换到
fun` 函数开头 - 对
b‘i0
执行操作 A,产生了b’i1
b'i1->di0
入栈,假设此时di0
满足了should_end
,那么,di0
出栈RIP
指针切换到操作 B 的地址,此时,栈顶为b'i1
- 对
b'i0, b'i1
执行操作 B,产生了b'i2
b'i2->d'i0
入栈,假设此时d'i0
满足了should_end
,那么,d'i0
出栈RIP
指针切换到操作 B 的地址,此时,栈顶为b'i2
- 对
b'i0, b'i1, b'i2
执行操作 C,然后它们均出栈
- ``ai2->b’i0
RIP
指针切换到操作 B 的地址,此时,栈顶为ai2
- 对
ai0, ai1, ai2
执行操作 C,然后它们均出栈 - 返回最终的执行结果
这里,可以用一颗树来演示上述过程:
根据这些行为,我们可以试着将其改造成一个非递归算法:
#define OP_A_ADDR 1
#define OP_B_ADDR 2
#define OP_C_ADDR 3
RET_TYPE fun(ARG_TYPE ai0)
{
PStack stack = InitStack();
PStack addrStack = InitStack();
int virtualRIP = OP_A_ADDR;
push(stack, ai0);
push(addrStack, OP_A_ADDR);
ARG_TYPE ai1, a_i1, ai2, a_i2;
RET_TYPE ret;
while (size(stack) > 0)
{
switch (virtualRIP) // 模拟根据地址计数器的值跳转执行流
{
case OP_A_ADDR:
{
if (should_end(top(stack))) // 判断递归边界条件实际上与操作A同属一个代码域
{
ret = OPERATION_END(pop(stack));
virtualRIP = pop(addrStack); // 模拟将返回地址传回地址计数器
}else{
OPERATION_A(top(stack, 0)); // 此过程中将产生 ai1, a_i1
// 局部变量 ai1, a_i1 入栈, ai1 表示需要传递的参数, a_i1 表示需要用到的其他局部变量
push(stack, a_i1);
push(stack, ai1);
/* 相当于 bi0 入栈, 因为考虑到对形参的修改不会改变实参
* 因此借助栈传递参数时, 很可能是在栈内存上进行复制*/
push(stack, ai1);
push(addrStack, OP_B_ADDR); // 模拟的返回地址, 即操作 B 的地址入栈
virtualRIP = OP_A_ADDR; // 将模拟地址寄存器的地址跳转到操作 A 的地址
}
break;
}
case OP_B_ADDR:
{
// 执行操作 B 时所需的局部变量
// 后续执行操作 C 时可能还会用到, 因此此处不进行 pop
ai1 = top(stack, 0);
a_i1 = top(stack, 1);
OPERATION_B(ai1, a_i1, ret); // 此过程将产生 ai2, a_i2
push(stack, a_i2);
push(stack, ai2);
push(stack, ai2);
push(addrStack, OP_C_ADDR);
virtualRIP = OP_A_ADDR;
break;
}
case OP_C_ADDR: {
// 执行操作 C 时所需的局部变量
// 已经是最后一步操作, 相关局部变量可直接 pop
ai2 = pop(stack);
a_i2 = pop(stack);
ai1 = pop(stack);
a_i1 = pop(stack);
ret = OPERATION_C(ai1, a_i2, ai2, a_i2, ret);
pop(stack); // 弹出 ai0
}
}
}
return ret;
}
接下来,我们尝试根据我们的理论,将使用了两次自身递归的归并排序算法改写成非递归的形式:
#define LENGTH(start, end) ((end) - (start) + 1)
void mergeSort(int *arr, int start, int end) {
if (LENGTH(start, end) < 2) {
return;
}
int mid = (start + end) / 2;
mergeSort(arr, start, mid);
int nextStart = mid + 1;
mergeSort(arr, nextStart, end);
// merge
int *temp = (int *)malloc(LENGTH(start, end));
int lstart = start, lend = mid;
int rstart = nextStart, rend = end;
int i = 0;
while(lstart <= lend && rstart <= rend) {
if(arr[lstart] > arr[rstart]) {
temp[i++] = arr[rstart++];
}else{
temp[i++] = arr[lstart++];
}
}
while(lstart <= lend)
temp[i++] = arr[lstart++];
while(rstart <= rend)
temp[i++] = arr[rstart++];
i = 0;
while(start <= end)
arr[start++] = temp[i++];
free(temp);
}
非递归的归并排序算法实现:
#define OP_A_ADDR 1
#define OP_B_ADDR 2
#define OP_C_ADDR 3
void mergeSort_new(int *arr, int start, int end) {
PStack stack = InitStack();
PStack addrStack = InitStack();
int virutalRIP = OP_A_ADDR;
push(stack, start);
push(stack, end);
push(addrStack, OP_A_ADDR);
int si0, ei0, ei1, si1;
while(size(stack) > 0) {
switch(virutalRIP) {
case OP_A_ADDR: {
si0 = top(stack, 1);
ei0 = top(stack, 0);
if(LENGTH(si0, ei0) < 2) {
pop(stack); // 形参 si0, ei0 出栈
pop(stack);
virutalRIP = pop(addrStack);
}else {
// 操作A, 局部变量ei1, 也是下一轮调用的 end
ei1 = (si0 + ei0) / 2;
// 局部变量入栈
push(stack, ei1);
// 传参
push(stack, si0);
push(stack, ei1);
push(addrStack, OP_B_ADDR);
virutalRIP = OP_A_ADDR;
}
break;
}
case OP_B_ADDR: {
ei1 = top(stack, 0);
ei0 = top(stack, 1);
si1 = ei1 + 1;
push(stack, si1);
// 传参
push(stack, si1);
push(stack, ei0);
push(addrStack, OP_C_ADDR);
virutalRIP = OP_A_ADDR;
break;
}
case OP_C_ADDR: {
si1 = pop(stack);
ei1 = pop(stack);
ei0 = pop(stack);
si0 = pop(stack);
int oriStart = si0;
int *temp = (int *)malloc(sizeof(int) * LENGTH(si0, ei0));
printf("si0 = %d, ei0 = %d, len = %d\n", si0, ei0, LENGTH(si0, ei0));
int i = 0;
while(si0 <= ei1 && si1 <= ei0) {
if(arr[si0] > arr[si1]) {
temp[i++] = arr[si1++];
}else{
temp[i++] = arr[si0++];
}
}
while(si0 <= ei1)
temp[i++] = arr[si0++];
while(si1 <= ei0)
temp[i++] = arr[si1++];
i = 0;
while(oriStart <= ei0)
arr[oriStart++] = temp[i++];
free(temp);
virutalRIP = pop(addrStack);
}
}
}
}
附录:所使用的栈的实现
struct Node
{
int value;
struct Node *next;
};
typedef struct Node *PNode;
struct Stack
{
int size;
PNode head;
};
typedef struct Stack *PStack;
// 初始化一个栈
PStack InitStack()
{
PStack stack = (PStack)malloc(sizeof(struct Stack));
stack->size = 0;
stack->head = NULL;
return stack;
}
// 向栈中压入元素
void push(PStack stack, int value)
{
stack->size++;
PNode newNode = (PNode)malloc(sizeof(struct Node));
newNode->value = value;
newNode->next = stack->head;
stack->head = newNode;
}
// 从栈中弹出元素
int pop(PStack stack)
{
if (stack->size <= 0)
return -1;
stack->size--;
PNode node = stack->head;
stack->head = node->next;
int value = node->value;
free(node);
return value;
}
// 获取栈顶元素, 但不弹出
int top(PStack stack, int step)
{
if (stack->size < step + 1) {
return -1;
}
PNode node = stack->head;
while(step--) {
node = node->next;
}
return node->value;
}
// 获取栈的大小
int size(PStack stack)
{
return stack->size;
}