【问题标题】:Subset (PowerSet) of an array not producing the correct output未产生正确输出的数组的子集 (PowerSet)
【发布时间】:2021-07-30 16:00:14
【问题描述】:

我正在解决一个问题,我必须找到一个数组的所有可能的子集 (powerSet)。我为此使用递归回溯。我正在使用解决方案来了解递归调用是如何连接的。我无法理解其中的某些部分。例如,这是我要从中创建子集的数组。

var arr = ['apple', 'banana', 'orange'];

我正在使用 Javascript。算法是:在每个递归步骤中,我要么包含当前元素,要么不包含。

解决方法代码如下:


// Code that works
var makeSubset = function (arr) {

  var output = []; // Array for storing all the subsets

  var subset = function(arr, soFar, idx) { // Helper function

    if (idx >= arr.length) {
      output.push([...soFar]);
      return;
    }
  
     soFar.push(arr[idx]);
     subset(arr, soFar, idx + 1); 
     soFar.pop(); // But this works
     subset(arr, soFar, idx + 1);


  };

  subset(arr, [], 0);
  return output;
}
console.log(makeSubset(['apple', 'banana', 'ornage']))
// code that does not work
var makeSubset1 = function (arr) {

  var output = []; // Array for storing all the subsets

  var subset = function(arr, soFar, idx) { // Helper function

    if (idx >= arr.length) {
      output.push([...soFar]);
      return;
    }
  
     soFar.push(arr[idx]);
     subset(arr, soFar, idx + 1); 
     soFar = soFar.slice(0, soFar.length - 1);
     subset(arr, soFar, idx + 1);


  };

  subset(arr, [], 0);
  return output;
}
console.log(makeSubset1(['apple', 'banana', 'ornage']))

现在,我稍微改变了解决方案,看看它是如何影响的。我没有将修改后的 soFar 数组作为参数传递,而是在外部进行更改。

soFar.push(arr[idx);

由于我更改了 soFar 数组,并且它是一个引用,因此在考虑不包括当前元素时,我撤消了更改。

soFar = soFar.slice(0, soFar.length - 1);

但是上面的行并没有像下面的行那样产生正确的输出。

soFar.pop();

我的问题是:他们不在这里做同样的事情吗?我知道这与参考有关,但我不明白问题出在哪里。此外,在使用引用类型数据结构(如递归回溯中的数组)时的任何一般建议。任何想法将不胜感激。 这是有效的代码 sn-p:

【问题讨论】:

  • 它们的工作原理相同,您是否在控制台中遇到错误?
  • 这是 soFar.slice() 代码 [ [ 'apple', 'banana', 'orange' ], [ 'apple', 'banana' ], [ 'apple', '香蕉','橙子'],['苹果','香蕉'],['苹果','香蕉','香蕉','橙子'],['苹果','香蕉','香蕉'], [“苹果”、“香蕉”、“香蕉”、“橙子”]、[“苹果”、“香蕉”、“香蕉”]]
  • 能否请您提供不起作用的代码的sn-p?现在看到代码混合在一起令人困惑,我们必须了解哪些部分在其中,哪些部分在外面,以便获得故障代码。只需提供两个单独的、可运行的 sn-ps,它们也使用示例输入调用函数,证明一个 sn-p 有效,另一个无效,只需我们运行它。
  • 嗨@trincot,我提供了一个包含功能代码和非功能代码的sn-p。请让我知道这是否有帮助。谢谢。

标签: javascript algorithm recursive-backtracking


【解决方案1】:

工作代码使用一个数组对象,在完整的递归练习期间,它会在其生命周期内扩展和收缩。

当需要将该数组的当前状态注册为输出时,会对其进行复制,但这里重要的是每个 soFar 变量(每个执行上下文中的局部变量)始终是 相同数组引用。

另外,由于每个push 都被对应的pop 镜像,所以makeSubset 函数的调用者可以确保soFar 数组处于相同 状态在调用之后,就像在调用之前一样。该不变量对于算法的正确运行至关重要。

在故障版本中,以下代码创建了一个数组:

 soFar = soFar.slice(0, soFar.length - 1);

这在下一次递归调用期间不是问题,但它对于调用者的问题,因为现在调用者将返回一个仍然有元素的数组(s) 由递归调用中的push 操作添加。原因是上述赋值并没有修改调用者作为参数传递的数组,而是创建了一个新数组并保留了原始数组,其中仍然包含推入的元素。

【讨论】:

  • 非常感谢您的回复。现在更有意义了。不过,我对你回复的最后一部分有点不清楚。我知道 caller 正在获取包含推送元素的旧数组,但是在对数组进行切片后,我再次将它分配给 soFar 引用,所以不应该引用元素被排除的新数组?
  • 不,赋值到一个变量从不改变调用者作为该局部变量的值传递的内容。这源于 JavaScript(和许多其他语言)使用的按值调用系统。通过分配给参数变量,您将自己与调用者传递的内容分离。进行调用者将看到的更改的唯一方法是 mutate 该参数变量...这就是 pushpop 所做的。
  • 您能否将答案标记为已接受?
【解决方案2】:

trincot 告诉你你的改变不起作用的原因。

我想指出,造成混淆的一个根本原因是我们的数据不断变化。如果您以不可变的方式编写此代码,您的代码可能会更容易理解,并且失败的可能性会大大降低。

这是我编写这样一个函数的一种方法:

const powerSet = (xs) =>
  xs.length == 0
    ? [[]]
    : powerSet (xs .slice (1)) .flatMap (s => [[xs [0], ...s], s])

    
console .log (powerSet (['a', 'b', 'c']))
.as-console-wrapper {max-height: 100% !important; top: 0}

(这会以与您的版本不同的顺序生成结果。如果这很重要,我们当然可以修复它以匹配。)

这里我们重复数组的长度。如果我们命中零,我们返回一个只包含空数组的数组。否则,我们在数组的尾部递归调用我们的函数,并且对于每个结果,我们返回两个值,一个包含头部值,一个不包含头部值。将这些与flatMap 结合起来,可以将其变成一个包含所有子集的平面数组。

这个函数有很多变体。我们可以像这样解构初始数组:

const powerSet = ([x, ...xs]) =>
  x == undefined
    ? [[]]
    : powerSet (xs) .flatMap (s => [[x, ...s], s])

或者我们可以使用一些可重复使用的 headtail 函数,如下所示:

const head = (xs) => xs [0]
const tail = (xs) => xs .slice (1)

const powerSet = (xs) =>
  xs.length == 0
    ? [[]]
    : powerSet (tail (xs)) .flatMap (s => [[head (xs), ...s], s])

无论我们如何编写它,我们都可以注意到在此过程中没有任何变量发生变异。在我看来,这是一个巨大的胜利。

【讨论】:

    【解决方案3】:

    我注意到错误实际上发生在 push() 方法中。为了简单起见,我已经删减了部分代码。这是使用push():

    var arr = ['apple', 'banana', 'orange'];
    
    var subset = function(soFar = [], idx = 0) {
      if (idx >= arr.length) {
        console.log(soFar);
        return;
      }
      
      //soFar = soFar.concat(arr[idx]);
      soFar.push(arr[idx]);
      
      subset(soFar, idx + 1); 
    
      soFar = soFar.slice(0, soFar.length - 1); 
      subset(soFar, idx + 1);
    };
    
    console.log(subset());

    您可以看到它没有给出您期望的结果。这是使用concat(),类似于push:

    var arr = ['apple', 'banana', 'orange'];
    
    var subset = function(soFar = [], idx = 0) {
      if (idx >= arr.length) {
        console.log(soFar);
        return;
      }
      
      soFar = soFar.concat(arr[idx]);
      //soFar.push(arr[idx]);
      
      subset(soFar, idx + 1); 
    
      soFar = soFar.slice(0, soFar.length - 1); 
      subset(soFar, idx + 1);
    };
    
    console.log(subset());

    实际上我看不出这两个脚本之间的区别,因为 push 和 concat 应该是一样的。似乎使用 push soFar 变量会丢失其上下文并获取最近调用函数的上下文,就像它是一个全局变量一样。所以会发生这种情况:

    
    soFar.push(arr[idx]);
     
    console.log(soFar);   // prints --> ['apple']    
    
    subset(soFar, idx + 1); 
    
    console.log(soFar);   // somehow prints --> ['apple', 'banana', 'orange'] after calling function
    

    我希望它能把问题弄清楚。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多