. 所有递归都能改写成循环吗?
可以。有些递归只需要一个循环就可以替代,而有些递归的改写需要循环+栈,即要利用一定的辅助空间记录过程中的某些数据才可以。那么什么样的递归只需要一个循环,什么样的递归需要循环+栈呢?这个问题是本篇文章将要解决的主要问题,下面会为大家分析。
. 反过来,所有循环都能改写成递归吗?
可以但没必要。循环和递归的共同特点是,它们都是会不断重复执行相同代码,每次重复执行时所使用的数据不一样(递归中每次调用的参数不同,循环中每次使用的 i 或其他变量会不同),直到达到结束条件为止,就停止重复执行,两者使用不当都会造成死循环。
. 同一个功能,用递归实现与用循环实现有什么区别?
循环的时间复杂度和空间复杂度都要优于递归,但递归的优越性在于条理清晰,可读性强,比较适宜于问题本身是递归性质的、用循环难于解决的问题。在二者都不难的情况下,一般都是优先选用循环来解决问题的。
. 什么样的递归改写成循环需要借助栈?
有些功能本身就比较适合用递归来写,如果非要写成循环,可能需要借助栈去存放一些数据。其中的一个例子就是我之前写的文章如何使用栈非递归地求解Ackerman函数。
这次我们举别的例子,将堆排序算法和快速排序算法作比较,分析什么样的递归改写成循环需要借助栈。
(本篇文章对这两种算法的具体思想不做全面的详细的介绍)
首先分析堆排序。
在堆排序中,对于“将序列中数值最大的元素调整至堆顶”的问题,可以划分成若干个“若某一结点的值小于其左右孩子的值,则将其值与其孩子最小值置换”的子问题。这种情况下,使用递归方法实现,确定方法的参数为需要与孩子结点比较的该结点的下标,根据二叉树相关的公式可知,该结点的下标为 n ,其左右孩子结点的下标即为 2n 和 2n+1 。代码如下:
/*对data[m-n]进行建堆*/
void CreateHeap(int data[], int m, int n)
{
int son = m * 2;
/*如果data[m]存在孩子结点则进行处理*/
if (son <= n)
{
/*记录较小孩子的下标*/
if (son + 1 < n && data[son] < data[son + 1])
{
son = son + 1;
}
/*若data[m]小于其孩子结点的值,则进行交换操作*/
if (data[son] < data[m])
{
int temp = data[m];
data[m] = data[son];
data[son] = temp;
/*如果发生交换,数值发生改变的孩子结点也应该重复上述对于父节点的操作*/
CreateHeap(data, son, n);
}
}
}
在上面那个递归函数中,每次调用的函数参数 data[] 和 n 不变,而参数 m 都在变化,特别注意的是,虽然 m 变化,但在本次调用中就能确定下次调用时 m 的值,且本次调用的 m 值只在本次调用会被使用,其他次调用时不需要使用本次调用的 m 值。这种情况下,若要使用循环来改写递归,是不需要栈来做辅助存储空间的。代码如下:
void CreateHeap(int data[], int m, int n)
{
for (int i = m, j = i * 2; j <= n; i = j, j = i * 2)
{
if (j + 1 <= n && data[j] > data[j + 1])
{
j = j + 1;
}
if (data[i] < data[j])
{
break;
}
else
{
int temp = data[i];
data[i] = data[j];
data[j] = temp;
}
}
}
接下来分析一下快速排序。
而在快速排序中,对于“将整个序列进行排序”的问题,可以划分成若干个“取第一个元素作为基准,通过一趟排序,将待排序的序列分成左右两个子序列”的子问题。这种情况下,使用递归方法实现,确定方法的参数为该趟排序的序列起始元素下标和结束元素下标。代码如下:
/*对子序列data[low-high]进行一次快速排序*/
/*data[]中data[0]为特殊存储单元,不存放序列中的数据*/
int Partition(int data[], int low, int high)
{
data[0] = data[low];
while (low < high)
{
while (data[high] >= data[0] && low < high)
{
high--;
}
data[low] = data[high];
while (data[low] <= data[0] && low < high)
{
low++;
}
data[high] = data[low];
}
//跳出循环后,low=high
data[low] = data[0];
return low;
}
void QuickSort(int data[], int low, int high)
{
if (low < high)
{
int pos = Partition(data, low, high);
printf("\n");
QuickSort(data, low, p - 1);
QuickSort(data, p + 1, high);
}
}
我们观察图片,第一趟排序,low = 0 & high = 14,划分的 pos = 9;第二趟排序, low = 0 & high = 8,pos = 5,low = 10 & high = 14,pos = 11;第等等趟排序。我们可以发现,第一趟排序得到的 pos = 9 是第二趟第三趟都要用到的 end 的值,但不是其后的每次递归都要用到的值,第二趟排序得到的 pos 同理。这些 pos 值,我们需要用栈来记录下来,需要用到时再拿出来,也就是说,这种情况下,若要使用循环来改写递归,需要栈来做辅助存储空间。代码如下:
typedef struct
{
int tag[MAXSIZE];
int top;
} Stack;
/*对子序列data[low-high]进行一次快速排序*/
int Partition(int data[], int low, int high)
{
int temp = data[low];
while (low < high)
{
while (data[high] >= temp && low < high)
{
high--;
}
data[low] = data[high];
while (data[low] <= temp && low < high)
{
low++;
}
data[high] = data[low];
}
//跳出循环后,low=high
data[low] = temp;
return low;
}
void QuickSort(int data[], int length)
{
Stack* SS = (Stack*)malloc(sizeof(Stack));
SS->tag[0] = length;
SS->tag[1] = -1;
SS->top = 1;
int loc = -1;
while (SS->top > 0)
{
if ((SS->tag[SS->top - 1] - SS->tag[SS->top]) > 2)
{
pos = Partition(data, SS->tag[SS->top] + 1, SS->tag[SS->top - 1] - 1);
SS->tag[SS->top + 1] = SS->tag[SS->top];
SS->tag[SS->top] = loc;
SS->top++;
}
else
{
SS->top--;
}
}
}