【问题标题】:Optimize recursive string manipulation function with JavaScript用 JavaScript 优化递归字符串操作函数
【发布时间】:2017-09-07 07:26:59
【问题描述】:

问题

我今天在算法课上遇到了这个问题:

给定函数maxSubstring(s, t),其中s 是一个字符串,ts 的一个子字符串,找出可以删除第一个或最后一个子字符串t 的最大迭代次数。

概念

这是在 s = banababbaat = ba 上调用的函数 maxSubstring 的可视化。

          b  a  n  a  b  b  a  a
1st move: n  a  b  a  b  b  a            or   b  a  n  a  b  a  b  a
2nd move: n a b b a a  or  n a b a b a        n a b a b a  or  b a n a b a
3rd move:    n a b a   or   n a b a             n a b a    or    n a b a
4th move:             n  a                                n  a

因此,此操作需要四步。

尝试

这是我对问题的解决方案。它可以工作,但是当我使用较大的字符串作为参数时它会很慢。

尝试 #1

function maxSubstring(s, t) {
    if (s.includes(t)) {
        var idxSubstr = s.replace(t, '');
        var lastIdxSubstr = s.substr(0, s.lastIndexOf(t)) + s.substr(s.lastIndexOf(t) + t.length, s.length);
        return 1 + Math.max(maxSubstring(idxSubstr, t), maxSubstring(lastIdxSubstr, t)));
    }
    return 0;
}

尝试 #2

function maxSubstring(s, t) {
    if (s.includes(t)) {
        var idx = s.indexOf(t), lastIdx = s.lastIndexOf(t);
        var idxSubstr = s.substr(0, idx) + s.substr(idx + t.length, s.length);
        var lastIdxSubstr = s.substr(0, lastIdx) + s.substr(lastIdx + t.length, s.length);
        if (idx != lastIdx) {
            return 1 + Math.max(maxSubstring(idxSubstr, t), maxSubstring(lastIdxSubstr, t));
        } else {
            return 1 + maxSubstring(idxSubstr, t);
        }
    }
    return 0;
}

更新原因:通过将indexOflastIndexOf 的值存储在变量中,效率略有变化。

尝试 #3

function maxSubstring(s, t) {
    var idx = s.indexOf(t);
    if (idx >= 0) {
        var lastIdx = s.lastIndexOf(t);
        var idxSubstr = s.substr(0, idx) + s.substr(idx + t.length);
        if (idx != lastIdx) {
            var lastIdxSubstr = s.substr(0, lastIdx) + s.substr(lastIdx + t.length);
            return 1 + Math.max(maxSubstring(idxSubstr, t), maxSubstring(lastIdxSubstr, t));
        } else {
            return 1 + maxSubstring(idxSubstr, t);
        }
    }
    return 0;
}

更新原因:减少了在检查第一个索引之前重新定义某些值并阻止 lastIndexOf 计算的实例。

回答要求

我可以使用任何算法或方法来优化此代码吗? Math.max 是罪魁祸首,所以如果有人知道如何完全避免使用这种方法,我将不胜感激。

换句话说,maxSubstring 只能在其自身内部调用一次,但Math.max 要求它被调用两次(一次用于子字符串的第一个索引,另一次用于该子字符串的最后一个索引)。

最后,您介意告诉我什么是大 O 表示法适用于我的解决方案,什么是大 O 表示法适用于您的解决方案吗?这不是最初挑战的一部分,但我自己很好奇。提前致谢。

【问题讨论】:

  • 如果Math.max只是你所担心的,那你为什么不使用<,>来比较,毕竟只有2个参数。使用三元运算符和比较运算符找到 2 之间的最大值。
  • 这里概述了这个概念,但适用于 Java。它在我的函数的运行时并没有显示出很大的变化。 stackoverflow.com/questions/2103606/…
  • 这不会产生很大的加速,但是你的 .includes() 和 .replace() 基本上做了两次在 s 中查找 t 的工作,所以你可以获得一点速度通过将它们组合起来。事实上,你的 lastIndexOf(t) 调用也在做同样昂贵的操作,所以可能只做一次而不是 4 次!
  • 嗯。其他复杂性包括 indexOf 和 lastIndexOf,它们最终都是循环。

标签: javascript string algorithm optimization


【解决方案1】:

您提出的朴素递归算法的主要问题是它在同一输入 s 上经常被调用 - 甚至是指数级的频繁,这正是导致显着减速的原因在较大的字符串上。

您可以做的就是使用memoisation - 记住查找表中特定输入的结果。

您可以做的另一个优化是检查删除第一个和最后一个是否会导致不同的结果。在大多数情况下,删除它们的顺序绝对无关紧要,可能的删除次数始终相同。但是,当匹配的子字符串可以与其自身重叠时,情况并非如此。例如,试试maxSubstring('ababaa', 'aba')

function maxSubstring(s, t, prevResults = new Map()) {
    function result(x) { prevResults.set(s, x); return x; }
    if (prevResults.has(s))
        return prevResults.get(s); // memoisation

    const first = s.indexOf(t);
    if (first == -1)
        return result(0);
    const withoutFirst = s.slice(0, first) + s.slice(first + t.length);

    const last = s.lastIndexOf(t);
    if (last == first) // only one match
        return result(1 + maxSubstring(withoutFirst, t, prevResults));

    if (t.lastIndexOf(t.charAt(t.length-1), t.length-1) == -1 // last character of t is found nowhere else in t
        || !t.includes(s.charAt(first+t.length))) // character after the match can never be part of a match
        // so this match is always removed in the optimal sequence and it doesn't matter whether as first or last
        return result(1 + maxSubstring(withoutFirst, t, prevResults));

    const withoutLast = s.slice(0, last) + s.slice(last + t.length);
    if (t.indexOf(t.charAt(0), 1) == -1 // first character of t is found nowhere else in t
        || !t.includes(s.charAt(last - 1))) // character before the match can never be part of a match
        // so this match is always removed and it doesn't matter when
        return result(1 + maxSubstring(withoutLast, t, prevResults));

    return result(1 + Math.max(maxSubstring(withoutFirst, t, prevResults),
                               maxSubstring(withoutLast, t, prevResults)));
}

时间复杂度分析

递归调用的次数应该与移除次数大致成二次方。根据我的第二个建议,在最好的情况下它可能会归结为线性(取决于模式)。

对于每个调用,考虑线性搜索(indexOfslice 等)和 Map 查找,尽管它们的平均复杂度将低于输入变得更小并且模式通常是在输入的早期发现。无论如何,复杂度是多项式的,而不是指数的。

【讨论】:

  • 感谢您的建议!我添加了一条语句,确保仅在 indexOflastIndexOf 值不相等时才调用 Math.max
  • @AnthonyKrivonos 我也想到了这一点,但它并没有真正帮助 - 无论如何这只发生在最后一次删除中。
  • @AnthonyKrivonos 更好的优化是将var idx = …, lastIdx = …;放在函数的第一行,然后测试if (idx == -1 && lastIdx == -1) return 0; else if (idx == lastIdx) return 1; else …,根本不调用includes
  • @AnthonyKrivonos 在您更新的尝试中(直到我写这篇文章时的第 3 条),您没有包括 Bergi 建议的大部分性能改进,特别是记忆,如果子字符串确实没有分析两条路径不重叠自身。我怀疑添加这些增强功能可能会解决您在测试用例中的超时问题。
  • @AnthonyKrivonos 我不知道,递归调用的数量应该与删除数量大致成二次方。根据我的第二个建议,在最好的情况下它可能会归结为线性(取决于模式)。对于每个调用,考虑线性搜索(indexOfslice 等)和地图查找,尽管它们的平均复杂度将低于输入变得更小并且模式通常在输入的早期发现。无论如何,复杂性是多项式而不是指数。
猜你喜欢
  • 2019-05-11
  • 2012-10-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-27
  • 1970-01-01
  • 2020-03-14
相关资源
最近更新 更多