合并 K 个排序数组/向量的复杂性
Posted
技术标签:
【中文标题】合并 K 个排序数组/向量的复杂性【英文标题】:Merging K Sorted Arrays/Vectors Complexity 【发布时间】:2016-08-29 03:12:01 【问题描述】:在研究合并 k 个已排序的连续数组/向量的问题以及它在实现上与合并 k 个已排序的链表有何不同时,我发现了两个相对简单的用于合并 k 个连续数组的简单解决方案和一个基于成对的优化方法——模拟 mergeSort() 工作方式的合并。我实现的两个简单的解决方案似乎具有相同的复杂性,但在我运行的一项大型随机测试中,似乎一个比另一个效率低得多。
简单合并
我的简单合并方法如下。我们创建一个输出vector<int>
并将其设置为我们给出的k
向量中的第一个。然后我们合并第二个向量,然后是第三个,依此类推。由于一个典型的merge()
方法接受两个向量并返回一个向量在空间和时间上与两个向量中的元素数量呈渐近线性,因此总复杂度将为O(n + 2n + 3n + ... + kn)
,其中n
是元素的平均数量每个列表。由于我们要添加1n + 2n + 3n + ... + kn
,我相信总复杂度是O(n*k^2)
。考虑以下代码:
vector<int> mergeInefficient(const vector<vector<int> >& multiList)
vector<int> finalList = multiList[0];
for (int j = 1; j < multiList.size(); ++j)
finalList = mergeLists(multiList[j], finalList);
return finalList;
天真的选择
我的第二个天真的解决方案如下:
/**
* The logic behind this algorithm is fairly simple and inefficient.
* Basically we want to start with the first values of each of the k
* vectors, pick the smallest value and push it to our finalList vector.
* We then need to be looking at the next value of the vector we took the
* value from so we don't keep taking the same value. A vector of vector
* iterators is used to hold our position in each vector. While all iterators
* are not at the .end() of their corresponding vector, we maintain a minValue
* variable initialized to INT_MAX, and a minValueIndex variable and iterate over
* each of the k vector iterators and if the current iterator is not an end position
* we check to see if it is smaller than our minValue. If it is, we update our minValue
* and set our minValue index (this is so we later know which iterator to increment after
* we iterate through all of them). We do a check after our iteration to see if minValue
* still equals INT_MAX. If it has, all iterators are at the .end() position, and we have
* exhausted every vector and can stop iterative over all k of them. Regarding the complexity
* of this method, we are iterating over `k` vectors so long as at least one value has not been
* accounted for. Since there are `nk` values where `n` is the average number of elements in each
* list, the time complexity = O(nk^2) like our other naive method.
*/
vector<int> mergeInefficientV2(const vector<vector<int> >& multiList)
vector<int> finalList;
vector<vector<int>::const_iterator> iterators(multiList.size());
// Set all iterators to the beginning of their corresponding vectors in multiList
for (int i = 0; i < multiList.size(); ++i) iterators[i] = multiList[i].begin();
int k = 0, minValue, minValueIndex;
while (1)
minValue = INT_MAX;
for (int i = 0; i < iterators.size(); ++i)
if (iterators[i] == multiList[i].end()) continue;
if (*iterators[i] < minValue)
minValue = *iterators[i];
minValueIndex = i;
iterators[minValueIndex]++;
if (minValue == INT_MAX) break;
finalList.push_back(minValue);
return finalList;
随机模拟
长话短说,我构建了一个简单的随机模拟,它构建了一个多维vector<vector<int>>
。多维向量以2
开始,每个向量大小为2
,并以600
向量结束,每个向量大小为600
。每个向量都被排序,并且较大的容器和每个子向量的大小每次迭代都会增加两个元素。我计算每个算法执行这样的操作需要多长时间:
clock_t clock_a_start = clock();
finalList = mergeInefficient(multiList);
clock_t clock_a_stop = clock();
clock_t clock_b_start = clock();
finalList = mergeInefficientV2(multiList);
clock_t clock_b_stop = clock();
然后我构建了以下情节:
我的计算表明这两个简单的解决方案(合并和选择)都具有相同的时间复杂度,但上图显示它们非常不同。起初我通过说一个比另一个可能有更多开销来合理化这一点,但后来意识到开销应该是一个常数因素,而不是产生如下图。对此有何解释?我假设我的复杂性分析是错误的?
【问题讨论】:
【参考方案1】:即使两种算法具有相同的复杂性(在您的情况下为O(nk^2)
),它们最终可能会根据您的输入大小和所涉及的“恒定”因素而产生截然不同的运行时间。
例如,如果一个算法在n/1000
时间运行,而另一个算法在1000n
时间运行,它们都具有相同的渐近复杂度,但对于n
的“合理”选择,它们的运行时间将非常不同。
此外,缓存、编译器优化等可能会显着改变运行时间。
对于您的情况,虽然您计算的复杂度似乎是正确的,但在第一种情况下,实际运行时间应为(nk^2 + nk)/2
,而在第二种情况下,运行时间应为nk^2
。请注意,除以2
可能很重要,因为随着k
的增加,nk
项可以忽略不计。
对于第三种算法,您可以通过维护包含所有k
向量的第一个元素的k
元素堆来修改朴素选择。那么您的选择过程将花费O(logk)
时间,因此复杂性将降低到O(nklogk)
。
【讨论】:
是的,我天真地(没有双关语)低估了仍然存在的低阶项以及常数因子的乘积(例如 1/2)。谢谢你的解释有道理。就O(nklog(k))
而言,我发现的三种方法是:1.) 对所有 nk 元素的数组进行排序,2.) 成对合并,以及 3.) 正如你所说的那样使用堆。以上是关于合并 K 个排序数组/向量的复杂性的主要内容,如果未能解决你的问题,请参考以下文章