从不断增长的集合中找出中值

时间:2009-09-07 03:57:16

标签: algorithm

我在一次采访中遇到了一个有趣的算法问题。我给出了答案,但不确定是否有更好的主意。所以我欢迎大家写一些关于他/她的想法。

你有一个空集。现在元素逐个放入集合中。我们假设所有元素都是整数,它们是不同的(根据集合的定义,我们不考虑具有相同值的两个元素)。

每次将新元素添加到集合中时,都会询问集合的中值。中值定义与数学中的相同:排序列表中的中间元素。这里,特别是当集合的大小是偶数时,假设set = 2 * x的大小,中值元素是集合的第x个元素。

一个例子: 从空集开始, 当添加12时,中位数为12, 当加7时,中位数为7, 当加8时,中位数为8, 当加11时,中位数为8, 当添加5时,中位数为8, 当添加16时,中位数为8, ...

请注意,首先,元素被添加到逐个设置,其次,我们不知道要添加的元素。

我的回答。

由于这是一个关于寻找中位数的问题,因此需要进行排序。最简单的解决方案是使用普通数组并保持数组排序。当新元素到来时,使用二进制搜索来查找元素的位置(log_n)并将元素添加到数组中。由于它是一个普通的数组,因此需要移动阵列的其余部分,其时间复杂度为n。插入元素后,我们可以使用实例时间立即获得中位数。

最差时间复杂度为:log_n + n + 1。

另一种解决方案是使用链接列表。使用链接列表的原因是消除了移动阵列的需要。但是找到新元素的位置需要线性搜索。添加元素需要立即时间,然后我们需要通过遍历数组的一半来找到中值,这总是需要n / 2次。

最差时间复杂度为:n + 1 + n / 2。

第三种解决方案是使用二叉搜索树。使用树,我们避免移动数组。但是使用二叉搜索树来查找中位数并不是很有吸引力。所以我改变二叉搜索树的方式总是左子树和右子树是平衡的。这意味着在任何时候,左子树和右子树都具有相同数量的节点,或者右子树比左子树中的节点多一个节点。换句话说,确保在任何时候,根元素是中值。当然,这需要改变树的构建方式。技术细节类似于旋转红黑树。

如果正确维护树,则确保WORST时间复杂度为O(n)。

因此,这三种算法都与集合的大小成线性关系。如果不存在子线性算法,则可以将这三种算法视为最优解。由于它们彼此之间没有太大区别,因此最好的是使用链接列表最容易实现,这是第二个。

所以我真的很想知道,这个问题会有一个子线性算法吗?如果是这样的话会是什么样子。任何想法的家伙?

史蒂夫。

8 个答案:

答案 0 :(得分:23)

您的复杂性分析令人困惑。假设添加了n个项目;我们想要有效地输出n个中间流(其中流中的第i个是前i个项的中位数)。

我相信这可以使用两个优先级队列(例如二进制或斐波纳契堆)在O(n * lg n)时间内完成;当前中位数以下项目的一个队列(所以最大元素位于顶部),另一个队列位于其上方(在此堆中,最小值位于底部)。注意,在fibonacci(和其他)堆中,插入是O(1)摊销;它只弹出一个O(lg n)元素。

这将被称为“在线中位数选择”算法,尽管Wikipedia仅讨论在线最小/最大选择。这里有一个approximate algorithm和一个lower bound关于确定性和近似在线中值选择(下限意味着没有更快的算法!)

如果与n相比存在少量可能的值,则可能会破坏基于比较的下限,就像您可以进行排序一样。

答案 1 :(得分:10)

我收到了同样的面试问题,并在争吵的帖子中提出了两堆解决方案。正如他所说,每次操作的时间是O(log n)最坏情况。 预期的时间也是O(log n),因为你必须假设随机输入“弹出一个元素”1/4的时间。

我随后进一步思考并想出如何获得恒定的预期时间;实际上,每个元素的预期比较数变为2 + o(1)。你可以在http://denenberg.com/omf.pdf看到我的写作。

BTW,这里讨论的解决方案都需要空间O(n),因为你必须保存所有元素。一种完全不同的方法,只需要O(log n)空间,可以得到中位数的近似值(不是精确的中位数)。抱歉,我无法发布链接(每个帖子我只限一个链接),但我的论文有指示。

答案 2 :(得分:8)

虽然已经回答了争吵,但我想描述一种亚线性搜索树方法的修改。

  • 我们使用平衡的二叉搜索树(AVL / Red-Black / etc),但不像你描述的那样超平衡。因此添加项目是O(log n)
  • 树的一个修改:对于每个节点,我们还在其子树中存储节点数。这不会改变复杂性。 (对于一个叶子,这个计数将是1,对于一个有两个叶子的节点,这将是3,等等)

我们现在可以使用以下计数访问O(log n)中的第K个最小元素:

def get_kth_item(subtree, k):
  left_size = 0 if subtree.left is None else subtree.left.size
  if k < left_size:
    return get_kth_item(subtree.left, k)
  elif k == left_size:
    return subtree.value
  else: # k > left_size
    return get_kth_item(subtree.right, k-1-left_size)

中位数是Kth最小元素的特例(假设你知道集合的大小)。

所有这一切都是另一个O(log n)解决方案。

答案 3 :(得分:2)

我们可以将min和max堆分别存储数字。另外,我们为数字集定义了一个类DynamicArray,它有两个函数:Insert和Getmedian。插入新数字的时间是O(lgn),而获得中位数的时间是O(1)。

此解决方案在C ++中实现如下:

template<typename T> class DynamicArray
{
public:
    void Insert(T num)
    {
        if(((minHeap.size() + maxHeap.size()) & 1) == 0)
        {
            if(maxHeap.size() > 0 && num < maxHeap[0])
            {
                maxHeap.push_back(num);
                push_heap(maxHeap.begin(), maxHeap.end(), less<T>());

                num = maxHeap[0];

                pop_heap(maxHeap.begin(), maxHeap.end(), less<T>());
                maxHeap.pop_back();
            }

            minHeap.push_back(num);
            push_heap(minHeap.begin(), minHeap.end(), greater<T>());
        }
        else
        {
            if(minHeap.size() > 0 && minHeap[0] < num)
            {
                minHeap.push_back(num);
                push_heap(minHeap.begin(), minHeap.end(), greater<T>());

                num = minHeap[0];

                pop_heap(minHeap.begin(), minHeap.end(), greater<T>());
                minHeap.pop_back();
            }

            maxHeap.push_back(num);
            push_heap(maxHeap.begin(), maxHeap.end(), less<T>());
        }
    }

    int GetMedian()
    {
        int size = minHeap.size() + maxHeap.size();
        if(size == 0)
            throw exception("No numbers are available");

        T median = 0;
        if(size & 1 == 1)
            median = minHeap[0];
        else
            median = (minHeap[0] + maxHeap[0]) / 2;

        return median;
    }

private:
    vector<T> minHeap;
    vector<T> maxHeap;
};

有关更详细的分析,请参阅我的博客:http://codercareer.blogspot.com/2012/01/no-30-median-in-stream.html

答案 4 :(得分:0)

1)与之前的建议一样,保留两个堆并缓存各自的大小。左堆保持低于中位数的值,右堆保持高于中位数的值。如果您只是否定右堆中的值,则最小值将位于根处,因此无需创建特殊数据结构。

2)当你添加一个新数字时,你可以根据你的两个堆的大小,当前的中位数和L&amp; R堆的两个根确定新的中位数,这只需要一段时间。

3)调用私有线程方法执行实际工作以执行插入和更新,但立即返回新的中值。您只需要阻塞,直到更新堆根。然后,执行插入的线程只需要在遍历祖父节点时保持对遍历祖父节点的锁定;这将导致您可以插入和重新平衡,而不会阻止其他子分支上的其他插入线程。

获得中位数成为一个恒定的时间过程,当然现在你可能不得不等待进一步添加的同步。

罗布

答案 5 :(得分:0)

具有增强的 size 字段的平衡树(例如R / B树)应该在最坏的情况下找到lg(n)时间的中值。我认为这是经典算法教科书的第14章。

答案 6 :(得分:0)

为了使说明简洁,您可以通过让每个节点在其左子树中存储节点数来有效地增加BST以选择O(h)中指定等级的密钥。如果可以保证树是平衡的,则可以将其减少为O(log(n))。考虑使用高度平衡的AVL(或大致平衡的红黑树),然后您可以选择O(log(n))中的任何键。当您将节点插入或删除到AVL中时,您可以递增或递减一个跟踪树中节点总数的变量,以确定中位数的等级,然后可以在O(log(n))中选择。

答案 7 :(得分:-2)

为了找到线性时间的中位数你可以尝试这个(它只是我想到的)。每次向集合中添加数字时都需要存储一些值,而不需要排序。在这里。

typedef struct
{
        int number;
        int lesser;
        int greater;
} record;

int median(record numbers[], int count, int n)
{
        int i;
        int m = VERY_BIG_NUMBER;

        int a, b;

        numbers[count + 1].number = n:
        for (i = 0; i < count + 1; i++)
        {
                if (n < numbers[i].number)
                {
                        numbers[i].lesser++;
                        numbers[count + 1].greater++;
                }
                else
                {
                        numbers[i].greater++;
                        numbers[count + 1].lesser++;
                }
                if (numbers[i].greater - numbers[i].lesser == 0)
                        m = numbers[i].number;
        }

        if (m == VERY_BIG_NUMBER)
        for (i = 0; i < count + 1; i++)
        { 
                if (numbers[i].greater - numbers[i].lesser == -1)
                        a = numbers[i].number;
                if (numbers[i].greater - numbers[i].lesser == 1)
                        b = numbers[i].number;

                m = (a + b) / 2;
        }

        return m;
}

这样做,每次向集合添加一个数字时,您现在必须有多少“数字小于”数字,以及“数字大于”数字的数量。因此,如果您的数字与“小于”和“大于”相同,则表示您的数字位于集合的正中间,而无需对其进行排序。如果你有一个偶数的数字,你可能有两个中位数的选择,所以你只需返回这两个的平均值。顺便说一句,这是C代码,我希望这有帮助。