【问题标题】:Issue with slicing in javascript recursionjavascript递归中的切片问题
【发布时间】:2021-10-06 20:51:20
【问题描述】:

大家!我知道这不是递归的最佳基本案例,而且我知道它有缺陷。但是,在处理它时,我意识到有几次,它会跳过对数组的第一个元素进行切片,最终返回 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"))

返回值为:

['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

【问题讨论】:

  • 该输出不是来自您展示的示例。
  • 在进行递归之前,您需要检查第一个和最后一个字符是否相同。
  • 不应该是“安妮,我投票让更多的车从罗马跑到维也纳”吗?
  • 您也在区分大小写。您需要将字符串转换为单个大小写。
  • 请注意,您在string 上调用slice,而不是str,因此它包含空格和任何其他非单词字符。我建议使用str.join('') 而不是string

标签: javascript recursion palindrome


【解决方案1】:

实施问题

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

首先,它忘记了字符大小写相同,'Anne' 中的首字母大写 'A''Vienna' 中的最后小写 '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 函数,该函数接受一串小写字母数字字符并返回一个布尔值。然后我们可以编写一个接受任何字符串的公共包装函数,通过删除所有非字母数字字符并将其小写来对其进行规范化,将结果传递给该内部函数并返回其结果。它可能看起来像这样1

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

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

在当今易于使用的 JS 模块中,可以将内部函数封装在模块中,只留下 main 函数公开可见。

处理更少的案例

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

对于递归函数,我们至少需要一个基本情况。我们什么时候不能再检查第一个和最后一个字符是否相同?答案很清楚:当我们没有单独的首尾字符时。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,取第一个和最后一个字符之间的所有字符。使用这些可以使我们的 main 函数更具可读性:

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)

正如我所说,这是一次愉快的意外。我在写代码时没有注意到,只是在输入这篇文章时才注意到。

简单多了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

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

4好吧,又是有限的

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

【讨论】:

  • 出色的帖子。政客们已经掌握了空洞讲真话的艺术!
  • Scott Sauyet 先生,非常感谢您!你的回复对我帮助很大!我现在绝对了解这个问题的一切。
  • @MilosPopovic:我很高兴它有帮助。请叫我斯科特。
【解决方案2】:

您在错误的元素上执行.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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-07-09
    • 2020-04-28
    • 2010-12-09
    • 1970-01-01
    • 2020-12-14
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多