递归程序往往可以用及其简洁的代码解决很复杂的问题,很多经典递归代码都是如此,比如树的遍历、快速排序等,如果让你写出快速排序的代码,大部分人都应该有清晰的思路,选基准,交换实现左小右大,然后递归。具体代码实现应该也不是难题。但是如果要求用非递归的形式实现呢,还会这么容易吗,如果换成其他的递归式呢,递归程序转换非递归有没有较为通用的方法。本篇文章来分享一些心得。
研究递归与非递归的转换很有意义,主要有以下几点:
1、并不是所有的编程语言都支持递归;
2、有助于理解递归的本质,理解函数调用过程中内存堆栈的变化,跳转和返回等底层实现;
3、能够更好的控制程序执行,必要时可以做出优化。
递归就是一种函数调用,区别只是调用的目标函数是自身,但是计算机并不能区分递归和普通函数调用,它仅仅是遇到CALL指令就跳转到目标地址执行。递归调用是在程序栈区实现的,这也就启发我们用栈数据结构去模拟递归过程,这是递归调用转换非递归形式的基础。
在上篇文章中已经分析,递归函数的调用过程可以展开为一颗递归树,这颗递归树可能是自顶向下的方式解决问题,也可能是自底向上的方式进行,具体区别就是每一层递归是先完成本步骤操作再进行递归调用,还是先执行递归调用后执行本步骤操作,更进一步可以将这个过程类比与树的前序、中序和后序遍历。树的遍历也是经典的递归问题,那本篇文章也就从树的遍历的递归形式和非递归形式转换开始。
树的前序、中序和后序遍历的前中后针对的是根节点,从根节点出发,往左往右(先只考虑二叉树)都都可以看作一颗子树,而且遍历过程也是一样的,正因为这些特征,递归可以十分优雅的解决这类问题,代码简洁清晰。
树的前序遍历
递归形式
void preorder_recursive(Bitree T) /* 前序遍历二叉树的递归算法 */ { if (T) { visit(T); /* 访问当前结点 */ preorder_recursive(T->lchild); /* 访问左子树 */ preorder_recursive(T->rchild); /* 访问右子树 */ } }
递归形式很优雅,如同情诗一般,短小精悍值得回味。如果要将其转换为非递归形式,恐怕就没有这么优雅的意境了。
本文中的栈使用C++中的STL栈,各函数的含义不再赘述。
上非递归的代码,注释比较清晰
void preorder_nonrecursive(Bitree T) /* 前序遍历二叉树的非递归算法 */ { initstack(S); push(S,T); /* 根指针进栈 */ while(!stackempty(S)) { while(gettop(S,p)&&p) { /* 向左走到尽头 */ visit(p); /* 每向前走一步都访问当前结点 */ push(S,p->lchild); }
pop(S,p); /* 弹出压栈的空值 */
if(!stackempty(S)) { /* 向右走一步 */ pop(S,p); push(S,p->rchild); } } }
实现二
void preorder_nonrecursive(Tree root){ stack<Node *> nodeStack; //使用C++的STL标准模板库 nodeStack.push(root); Node *node; while(!nodeStack.empty()){ node = nodeStack.top(); printf(format, node->data); //遍历根结点 nodeStack.pop(); if(node->rchild){ nodeStack.push(node->rchild); //先将右子树压栈 } if(node->lchild){ nodeStack.push(node->lchild); //再将左子树压栈 } } }
中序遍历
中序遍历情形类似,直接上代码
递归形式
void inorder_recursive(Bitree T) /* 中序遍历二叉树的递归算法 */ { if (T) { inorder_recursive(T->lchild); /* 访问左子树 */ visit(T); /* 访问当前结点 */ inorder_recursive(T->rchild); /* 访问右子树 */ } }
非递归形式
void inorder_nonrecursive(Bitree T) { initstack(S); /* 初始化栈 */ push(S, T); /* 根指针入栈 */ while (!stackempty(S)) { while (gettop(S, p) && p) /* 向左走到尽头 */ push(S, p->;lchild);
pop(S, p); /* 空指针退栈 */ if (!stackempty(S)) { pop(S, p); visit(p); /* 访问当前结点 */ push(S, p->;rchild); /* 向右走一步 */ } } }
后续遍历
递归代码与上述递归类此,只是最后visit当前结点。
非递归形式
后序遍历的非递归形式和上述代码有不同,前序遍历和中序遍历中,新结点进栈,当遍历完该结点的左子树再次回到该结点时,该结点出栈,访问右子树,前序和中序的区别在于前序是新结点进栈则访问,而中序是左子树访问完才访问该结点,而后序遍历不同,新结点进栈后遍历左子树,左子树遍历完回到该结点时访问右子树,再回到该结点才访问,所以需要区分该结点时第一次进栈,左子树访问完成回到该结点还是右子树方法完全回到该结点。因此需要使用标志位来区分。
typedef struct { BTNode* ptr; enum {0,1,2} mark; } PMType; /* 有mark域的结点指针类型 */ void postorder_nonrecursive(BiTree T) /* 后续遍历二叉树的非递归算法 */ { PMType a; initstack(S); /* S的元素为PMType类型 */ push (S,{T,0}); /* 根结点入栈 */ while(!stackempty(S)) { pop(S,a); switch(a.mark) { case 0: push(S,{a.ptr,1}); /* 修改mark域 */ if(a.ptr->lchild) push(S,{a.ptr->lchild,0}); /* 访问左子树 */ break; case 1: push(S,{a.ptr,2}); /* 修改mark域 */ if(a.ptr->rchild) push(S,{a.ptr->rchild,0}); /* 访问右子树 */ break; case 2: visit(a.ptr); /* 访问结点 */ } } }
事实上只需要两个标志位就可以实现,代码需要改动,上述代码使用三个标志位,可以使各步骤操作更一致,逻辑更清晰。
到这里树的前序、中序和后序遍历就介绍完了,这是几个基本的递归转换非递归的例子,但又不仅仅是几个例子那么简单,而是更多递归转换非递归的基础。
可以看到树遍历的递归形式非常简单,一个visit操作,两步递归,只是visit的执行位置不一样,所以有三种遍历形式,根据之前分析的递归满足树结构,通过几个递归的例子就可以看到,复杂的递归也逃不出这几种形式,无非是visit步骤多一些,递归方程复杂些,甚至多几个递归式。
先来看一个较为接近的例子
f(n) = n + 1; (n <2)
f(n) = f(n/2) + f(n/4) (n >= 2);
int f_recursive(int n) { int u1, u2, f; if (n < 2) f = n + 1; else { u1 = f_recursive((int)(n/2)); u2 = f_recursive((int)(n/4)); f = u1 * u2; } return f; }
分析上述递归程序,可以看出是自底向上的解决方法,n/2看作左子树,n/4看做右子树,递归的时候假设后续操作已经完成,因此u1和u2已经计算完成,此时计算本步骤的u1*u2。先左子树,后右子树,最后执行本步骤操作,显然这是后序遍历的方法,因此可以参考后序遍历树的非递归形式代码写出此程序的非递归形式。
int f_nonrecursive(int n) { int stack[20], flag[20], cp; /* 初始化栈和栈顶指针 */ cp = 0; stack[0] = n; flag[0] = 0; while (cp >;= 0) { switch(flag[cp]) { case 0: /* 访问的是根结点 */ if (stack[cp] >;= 2) { /* 左子树入栈 */ flag[cp] = 1; /* 修改标志域 */ cp++; stack[cp] = (int)(stack[cp - 1] / 2); flag[cp] = 0; } else { /* 否则为叶子结点 */ stack[cp] += 1; flag[cp] = 2; } break; case 1: /* 访问的是左子树 */ if (stack[cp] >;= 2) { /* 右子树入栈 */ flag[cp] = 2; /* 修改标志域 */ cp += 2; stack[cp] = (int)(stack[cp - 2] / 4); flag[cp] = 1; } else { /* 否则为叶子结点 */ stack[cp] += 1; flag[cp] = 2; } break; case 2: /* */ if (flag[cp - 1] == 2) { /* 当前是右子树吗? */ /* * 如果是右子树, 那么对某一棵子树的后序遍历已经 * 结束,接下来就是对这棵子树的根结点的访问 */ stack[cp - 2] = stack[cp] * stack[cp - 1]; flag[cp - 2] = 2; cp = cp - 2; } else /* 否则退回到后序遍历的上一个结点 */ cp--; break; } } return stack[0]; }
采用数组模拟堆栈是经常采用的方法,此程序满足后序遍历的形式,但和后续遍历还有一些区别,就是树的遍历只需要访问结点,不需要保存信息,而此程序中计算的结果需要保持供上一层递归使用,因此需要保持值的信息。到这一步程序变得并不太容易理解了,还需要仔细思考。
回到文章开头的问题。
快速排序
经典递归算法
void quick_sort(int a[], int low, int high) { if (low >= high) return; int l = low, r = high; int pivot = a[low]; while (low < high) { while (low < high && a[high] <= pivot) high--; a[low] = a[high]; while (low < high && a[low] >= pivot) low++; a[high] = a[low]; } a[low] = pivot; quick_sort(a, l, low-1); quick_sort(a, low+1, r); }
很美的递归代码,先partition,后递归,和前序遍历相比,只是visit的步骤变成了partition,操作长了一点,接下来根据树的前序遍历的非递归形式可以写出快速排序的非递归代码
void quicksort_nonrecursive(int array[], int low, int high) { int m[50], n[50], cp, p; /* 初始化栈和栈顶指针 */ cp = 0; m[0] = low; n[0] = high; while (m[cp] < n[cp]) { while (m[cp] < n[cp]) { /* 向左走到尽头 */ p = partition(array, m[cp], n[cp]); /* 对当前结点的访问 */ cp++; m[cp] = m[cp - 1]; n[cp] = p - 1; } /* 向右走一步 */ m[cp + 1] = n[cp] + 2; n[cp + 1] = n[cp - 1]; cp++; } }
这个代码很清晰,注释也很明白,以后再被问到快速排序的非递归实现就不会再苦思冥想无果了吧。
快速排序的非递归形式还可以优化,因此每一步完成,该步骤的low和high信息就不再有用了,因此执行完一步下一步可以覆盖这一步的信息,可以开更小的数组就可以实现。一个参考代码如下。
void quicksort_nonrecursive2( int a[], int n ) { struct sbe { int low; int high; }; sbe* arr = new sbe[n]; arr[0].low = 0; arr[0].high = n-1; int m = 0; while (m >= 0) { int low = arr[m].low; int high = arr[m].high; m--; int pivot = a[low]; int l = low, r = high; if (l >= r) continue; while (low < high) { while (low < high && a[high] >= pivot) high--; a[low] = a[high]; while (low < high && a[low] <= pivot) low++; a[high] = a[low]; } a[low] = pivot; m++; arr[m].low = l; arr[m].high = low-1; m++; arr[m].low = low+1; arr[m].high = r; }
此代码仅作为一个参考,有点偏离上述分析,但总体还是提醒树的遍历思想,此代码是采用先遍历右子树的前序遍历方式进行。
以上递归转非递归均为用栈模拟递归的思想,还有一部分递归是不需要用栈就可以简单实现的,比如阶乘递归可以直接用for循环实现。这些递归有个特点就是每一层函数里只有一个递归调用,而不是有两个甚至多个,这种只有一个递归调用的递归称作尾递归,而有多个递归调用的称作非尾递归。
除了阶乘之外再给个尾递归的例子
f(n) = n + 1 (n = 0)
n * f(n/2) (n >; 0)
递归实现
int f_recursive(int n) { if (n == 0) return 1; return (n * f_recurse(n/2)); }
非递归实现
int f_nonrecursive(int n) { int m; for (m = 1; n >; 0; n /= 2) m *= n; return m++; }
这种递归很简单吧,这篇文章也就要在这种轻松愉快的心情中结束了,莫非这就是所谓的深入浅出。
最后再强调一下递归转换非递归的几个要点:
首先是栈的思想,虽然没有用递归,但还保留了递归的思想;
另一个是递归树的理解,把树的三种遍历的非递归代码作为模板。
在具体问题中
首先分析问题属于哪种树的遍历类型;
接下来思考栈中需要保存哪些信息。
注:本篇文章参考链接:http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=331522