【问题标题】:Recursion return 0 instead of 2?递归返回 0 而不是 2?
【发布时间】:2021-02-13 00:05:55
【问题描述】:

我试图解决应该从字符串中删除多少符号以使字符串没有匹配的相邻字符的挑战,例如我有一个字符串AABA 一个A 应该从它和之后删除删除它看起来ABA,因为AAAAB 3 'A' 符号应该被删除到AB 和它的不同变体。

我决定用递归来解决它:

function solution(s, isA = s[0] === "A", count = 0) {
  if ((typeof s === "string" && !s.includes("B")) || !s.includes("A")) {
    return s.length - 1;
  }

  const strArr = Array.isArray(s) ? s : s.split("");
  const currentSymbol = strArr.shift();

  if ((currentSymbol === "A" && isA) || (currentSymbol === "B" && !isA)) {
    return solution(strArr, false, count);
  }

  if ((currentSymbol === "A" && !isA) || (currentSymbol === "B" && isA)) {
    return solution(strArr, false, ++count);
  }

  return count;
}

console.log(solution("AAAB"));

在调试AAAB 字符串时,计数等于 2,但在最后一刻变为 0(调用堆栈中的第一个值),我该如何解决?

【问题讨论】:

  • 不应该 return s.length - 1;return count 吗?我不明白你为什么要在这里返回 count 以外的东西,除非我遗漏了一些东西。
  • 使用正则表达式的非递归解决方案:"AAAB".replace(/(.)\1+/g, '$1')
  • BB 是否要替换为B?是否有任何给定的 symbols 列表被替换或者是否应该替换任何多次重复的字符?
  • @uladz 您正在尝试在非常低的级别上重新创建类似于正则表达式引擎的东西。如果你想玩得开心,那没关系。但是,如果您想找到解决问题的方法。我建议你学习正则表达式
  • @yunzen 是的,正则表达式很简单,但我认为即使是一个简单的循环也足够了。我很难理解这个算法应该如何工作,我也不认为递归是我的首选。归根结底,这是寻找线性字符序列的问题,因此基于线性扫描(循环)似乎是最明显的选择。

标签: javascript recursion


【解决方案1】:

认识你的祖先

递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳效果。这意味着要避免诸如突变、变量重新分配和其他副作用之类的事情。

  1. 如果第一个字符s0 不存在,则已达到基本情况。返回空结果。
  2. (归纳)第一个字符是 not null。如果等于下一个字符s1,则这两个字符是相邻的重复。丢弃第一个字符,返回子问题[s1, ...s]的结果。
  3. (归纳)第一个字符不为空并且它确实 not 等于第二个字符。这两个字符不重复。将第一个字符附加到子问题的结果[s1, ...s]

function removeAdjacentDuplicates ([s0, s1, ...s])
{ if (s0 == null)
    return ""                                        // #1
  else if (s0 == s1)
    return removeAdjacentDuplicates([s1, ...s])      // #2
  else
    return s0 + removeAdjacentDuplicates([s1, ...s]) // #3
}

console.log(removeAdjacentDuplicates("AAABBCCCDDDEEEEE"))
// ABCDE

console.log(removeAdjacentDuplicates("ABBXYY"))
// ABXY

console.log(removeAdjacentDuplicates("LMNOP"))
// LMNOP

以函数式风格构建复杂的程序是将几个简单的程序组合在一起的问题。为了得到你的问题的答案,我们可以写solution如下-

const solution = s =>
  s.length - removeAdjacentDuplicates(s).length

console.log(solution("AAABBCCCDDDEEEEE")) // 11
console.log(solution("ABBXYY"))           // 2
console.log(solution("LMNOP"))            // 0

展开下面的sn-p,在自己的浏览器中验证结果-

function removeAdjacentDuplicates ([s0, s1, ...s])
{ if (s0 == null)
    return ""
  else if (s0 == s1)
    return removeAdjacentDuplicates([s1, ...s])
  else
    return s0 + removeAdjacentDuplicates([s1, ...s])
}

const solution = s =>
  s.length - removeAdjacentDuplicates(s).length
  
console.log(solution("AAABBCCCDDDEEEEE")) // 11
console.log(solution("ABBXYY"))           // 2
console.log(solution("LMNOP"))            // 0
11
2
0

减少,简化

确定removeAdjacentDuplicates 的逻辑结构后,我们可以使用逻辑变换来操作它,就像我们使用定律来变换数学公式一样。下面我们将两个逻辑分支合并为一个以减少一些代码重复-

function removeAdjacentDuplicates ([s0, s1, ...s])
{ if (s0 == null)
    return ""    
  else
    return (s0 == s1 ? "" : s0)
      + removeAdjacentDuplicates([s1, ...s])
}

console.log(removeAdjacentDuplicates("AAABBCCCDDDEEEEE"))
// ABCDE

console.log(removeAdjacentDuplicates("ABBXYY"))
// ABXY

console.log(removeAdjacentDuplicates("LMNOP"))
// LMNOP

请给一个函数!

如果我们愿意的话,我们可以把所有东西都写成一个大函数,但这违背了函数式风格。在上面的实现中,removeAdjacentDuplicates 本身就是一个有用的函数。由于我们程序的多个关注点的分离,它很容易编写,solution 也很容易编写。作为奖励,分离的函数可以在其他程序中重复使用。

下面,solution 被重写为单个函数,其意图现在不太清楚。它更难编写,更难调试,也不容易在其他程序中重用 -

function solution ([s0, s1, ...s], n = 0)
{ if (s0 == null)
    return n
  else
    return solution([s1, ...s], n + Number(s0 == s1))
}

console.log(solution("AAABBCCCDDDEEEEE"))
// 11

console.log(solution("ABBXYY"))
// 2

console.log(solution("LMNOP"))
// 0

请用一个表情!

我们可以使用另一种变体,将尾递归形式换成树递归形式。交换揭示了一个不需要带有默认参数的额外参数的表单 -

function solution ([s0, s1, ...s])
{ if (s0 == null)
    return 0
  else
    return Number(s0 == s1) + solution([s1, ...s])
}

正如 Scott 所指出的,这是一种不同于单一、纯函数表达式的语法转换 -

const countAdjacentDuplicates = ([s0, s1, ...s]) =>
  s0 == null
    ? 0
    : Number(s0 == s1) + countAdjacentDuplicates([s1, ...s])

console.log(countAdjacentDuplicates("AAABBCCCDDDEEEEE"))
// 11

console.log(countAdjacentDuplicates("ABBXYY"))
// 2

console.log(countAdjacentDuplicates("LMNOP"))
// 0

在某种程度上,这消除了我试图教授的一些课程,但希望你仍然能够从这篇文章中学到一些东西。

【讨论】:

  • 我在最终版本中看到的唯一痛苦(除了从问题继承的无意义的函数名称)是n + Number (s0 == s1)。如果它改用n + (s0 == s1 ? 1 : 0),对我来说将是完美的表现。当然,如果没有尾部调用功能,它会更加清晰。但有一天,如果我们幸运的话,这些真的很重要。
  • 也就是说,我觉得这个版本很清楚:const countAdjDups = ([s0, s1, ...ss]) => s0 == null ? 0 : (s0 == s1 ? 1 : 0) + countAdjDups ([s1, ... ss])。这一切都不应影响这是一个很好的答案这一事实。
  • 感谢斯科特的反馈。 bool->number 转换应该是直截了当的,我认为,但你的三元表达式可能更清楚一点。您的递归表达式很好,我想我应该提供它作为替代方案......即使它不利于我试图教授的课程。一定要客观!妈
  • 我听到你说:计算它们与实际做去除它们的工作。我通常倾向于分解方法,因为单个函数将计数与计算它们的实际工作混合在一起。这是你看到我在我的许多新作品中使用生成器的主要原因,因为它将用户的预期效果推到了我们程序的最远边缘。考虑一下这个very relevant Q&A在这个答案之后不久发布的。
  • 这个问题是精神分裂症,它是否需要这些值或它们的计数,而您的答案对两者都很好。如果你只是想要一个计数,像const countNo00 = (n) => n <= 0 ? 1 : countNo00(n - 1) + countNo00(n - 2) 这样的实现可以很快让你意识到问题与斐波那契数有关,它可能被像const findNo00 = (n) => n <= 1 ? [''] : [...findNo00 (n - 1) .map (s => '1' + s), ...findNo00 (n - 2) .map (s => '01' + s)] 这样的函数隐藏。我只是说,有时计算建筑物的顶部很重要,有时它是矫枉过正的。
【解决方案2】:

试试这个

function solution(s, i = 0, count = 0) {
  let pre = "";

  if(i>=1 && s[i-1] === s[i]) {
    count++;
  }

  if(s.length>i+1) {
    return solution(s,i+1,count);
  }

  return count;
}

console.log(solution("AAAB"));

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-21
    • 2012-02-23
    • 2021-09-20
    • 2013-10-15
    • 1970-01-01
    • 2017-10-18
    • 2016-07-25
    • 2012-03-14
    相关资源
    最近更新 更多