【问题标题】:What is the time/space complexity of this algorithm to get all the sub arrays of an array divided by a given number该算法将数组的所有子数组除以给定数字的时间/空间复杂度是多少
【发布时间】:2022-09-16 11:19:01
【问题描述】:

我正在编写一个函数,它接受一个数组和一个整数并返回一个子数组数组。子数组的数量正是传递给函数的整数。并且子数组必须是连续的,这意味着必须保留数组中项目的原始顺序。也没有子数组可以为空。他们必须至少有一个项目。例如:

const array = [2,3,5,4]
const numOfSubarray = 3

const subarrays = getSubarrays(arraym numOfSubarray)

在这种情况下subarrays 是这样的:

[
  [[2, 3], [5], [4]],
  [[2], [3, 5], [4]],
  [[2], [3], [5, 4]],
]

这是我的尝试:

function getSubarrays(array, numOfSubarray) {
  const results = []

  const recurse = (index, subArrays) => {
    if (index === array.length && subArrays.length === numOfSubarray) {
      results.push([...subArrays])
      return
    }
    if (index === array.length) return

    // 1. push current item to the current subarray
    // when the remaining items are more than the remaining sub arrays needed

    if (array.length - index - 1 >= numOfSubarray - subArrays.length) {
      recurse(
        index + 1,
        subArrays.slice(0, -1).concat([subArrays.at(-1).concat(array[index])])
      )
    }
    // 2. start a new subarray when the current subarray is not empty

    if (subArrays.at(-1).length !== 0)
      recurse(index + 1, subArrays.concat([[array[index]]]))
  }

  recurse(0, [[]], 0)
  return results
}

现在它似乎正在工作。但我想知道什么是时间/空间复杂度这个算法的。我认为它肯定比O(2^n) 慢。有什么办法可以改善吗?或者我们可以用来改进算法的任何其他解决方案?

  • 为什么你\"认为它肯定比O(2^n)\"慢吗?
  • 你知道有多少个“子数组”吗?这会给你一个复杂性的下限。
  • [2], [3], [5] 也是有效的子数组吗?还是子数组必须不相交并且联合必须等于原始数组的约束之一?
  • 在您的示例中,您没有返回子数组数组,而是返回子数组数组(3D)。问题实际上是关于返回所有有效的子数组组合吗?我认为此时问题太多了,所以请给我们一个正式的问题定义。
  • 在您的示例中,为什么 [[2], [3, 5], [4]] 不是一个有效的子数组?这是错字吗?

标签: javascript arrays algorithm recursion big-o


【解决方案1】:

恐怕你无法得到像2<sup>n</sup> 这样的答案。这比这增长得更快,因为答案与二项式系数有关,其定义具有基本的阶乘部分,其approximations 涉及诸如n<sup>n</sup> 之类的术语。

您的解决方案似乎比必要的更糟糕,因为解决最简单情况所需的调用次数呈指数级,当numOfSubarrays1 时,您应该能够返回[array]。但至于全面分析,我不确定。

正如第一条评论所示,上述分析完全错误。

但是,如果您对另一种方法感兴趣,我可能会这样做,基于其他人提到的相同见解,这样做的方法是找到您的值之间位置的所有 numOfSubarrays 索引集,然后将它们转换为您的最终格式:

const choose = (n, k) => 
  k == 0
    ? [[]]
  : n == 0
    ? []
    : [... choose (n - 1, k), ... choose (n - 1, k - 1). map (xs => [...xs, n])]

const breakAt = (xs) => (ns) =>
  [...ns, xs .length] .map ((n, i) => xs .slice (i == 0 ? 0 : ns [i - 1], n))

const subarrays = (xs, n) =>
  choose (xs .length - 1, n - 1) .map (breakAt (xs))


console .log (subarrays ([2, 3, 5, 4], 3) // combine for easier demo
  .map (xs => xs .map (ys => ys .join ('')) .join('-')) //=> ["23-5-4", "2-35-4", "2-3-54"]
)

console .log (subarrays ([2, 3, 5, 4], 3)) // pure result
.as-console-wrapper {max-height: 100% !important; top: 0}

在这里,choose (n, k) 找到所有可能的方法来从数字 123、...、n 中选择 k 元素。因此,例如,choose (4, 2) 将产生[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

breakAt 在一组索引处将数组分解为子数组。所以

breakAt ([8, 6, 7, 5, 3, 0, 9]) ([3, 5])
//                3     5  ///=> [[8, 6, 7], [5, 3], [0, 9]]

subarrays 只是将这些组合起来,从数组长度中减去一个,从 numOfSubarrays 中减去一个,用这两个值调用 choose,然后对于每个结果,使用原始数组和这组索引调用 breakAt .

即使在这里我也没有尝试分析复杂性,但是由于输出是阶乘的,因此该过程将花费阶乘时间。

【讨论】:

  • 您提到的维基百科链接近似(2n 选择 n)~2^(2n)/ sqrt(n* pi)<= 2^(2n)。它表明二项式系数的增长速度比以2 为基数的指数慢
  • @yjay:你说的很对。我已经更新了上面的内容以删除伪劣分析。
【解决方案2】:

如果要将n 元素列表完全拆分为k 分离的连续子列表,这就像将k-1 拆分点放入元素之间的n-1 间隙中一样:

2 | 3 | 5   4
2 | 3   5 | 4
2   3 | 5 | 4

在组合学中,这是从n-1 中获取k-1。所以我认为输出的结果大小将是n-1 take k-1 = (n-1)! / ((k-1)! * (n-k)!)。因此,复杂性是多项式,例如 O(n^(k-1)) 用于常量 k。如果你不修复k,而是用n 提高它,比如k = n/2,复杂性将呈指数级增长。

我不认为你可以改进这一点,因为输出的大小正在增加这种复杂性。

【讨论】:

    【解决方案3】:

    tl;博士

    解决方案的数量与(如@gimix 提到的)二项式系数有关,所以如果我理解正确,它是悲观指数的 https://en.wikipedia.org/wiki/Binomial_coefficient#Bounds_and_asymptotic_formulas

    如果我没记错的话,这会使您的算法成为指数 * n(对于每个解决方案的每个元素)* n(因为几乎在每个步骤中,您复制的数组的长度可能取决于n)。

    • 修复第二个 if - 仅在 subArrays.length < numOfSubarrays 时调用递归
    • 您需要大量复制数组 - 切片、连接、扩展运算符 - 所有这些都会创建新数组。如果对于每个解决方案(其长度可能取决于n)在每一步都复制此解决方案(我认为在这里发生),则将复杂性乘以n
    • 空间复杂度也是指数的 *n - 你存储指数数量的解,可能长度取决于n。使用生成器并在当时返回一个解决方案可以大大改善这一点。正如@gimix 提到的那样,组合可能是最简单的方法。 python中的组合生成器:https://docs.python.org/3/library/itertools.html#itertools.combinations

    考虑复杂性:

    我认为你对慢于指数复杂性的看法是正确的,但是 - 对我来说 - 你对斐波那契数列了解多少? ;)

    让我们考虑输入:

    array = [1, 2, ..., n]
    numOfSubarrays = 1
    

    我们可以将递归调用视为具有if 1. 保护左孩子(第一个recurse 调用)和if 2. 保护右孩子(第二个recurse 调用)的二叉树。

    对于每个递归调用if 1. 将被满足 - 项目比需要的子数组多。

    第二个if 仅当当前子数组有一些元素时才为真。这是一个棘手的条件 - 当且仅当它成功高一帧时它才会失败 - 一开始就添加了一个空数组(根调用除外 - 它没有父级)。就树而言,这意味着我们在正确的孩子中 - 父母必须刚刚添加了一个空子数组作为当前。另一方面,对于左孩子,父母刚刚将(又一个?)元素推送到当前子数组,我们确信 if 2. 会成功。

    好的,但它说明了复杂性是什么?

    好吧,我们可以计算树中节点的数量,乘以它们执行的操作的数量——其中大多数是一个常数——我们就得到了复杂度。那么有多少呢?

    我将在每个级别上分别跟踪左右节点。很快就会有用的。为方便起见,我将忽略根调用(我可以将其视为右节点 - 它具有空子数组 - 但它会破坏最终效果)并从级别 1 开始 - 根调用的左子节点。

    r1= 0
    l1= 1

    作为左节点(子数组不为空),它有两个子节点:

    r2= 1
    l2= 1

    现在,左节点总是有两个子节点(1. 总是满足;2. 为真,因为父节点将元素推送到当前子数组),右节点只有左子节点:

    r3= r2+ l2= 1 + 1 = 2
    l3= r2= 1

    我们可以继续。结果是:

    l r
    1 0
    1 1
    2 1
    3 2
    5 3

    嗯……奇怪的熟悉,不是吗?

    好吧,很明显,复杂度是 O(Σ(F一世+ F我-1) 其中 1 <= i <= n)。

    好吧,但它的真正含义是什么? 有一个非常酷的prove,即 S(n) - 从 0 到 n 的斐波那契数之和等于 F(n+2) - 1。它将复杂性简化为:

    O(S(n) + S(n-1)) = O(F(n+2) - 1 + F(n+1) - 1) = O(F(n+3) - 2) = O(F(n+3))
    

    我们可以忘记 +3,因为 F(n+3) < 2 * F(n+2) < 4 * F(n+1) < 8 * F(n)。

    最后一个问题,斐波那契数列是指数的吗?是的,显然不是。 不是满足 x 的数n= F(n) - 值在 2 和 √2 之间波动,因为 F(n+1) < 2 * F(n) < F(n+2)。

    但事实证明,lim(n->∞) F(n+1) / F(n) = φ - 黄金比例。这意味着 O(F(n)) = O(φn)。 (实际上,你复制数组很多,所以它更像 O(φn*n))

    如何解决?您可以在递归 if 2 之前检查是否没有太多数组。

    除此之外,正如@Markus 提到的,根据输入,解决方案的数量可能是指数的,因此获得它们的算法也必须是指数的。但这并不是每个输入都是如此,所以让我们将这些情况保持在最低限度:D

    【讨论】:

    • 作为问题的答案,这似乎令人困惑。二项式系数不是指数的。请参阅该维基百科文章的next section,该文章显示了斯特林的近似值,并注意其中有一个术语n ^ n。它的增长速度比任何指数函数都快。我不确定你想用斐波那契数字证明什么,但这似乎与问题无关。我的意思是,Fibonacci 和 Factorial 都以F 开头,并且可以通过递归来处理......但仅此而已。
    • 我同意,抱歉,偏离了方向:D 但我认为我对二项式系数增长速度的看法是正确的。快速论证:(n 选择 k)=((n-1)选择 k)+((n-1)选择(k-1))。这表明对于每个 n, k 都有这样的 t, v : (n 选择 k) <= ((n-1) 选择 t) + ((n-1) 选择 v)。这意味着 max(n 选择 k) <= 2 * max((n-1) 选择 t),这表明它的增长速度不会超过 2^n。不等式也是严格的,例如因为n = 1。我对这个笑话既恼火又好笑,哈哈,干杯!
    • 很难用两个主要是自变量的方式来谈论增长。您的演示是针对固定的k。如果我们观看2n choose n,我们将看到something different。封闭形式解决方案中阶乘的存在应该表明这是超指数的......不是很有趣吗?
    • 您的模拟应该将 n 作为两个函数的输入 - 指数和二项式系数,但您将 n 作为指数的输入,将 2n 作为二项式的输入。
    • 您发送的维基百科链接还显示:(2n 选择 n) ~ (2 ^ (2n)) / sqrt(n * pi) < 2 ^ (2n)。真的很神奇,从阶乘除法中得出的结果表现为指数:D
    【解决方案4】:

    该问题可以通过不同的方法解决:

    • 计算从1到array长度范围内的numOfSubarray数字的所有组合
    • 每个组合都是array 的有效切片。例如,您希望在示例中的位置 (1, 2)、(1, 3)、(2, 3) 处对数组进行切片,从而产生子数组 [[2],[3],[5,4]][[2],[3,5],[4]][[2,3],[5],[4]]

    我相信时间复杂度是 O(r(nCr)),其中 n 是(数组的长度 - 1),r 是(子数组的数量 - 1)。

    要可视化它的工作方式和原因,请查看stars and bars 技术

    【讨论】:

      猜你喜欢
      • 2014-08-29
      • 2021-12-05
      • 1970-01-01
      • 1970-01-01
      • 2021-02-10
      • 1970-01-01
      • 2015-12-01
      • 1970-01-01
      相关资源
      最近更新 更多