今天我参加了一场工作面试,被问及std:set_intersection
的复杂度。当我回答时,我提到:
O(n+m)
等于:
O(max(n,m))
然而我被告知这是不正确的,我无功而返,试图证明以下等式成立:
O(0.5*(n+m)) ≤ O(max(n,m)) ≤ O(n+m)
我的问题是:我真的错了吗?
今天我参加了一场工作面试,被问及std:set_intersection
的复杂度。当我回答时,我提到:
O(n+m)
等于:
O(max(n,m))
然而我被告知这是不正确的,我无功而返,试图证明以下等式成立:
O(0.5*(n+m)) ≤ O(max(n,m)) ≤ O(n+m)
我的问题是:我真的错了吗?
对于所有的m,n ≥ 0,都成立max(m, n) ≤ m + n → max(m, n)在O(m + n)中,且m + n ≤ 2max(m, n) → m + n在O(max(m, n))中。
因此,O(max(m, n)) = O(m + n)。
补充说明:如果f属于O(m + n),那么存在一个常数D > 0,使得对于足够大的m和n,f(n, m) < D * (m + n)。因此,f(n, m) < 2 D * max(m, n),并且O(m + n)必须是O(max(m, n))的子集。证明O(max(m, n))是O(m + n)的子集是类似的。
Infinite since F+c is contained and c can take infinite values.
So I understand what you are thinking that n+m and max(n,m) have same
order but **the right way to express that** is by saying n+m is
O(max(n,m)) and max(n,m)is O(m+n) ( O(m+n) is equal to O(max(m,n))
may better requires a proof).
我还有一件事要说,我们说这些函数的阶数相同是绝对正确的数学概念,但是在尝试优化算法时,你可能需要考虑一些低阶因素,然后它们可能会给出略微不同的结果,但渐近行为被证明是相同的。
结论
正如您可以在wikipedia(以及每个大学的所有cs课程或每本算法书中)中阅读到的那样,Big O/θ/Ω/ω/ο符号帮助我们比较函数并找到它们的增长顺序,而不是用于函数集。这就是为什么你被告知你是错误的原因。虽然说O(n^2)是O(n)的子集很容易,但是比较无限集合来判断两个集合是否相等非常困难。 Cantor已经开始对无限集进行分类,例如,我们知道自然数是可数无限的,而实数是不可数无限的,因此实数多于自然数,即使两者都是无限的。当尝试对无限集进行排序和分类时,变得非常复杂,这将更像数学研究而不是比较函数的方法。
更新
事实证明,你可以简单地证明O(n+m)等于O(max(n,m)):
对于每个属于O(n+m)的函数F,这意味着存在常数c和k:
F <= c(n+m) for every n>=k and m>=k
然后也是:
F <= c(n+m)<= 2c*max(n,m)
F <= c*max(n+m) for every n>=k and m>=k
我们还有:
F <= c*max(n+m)<=2c(m+n) for every n>=k and m>=k
std::set_intersection
的渐近复杂度,两种方法都导致了算法以线性时间运行的共识。
让我们先看一下cppreference上std::set_intersection
的参考文献(强调是我的)。
Parameters
first1
,last1
- the first range of elements to examine
first2
,last2
- the second range of elements to examineComplexity
At most
2·(N1+N2-1)
comparisons, where
N1 = std::distance(first1, last1) N2 = std::distance(first2, last2)
std::distance
本身是自然线性的(最坏情况:没有随机访问)
std::distance
...
返回
first
和last
之间的元素数量。
我们将简要回顾大O符号的基础知识。
我们松散地陈述一个函数或算法f
在O(g(n))
中的定义(严格来说,O(g(n))
是一组函数,因此f∈O(...)
,而不是常见的误用f(n)∈O(...)
)。
如果一个函数
f
在O(g(n))
中,那么c·g(n)
是f(n)
的上界,对于某个非负常数c
,使得f(n)≤c·g(n)
成立,对于足够大的n
(即n≥n0
,其中n0
是某个常数)。
因此,要证明f∈O(g(n))
,我们需要找到一组(非负)常数(c, n0)
,满足
f(n) ≤ c · g(n), for all n ≥ n0, (+)
(c, n0)
使得(+)成立的问题是退化的。事实上,如果存在任何这样的常数对,将存在无限数量的不同常数对。std::set_intersection
进行大O分析。
set_intersection
示例现在考虑两个元素范围,例如range1
和range2
,并假设这两个范围中包含的元素数量分别为m
和n
。
k = m+n
成为选择的参数:我们仍然会得出结论,即std::set_intersection
具有线性时间复杂度,但是是以k
(它等于m+n
而不是max(m, n)
)而不是m
和n
中最大的一个作为参考。这些只是我们在进行Big-O符号/渐进分析之前自由选择设定的先决条件,很可能面试官更喜欢选择使用k
作为增长参数来分析复杂度,而不是它的两个组成部分中最大的一个。现在,我们知道在最坏情况下,std::set_intersection
将运行2 · (m + n - 1)
次比较/基本操作。
h(n, m) = 2 · (m + n - 1)
n > m
,并定义。f(n) = 2 · (n + n - 1) = 4n - 2 > h(n,m) (*)
f(n)
的渐近复杂度,使用大O符号表示。让g(n) = n
请注意
f(n) = 4n - 2 < 4n = 4 · g(n)
现在(选择)让 c = 4
和 n0 = 1
,那么我们可以陈述以下事实:
f(n) < 4 · g(n) = c · g(n), for all n ≥ n0, (**)
(**)
,根据 (+)
,我们现在已经证明了。f ∈ O(g(n)) = O(n)
h ∈ O(g(n)) = O(n), assuming n > m (i)
持有。
如果我们改变最初的假设,假设m > n
,重新追踪上面的分析,相反地,将会得到类似的结果。
h ∈ O(g(m)) = O(m), assuming m > n (ii)
因此,假设有两个范围range1
和range2
,分别包含m
和n
个元素,我们已经证明了对这两个范围应用std::set_intersection
的渐近复杂度确实为
O(max(m, n))
我们选择m
和n
中最大的作为我们分析增长的参数,但是这并不是描述Big-O符号的有效注释(至少不常见)。当我们使用Big-O符号来描述某个算法或函数的渐近复杂度时,我们会考虑某个增长的单一参数(而非两个)。
与其回答复杂度为O(max(m, n))
,不失一般性,我们可以假设n
描述了具有最多元素范围的元素数量,并在此假设的基础上简单地说明std::set_intersection
的渐近复杂度的上限为O(n)
(线性时间)。
k = m+n
而不是其两个组成部分中的最大值。另一种可能性自然是面试官混淆了有关std::set_intersection
的实际比较次数的最坏情况和Big-O符号以及渐近复杂度的分离问题,并对此进行了混淆查询。
需要注意的是,对于常见的非有序集合交集问题,std::set_intersect
的最坏情况复杂度分析并不具有代表性:前者适用于已经排序的范围(请参见Boost的set_intersection
引用下面的内容:即std::set_intersection
的起源),而在后者中,我们研究的是非有序集合的交集计算。
作为后者的一个例子,Python的描述
set_intersection
构造一个已排序范围,该范围是rng1
和rng2
的交集。返回值是输出范围的末尾。
Intersection
集合方法适用于非有序集合,并应用于例如集合s
和t
,它的平均情况和最坏情况的复杂度分别为O(min(len(s), len(t)))
和O(len(s) * len(t))
。这种实现中平均情况和最坏情况之间的巨大差异源于哈希解决方案通常在实践中工作得非常好,但对于某些应用程序,在理论上可能具有非常差的最坏情况性能。