[英]What is the time/space complexity of this algorithm to get all the sub arrays of an array divided by a given number
I am writing a function that takes an array and an integer number and returns an array of subarrays.我正在编写一个 function ,它接受一个数组和一个 integer 数字并返回一个子数组数组。 The number of subarrays is exact the integer number passed to the function.
子阵列的数量与传递给 function 的 integer 数量完全相同。 And the subarrays have to be continuous, meaning the original order of items in the array has to be preserved.
并且子数组必须是连续的,这意味着必须保留数组中项目的原始顺序。 Also no subarray can be empty.
也没有子数组可以为空。 They have to have at least one item in it.
他们必须至少有一个项目。 For example:
例如:
const array = [2,3,5,4]
const numOfSubarray = 3
const subarrays = getSubarrays(arraym numOfSubarray)
In this case subarrays
is this:在这种情况下,
subarrays
是这样的:
[
[[2, 3], [5], [4]],
[[2], [3, 5], [4]],
[[2], [3], [5, 4]],
]
Here is my attempt:这是我的尝试:
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
}
Right now it seems to be working.现在它似乎正在工作。 But I wanted to know what is the time/space complexity of this algorithm.
但我想知道这个算法的时间/空间复杂度是多少。 I think it is definitely slower than
O(2^n)
.我认为它肯定比
O(2^n)
慢。 Is there any way to improve it?有什么办法可以改善吗? Or any other solutions we can use to improve the algorithm here?
或者我们可以用来改进算法的任何其他解决方案?
You could take a recursive approach.您可以采用递归方法。
The time complecity is the binomial coefficient : array.length - 1
choose length - 1
because of unwanted empty items.时间复杂度是二项式系数:
array.length - 1
选择length - 1
因为不需要的空项目。
function getSubarrays(array, length) { if (length === 1) return [[array]]; const result = []; let i = array.length - length + 1; do result.push(...getSubarrays(array.slice(i), length - 1).map(a => [array.slice(0, i), ...a])); while (--i) return result; } console.log(JSON.stringify(getSubarrays([2, 3, 5, 4], 3))); console.log(JSON.stringify(getSubarrays([2, 3, 5, 4, 5], 3)));
The problem can be solved by a different approach:该问题可以通过不同的方法解决:
numOfSubarray
numbers ranging from 1 to the length of array
array
长度的numOfSubarray
数字的所有组合array
.array
的有效切片。 For instance, you want to slice the array in your example at positions (1, 2), (1, 3), (2, 3), yielding subarrays [[2],[3],[5,4]]
, [[2],[3,5],[4]]
, and [[2,3],[5],[4]]
[[2],[3],[5,4]]
, [[2],[3,5],[4]]
和[[2,3],[5],[4]]
Time complexity is, I believe, O(r(nCr)) where n is (length of array - 1) and r is (number of subarrays - 1).我相信时间复杂度是 O(r(nCr)),其中 n 是(数组的长度 - 1),r 是(子数组的数量 - 1)。
To visualize how and why it works have a look at the stars and bars technique要可视化它的工作方式和原因,请查看星条技术
If you want to completely split a list of n
elements into k
disjunct, continuous sub-lists this is like placing k-1
split points into the n-1
gaps between the elements:如果要将
n
元素的列表完全拆分为k
个分离的连续子列表,这就像将k-1
拆分点放入元素之间的n-1
间隙中:
2 | 3 | 5 4
2 | 3 5 | 4
2 3 | 5 | 4
In combinatorics this is taking k-1
from n-1
.在组合学中,这是从
n-1
中获取k-1
1 。 So I think the result size of the ouput will be n-1 take k-1 = (n-1)! / ((k-1)! * (nk)!)
所以我认为输出的结果大小将是
n-1 take k-1 = (n-1)! / ((k-1)! * (nk)!)
n-1 take k-1 = (n-1)! / ((k-1)! * (nk)!)
. n-1 take k-1 = (n-1)! / ((k-1)! * (nk)!)
. Thus the complexity is something polynomial like O(n^(k-1))
for constant k
.因此,复杂性是多项式,例如常数
k
O(n^(k-1))
。 If you don't fix k
but raise it with n
like k = n/2
the complexity will get exponential.如果你不修复
k
而是用n
提高它,比如k = n/2
,复杂性将呈指数增长。
I don't think that you can improve this, because the output's size is increasing by this complexity.我不认为你可以改进这一点,因为输出的大小正在增加这种复杂性。
The number of solutions is bound to (as @gimix mentioned) binomial coefficient, so if I understand correctly it's pessimistically exponential https://en.wikipedia.org/wiki/Binomial_coefficient#Bounds_and_asymptotic_formulas .解决方案的数量与(如@gimix提到的)二项式系数有关,所以如果我理解正确,它是悲观的指数https://en.wikipedia.org/wiki/Binomial_coefficient#Bounds_and_asymptotic_formulas 。
If I'm not mistaken that makes your algorithm this exponential * n (for each element of each solution) * n (because on nearly every step you copy array which length might be dependent on n
).如果我没记错的话,这会使您的算法成为指数 * n (对于每个解决方案的每个元素) * n (因为几乎在每个步骤中,您都复制了长度可能取决于
n
的数组)。
n
) on every step you copy this solution (which I think is happening here) you multiply the complexity by n
.n
)在每个步骤中复制此解决方案(我认为在这里发生),您将复杂性乘以n
。n
- you store the exponential number of solutions, possibly of length dependent on n
.n
- 您存储指数数量的解,可能长度取决于n
。 Using a generator and returning one solution at the time could greatly improve that. I think you are right about the slower than exponential complexity, but - bare with me - how much do you know about Fibonacci's sequence?我认为你对慢于指数复杂性的看法是正确的,但是 - 对我来说 - 你对斐波那契数列了解多少? ;)
;)
Let's consider input:让我们考虑输入:
array = [1, 2, ..., n]
numOfSubarrays = 1
We can consider the recursive calls a binary tree with if
1. guarding the left child (first recurse
call) and if
2. guarding the right child (second recurse
call).我们可以考虑递归调用一棵二叉树,
if
1. 保护左孩子(第一次recurse
调用), if
2. 保护右孩子(第二次recurse
调用)。
For each recurse call if
1. will be fulfilled - there are more items than sub arrays needed.对于每个递归调用
if
1. 将被满足 - 项目比子 arrays 需要的多。
Second if
will be true only if current sub array has some elements.第二个
if
仅当当前子数组有一些元素时才为真。 It's a tricky condition - it fails if, and only if, it succeeded one frame higher - an empty array has been added at the very beginning (except for the root call - it has no parent).这是一个棘手的条件 - 当且仅当它成功高一帧时它才会失败 - 一开始就添加了一个空数组(根调用除外 - 它没有父级)。 Speaking in terms of a tree, it means we are in the right child - the parent must have just added an empty sub array as a current.
就树而言,这意味着我们在正确的孩子中 - 父母必须刚刚添加了一个空子数组作为当前。 On the other hand, for the left child parent has just pushed (yet another?) element to the current sub array and we are sure the if 2. will succeed.
另一方面,对于左孩子,父母刚刚将(又一个?)元素推送到当前子数组,我们确信 if 2. 会成功。
Okay, but what does it say about the complexity?好的,但它说明了复杂性是什么?
Well, we can count the number of nodes in the tree, multiply by the number of operations they perform - most of them a constant number - and we get the complexity.好吧,我们可以计算树中节点的数量,乘以它们执行的操作的数量——其中大多数是一个常数——我们就得到了复杂度。 So how many are there?
那么有多少呢?
I'll keep track of left and right nodes separately on every level.我将在每个级别上分别跟踪左右节点。 It's gonna be useful soon.
很快就会有用的。 For convenience I'll ignore root call (I could treat it as a right node - it has empty sub array - but it messes up the final effect) and start from level 1 - the left child of the root call.
为方便起见,我将忽略根调用(我可以将其视为右节点 - 它具有空子数组 - 但它会破坏最终效果)并从级别 1 开始 - 根调用的左子节点。
r 1 = 0 r 1 = 0
l 1 = 1 l 1 = 1
As a left node (sub array isn't empty) it has two children:作为左节点(子数组不为空),它有两个子节点:
r 2 = 1 r 2 = 1
l 2 = 1 l 2 = 1
Now, the left node always has two children (1. is always fulfilled; 2. is true because parent pushed element to current sub array) and the right node has only the left child:现在,左节点总是有两个子节点(1. 总是满足;2. 为真,因为父节点将元素推送到当前子数组),右节点只有左子节点:
r 3 = r 2 + l 2 = 1 + 1 = 2 r 3 = r 2 + l 2 = 1 + 1 = 2
l 3 = r 2 = 1 l 3 = r 2 = 1
we could continue.我们可以继续。 The results are:
结果是:
l ![]() |
r ![]() |
---|---|
1 ![]() |
0 ![]() |
1 ![]() |
1 ![]() |
2 ![]() |
1 ![]() |
3 ![]() |
2 ![]() |
5 ![]() |
3 ![]() |
well... it's oddly familiar, isn't it?嗯……奇怪的熟悉,不是吗?
Okay, so apparently, the complexity is O(Σ(F i + F i-1 ) where 1 <= i <= n).好吧,很明显,复杂度是 O(Σ(F i + F i-1 ),其中 1 <= i <= n)。
Alright, but what does it really mean?好吧,但它的真正含义是什么? There is a very cool prove that S(n) - sum of the Fibonacci numbers from 0 to n is equal F(n+2) - 1. It simplifies the complexity to:
有一个非常酷的证明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))
We can forget about the +3 since F(n+3) < 2 * F(n+2) < 4 * F(n+1) < 8 * F(n).我们可以忘记 +3,因为 F(n+3) < 2 * F(n+2) < 4 * F(n+1) < 8 * F(n)。
The final question, is Fibonacci sequence exponential?最后一个问题,斐波那契数列是指数的吗? Yes and no apparently.
是的,显然不是。 The is no number that would fulfil the x n = F(n) - the value oscillates between 2 and √2, because for F(n+1) < 2 * F(n) < F(n+2).
不是可以满足 x n = F(n) 的数字 - 该值在 2 和 √2 之间波动,因为对于 F(n+1) < 2 * F(n) < F(n+2)。
It's proven though, that lim(n->∞) F(n+1) / F(n) = φ - the golden ratio.但事实证明,lim(n->∞) F(n+1) / F(n) = φ - 黄金比例。 It means the O(F(n)) = O(φ n ).
这意味着 O(F(n)) = O(φ n )。 (Actually, you copy arrays a lot, so it's more like O(φ n *n))
(其实你复制arrays很多,所以更像O(φ n *n))
How to fix it?如何解决? You could check if there isn't too many arrays before recursing in if 2.
您可以在 if 2 递归之前检查是否没有太多 arrays 。
Other than that, just as @Markus mentioned, depending on the input, the number of solutions might be exponential, so the algorithm to get them also has to be exponential.除此之外,正如@Markus 提到的,根据输入,解决方案的数量可能是指数的,因此获得它们的算法也必须是指数的。 But that's not true for every input, so let's keep those cases to minimum:D
但这并非对每个输入都是如此,所以让我们将这些情况保持在最低限度:D
You can't get an answer down to anything like 2 n
, I'm afraid.恐怕你无法得到像
2 n
这样的答案。 This grows much faster than that, because the answer has to do with the binomial coefficients, whose definitions have fundamental factorial parts, and whose approximations involve terms like n n
.这比这增长得快得多,因为答案与二项式系数有关,其定义具有基本的阶乘部分,并且其近似值涉及
n n
之类的项。
Your solution seems likely to be worse than necessary, noted because of the exponential number of calls required to solve the simplest case, when numOfSubarrays
is 1
, and you should just be able to return [array]
.您的解决方案似乎比必要的更糟,因为解决最简单情况所需的调用次数呈指数级,当
numOfSubarrays
为1
时,您应该能够返回[array]
。 But as to full analysis, I'm not certain.但至于全面分析,我不确定。
However, if your're interested in another approach, here's how I might do it, based on the same insight others have mentioned, that the way to do this is to find all sets of numOfSubarrays
indices of the positions between your values, and then convert them to your final format:但是,如果您对另一种方法感兴趣,我可能会这样做,基于其他人提到的相同见解,这样做的方法是找到您的值之间位置的所有
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}
Here, choose (n, k)
finds all the possible ways to choose k
elements from the numbers 1
, 2
, 3
, ..., n
.在这里,
choose (n, k)
找到从数字1
、 2
、 3
、 ...、 n
中选择k
个元素的所有可能方法。 So, for instance, choose (4, 2)
would yield [[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
.因此,例如,
choose (4, 2)
将产生[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]
.
breakAt
breaks an array into sub-arrays at a set of indices. breakAt
将数组分解为一组索引处的子数组。 So所以
breakAt ([8, 6, 7, 5, 3, 0, 9]) ([3, 5])
// 3 5 ///=> [[8, 6, 7], [5, 3], [0, 9]]
And subarrays
simply combines these, subtracting one from the array length, subtracting one from numOfSubarrays
, calling choose
with those two values, and then for each result, calling breakAt
with the original array and this set of indices. subarrays
数组只是将这些组合起来,从数组长度中减去一个,从numOfSubarrays
中减去一个,使用这两个值调用choose
,然后对于每个结果,使用原始数组和这组索引调用breakAt
。
Even here I haven't tried to analyze the complexity, but since the output is factorial, the process will take a factorial amount of time.即使在这里我也没有尝试分析复杂性,但由于 output 是阶乘,因此该过程将花费阶乘时间。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.