javascript 递归中的切片问题

Issue with slicing in javascript recursion

各位!我知道这不是递归的最佳可能基本情况,而且我知道它有缺陷。但是,在处理它时,我意识到有几次它跳过了对数组第一个元素的切片,最终 returning false 而不是 true。 我可能遗漏了什么,但我很想知道为什么会这样?

PS。如果这个问题的措辞不够完美,我深表歉意,但我希望代码能给你一些更好的想法。

代码:

function palindrome(string) {
  let str = string.replace(/[^A-Za-z0-9]/g, '')
  console.log( str )
  str = str.split("")
  if (str.length === 3 && str[0] === str[2] || str.length === 1) {
    return true
  } else if (str.length === 2 && str[0] !== str[1]) {
    return false
  } else {
    return palindrome(string.slice(1, -1))
  }
  return false
}
console.log(palindrome("Anne I vote more cars race Rome to Vienna"))

与return值:

['A', 'n', 'n', 'a', 'I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V', 'i', 'e', 'n', 'n', 'a']
['n', 'n', 'a', 'I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V', 'i', 'e', 'n', 'n']
['n', 'a', 'I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V', 'i', 'e', 'n']
['a', 'I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V', 'i', 'e']
['I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V', 'i']
['I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o', 'V']
['I', 'v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o']
['v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't', 'o']
['v', 'o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e', 't']
['o', 't', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e']
['t', 'e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm', 'e']
['e', 'm', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o', 'm']
['m', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R', 'o']
['m', 'o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e', 'R']
['o', 'r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e']
['r', 'e', 'c', 'a', 'r', 's', 'r', 'a', 'c', 'e']
['e', 'c', 'a', 'r', 's', 'r', 'a', 'c']
['c', 'a', 'r', 's', 'r', 'a']
['c', 'a', 'r', 's', 'r']
['a', 'r', 's']
['r', 's']
false

您在错误的元素上执行了 .slice(1, -1),因为它保留了空格,而这些空格在回文中不匹配。 而您的代码只测试中间的最后一个字母,而忘记周围的所有其他字母。

我宁愿这样看代码

console.log(palindrome("Anne I vote more cars race Rome to Vienna"))

function palindrome( str )
  {
  return recussiv( str.replace(/[^A-Za-z0-9]/g, '').toLowerCase() )

  function recussiv(s)
    {
    console.log('-->', s )
    if (s.length > 1)
      {
      return (s[0] === s[s.length-1]) ? recussiv(s.slice(1, -1)) : false
      }
    else return true
    }
  }
.as-console-wrapper { max-height: 100% !important; top: 0

实施问题

恐怕此实现至少存在三个不同的问题。

首先,它忘记了对字符进行相同大小写处理,'Anne' 中的首字母大写 'A' 与 [=20= 中的最终小写 'a' 不匹配].

其次,它正确地改变了 string 输入以删除非字母数字字符,但是当它再次出现时,它是在这个原始的 string 而不是新的 str.

第三,也是最根本的一点,它在重现之前不检查最重要的事实:它不测试第一个字符是否与最后一个字符匹配。

开始使用初始版本

我们可以修复所有这些问题并获得如下所示的版本:

function palindrome(string) {
  let str = string.replace(/[^A-Za-z0-9]/g, '') .toLowerCase ().split("") // Issue 1 -- case
  // console.log( str.join('') )
  if (str.length === 3 && str[0] === str[2] || str.length === 1) {
    return true
  } else if (str.length === 2 && str[0] !== str[1]) { // Issue 3 -- compare first and last
    return false
  } else if (str [0] !== str [str .length - 1]) {
    return false
  } else {
    return palindrome(str.slice(1, -1).join('')) // Issue 2 -- recur on correct item
  }
  return false
}

console .log (palindrome ("Anne I vote more cars race Rome to Vienna"))   //=> true
console .log (palindrome ("Anne I vote more cars X race Rome to Vienna")) //=> false
console .log (palindrome ("A man, a plan, a canal: Panama!"))             //=> true
console .log (palindrome ("A man, a plan, a banal palindrome."))          //=> false

遗留问题

但即使在这里,也有一些真正的丑陋之处。我们在每次调用时在字符串和数组之间来回转换。我们 运行 在每次调用时清理字符串,当它在第一次调用后不需要时。而且它包含比真正需要的更多的独立案例。

不要继续转换

第一个修复是微不足道的。我们这里根本不需要数组。我们可以对字符串进行所有测试。字符串中的字符可以完全像数组项一样进行索引,并且它们具有等效的 length 属性.

只初始化一次

第二个问题,重新运行每次调用的初始化通常通过以下两种方式之一解决:添加初始调用时默认的额外参数,或添加辅助函数。我们将选择后者,因为前者存在一些真正的潜在问题(尽管它通常仍然值得考虑。)这分为两种风格。我们可以将辅助函数嵌套在主函数中,然后从内部调用它。或者我们可以使它成为我们刚刚调用的外部函数。 MisterJoJo 的回答恰如其分地展示了第一种风格,所以我们可以看看第二种风格。在这里,我们可以编写一个内部递归 _isPalindrome 函数,它接受一串小写字母数字字符和 returns 一个布尔值。然后我们可以编写一个 public 包装函数来接受任何字符串,通过删除所有非字母数字字符并将其小写来对其进行规范化,将结果传递给该内部函数并返回其结果。它可能看起来像这样1:

const isPalindrome = (string) =>
  _isPalindrome (string .replace (/\W/g, '') .toLowerCase())

const _isPalindrome = (cs) => /* ... recursive version here ... */

现在JS模块好用的时代,可以把那个内部函数封装在模块里,只留下main函数public可见

处理更少的案件

对于第三期,我们需要真正了解什么是回文。从根本上说,它只是意味着字符串向前和向后读取相同。这个答案代表的直觉是,这意味着第一个和最后一个字符相同,并且它们之间的剩余字符串也是回文。

对于递归函数,我们至少需要一个基本情况。我们什么时候可以不再检查第一个字符和最后一个字符是否相同?答案很清楚:当我们没有分开的第一个和最后一个字符时。2。所以我们的基本情况是字符串中有零个或一个字符。单字符字符串显然是回文。零字符字符串也是如此;如果这听起来很奇怪,请查找 vacuous truths。所以我们现在有一个简单的基本情况和一个简单的递归情况,我们可以编写我们的函数:

const isPalindrome = (string) => 
  _isPalindrome (string .replace (/\W/g, '') .toLowerCase ())

const _isPalindrome = (cs) => 
  cs .length < 2 
    ? true 
    : cs [0] == cs [cs .length - 1] && _isPalindrome (cs .slice (1, -1))

console .log (isPalindrome ("Anne I vote more cars race Rome to Vienna"))   //=> true
console .log (isPalindrome ("Anne I vote more cars X race Rome to Vienna")) //=> false
console .log (isPalindrome ("A man, a plan, a canal: Panama!"))             //=> true
console .log (isPalindrome ("A man, a plan, a banal palindrome."))          //=> false

使用实用函数

我们可以轻松地在这里停下来。但是该代码中仍然存在一些丑陋之处。所有索引操作都需要一些阅读才能理解。有一个强有力的论据将这种丑陋转移到命名良好的辅助函数中。我通常在助手 lastfirst3 周围放置,而且写一个 middle 也很容易。这些获取字符串或数组的第一个或最后一个字符,或者对于 middle,获取第一个和最后一个字符之间的所有字符。使用它们可以使我们的主要功能更具可读性:

const first = (xs) => xs [0]
const last = (xs) => xs [xs .length - 1]
const middle = (xs) => xs .slice (1, -1)

const isPalindrome = (string) => 
  _isPalindrome (string .replace (/\W/g, '') .toLowerCase ())

const _isPalindrome = (cs) => 
  cs .length < 2 
    ? true 
    : first (cs) == last (cs) && _isPalindrome (middle (cs))

通用处理

我们不小心写了一个比我们试图写的更通用的函数版本!请注意,我们可以在数组上使用相同的内部函数:

_isPalindrome (['foo', 'bar', 'qux', 'bar', 'foo']) //=> true
_isPalindrome (['foo', 'bar', 'qux', 'foo'])        //=> false

语义很简单。如果数组的元素少于两个,或者如果第一个和最后一个元素相同且其余元素形成回文,则数组是回文。

这主要是一个愉快的意外。当我注意到它时,我将最初编写的参数名称 letters 更改为更通用的 cs,这可能会让人想起 characters 但并不坚持。

注意到此实现的通用性质,我可能会更改 middle 以便我们可以将其用于更多类型,任何有限的可迭代类型。 middle 现在不会处理这个,因为并非所有此类可迭代对象都有 slice 方法。但是都是4可以转换成数组,然后我们就可以使用数组slice的方法

这里是对 middle 的简单更新,使该函数更加通用:

const middle = (xs) => [...xs] .slice (1, -1)

正如我所说,这是一个快乐的意外。我在写代码的时候没有注意到它,只是在输入这个 post.

简单多了isPalindrome

最后,问题指出这个问题可能不是用递归解决的最佳方法。这是千真万确的。我们之前对它进行了掩饰,但确实提到如果字符串正向和反向读取相同,则它是回文。我们可以直接这样写:

const isPalindrome = (string) => 
  _isPalindrome (string .replace (/\W/g, '') .toLowerCase ())

const _isPalindrome = (s) => 
  s == s .split ('') .reverse () .join ('')

console .log (isPalindrome ("Anne I vote more cars race Rome to Vienna"))   //=> true
console .log (isPalindrome ("Anne I vote more cars X race Rome to Vienna")) //=> false
console .log (isPalindrome ("A man, a plan, a canal: Panama!"))             //=> true
console .log (isPalindrome ("A man, a plan, a banal palindrome."))          //=> false

虽然 String 没有 reverse 方法,但我们可以轻松地转换为数组,反转数组,并将结果连接回字符串。然后我们简单测试一下反转后的字符串是否和原来的一样。5

这个版本不像上一个那样通用,但它绝对是将回文的想法直接转化为代码的更直接的翻译。




1 请注意,正则表达式 /\W/g 正好等同于 /[^A-Za-z0-9]/g。它是 \w 的反义词,表示 [a-zA-z0-9],但输入起来更简单。

2我们还可以注意到,如果第一个和最后一个字符是相同的,也就是说,如果我们的字符串只有一个字符长,我们可以使用相同的过程。所有会改变的是我们测试的长度是小于 2 还是小于 1.

3 first 通常被称为 head,原因不值得在这里讨论。

4 嗯,又是有限的

5 出于与上面 firstlast 相同的原因,可能值得从中提取一个 reverseString 辅助函数。这是一个真正可重用的简单函数。这留作练习。