鉴于从数据流中读取整数。到目前为止,以有效的方式查找元素的中位数。
解决方案我已经读过:我们可以使用左侧的最大堆来表示小于有效中位数的元素,使用右侧的最小堆来表示大于有效中位数的元素。
处理传入元素后,堆中元素的数量最多相差1个元素。当两个堆包含相同数量的元素时,我们发现堆的根数据的平均值为有效中值。当堆不平衡时,我们从包含更多元素的堆的根中选择有效中值。
但是我们如何构建最大堆和最小堆,即我们如何知道这里的有效中位数?我认为我们会在max-heap中插入1个元素,然后在min-heap中插入下一个元素,依此类推所有元素。纠正我如果我在这里错了。
答案 0 :(得分:363)
有许多不同的解决方案可以从流数据中查找运行中位数,我将在答案的最后简要介绍它们。
问题是关于特定解决方案(最大堆/最小堆解决方案)的详细信息,以及基于堆的解决方案如何工作的解释如下:
对于前两个元素,将较小的一个添加到左侧的maxHeap,将较大的一个添加到右侧的minHeap。然后逐个处理流数据,
Step 1: Add next item to one of the heaps
if next item is smaller than maxHeap root add it to maxHeap,
else add it to minHeap
Step 2: Balance the heaps (after this step heaps will be either balanced or
one of them will contain 1 more item)
if number of elements in one of the heaps is greater than the other by
more than 1, remove the root element from the one containing more elements and
add to the other one
然后在任何给定时间你都可以像这样计算中位数:
If the heaps contain equal amount of elements;
median = (root of maxHeap + root of minHeap)/2
Else
median = root of the heap with more elements
现在我将按照答案开头所承诺的一般性问题进行讨论。从数据流中查找运行中位数是一个棘手的问题,对于一般情况,找到具有内存约束的精确解可能是不可能的。另一方面,如果数据具有我们可以利用的一些特征,我们可以开发有效的专业解决方案。例如,如果我们知道数据是整数类型,那么我们可以使用counting sort,它可以为您提供恒定的内存常量时间算法。基于堆的解决方案是一种更通用的解决方案,因为它也可用于其他数据类型(双精度)。最后,如果不需要精确的中位数并且近似值足够,您可以尝试估算数据的概率密度函数并使用该函数估计中值。
答案 1 :(得分:48)
如果您无法同时将所有项目保存在内存中,则此问题会变得更加困难。堆解决方案要求您一次将所有元素保存在内存中。在这个问题的大多数实际应用中,这是不可能的。
相反,当您看到数字时,请跟踪您看到每个整数的次数的计数。假设有4个字节的整数,即2 ^ 32个桶,或者最多2 ^ 33个整数(每个int的密钥和计数),即2 ^ 35字节或32GB。它可能比这少得多,因为你不需要存储那些为0的条目的键或计数(即像python中的defaultdict)。这需要花费一些时间来插入每个新整数。
然后在任何时候,要找到中位数,只需使用计数来确定哪个整数是中间元素。这需要恒定的时间(尽管是一个很大的常数,但仍然是恒定的)。
答案 2 :(得分:44)
如果输入的方差是统计分布的(例如正态,对数正态等),那么水库采样是从任意长的数字流估计百分位数/中位数的合理方式。
int n = 0; // Running count of elements observed so far
#define SIZE 10000
int reservoir[SIZE];
while(streamHasData())
{
int x = readNumberFromStream();
if (n < SIZE)
{
reservoir[n++] = x;
}
else
{
int p = random(++n); // Choose a random number 0 >= p < n
if (p < SIZE)
{
reservoir[p] = x;
}
}
}
“水库”是一个运行的,统一的(公平的)所有输入的样本 - 无论大小如何。找到中位数(或任何百分位数)是分类水库和轮询有趣点的直接问题。
由于储层是固定大小的,因此可以认为排序实际上是O(1) - 并且该方法在恒定时间和内存消耗的情况下运行。
答案 3 :(得分:26)
这个问题有一个确切的解决方案,只需要将 n 最近看到的元素保存在内存中。它速度快,可以很好地扩展。
indexable skiplist支持O(ln n)插入,删除和索引搜索任意元素,同时保持排序顺序。当跟踪跟踪第n个最旧条目的FIFO queue时,解决方案很简单:
class RunningMedian:
'Fast running median with O(lg n) updates where n is the window size'
def __init__(self, n, iterable):
self.it = iter(iterable)
self.queue = deque(islice(self.it, n))
self.skiplist = IndexableSkiplist(n)
for elem in self.queue:
self.skiplist.insert(elem)
def __iter__(self):
queue = self.queue
skiplist = self.skiplist
midpoint = len(queue) // 2
yield skiplist[midpoint]
for newelem in self.it:
oldelem = queue.popleft()
skiplist.remove(oldelem)
queue.append(newelem)
skiplist.insert(newelem)
yield skiplist[midpoint]
以下是完整工作代码的链接(易于理解的类版本和优化的生成器版本,内置可索引的skiplist代码):
答案 4 :(得分:25)
计算我发现的流的百分位数的最有效方法是P²算法:Raj Jain, Imrich Chlamtac: The P² Algorithm for Dynamic Calculation of Quantiiles and Histograms Without Storing Observations. Commun. ACM 28(10): 1076-1085 (1985)
该算法很容易实现并且工作得非常好。不过,这是一个估计,所以请记住这一点。从摘要:
提出了一种启发式算法,用于中值和其他分位数的动态计算。估计是在生成观测值时动态生成的。没有存储观察结果;因此,无论观测数量多少,该算法都具有非常小且固定的存储要求。这使其成为可用于工业控制器和记录器的分位数芯片的理想选择。该算法进一步扩展到直方图绘图。分析了算法的准确性。
答案 5 :(得分:16)
一种直观的思考方式是,如果你有一个完全平衡的二元搜索树,那么根就是中间元素,因为那里会有相同数量的更小和更大的元素。 现在,如果树没有满,那么情况就不是这样了,因为最后一级会缺少元素。
所以我们可以做的是使用中位数和两个平衡二叉树,一个用于小于中位数的元素,一个用于大于中位数的元素。这两棵树必须保持相同的大小。
当我们从数据流中获得一个新的整数时,我们将它与中位数进行比较。如果它大于中位数,我们将它添加到正确的树中。如果两个树的大小差异大于1,我们删除右树的min元素,使其成为新的中位数,并将旧的中位数放在左侧树中。同样适用于较小的。
答案 6 :(得分:6)
高效是一个取决于背景的词。此问题的解决方案取决于相对于插入量执行的查询量。假设您在中位数感兴趣的末尾插入N个数字和K次。基于堆的算法的复杂度为O(N log N + K)。
考虑以下备选方案。将数字插入数组中,对于每个查询,运行线性选择算法(比如使用快速排序轴)。现在你有一个运行时间为O(K N)的算法。
现在,如果K足够小(不频繁的查询),后一种算法实际上更有效,反之亦然。
答案 7 :(得分:0)
这是我简单但有效的算法(在C ++中),用于从整数流中计算运行中位数:
#include<algorithm>
#include<fstream>
#include<vector>
#include<list>
using namespace std;
void runningMedian(std::ifstream& ifs, std::ofstream& ofs, const unsigned bufSize) {
if (bufSize < 1)
throw exception("Wrong buffer size.");
bool evenSize = bufSize % 2 == 0 ? true : false;
list<int> q;
vector<int> nums;
int n;
unsigned count = 0;
while (ifs.good()) {
ifs >> n;
q.push_back(n);
auto ub = std::upper_bound(nums.begin(), nums.end(), n);
nums.insert(ub, n);
count++;
if (nums.size() >= bufSize) {
auto it = std::find(nums.begin(), nums.end(), q.front());
nums.erase(it);
q.pop_front();
if (evenSize)
ofs << count << ": " << (static_cast<double>(nums[nums.size() / 2 - 1] +
static_cast<double>(nums[nums.size() / 2]))) / 2.0 << '\n';
else
ofs << count << ": " << static_cast<double>(nums[nums.size() / 2]);
}
}
}
bufferSize
指定数字序列的大小,必须在该数字序列上计算运行中值。从输入流ifs
中读取数字时,大小为bufferSize
的向量将按排序的顺序维护。如果bufferSize
为奇数,则取排序向量的中间值;如果bufferSize
为偶数,则取两个中间元素之和除以2,得出中位数。另外,我维护从输入读取的最后bufferSize
个元素的列表。添加新元素时,我将其放在排序向量中的正确位置,并从向量中删除之前bufferSize
个步骤添加的元素(该元素的值保留在列表的前面)。同时,我从列表中删除了旧元素:每个新元素都位于列表的后面,每个旧元素都从列表中删除了。到达bufferSize
之后,列表和向量都停止增长,并且每次删除新元素都会得到补偿,方法是删除旧元素,并将其放置在列表bufferSize
之前的步骤中。请注意,我不在乎是从向量中完全删除元素,还是在元素bufferSize
之前放置元素,还是仅删除具有相同值的元素。对于中位数的值没有关系。
所有计算出的中值都会在输出流中输出。
答案 8 :(得分:-1)
你不能只用一个堆来做这个吗? 更新:没有。请参阅评论。
不变:在读取2*n
个输入后,最小堆保存最大n
个。
循环:读取2个输入。将它们都添加到堆中,并删除堆的min。这会重新建立不变量。
因此,当读取2n
个输入时,堆的min是第n个最大值。需要一些额外的复杂功能来平均中间位置周围的两个元素,并在奇数个输入后处理查询。