递归是好理解还是不好理解?
很多人说,递归很简单,但是面对很多递归的题目,我却觉得十分困难。递归是一种工具
我们有几个步骤去做
1.首先,找出关键的步骤,这是算法的核心,每一步都要循环的列子,即大问题分成小问题适用的算法。
2.然后找出停止规则,递归不能无限向前,必须停止,然后往回走。
3.列出算法大纲,即代码框架。
4.检查终止。
5.画出递归树,观察是否正确。
有人说
递归就是有去(递去)有回(归来)。
所以递归过程可以分解为两部分,一部分为分解,一部分为求解。
具体来说,为什么可以有去?
这要求递归的问题需要是可以用同样的解题思路来回答除了规模大>小不同其他完全一样的问题。
为什么可以有回?
这要求这些问题不断从大到小,从近及远的过程中,会有一个终点,一个临界点,一个baseline,一个你到了那个点就不用再往更小,更远的地方走下去的点,然后从那个点开始,原路返回到原点。
当思考递归题目时,很多人去探寻递归的每一步,思考全过程是怎么解决问题的,所以一直往前,到底后,往后,来来往往,思考不出各结果。
但观察递归的基本思想——把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。
所以我们考虑的仅仅是一个大的问题,然后把这个问题去拆分为相同的小问题即可,这才是递归的思想。
如果将其用程序表达出来,则可表示为
- 很多时候,我们可以用上简单的递归,比如很明显的函数关系,反转,遍历等。但是,有些递归却让我摸不着头脑,比如,全排列,子集。
这时候我们需要递归树的帮忙
以上图为列,箭头表示递归的走向,图为3层汉诺塔的示意图,当Move第一个参数为0时,停止,所以,递归中,总共进行了7次的实际操作,这也是汉诺塔步数的公式,2的n次方-1,即完全二叉树的分支节点数。在此从简短的过程中,我们可以思考递归的走向。尾递归
如果函数最后执行的语句是对函数自身的递归调用,则可以调用参数赋给递归调用中指定的值并重复整个函数而消除此调用。
如图,右边P可以直接返回第一步。
尾递归和迭代相似,一般不单独使用。
从图中可以看出,尾递归前的操作都已经完成,尾递归只是调用回前一个递归中,所以可以看作回到第一个递归,调用回去的时候并无其他操作(有时会有参数的变化),所以可以理解为一个循环,即迭代。如下面代码,函数中尾递归和迭代效果相同。
#include <iostream>
using namespace std;
int fun(int n) {
/*if (n > 0) {
cout << n << " ";
fun(n-1);
}*/
while (n > 0) {
cout << n << " ";
n--;
}
}
int main () {
fun(5);
}
- 回溯
当发生与问题需求不一致的情况时,算法移构造的解部分并进行倒退,以尝试另外一种可能。
1.针对所给问题,确定问题的解空间:
首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
2确定结点的扩展搜索规则
3以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
所以回溯可以差异地理解为暴力搜索?(个人认为)当一个方向错误后,回到上一个递归中。代码结构如下:
int a[n];
try(int i) {
// 终止条件 任意条件
if(i>n)
输出结果;
else {
// 枚举i所有可能的路径
for(j = 下界; j <= 上界; j=j+1) {
// 满足限界函数和约束条件
if(fun(j)) {
a[i] = j;
// 其他操作
...
try(i+1);
//回溯前的清理工作(如a[i]置空值等);
}
}
}
}
- 由代码可以看出,for枚举所有可能性,满足条件将进行下一步递归操作,当被约束时的枚举没有满足if的条件时,最底层递归终止,进行回溯,之前应该做必要的清理工作,如此直到满足终止条件为止。
实际例子1:背包问题
#include <iostream>
#include <vector>
using namespace std;
int sum = 0;
// 物品重量刚好的数组
vector<int> bark;
void fun (vector<int>& tmp, int weight, int index) {
// 如果大于所需条件 则函数终止
if (sum > weight) {
return;
}
// 如果刚好等于 满足条件 输出
else if (sum == weight) {
// 构造一个新数组用于输出,让旧数组回溯直至所有结果
vector<int> q = bark;
for (int i = 0; i < q.size(); ++i) {
cout << q[i] << " ";
}
cout << endl;
} else {
// 从第一件物品开始到最后一件物品开始
for (int i = index; i < 5; ++i) {
if (sum < weight) {
// 小于所需重量 继续Push物品
bark.push_back(tmp[i]);
sum += bark.back();
// 递归相加 注意 这里第三个参数,如果用 i 就从已经递归的数的后面开始 如果用 Index 会从0开始
fun(tmp, weight, i+1);
sum -= bark.back();
bark.pop_back();
}
}
}
}
int main () {
// 初始化一个背包
vector<int> bag;
// 输入背包中各物品重量,这里简化,直接将重量输入数组,不做重量,价格等等数据
for (int i = 1; i <= 5; ++i) {
bag.push_back(i);
}
int weight;
cin >> weight;
fun(bag,weight,0);
}
实际例子2:八皇后问题
#include <iostream>
#define MAX 30
using namespace std;
class Queens {
public:
Queens(int n) {
size = n;
count = 0;
for (int i = 0; i < MAX; ++i) {
for (int j = 0; j < MAX; ++j) {
square[i][j] = false;
}
}
}
bool is_solve() {
// 如果皇后数等于棋盘大小 安防皇后完成
return count == size;
}
void print() {
for (int i = 0; i < size; ++i) {
for (int j = 0; j < size; ++j) {
cout << square[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
bool ungarded(int col) {
int i;
bool ok = true;
// 从列的往上是否有皇后
for(i = 0; ok && i < count; ++i) {
ok = !square[i][col];
}
// 从左斜线往上是否有皇后
for (i = 1; ok && count - i >= 0 && col - i >= 0; ++i) {
ok = !square[count-i][col-i];
}
// 从右斜线往上是否右皇后
for (i = 1; ok && count - i >= 0 && col + i <= size; ++i) {
ok = !square[count-i][col+i];
}
return ok;
}
void insert(int col) {
// 此格插入皇后 行增加
square[count++][col] = true;
}
void remove(int col) {
// 上次添加皇后后 无法完成棋盘 删除上个皇后
square[--count][col] = false;
}
int size;
private:
// count可以代表行数
int count;
bool square[MAX][MAX];
};
// 递归函数
void solve_from (Queens &test) {
// 终止条件
if (test.is_solve()) {
test.print();
} else{
// 从第一列开始 到最后一列
for (int col = 0; col < test.size; ++col) {
if (test.ungarded(col)) {
test.insert(col);
// 插入后 行增加 新行继续判断
solve_from(test);
// 如果从最后的插入导致 无法解决问题 即回溯 此时 消除上次插入的皇后
test.remove(col);
}
}
}
}
int main () {
int size;
cout << "请输入棋盘的大小" << endl;
cin >> size;
Queens test(size);
solve_from(test);
}