循环和递归可以相互替代吗?

本文探讨了递归与循环的关系,指出所有递归都可以转化为循环,但反之不一定。递归实现清晰易读,但可能消耗更多空间,而循环效率更高。在特定条件下,如堆排序,递归改写为循环无需借助栈,而快速排序则需要。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

. 所有递归都能改写成循环吗?

可以。有些递归只需要一个循环就可以替代,而有些递归的改写需要循环+栈,即要利用一定的辅助空间记录过程中的某些数据才可以。那么什么样的递归只需要一个循环,什么样的递归需要循环+栈呢?这个问题是本篇文章将要解决的主要问题,下面会为大家分析。

. 反过来,所有循环都能改写成递归吗?

可以但没必要。循环和递归的共同特点是,它们都是会不断重复执行相同代码,每次重复执行时所使用的数据不一样(递归中每次调用的参数不同,循环中每次使用的 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--;
		}
	}
}

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值