C++算法程序整理——递归算法

本文介绍了递归算法的概念和特点,以及使用递归解决实际问题的条件。详细阐述了递归模型的构造方法,包括递归出口和递归体,并以n皇后问题和排序算法为例,展示了递归算法的设计过程和示例代码。

递归算法

递归,定义一个过程或函数时出现调用本过程或本函数的成分,称之为递归。

  • 一般来说,能够用递归解决的问题应该满足以下三个条件:

    • 需要解决的问题可以转化为一个或多个同构(或同样性质的)子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同。
    • 递归调用的次数必须是有限的。
    • 必须有结束递归的条件来终止递归。
  • 什么情况下使用递归:

    • 定义是递归的,如n!,Fibonacci数列等,求解这类问题可将其递归定义直接转换成对应的递归算法。
    • 数据结构(如树,单链表等)是递归的。结构体中的指针指向了自身的类型,是一种递归的数据结构。对于这类递归结构,采用递归的方法编写算法既方便,又高效。
    • 问题的求解方法是递归的, 如 Hanoi 问题的求解。
  • 递归模型,递归模型是递归算法的抽象,它反映了一个递归问题的递归结构。以求解n!为例,其递归模型如下:

    	fun(1) = 1                    (1)
    	fun(n) = n*fun(n-1)    n > 1  (2)
    

递归模型一般由递归出口递归体组成。递归出口给出了递归的终止条件,递归体给出了fun(n)的值与fun(n-1)的值之间的联系。

  • 递归算法的执行过程,
    • 一个正确的递归程序虽然每次调用的是相同的子程序,但它的参量、输入数据等均有变化。
    • 在正常的情况下,随着调用的不断深入,必定会出现调用到某一层的函数时,不再执行递归调用而终止函数的执行,遇到递归出口便是这种情况。
    • 递归调用是函数嵌套调用的一种特殊情况,即它是调用自身代码。也可以把每一次递归调用理解成调用自身代码的一个复制件。由于每次调用时,它的参量和局部变量均不相同,因而也就保证了各个复制件执行时的独立性。
    • 系统为每一次调用开辟一组存储单元,用来存放本次调用的返回地址以及被中断的函数的参量值。这些单元以系统栈的形式存放,每调用一次进栈一次,当返回时执行出栈操作,把当前栈顶保留的值送回相应的参量中进行恢复,并按栈顶中的返回地址,从断点继续执行。
    • 每递归调用一次,就需进栈一次,最多的进栈元素个数称为递归深度,当n越大,递归深度越深,开辟的栈空间也越大。每当遇到递归出口或完成本次执行时,需退栈一次,并恢复参量值,当全部执行完毕时,栈应为空。
      在这里插入图片描述
    • 在递归函数执行时,形参会随着递归调用发生变化,但每次调用后会恢复为调用前的形参,将递归函数的非引用型形参的取值称为状态。递归函数的引用型形参在执行后会回传给实参,有时类似全局变量,不作为状态的一部分,在调用过程中状态会发生变化,而一次调用后会自动恢复为调用前的状态。

归纳起来,递归调用的实现是分两步进行的,第一步是分解过程,即用递归体将“大问题”分解成“小问题”,直到递归出口为止,然后进行第二步的求值过程,即已知“小问题”,计算“大问题”。

  • 递归算法设计,获取递归模型:
    • 对原问题f(sn)f(s_n)f(sn)进行分析,抽象出合理的“小问题”f(sn−1)f(s_{n-1})f(sn1)(与数学归纳法中假设n=k-1时等式成立相似);
    • 假设f(sn−1)f(s_{n-1})f(sn1)是可解的,在此基础上确定f(sn)f(s_n)f(sn)的解,即给出f(sn)f(s_n)f(sn)f(sn−1)f(s_{n-1})f(sn1)之间的关系(与数学归纳法中求证n=k时等式成立的过程相似);
    • 确定一个特定情况(如f(1)f(1)f(1)f(0)f(0)f(0))的解,由此作为递归出口(与数学归纳法中求证n=1或n=0时等式成立相似)。

例1: 在n×n的方格棋盘上,放置n个皇后,要求每个皇后不同行、不同列、不同左右对角线。

问题分析:若它们同列,则有q[k]==j;对角线有两条,若它们在任一条对角线上,则构成一个等边直角三角形,即|q[k]-j|==|i-k|。设queen(i,n)是在1~i-1列上已经放好了i-1个皇后,用于在i~n行放置n-i+1个皇后,则queen(i+1,n)表示在1~i行上已经放好了i个皇后,用于在i+1~n行放置n-i个皇后。queen(i+1,n)queen(i,n)少放置一个皇后,所以queen(i+1,n)是“小问题”,queen(i,n)是“大问题”。因此,

递归模型:
当 i>n 时,
    queen(i,n) 表示n个皇后放置完毕。
否则,
    queen(i,n) 表示在第 i 行的合适的位置(i,j)在其上放置一个皇后;
    queen(i+1,n);

n皇后问题示例代码:

#include <iostream>

const int n = 6;  //  n 等于几,就表示几皇后问题
int q[n+1] = { 0 };

void dispasolution(int n)
{
	for (int i = 1; i <= n; ++i)
		std::cout << "(" << i << ", " << q[i] << ")\t";
	std::cout << std::endl;
}

bool place(int i, int j)		//测试(i,j)位置能否摆放皇后
{
	if (i == 1) return true;		//第一个皇后总是可以放置
	int k = 1;
	while (k < i)			//k=1~i-1是已放置了皇后的行
	{
		if ((q[k] == j) || (abs(q[k] - j) == abs(i - k)))
			return false;
		k++;
	}
	return true;
}

void queen(int i, int n)		//放置1~i的皇后
{
	if (i > n)
		dispasolution(n);		//所有皇后放置结束
	else
	{
		for (int j = 1; j <= n; j++)	//在第i行上试探每一个列j
			if (place(i, j))		//在第i行上找到一个合适位置(i,j)
			{
				q[i] = j;
				queen(i + 1, n);
			}
	}
}

int main()
{
	std::cout << n << " 皇后问题可行解:\n";
	queen(1, n);

	system("pause");
	return 0;
}

结果显示:
在这里插入图片描述

例2:对于给定的含有n个元素的数组a,分别采用简单选择排序和冒泡排序方法对其按元素值递增排序。

问题分析:设f(a,n,i)用于对a[i..n-1]元素序列(共n-i个元素)进行冒泡排序,是“大问题”,则f(a,n,i+1)用于对a[i+1..n-1]元素序列(共n-i-1个元素)进行冒泡排序,是“小问题”。当i=n-1时所有元素有序,算法结束。

2.1,选择排序递归模型:
当 i=n-1 时,
    f(a,n,i) 不做任何事情,算法结束。
否则,
    f(a,n,i) 通过简单比较挑选 a[i…n-1] 中的最小元素 a[k] 放在 a[i] 处;
    f(a,n,i+1);

2.2,冒泡排序递归模型:
当 i=n-1 时,
    f(a,n,i) 不做任何事情,算法结束。
否则,
    f(a,n,i) 从a[n-1]开始对,a[i…n-1]元素序列进行相邻元素比较,若相邻两元素反序,则将两者交换。
   上面循环过程执行完成后,若没有发生过交换,
        返回;
    否则,
        f(a,n,i+1);

选择,冒泡排序示例代码

#include <iostream>
#include <string>

void print(std::string str, int a[], int n)
{
	std::cout << str << std::endl;
	for (int i = 0; i < n; ++i)
		std::cout << a[i] << " ";
	std::cout << std::endl;
}

void swap(int& a, int& b)
{
	int tmp = a; a = b; b = tmp;
}

// 基于递归的选择排序:
void SelectSort(int a[], int n, int i)
{
	int j, k;
	if (i == n - 1)
		return;	// 满足递归出口条件
	else
	{
		k = i;			// k记录a[i..n-1]中最小元素的下标
		for (j = i + 1; j < n; j++)  	// 在a[i..n-1]中找最小元素
			if (a[j] < a[k])
				k = j;
		if (k != i)		// 若最小元素不是a[i]
			swap(a[i], a[k]);	// a[i]和a[k]交换
		SelectSort(a, n, i + 1);
	}
}

// 基于递归的冒泡排序:
void BubbleSort(int a[], int n, int i)
{
	int  j;
	bool exchange;
	if (i == n - 1)
		return;		//满足递归出口条件
	else
	{
		exchange = false;		//置exchange为false
		for (j = n - 1; j > i; j--)
			if (a[j] < a[j - 1])		//当相邻元素反序时
			{
				swap(a[j], a[j - 1]);
				exchange = true;	//发生交换置exchange为true
			}
		if (exchange == false)		//未发生交换时直接返回   
			return;
		else				//发生交换时继续递归调用
			BubbleSort(a, n, i + 1);
	}
}

int main()
{
	int a[15] = { 1, 3, 5, 7, 9, 8, 6, 4, 2, 23, 2, 0, 10, 4, 8 };
	int b[15] = { 1, 3, 5, 7, 9, 8, 6, 4, 2, 23, 2, 0, 10, 4, 8 };
	print("排序前:", a, 15);
	SelectSort(a, 15, 0);
	print("选择排序后:", a, 15);
	BubbleSort(b, 15, 0);
	print("冒泡排序后:", b, 15);

	system("pause");
	return 0;
}

结果显示:
在这里插入图片描述

<think>好的,用户现在问的是“递归排序”。首先,我需要明确“递归排序”具体指的是什么。通常,递归排序可能指的是使用递归方法实现的排序算法,比如快速排序、归并排序等。但有时候用户可能只是泛指利用递归思想的排序方法。 接下来,我要回顾之前的对话历史。用户之前请求生成快速排序的动态图,我详细解释了快速排序的步骤、动态图的生成方法以及时间复杂度。现在用户的新问题是关于递归排序,可能需要更广泛的解释,涵盖不同的递归排序算法,而不仅仅是快速排序。 用户可能的真实需求是理解递归在排序中的应用,常见的递归排序算法有哪些,以及它们的工作原理。他们可能对递归的概念不太熟悉,或者想了解不同递归排序算法的区别和优劣。 我需要先定义递归排序,然后列举常见的递归排序算法,比如归并排序、快速排序、二叉树排序等。然后分别解释每种算法的基本思想、步骤和时间复杂度,同时比较它们的优缺点。此外,用户可能对递归机制如何应用于排序感兴趣,所以需要解释递归的分治策略。 要确保回答结构清晰,分点说明,使用中文,并且遵循之前的格式要求,比如数学公式用$...$,独立公式用$$...$$。同时,可能需要给出示例,比如归并排序的分治步骤,快速排序的分区过程,以及二叉树排序的构建过程。 需要注意用户可能混淆递归排序与特定算法,所以需要明确说明递归是一种实现方法,不是具体的排序算法。同时,时间复杂度的对比可以帮助用户理解不同算法的性能差异。最后,总结如何选择适合的递归排序算法,帮助用户根据实际需求做出决策。</think>我们将通过以下步骤解析递归排序的核心原理与实现方式: 一、递归排序的本质特征 1. 分治策略(Divide and Conquer)三要素: $$ \text{递归排序} = \text{分解} + \text{解决} + \text{合并} $$ 2. 递归终止条件:当子数组长度 $\leq 1$ 时自动返回 二、典型递归排序算法对比 通过表格对比两种经典实现: | 特征 | 归并排序 | 快速排序 | |---------------|----------------------------------|----------------------------------| | 核心操作 | 后处理合并 | 预处理分区 | | 时间复杂度 | 稳定 $O(n \log n)$ | 平均 $O(n \log n)$,最差 $O(n^2)$ | | 空间复杂度 | $O(n)$ | $O(\log n)$ 栈空间 | | 稳定性 | 稳定 | 不稳定 | | 关键方程 | $T(n) = 2T(n/2) + O(n)$ | $T(n) = T(k) + T(n-k-1) + O(n)$ | 三、归并排序动态演示(递归过程) 以数组 $[38, 27, 43, 3, 9, 82, 10]$ 为例: 1. 分解阶段: $$ [38, 27, 43, 3] \quad \text{与} \quad [9, 82, 10] $$ $$ \downarrow \quad \text{继续分解} \quad \downarrow $$ $$ [38,27] \quad [43,3] \quad [9,82] \quad [10] $$ 2. 合并阶段: ``` [27,38] ← 合并 ← [3,43] [9,82] ← 合并 ← [10] ↓ ↓ [3,27,38,43] [9,10,82] ↓ ↗ [3,9,10,27,38,43,82] ``` 四、递归调用栈可视化(以深度3为例) ```python merge_sort(arr, 0, 6) ├── merge_sort(left: 0-3) │ ├── merge_sort(0-1) │ │ ├── 处理 [38] │ │ └── 处理 [27] │ └── merge_sort(2-3) ├── merge_sort(right: 4-6) │ ├── merge_sort(4-5) │ └── 处理 [10] └── 合并左右子数组 ``` 五、算法选择建议 1. 内存充足时优先选归并排序(稳定且性能可预测) 2. 大规模数据优先快速排序(缓存友好性更佳) 3. 实际应用常采用混合策略:如Timsort(归并+插入) 六、递归深度优化技巧 1. 尾递归优化:将递归调用置于函数最后一步 2. 设置递归深度阈值:当 $n < 64$ 时切换为插入排序 3. 栈空间保护:通过迭代模拟递归(防止栈溢出) 可通过访问 https://www.cs.usfca.edu/~galles/visualization/ComparisonSort.html 观察不同递归排序算法的实时执行过程。关键要注意递归树的高度与分治策略的效率关系。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值