什么是递归思维算法? (就具体例子而言)

时间:2014-03-04 02:42:24

标签: algorithm recursion data-structures permutation tail-recursion

我无法绕过递归。我理解所有的概念(将解决方案分解为更小的案例),并且在我一遍又一遍地阅读之后我能理解解决方案。但我永远无法弄清楚如何使用递归来解决问题。是否有任何系统的方法来提出递归解决方案?

有人可以在尝试解决以下递归问题时向我解释他们的思考过程:“使用递归返回字符串的所有排列”

以下是我解决此问题的思维过程示例。

  • 检查字符串长度是否等于2.如果是,则交换值(基本情况)
  • 否则:对于每个第一个值,返回第一个值+递归(没有第一个值的字符串)

有人可以给我一些提示来改变我的思考过程或以更好的方式思考递归,这样我就可以解决递归问题而不需要查找答案。

编辑:以下是编写此问题的另一种解决方案的思考过程。

  • 基本情况是字符串长度= 0
  • 如果不是基本情况,那么对于字符串的每个第一个字母,将第一个字母插入字符串的每个排列的每个位置,而不是第一个字母
  • 例如:字符串是“abc”,第一个字母是a,所以在“bc”的排列的每个位置插入一个。 [bc,cb] => [abc,bac,bca,acb,cab,cba]

伪代码

permutation(String s, List l) {
   if(s.length() == 0) {
      return list;
   }
   else {
     String a = s.firstLetter();
     l = permutation(s.substring(1, s.length) , l)

     for(int i = 0; i < s.length(); i++) {            
        // loop that iterates through list of permutations 
        // that have been "solved"
        for(int j=0; j < l.size(); j++) {                 
           // loop that iterates through all positions of every 
           // permutation and inserts the first letter
           String permutation Stringbuilder(l[i]).insert(a, j);           
           // insert the first letter at every position in the 
           // single permutation l[i]
           l.add(permutation);
        }
     }
   }
}

4 个答案:

答案 0 :(得分:3)

思考递归

我认为,为了理解复杂的概念,你应该从一个笑话(但正确的)解释开始。

所以,采用Recursive Salad的定义:

Recursive Salad is made of apples, cucumbers and Recursive Salad.

至于分析,它类似于数学归纳法。

  • 你必须定义基础 - 当工作已经完成并且我们必须结束时会发生什么。写下这部分代码。
  • 想象一下这个过程应该从ALMOST完成的任务步骤到完成的任务,我们如何才能完成最后一步。帮助自己编写the last step的代码。
  • 如果你还不能抽象到最后一步N,请为last-1创建代码。比较,抽象它。
  • 最后做N步骤
  • 分析你刚开始时要做什么。

如何解决任务

“将解决方案打入较小的案件”远远不够。主要规则是:每个数学任务比2x2更复杂,应该从最后解决。不仅是递归的。如果您遵循该规则,数学将成为您的玩具。如果你不这样做,那么除了偶然之外,你总是会遇到严重的问题而无法解决任何任务。

设置任务的方式很糟糕。你必须解决任务,而不是通过任何具体的方法解决任务。从目标开始,而不是从给定的工具或数据开始。并逐步转移到数据,有时使用一些方便的方法。递归解决方案应该自然而然地呈现给您。或者它不应该,你会以其他方式做到这一点。

阅读G.Polya的“如何解决它”一书。如果您的数学/ IT老师没有建议,他应该被解雇。问题是,99%的人应该被解雇...... :-(。不要认为,互联网上的引用就足够了。阅读本书。这是the king's way into maths


如何思考递归 - 例子

(代码不是真正的Java)(代码不是有效的)

我们需要:包含所有不同字符的字符串的排列列表。

可以写成List

因此,该函数准备就绪后,将对该字符串进行置换并返回该列表

List<String> allPermutations(String source){}

要返回该列表,该函数必须将此列表作为局部变量。

List<String> allPermutations(String source){
  List<String> permutResult=new List<String>();
  return permutResult;
}

让我们想象一下,我们已经找到了几乎所有字符串的排列,但是它中的最后一个字符。

List<String> allPermutations(String source){
  List<String> permutResult=new List<String>();
  ...we have found permutations to all chars but the last
  We have to take that last char and put it into every possible place in every already found permutation.
  return permutResult;
}

但是已经找到的排列我们可以写成一个更短的字符串的函数!

List<String> allPermutations(String source){
  List<String> permutResult=new List<String>();
  permutFound=allPermutations(substr(source,source.length-1));
  for (String permutation: permutFound){
    for (int i=0;i<=permutation.length;i++){
      String newPermutation=permutation.insert(source[last],i);
      permutResult.add(newPermutation);
    }
  }
  return permutResult;
}

很高兴,我们不需要计算和使用源字符串的当前长度 - 我们一直在使用最后一个字符...但是从一开始呢?我们不能在空源中使用我们的函数。但我们可以改变它,以便我们能够使用它!首先,我们需要一个带空字符串的排列。让我们回来吧。

List<String> allPermutations(String source){
  List<String> permutResult=new List<String>();
  if (source.length==0){
    permutResult.add("");
  }     
  permutFound=allPermutations(substr(source,source.length-1));
  for (String permutation: permutFound){
    for (int i=0;i<=permutation.length;i++){
      String newPermutation=permutation.insert(source[last],i);
      permutResult.add(newPermutation);
    }
  }
  return permutResult;
}

所以,最后我们也让程序在开始时工作。就是这样。

答案 1 :(得分:1)

您可以尝试考虑如何解决问题,解决更简单的问题。如果你已经解决了尺寸i-1的问题,你将如何解决尺寸i的问题,或者如果步骤i-1和所有前面的步骤已经解决,你将如何在步骤i解决问题。 / p>

递归正在通过归纳[1]进行思考。

在排列的情况下,你的基本情况是好的,(它也可以是一个空字符串或带有1个元素的字符串,该字符串的排列是相同的字符串)。

但是你的归纳步骤失败了,试着想一想如果你的字符串长度为i,并且你已经有一组长度为(i-1)的所有字符串排列,你将如何创建字符串的所有排列通过拥有额外的第i个角色?

现在在小案例中思考是有帮助的,例如2个元素:{“ab”,“ba”} 如果给你第三个元素“c”怎么办,你如何使用上面的元素和“ab”的解决方案创建字符串“abc”的排列?

答案是:{“cab”,“acb”,“abc”,“cba”,“bca”,“bac”}

注意“c”的位置,它会插入上一个解决方案中每个字符串的每个位置。那是(伪代码):

res = {}
for s in {"ab", "ba"}:
  for i = 0 to len(s):
    res.add(s.insert("c", i))

现在用一个带有i-1字符串的递归调用替换{“ab”,“ba”}并且你有递归函数。

随意询问这是否不够明确。

答案 2 :(得分:1)

思考过程:

鉴于 :字符串。

目标: 构建包含所有排列的列表。

涉及的类型 :字符串的排列是字符串的列表(集合),其中每个字符串都是输入字符串的一些排列。 String是字符的列表(序列)。

分析 :字符串可以拆分为 head 元素(字符)和元素的 rest ,如果不是空的Ergo,如果我们知道如何找到 rest 的排列,我们可以找到整个的排列,如果我们找到了一种方法来组合 排列静止

基本案例 :包含空字符串的所有排列的列表是一个空字符串的列表。

组合 :对于排列的静止中的每个排列(这是一个列表),插入除非置换为空,否则置于置换的元素之间以及两端的每个位置。在这种情况下,带有一个元素 head 的字符串是唯一产生的排列。

归纳步骤 :假设我们已经知道如何置换其余内容。

完成。


这种事物被称为“结构递归”(参见this answer)或“折叠”或“catamorphism”:我们拆分输入,并结合递归应用我们的转换结果零件,以获得综合结果。

string_permutations []     = [[]]
string_permutations (x:xs) = 
       for each p in string_permutations(xs):
          for each q in insert_everywhere(x,p): 
             yield q

insert_everywhere(x,abc)必须导致[ [xabc], [axbc], [abxc], [abcx]]insert_everywhere(x,[])必须导致[ [x] ]

yield表示“投入到最终的整体收藏中”。


在具有列表推导的语言中,上述内容可以写成

string_permutations []     = [[]]
string_permutations (x:xs) = [q | p <- string_permutations(xs)
                                , q <- insert_everywhere(x,p)]

原理很简单:将其解构为部分,递归地执行部分,合并结果。当然,诀窍是在每一步都保持“true”:不违反关于它的某些法律,不要打破它的一些不变量。如果较小的问题必须“相似”更大的问题:同样的法律必须适用,相同的“理论”(即“我们可以正确地说出它”)。

通常,我们应该以最直接和最简单的方式进行解构。在我们的例子中,我们可以尝试将字符串分成两半 - 但是然后组合步骤将是非平凡的。

结构递归特别容易:我们给出了一个开始的结构,通常定义为从其组成部分构建,开头。 :)你只需要学会放弃势在必行的挂机,就像对自己说“我怎么能处理子部件,而我还没有完成这件事呢?”。

精神伎俩是想象自己的副本为每个子部分做同样的工作,遵循完全相同的规则,配方和法律。这实际上是如何在计算机中完成递归调用:单独调用 - 复制 - 执行相同的过程,但是在一个全新的环境框架中(在“堆栈”上)。然后,当每个副本完成后,它会将结果返回给调用者,调用者将这些结果组合成 结果。

(啊,reading SICP有帮助!:))

答案 3 :(得分:0)

如果您熟悉它,我认为递归是更直观的解决方法。简单的规则是将您的功能想象为具有较小输入的相同功能的组合。在某些情况下,递归比其他递归更明显。例如,排列是一种这样的情况。想象一下permut(S) = List{a+permut(S-{a})} for all a in S,其中S由唯一的字符组成。想法是在字符串中选择一个字符并将其与剩余字符的所有排列连接起来,这将给出从该字符开始的字符串的所有唯一排列。

示例伪代码: -

Permutation(S,List) {

    if(S.length>0) {

        for all a in S {

            Permutation(S.remove(a),List.add(a));
        }

    }

    else  print List;

 }

上面的代码根据我最​​容易理解的排列,因为它直接翻译了重复关系,我们从字符串中选择一个字符,然后将所有其他较小字符串的组合连接起来。

注意: - 使用数组和交换可以更有效地完成此操作,但理解起来会更复杂。