要找到未排序数组的中位数,我们可以在O(nlogn)时间内为n个元素创建一个最小堆,然后我们可以一次提取n/2个元素,以获取中位数。但这种方法需要O(nlogn)的时间。
我们能否通过某种O(n)时间的方法做到同样的事情呢? 如果可以,请告诉或建议一些方法。
要找到未排序数组的中位数,我们可以在O(nlogn)时间内为n个元素创建一个最小堆,然后我们可以一次提取n/2个元素,以获取中位数。但这种方法需要O(nlogn)的时间。
我们能否通过某种O(n)时间的方法做到同样的事情呢? 如果可以,请告诉或建议一些方法。
我已经点赞了@dasblinkenlight 的答案,因为中位数算法实际上可以在O(n)的时间内解决这个问题。我只想补充一下,使用堆也可以在O(n)的时间内解决这个问题。通过自底向上的方式可以在O(n)的时间内构建堆。请查看以下文章以获取详细说明堆排序
假设你的数组有N个元素,你需要构建两个堆:一个MaxHeap包含前N/2个元素(或(N/2)+1如果N是奇数),另一个MinHeap包含剩余的元素。如果N是奇数,则中位数是MaxHeap的最大元素(通过获取max来进行O(1)操作)。如果N是偶数,则中位数是(MaxHeap.max()+MinHeap.min())/2,这也需要O(1)的时间。因此,整个操作的真正成本是堆构建操作,其时间复杂度为O(n)。
顺便说一下,当您事先不知道数组元素数量时(例如要为整数流解决相同的问题),这个MaxHeap/MinHeap算法也可以工作。您可以在以下文章中找到更多有关如何解决这个问题的详细信息整数流的中位数
快速选择在O(n)时间内完成,这也被用于快速排序的分区步骤中。
快速选择算法可以在线性时间 (O(n)
) 内找到数组中第k小的元素。以下是Python实现:
import random
def partition(L, v):
smaller = []
bigger = []
for val in L:
if val < v: smaller += [val]
if val > v: bigger += [val]
return (smaller, [v], bigger)
def top_k(L, k):
v = L[random.randrange(len(L))]
(left, middle, right) = partition(L, v)
# middle used below (in place of [v]) for clarity
if len(left) == k: return left
if len(left)+1 == k: return left + middle
if len(left) > k: return top_k(left, k)
return left + middle + top_k(right, k - len(left) - len(middle))
def median(L):
n = len(L)
l = top_k(L, n / 2 + 1)
return max(l)
正如维基百科所说,中位数算法在理论上是O(N)的,但实际上并不常用,因为寻找“好”的枢轴的开销使其变得太慢。
http://en.wikipedia.org/wiki/Selection_algorithm
这里是一个Java源代码,用于查找数组中第k个元素的快速选择算法:
/**
* Returns position of k'th largest element of sub-list.
*
* @param list list to search, whose sub-list may be shuffled before
* returning
* @param lo first element of sub-list in list
* @param hi just after last element of sub-list in list
* @param k
* @return position of k'th largest element of (possibly shuffled) sub-list.
*/
static int select(double[] list, int lo, int hi, int k) {
int n = hi - lo;
if (n < 2)
return lo;
double pivot = list[lo + (k * 7919) % n]; // Pick a random pivot
// Triage list to [<pivot][=pivot][>pivot]
int nLess = 0, nSame = 0, nMore = 0;
int lo3 = lo;
int hi3 = hi;
while (lo3 < hi3) {
double e = list[lo3];
int cmp = compare(e, pivot);
if (cmp < 0) {
nLess++;
lo3++;
} else if (cmp > 0) {
swap(list, lo3, --hi3);
if (nSame > 0)
swap(list, hi3, hi3 + nSame);
nMore++;
} else {
nSame++;
swap(list, lo3, --hi3);
}
}
assert (nSame > 0);
assert (nLess + nSame + nMore == n);
assert (list[lo + nLess] == pivot);
assert (list[hi - nMore - 1] == pivot);
if (k >= n - nMore)
return select(list, hi - nMore, hi, k - nLess - nSame);
else if (k < nLess)
return select(list, lo, lo + nLess, k);
return lo + k;
}
我没有包含比较和交换方法的源代码,所以很容易将代码更改为使用Object[]而不是double[]。
实际上,您可以预期上述代码的时间复杂度为O(N)。
问题是:在未排序的数组中找到第K大的元素。
将数组分成n/5组,每组包含5个元素。
现在a1、a2、a3...a(n/5)表示每个组的中位数。
x = 元素a1、a2、.....a(n/5)的中位数。
如果k<n/2,则可以删除中位数大于x的组的最大值、第二大值和第三大值。我们现在可以使用7n/10个元素再次调用函数并找到第k大的值。
否则,如果k>n/2,则可以删除中位数小于x的组的最小值、第二小值和第三小值。我们现在可以使用7n/10个元素再次调用函数,并找到第(k-3n/10)大的值。
时间复杂度分析: T(n)是在大小为n的数组中查找第k大的时间复杂度。
T(n) = T(n/5) + T(7n/10) + O(n)
如果您解决了这个问题,您会发现T(n)实际上是O(n)。
n/5 + 7n/10 = 9n/10 < n