递归思维的算法是啥? (关于具体例子)

Posted

技术标签:

【中文标题】递归思维的算法是啥? (关于具体例子)【英文标题】:What is the algorithm of thinking recursive? (on the specific example)递归思维的算法是什么? (关于具体例子) 【发布时间】:2014-04-05 09:21:20 【问题描述】:

我只是无法理解递归。我了解所有概念(将解决方案分解为较小的案例),并且在我一遍又一遍地阅读它们之后我可以理解解决方案。但我永远无法弄清楚如何使用递归来解决问题。有没有系统的方法来提出递归解决方案?

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

这是我解决这个问题的思考过程的一个例子。

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

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

编辑:这是我在编写此问题的另一种解决方案时的思考过程。

基本情况是字符串长度 = 0 如果不是基本情况,则对于字符串的每个第一个字母,将第一个字母插入字符串的每个排列的每个位置,而不是第一个字母 例如:字符串是“abc”,第一个字母是a,所以在“bc”排列的每个位置插入a。 [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);
        
     
   

【问题讨论】:

你快到了。但是想想看。如果您置换“abc”,您的算法将只返回“abc”和“acb”。问题是您仅将“a”放在其他排列的开头。您需要将其放置在每个可能的位置(在这种情况下有 3 个可能的位置) 【参考方案1】:

您可以尝试考虑如何通过解决更简单问题的方法来解决问题。如果您已经解决了大小 i-1 的问题,您将如何解决大小 i 的问题,或者如果步骤 i-1 和之前的所有步骤都已解决,您将如何解决步骤 i 的问题。

递归是通过归纳思考 [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))

现在将 "ab", "ba" 替换为 i-1 字符串的递归调用,您就拥有了递归函数。

如果这还不够清楚,请随时提问。

【讨论】:

这是一种用递归生成排列的特别低效的方法,但我想它传达了这一点。这是低效的,因为s.insert("c", i) 将是一个 O(n) 操作(至少具有类似数组的结构,尽管 O(log n) 也是可能的),如果你只是在 O(1) 中完成通过交换从左到右生成字符。 我刚刚在编辑中意识到,我尝试使用伪代码实现您的解决方案。我的实现是否正确? @Dukeling 使用交换的好处。但解决方案仍然是 O(n),因为您需要复制字符串。但是如果你只想打印一个很好的优化字符串。 @QQQ 使用此解决方案,您需要在每个递归步骤中复制字符串。如果你交换,使用字符数组或可变字符串,你只需要在最底层的递归步骤复制它。我最初认为这会大大增加渐近复杂性,但现在我不再那么确定了,因为无论如何你都会在下一个递归步骤中做 O(n) 工作 - 但它肯定会是 较慢.【参考方案2】:

如果您熟悉递归,我认为递归是更直观的解决方法。简单的规则是将您的功能想象为相同功能与较小输入的组合。在某些情况下,递归比其他情况更明显。例如排列就是这样一种情况。想象一下permut(S) = Lista+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;

 

根据我的说法,上面的代码对于置换来说是最容易理解的,因为它直接转换了递归关系,我们从字符串中选择一个字符,然后将其连接到较小字符串的所有其他组合。

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

【讨论】:

【参考方案3】:

思考过程:

给定:一个字符串。

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

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

解析:字符串可以拆分成一个head元素(字符)和rest元素,如果不为空。因此,if 我们知道如何找到 rest 的排列,我们可以找到 whole 的排列,if we' d 找到了一种将 headpermutations-of-rest 结合起来的方法。

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

组合:对于permutations-of-rest(这是一个列表)中的每个permutation,插入head 插入到 permutation 元素之间的每个位置以及它的两端,除非 permutation 为空。在这种情况下,具有一个元素的字符串 head 是唯一的结果排列。

归纳步骤:假设我们已经知道如何置换rest

完成。


这种事情被称为“结构递归”(参见this answer)或“折叠”或“变态”:我们拆开一个输入,并将递归应用我们的变换的结果组合在这些部分上,得到组合结果。

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) ]

原理很简单:将其解构为部分,递归地做部分,组合结果。诀窍当然是在每一步都保持它“真实”:不违反某些法律,不破坏某些不变性。 IOW 较小的问题必须与较大的问题“相似”:必须适用相同的法律,必须适用相同的“理论”(即“我们可以正确地说出什么”)。

通常,我们应该以最直接和最简单的方式进行解构。在我们的示例中,我们可以尝试将字符串分成两半——但这样的组合步骤将是非常重要的。

结构递归特别容易:我们给定一个结构开始,它通常被定义为从其组成部分构建,开始。 :) 你只需要学会放下必要的挂断,比如对自己说“我怎么可能处理子部分,而我还没有完成事情本身呢? .".

心理技巧是想象自己的副本为与整个问题相似的每个子部分做相同的工作,完全遵循相同的组规则、食谱和法律。这实际上是在计算机中进行递归调用的方式:对同一过程进行单独调用——copy——但在一个全新的环境框架中(在“堆栈”上) )。然后,当每个副本完成后,它会将其结果返回给它的调用者,后者将这些结果组合成它的个结果。

(啊,还有reading SICP 帮忙!:))

Recursion is a tool which is there to help us, to make programming easier.

【讨论】:

我已经更新了我的答案以尝试实施您的最后评论 一般结构没问题,但是你有错误(第二个循环必须检查 l[i].size()l = permutation( ..., l) 可能不应该在两边都有 l ,诸如此类)。您现在可以尝试编写一些真正的代码并对其进行调试。 :) 不要忘记接受任何最有帮助的答案,如果有的话。 :) 谢谢。我只有最后一个问题。您是否能够通过大量练习提出递归解决方案,或者您通常只是不费吹灰之力就理解了这种类型的思维。似乎很多人只了解递归。我现在能够理解这个特定的问题,但我认为我离解决这个难度的任何其他递归问题还差得远。也许这正是我的大脑达到极限的地方。【参考方案4】:

思考递归

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

所以,请看递归沙拉的定义:

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

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

您必须定义基础 - 当工作已经完成并且我们必须结束时会发生什么。编写这部分代码。 想象一下流程应该如何从几乎完成的任务到完成的任务,我们如何完成最后一步。帮助自己编写the last step 的代码。 如果您还不能抽象到 last-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;

所以,最后我们让程序在启动时也能正常工作。就是这样。

【讨论】:

喜欢“笑话”。 :) 很棒。

以上是关于递归思维的算法是啥? (关于具体例子)的主要内容,如果未能解决你的问题,请参考以下文章

算法漫游指北(第十篇):泛型递归递归代码模板递归思维要点分治算法回溯算法

递归算法总结

递归算法

设计生成所有 n 位数字组合的递归函数的最佳方法是啥?

讲一下c语言中递归函数的使用方法有哪些?

递归思想之---阶乘算法