【译文】在 Javascript 中查找字符串的所有排列

361 阅读4分钟

原文地址Find All Permutations of a String in Javascript

GitHub 存储库,包含完整的解决方案代码和测试套件。

给定一个字符串,返回该字符串的所有排列。

当我坐下来解决这个问题时,我发现这是一个很大的算法挑战。为什么?虽然操作字符串的任务表面上看起来很熟悉,但实际上找到一个完整的解决方案需要我们处理一些意想不到的复杂性,这提供了一个机会让我们使用递归树并熟悉主定理

注意 :解决此问题的方法不止一种。我在这里探索的解决方案模型利用了我发现的对解决算法挑战具有广泛价值的工具和概念,以及在 Javascript 中进行字符串操作的直观方法。

首先要做的事情是:什么是排列

排列

/ˌpərmyo͝oˈtāSH(ə)n/

学习发音

名词

一种方式,尤其是几种可能的变体之一,其中可以排列或组合一组或许多事物。

所以每个字符串都有许多排列,即字符串中的字符可以重新被排列。字符串排列类似于猜字谜。然而,它不必是现有的单词,而可以只是对字符的重新排列。

一个非字符串的模型排列示例如下:

image.png

对于三种颜色,我们可以有六种不同的排列,或者说是这些颜色的有序组合。

排列的另一个例子是组合锁:

image.png

呃哦。密码锁的全部意义在于,相对少量的数字可以创建足够多的有序组合,以防止锁被随意打开。

突然之间,整个字符串操作问题似乎变得更加吓人了。

所以我们已经弄清楚什么是排列,并确定(取决于字符串的长度)我们可能正在寻找很多排列。然而从什么地方开始呢?

当我看到这样的挑战时,我的第一直觉是做两件事:

1:制作一个空数组。如果我的最终解决方案可能返回多个“正确”元素(在本例中为排列),则在返回完整解决方案之前我需要一个地方来存储它们。

2:迭代!如果我需要查找字符串中所有有序的字符组合,创建一个循环来遍历字符串中的所有字符似乎是一个不错的起点。

let findPermutations = (string) => {

  let permutationsArray = [] 
  
  for (let i = 0; i < string.length; i++){
    // 做点什么
  }
  return permutationsArray
}

在我们直接进入迭代之前,让我们先解决一些问题。

如果用户输入空字符串或整数,或者尝试在不输入任何内容的情况下运行该函数,会怎样?如果没有字符串,我们就无法得到字符串的所有排列。

let findPermutations = (string) => {
  
   if (!string || typeof string !== "string"){
    return "请输入一个字符串"
  }

  let permutationsArray = [] 
  
  for (let i = 0; i < string.length; i++){
    // 做点什么
  }
  return permutationsArray
}

如果输入到函数中的参数是假值-falsey,或者如果它不是字符串,新的代码行将返回一条错误消息。

好,太棒了!

但是如果字符串真的很短怎么办?比如只有一个字符?在这种情况下,我们真的不需要搞砸整个迭代并将内容推入数组位。例如,如果我们的字符串只是“a”,它只有一个排列 —— “a”。我们可以只返回“a”。

let findPermutations = (string) => {
  
   if (!string || typeof string !== "string"){
    return "Please enter a string"
  } else if (string.length < 2 ){
    return string
  }

  let permutationsArray = [] 
  
  for (let i = 0; i < string.length; i++){
    // do something
  }
  return permutationsArray
}

好了,现在这些杂活全部搞定了,我们可以回到我们的迭代循环。

我们当前状态下的函数结构现在看起来有点类似于主定理。

主定理

什么是主定理

它是将潜在的复杂挑战分解为一系列较小问题的一组步骤。许多问题或技术挑战都属于分而治之算法的范畴,此算法需要潜在的求解者将一段数据分解成更小的部分,直到这些部分足够简单,可以直接解决。

用伪代码写出来,它看起来像这样:

**procedure** p( input *x* of size *n* ):  
    **if** *n* < 某个常量 *k*:  
        直接求解 *x* 无需递归  
    **else**:  
        创建 *x* 的子问题 *a*,每个子问题的大小为 *n/*b*
        在每个子问题上递归调用过程 p
        组合子问题的结果

这里发生了一些重要的事情:

1:条件检查输入的大小是否小于常量。

2:如果输入大于所述常数,输入被分解成更小的部分,直到它们都小到足以直接运行过程p

3:最后,所有递归调用过程p的结果可以合并作为一个大数据位返回。

这种分解问题的方法通常被可视化为一棵树(特别是因为这通常有助于确定问题的时间复杂度。您可以在此处阅读有关时间复杂度和主要方法的更多信息)。

image.png

想阅读更多关于递归树和主定理的信息吗?我喜欢康奈尔大学的这篇大纲

请注意此结构与下图何其相似,该图展示了我们寻找字符串的所有排列的具体挑战:

image.png

虽然我们当前的函数与我们的主定理的抽象伪代码不完全相同,但我们已经建立了一个解决方案的逻辑路径:如果我们的输入小于常数(在我们的例子中,如果string.length小于2 )则返回,如果没有,则创建一个要解决的子问题列表。

如果您以前展开平铺过嵌套数组,您可能会觉得这种方法很熟悉。它可以是应对各种挑战的良好起点 —— 它不会是解决所有问题的相关方法,但提供了一个不错的起点。

注意:这种方法确实利用了递归

image.png

您可以在此处此处javascript 中的代码示例)、此处javascript中的代码示例)、此处ruby中的代码示例)和此处python中的代码示例)阅读有关递归 的更多信息。

好的,回到我们的代码。

现在,如果我们想利用主定理方法,我们可以将我们的计划更新为比// 做点什么更清晰的内容。

let findPermutations = (string) => {
  
   if (!string || typeof string !== "string"){
    return "请输入字符串"
  } else if (string.length < 2 ){
    return string
  }

  let permutationsArray = [] 
  
  for (let i = 0; i < string.length; i++){
    // 创建一个字符串的子问题,每个子问题的大小为 n/b
    // 在每个子问题上递归调用过程 p
    // 组合子问题的结果
  }
  return permutationsArray
}

为方便起见,我想将我们正在迭代的当前元素分配给变量char

所以我们应该做的第一件事就是把我们的字符串分解成子问题。

首先,我们有我们当前的字符,又名string[i] ,又名char。要开始分解字符串的其余部分,我们需要拿到剩余的字符。

let findPermutations = (string) => {
  
   if (!string || typeof string !== "string"){
    return "请输入字符串"
  } else if (string.length < 2 ){
    return string
  }

  let permutationsArray = [] 
  
  for (let i = 0; i < string.length; i++){
    let char = string[i]
    let remainingChars = string.slice(0, i) + string.slice(i + 1, string.length)
    
    // 在每个子问题上递归调用过程 p
    // 组合子问题的结果
  }
  return permutationsArray
}

正如我们将当前字符分配给变量char一样,让我们将剩余字符分配给变量remainingChars

注意获取remainingChars的方法有很多种。这只是一种方法。

要收集这些字符,我们可以使用字符串方法sliceSubstring是一个类似的方法,所以如果你更熟悉它,你可以改用它。Slice是非破坏性的,所以我们不需要担心改变我们的原始字符串——我们通过切片我们的字符串得到的结果将是一个的新字符串。

因此,我们将从索引0(字符串中的第一个字符)到索引i(我们的当前字符char)的字符进行切片。然后,我们将索引i + 1char之后的下一个字符)中的字符连接到索引string.length ( string中的最后一个字符)。

所以现在我们有两个较小的字符串 — charremainingChars

现在怎么办?

好吧,让我们参考一下主定理:

在每个子问题上递归调用过程p

因此,我们将对remainingChars字符串调用findPermutations函数。

然后呢?

合并子问题的结果

我知道我们需要那个空数组。

好的,那么这在 JavaScript 中是什么样子的呢?

let findPermutations = (string) => {
  if (!string || typeof string !== "string"){
    return "请输入字符串"
  } else if (string.length < 2 ){
    return string
  }

  let permutationsArray = [] 
   
  for (let i = 0; i < string.length; i++){
    let char = string[i]

    let remainingChars = string.slice(0, i) + string.slice(i + 1, string.length)

    for (let permutation of findPermutations(remainingChars)){
      permutationsArray.push(char + permutation) }
  }
  return permutationsArray
}

所以我们在这里做了一些事情。

我们在remainingChars上递归调用findPermutations。该函数的每个结果,我分配给了一个名为permutation的变量(它是charpermutation的组合),将此变量push到我们的permutationsArray数组中。

因此,让我们看看返回permutationsArray时会得到什么。

findPermutations("abc")

(6) ["abc", "acb", "bac", "bca", "cab", "cba"]

好,太棒了!当给定输入 “abc” 时,我们的findPermutations函数返回所有六个排列!

不过,让我再尝试一件事。

findPermutations("aabc")

(24) ["aabc", "aacb", "abac", "abca", "acab", "acba", "aabc", "aacb", "abac", "abca", "acab", "acba", "baac", "baca", "baac", "baca", "bcaa", "bcaa", "caab", "caba", "caab", "caba", "cbaa", "cbaa"]

好吧,那可不好。如果我们的字符串中的一个字符重复,我们会得到每个排列两次。许多字符串都有重复字符。

let findPermutations = (string) => {
  if (!string || typeof string !== "string"){
    return "请输入字符串"
  } else if (string.length < 2 ){
    return string
  }

  let permutationsArray = [] 
   
  for (let i = 0; i < string.length; i++){
    let char = string[i]

    if (string.indexOf(char) != i)
    continue

    let remainingChars = string.slice(0, i) + string.slice(i + 1, string.length)

    for (let permutation of findPermutations(remainingChars)){
      permutationsArray.push(char + permutation) }
  }
  return permutationsArray
}

有很多不同的方法可以删除多余的元素,但我选择使用 Javascript 的indexOf方法来识别当前字符是否已经通过我们的findPermutations方法运行。indexOf 返回字符的第一个索引,因此如果我们已经为“a”运行了findPermutations,例如,indexOf(“a”) 将不同于char的索引,即当前的、后来的“a”。

如果返回true,我们可以continue - 继续,这实际上将跳过当前的迭代循环并继续进行下一个。

添加了这个逻辑之后,让我们来运行findPermutation。

findPermutations("aabc")

(12) ["aabc", "aacb", "abac", "abca", "acab", "acba", "baac", "baca", "bcaa", "caab", "caba", "cbaa"]

很完美!🌟 基于主定理的方法使我们能够快速将这个问题分解成小块并开始返回正确的结果,只需要在这里和那里进行一些调整即可以完全以所需的格式提供给我们解决方案。

审查:

那么我们基于主定理的方法又是什么呢?

1:建立一个基本案例 —— 如果我们的输入大小小于某个常数,直接解决它而不用递归。

2:如果输入大于所述常数,将其分解成更小的部分。

3:对分解的部分递归调用函数,直到其小到可以直接求解为止。

4:合并分解部分的结果,返回完成的解。

我发现这个模型是一个非常方便的工具,它可靠地为我提供了一个解决算法挑战的起点。虽然并不特别适用于每个算法问题,也不总是最高效或最优雅的解决方案,但它是一个可靠的主力模型,可以很好地为您服务!

这里有包含解决方案代码的GitHub 存储库还附带一个测试套件,因此您可以根据需要练习或尝试为该问题寻找替代解决方案。

如果你想进一步探索,你可以尝试使用上面使用的解决方案模型来找到密码锁的所有组合?它有效吗?你需要做任何改变吗?

有兴趣用 Javascript 解决另一种流行的算法吗?点击这里。