递归算法求解遍历(或穷举)问题

递归问题可以理解为遍历问题,必须遍历出所有的数据来,才能进行相应的运算,比如Fibonacci问题、阶乘问题,必须把每一步的值都遍历出来,然后才能做加法或乘法。

递归算法解决问题的特点:

(1) 必须有一个明确的递归结束条件,称为递归出口。

(2) 根据当前状态的值推断下一个状态值的个数n与条件,本次递归调用将根据条件调用n个自身(根据条件,遍历不同分支,如二叉树中前序、中序和后序遍历)。

递归算法优缺点:借助递归方法,我们可以把一个相对复杂的问题转化为一个与原问题相似的规模较小的问题来求解,递归方法只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。但在带来便捷的同时,也会有一些缺点,也即:通常用递归方法的运行效率不高。

使用递归算法处理的几类问题

1.Fibonacci问题

讲到递归,我们最先接触到的一个实例便是斐波那契数列。

斐波那契数列指的是这样一个数列 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

特别指出:第0项是0,第1项是第一个1。

这个数列从第二项开始,每一项都等于前两项之和。

斐波那契数列递归法实现:

int Fib(int n)

{

if(n<1)         /*结束条件*/

{

return -1;

}

if(n == 1|| n == 2)  /*结束条件*/

{

return 1;

}

return Fib(n-1)+Fib(n-2);  //很庆幸,Fibonacci有明显的递归方程

}

2.二叉树遍历问题

二叉树的遍历分为三种:前(先)序、中序、后序遍历。

设L、D、R分别表示二叉树的左子树、根结点和遍历右子树,则先(根)序遍历二叉树的顺序是DLR,中(根)序遍历二叉树的顺序是LDR,后(根)序遍历二叉树的顺

序是LRD。还有按层遍历二叉树。这些方法的时间复杂度都是O(n),n为结点个数。

假设我们有一个包含值的value和指向两个子结点的left和right的树结点结构。我们可以写出这样的过程:

先序遍历(递归实现):

visit(node)

{

print node.value    /*当前状态有两个可能分支*/

if node.left  != null then visit(node.left)

if node.right != null then visit(node.right)

return

}

中序遍历(递归实现):

visit(node)

{

if node.left  != null then visit(node.left)

print node.value

if node.right != null then visit(node.right)

return

}

后序遍历(递归实现):

visit(node)

{

if node.left  != null then visit(node.left)

if node.right != null then visit(node.right)

print node.value

return

}

3.字符串全排序或条件全遍历问题

问题:写一个函数返回一个串的所有排列。

解析:对于一个长度为n的串,它的全排列共有A(n, n)=n!种。这个问题也是一个递归的问题, 不过我们可以用不同的思路去理解它。为了方便讲解,假设我们要考察的串是”abc”, 递归函数名叫permu。

思路一:我们可以把串“abc”中的第0个字符a取出来,然后递归调用permu计算剩余的串“bc” 的排列,得到{bc, cb}。然后再将字符a插入这两个串中的任何一个空位(插空法), 得到最终所有的排列。比如,a插入串bc的所有(3个)空位,得到{abc,bac,bca}。 递归的终止条件是什么呢?当一个串为空,就无法再取出其中的第0个字符了, 所以此时返回一个空的排列。代码如下:

typedef vector<string> vs;

vs permu(string s){              /*返回当前次排序的结果VS*/

vs result;

if(s == ""){                    /*退出条件*/

result.push_back("");

return result;

}

string c = s.substr(0, 1);

vs res = permu(s.substr(1));     /*与上一次排序结果的关系*/

for(int i=0; i<res.size(); ++i){

string t = res[i];

for(int j=0; j<=t.length(); ++j){

string u = t;

u.insert(j, c);

result.push_back(u);

}

}

return result; //调用result的拷贝构造函数,返回它的一份copy,然后这个局部变量销毁(与基本类型一样)

}

思路二:我们还可以用另一种思路来递归解这个问题。还是针对串“abc”, 我依次取出这个串中的每个字符,然后调用permu去计算剩余串的排列。 然后只需要把取出的字符加到剩余串排列的每个字符前即可。对于这个例子, 程序先取出a,然后计算剩余串的排列得到{bc,cb},然后把a加到它们的前面,得到 {abc,acb};接着取出b,计算剩余串的排列得到{ac,ca},然后把b加到它们前面, 得到{bac,bca};后面的同理。最后就可以得到“abc”的全序列。

vs permu1(string s){

vs result;

if(s == ""){

result.push_back("");

return result;

}

for(int i=0; i<s.length(); ++i){

string c = s.substr(i, 1);

string t = s;

vs res = permu1(t.erase(i, 1));

for(int j=0; j<res.size(); ++j){

result.push_back(c + res[j]);

}

}

return result;

}

4.汉罗塔问题

汉诺塔是根据一个传说形成的数学问题:有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:每次只能移动一个圆盘;大盘不能叠在小盘上面。提示:可将圆盘临时置于B杆,也可将从A杆移出的圆盘重新移回A杆,但都必须遵循上述两条规则。

问:如何移?最少要移动多少次?

#include <iostream>

#include <cstdio>

using namespace std;

void hannoi (int n, char from, char buffer, char to)

{    if (n == 1)

{        cout << "Move disk " << n << " from " << from << " to " << to << endl;

}

else

{

hannoi (n-1, from, to, buffer);

cout << "Move disk " << n << " from " << from << " to " << to << endl;

hannoi (n-1, buffer, from, to);    }}

int main(){

int n;    cin >> n;

hannoi (n, ‘A‘, ‘B‘, ‘C‘);

return 0;

}

5.八皇后问题

问题:

经典的八皇后问题,即在一个8*8的棋盘上放8个皇后,使得这8个皇后无法互相攻击( 任意2个皇后不能处于同一行,同一列或是对角线上),输出所有可能的摆放情况。

解析:

8皇后是个经典的问题,如果使用暴力法,每个格子都去考虑放皇后与否,一共有264 种可能。所以暴力法并不是个好办法。由于皇后们是不能放在同一行的, 所以我们可以去掉“行”这个因素,即我第1次考虑把皇后放在第1行的某个位置, 第2次放的时候就不用去放在第一行了,因为这样放皇后间是可以互相攻击的。 第2次我就考虑把皇后放在第2行的某个位置,第3次我考虑把皇后放在第3行的某个位置, 这样依次去递归。每计算1行,递归一次,每次递归里面考虑8列, 即对每一行皇后有8个可能的位置可以放。找到一个与前面行的皇后都不会互相攻击的位置, 然后再递归进入下一行。找到一组可行解即可输出,然后程序回溯去找下一组可靠解。

我们用一个一维数组来表示相应行对应的列,比如c[i]=j表示, 第i行的皇后放在第j列。如果当前行是r,皇后放在哪一列呢?c[r]列。 一共有8列,所以我们要让c[r]依次取第0列,第1列,第2列……一直到第7列, 每取一次我们就去考虑,皇后放的位置会不会和前面已经放了的皇后有冲突。 怎样是有冲突呢?同行,同列,对角线。由于已经不会同行了,所以不用考虑这一点。 同列:c[r]==c[j]; 同对角线有两种可能,即主对角线方向和副对角线方向。 主对角线方向满足,行之差等于列之差:r-j==c[r]-c[j]; 副对角线方向满足, 行之差等于列之差的相反数:r-j==c[j]-c[r]。 只有满足了当前皇后和前面所有的皇后都不会互相攻击的时候,才能进入下一级递归。

#include <iostream>

using namespace std;

int c[20], n=8, cnt=0;

void print()

{

for(int i=0; i<n; ++i)

{

for(int j=0; j<n; ++j)

{

if(j == c[i])

cout<<"1 ";

else cout<<"0 ";

}

cout<<endl;

}

cout<<endl;

}

void search(int r)

{

if(r == n)

{

print();

++cnt;

return;

}

for(int i=0; i<n; ++i)

{

c[r] = i;

int ok = 1;

for(int j=0; j<r; ++j)

if(c[r]==c[j] || r-j==c[r]-c[j] || r-j==c[j]-c[r])

{

ok = 0;

break;

}

if(ok) search(r+1);

}

}

int main()

{

search(0);

cout<<cnt<<endl;

return 0;

}

在实际应用中,碰到最多的是第1,2,3类遍历问题,如目录下所有文件的遍历类似于二叉树问题,阶乘问题类似于Fibonacci问题。

下面是一个条件全遍历的例子:

给定一个n,写出n对‘()’的全部正确组合。

例如,给定n=3,输出结果为:

"((()))", "(()())", "(())()", "()(())", "()()()"

采用递归树的思想,遍历条件:

当左括号数大于右括号数时可以加左或者右括号;否则只能加左括号;当左括号数达到n时,剩下全部加右括号

在该方法中采用的正向遍历。

class Solution

{

public:

vector<string> generateParenthesis(int n)

{

vector<string> res;

generate(res, "", 0, 0, n);

return res;

}

void generate(vector<string> res, string tmp, int lhs, int rhs, int n)  /*当前状态*/

{

if(lhs == n)

{

for(int i = 0; i < n - rhs; i++)

{

tmp += ")";

}

res.add(tmp);

return ;

}

if(lhs > rhs)                    /*遍历分支:经典之处*/

{

/*先遍历当前状态后,下一个状态是’(’的所有状态*/

generate(res, tmp + "(", lhs + 1, rhs, n); /*下一个可能状态1*/

/*然后遍历当前状态后,下一个状态是’)’的所有状态*/

generate(res, tmp + ")", lhs, rhs + 1, n); /*下一个可能状态2*/

}

else

{

generate(res, tmp + "(", lhs + 1, rhs, n); /*下一个可能状态3*/

}

}

}

当前状态能推断下一个状态,或由下一个状态能推断当前状态的情况,都适合递归。与状态有关的值需要当做入参或返回值传递。

时间: 2024-10-06 01:20:27

递归算法求解遍历(或穷举)问题的相关文章

穷举递归和回溯算法终结篇

穷举递归和回溯算法 在一般的递归函数中,如二分查找.反转文件等,在每个决策点只需要调用一个递归(比如在二分查找,在每个节点我们只需要选择递归左子树或者右子树),在这样的递归调用中,递归调用形成了一个线性结构,而算法的性能取决于调用函数的栈深度.比如对于反转文件,调用栈的深度等于文件的大小:再比如二分查找,递归深度为O(nlogn),这两类递归调用都非常高效. 现在考虑子集问题或者全排列问题,在每一个决策点我们不在只是选择一个分支进行递归调用,而是要尝试所有的分支进行递归调用.在每一个决策点有多种

1-5、算法设计常用思想之穷举法

穷举法又称穷举搜索法,是一种在问题域的解空间中对所有可能的解穷举搜索,并根据条件选择最优解的方法的总称.数学上也把穷举法称为枚举法,就是在一个由有限个元素构成的集合中,把所有元素一一枚举研究的方法. 使用穷举法解决问题,基本上就是以下两个步骤: • 确定问题的解(或状态)的定义.解空间的范围以及正确解的判定条件: • 根据解空间的特点来选择搜索策略,逐个检验解空间中的候选解是否正确: 解空间的定义解空间就是全部可能的候选解的一个约束范围,确定问题的解就在这个约束范围内,将搜索策略应用到这个约束范

2016年10月10日--穷举、迭代、while循环

穷举 将所有可能性全部全部走一遍,使用IF筛选出满足的情况 练习: 1.单位给发了一张150元购物卡, 拿着到超市买三类洗化用品. 洗发水15元,香皂2元,牙刷5元. 求刚好花完150元,有多少种买法, 没种买法都是各买几样? int i = 0; int j = 0; for (int x = 0; x <= 10; x++) { for (int y = 0; y <= 30; y++) { for (int z = 0; z <= 75; z++) { j++; if (x * 1

for 穷举 迭代

for循环拥有两类:穷举:把所有可能的情况都走一遍,使用if条件筛选出来满足条件的情况. 迭代:从初始情况按照规律不断求解中间情况,最终推导出结果. 案例: //单位给发了一张150元购物卡, //拿着到超市买三类洗化用品. //洗发水15元,香皂2元,牙刷5元. //求刚好花完150元,有多少种买法, //每种买法都是各买几样? //设洗发水x 150/15==10 //牙刷y 150/5==30 //香皂z 150/2==75 int sum = 0; int biao = 0; for (

什么叫穷举法?

穷举法的基本思想是根据题目的部分条件确定答案的大致范围,并在此范围内对所有可能的情况逐一验证,直到全部情况验证完毕.若某个情况验证符合题目的全部条件,则为本问题的一个解:若全部情况验证后都不符合题目的全部条件,则本题无解.穷举法也称为枚举法. 用穷举法解题时,就是按照某种方式列举问题答案的过程.针对问题的数据类型而言,常用的列举方法一有如下三种: (1)顺序列举 是指答案范围内的各种情况很容易与自然数对应甚至就是自然数,可以按自然数的变化顺序去列举. (2)排列列举 有时答案的数据形式是一组数的

基本算法之穷举算法

穷举算法的思想:从所有的可能的情况搜索正确的答案,其中执行的步骤: 对于一种可能的情况,计算其中的结果. 如果判断的结果的不符合要求就执行第一步来搜索下一个可能的情况 package Main; import java.util.Scanner; public class demo2 { /** * 穷举算法求解鸡兔同笼 * @param args */ static int chichen,habbit; public static void main(String[] args) { int

for循坏的穷举与迭代,while、do while循环

for循环 穷举:所有情况走一遍,使用if筛选出符合的情况. 1.单位给发了一张150元购物卡,拿着到超市买三类洗化用品.洗发水15元,香皂2元,牙刷5元.求刚好花完150元,有多少种买法,没种买法都是各买几样? int d=0; int e = 0; for (int a = 1; a <= 10;a++ ) { for (int b = 1; b <= 30;b++ ) { for (int c = 1; c <= 75;c++ ) { if(a*15+b*5+c*2==150) {

while do while以及穷举和迭代

今天的新内容1:while循环 格式: while() { } 初始状态要在循环外提前规定 状态改变要写在花括号里面 括号内是循环条件 for循环与while循环的对比: 2:do while 不管循环条件是否满足 先执行一遍循环体 格式为: do { }while() 如上图 条件不满足 但仍然输出了一遍WORLD 3:穷举 经典题目百鸡百钱:公鸡2文钱一只,母鸡1文钱一只,小鸡半文钱一只,总共只有100文钱,如何在凑够100只鸡的情况下刚好花完100文钱? 使用while循环实现: 4:迭代

穷举和迭代

通过循环可以解决两类问题:穷举:在不知道什么情况下才真的是我们需要的结果的时候,只能够让它一个一个的情况都给走一遍举例:公司给发了150元的购物卡,刚好想去超市购买洗发水(15元).牙刷(5元).香皂(2).只买着三个种类的商品,要求全部花完150元,有多少种买法,每种买法都是各买几样?<br /> <script>var zong =0;var sum=0;for(var x= 0;x<=10;x++){ for(var y =0;y<=30;y++) { for(v