递归算法改成非递归算法的思路

递归算法改成非递归算法的思路

撰稿日期: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,然后它们均出栈
  • RIP 指针切换到操作 B 的地址,此时,栈顶为 ai2
  • ai0, ai1, ai2 执行操作 C,然后它们均出栈
  • 返回最终的执行结果

这里,可以用一颗树来演示上述过程:

操作A
操作B
操作A
操作B
操作A
操作B
fun(ai0)
fun(bi0)
fun(ci0)
fun(c'i0)
fun(b'i0)
fun(di0)
fun(d'i0)

根据这些行为,我们可以试着将其改造成一个非递归算法:

#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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值