以排序顺序保留前n个元素的最佳数据结构是什么?

时间:2013-02-19 23:57:22

标签: data-structures language-agnostic

我正在寻找一个数据结构,它保留顶部 n 元素,类似于this question,但增加了维护排序顺序的要求。显然我可以在最后排序,但可能有更有效的方法来实现它。只有插入,永远不会删除,然后通过最后的 n 元素进行迭代。

这个问题与语言无关,但它将在C#中,因此最好使用本机.NET集合的答案。

编辑:我应该澄清,排序顺序仅在顶部 n 元素被迭代时最终才重要。在插入过程中,只要保留顶部 n 元素,排序顺序就无关紧要。

6 个答案:

答案 0 :(得分:1)

如果您确实需要始终对它们进行排序,则必须使用自平衡二叉搜索树。但是要考虑到这一点(保持元素排序)不是优化,而是具有成本的奢侈品。

自平衡二进制搜索树比隐式堆慢一个常数因子。

您想如何访问已排序的元素?如果您只想迭代排序的序列,那么正常的自平衡二叉搜索树就足够了。

如果您想在任何时间按排序顺序访问任何元素(另一个豪华......),那么您需要扩充树。基本上,每个节点都有一个额外的字段,用于计算其子树中的节点数,包括其自身。

答案 1 :(得分:1)

您必须告诉我们 n 的顺序以及要插入的项目数。

我认为排序顺序是相关的,你怎么知道哪些元素是顶部 n 的一部分?仅仅因为您只希望在所有插入结束时顶部的 n 可能会对结构或算法产生偏差。

为什么要保留不在顶部 n 中的任何项目?您可以使用大小为n + 1的有序集(thinking of Python's deque)。添加时,遍历列表并将新项插入集中的正确位置。当集合达到大小(n + 1)时,每个插入后面都会删除最后一个项目。这样,您始终拥有顶部的 n 项目,而不会使用永远不会检索的项目来增加数据结构。

此外,如果你保留底部元素的值( n 的最后一个,称之为 b ),那么你可以使用它来完全跳过插入。当新项目 x 大于 b 时,这会将比较次数限制为O(1)。

答案 2 :(得分:1)

这个答案类似于Kelly,但有一个经过测试的代码示例。由于N的尺寸小而< 100,我使用了一个简单的插入排序,如果项目数超过某些(非优化的)值(例如20个项目),则使用二进制搜索查找进行修改。我已经包含了一个示例控制台应用程序(C#)来显示其用途。我进行了简单的测试,以确保它有效,但我目前没有对它进行全面分析。此结构已针对减少内存使用进行了优化。

public class TopNStructure<T> : IEnumerable<T> where T : IComparable<T>
{
    private const int SizeForLinearOrBinaryInsert = 20;

    private int _maxSize;
    private int _currentSize;
    private T[] _items;
    private IComparer<T> _comparer;

    /// <summary>
    /// The number of items
    /// </summary>
    public int Count { get { return _currentSize; } }

    public TopNStructure(int maxSize, IComparer<T> comparer)
    {
        if (maxSize <= 0)
        {
            throw new ArgumentOutOfRangeException("Max size must be a postive, non-zero value");
        }
        _maxSize = maxSize;
        _currentSize = 0;
        _items = new T[maxSize];
        _comparer = comparer;
    }

    public TopNStructure(int maxSize)
        : this(maxSize, Comparer<T>.Default) { }

    /// <summary>
    /// Adds an item to this structure
    /// </summary>
    /// <param name="item">The item to add</param>
    /// <returns>True if the item was added, false otherwise</returns>
    public bool Add(T item)
    {
        if (_currentSize == 0)
        {
            _items[0] = item;              
        }
        else if (_currentSize == _maxSize)
        {
            if (_comparer.Compare(_items[_currentSize - 1], item) <= 0)
            {
                return false;
            }
            else
            {
                Insert(item);
                return true;
            }
        }
        else if (_currentSize == 1)
        {   
            if (_comparer.Compare(_items[0], item) <= 0)
            {
                _items[1] = item;
            }
            else
            {
                _items[1] = _items[0];
                _items[0] = item;
            }               
        } 
        else 
        {
            if (_comparer.Compare(_items[_currentSize - 1], item) <= 0)
            {
                _items[_currentSize] = item;
            }
            else
            {
                Insert(item);
            }
        }
        _currentSize++;
        return true;
    }

    /// <summary>
    /// Insert the item into the data structure
    /// </summary>
    /// <param name="item">The item to insert</param>
    private void Insert(T item)
    {
        int start = 0;
        if (_currentSize >= SizeForLinearOrBinaryInsert)
        {
            start = Array.BinarySearch<T>(_items, 0, _currentSize, item, _comparer);
            if (start < 0)
            {
                start = ~start;
            }
            ShiftAndInsert(item, start, _currentSize);                
            return;
        }
        else
        {
            for (int i = start; i < _currentSize; i++)
            {
                if (_comparer.Compare(_items[i], item) > 0)
                {
                    ShiftAndInsert(item, i, _currentSize);                      
                    return;
                }
            }
            _items[_currentSize] = item;
        }                           
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="index"></param>
    /// <param name="maxIndex"></param>
    private void ShiftAndInsert(T item, int index, int maxIndex)
    {
        if (maxIndex >= _maxSize)
        {
            maxIndex = _maxSize - 1;
        }
        for (int i = maxIndex; i > index; i--)
        {
            _items[i] = _items[i - 1];
        }
        _items[index] = item;
    }


    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)_items).GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _items.GetEnumerator();
    }
}

static void Main(string[] args)
{
    TopNStructure<double> data = new TopNStructure<double>(25);

    Random rand = new Random(132151);
    for (int i = 0; i < 50; i++)
    {
        double value = rand.NextDouble();
        data.Add(value);
    }

    int j = 0;
    foreach (double value in data)
    {
        Console.WriteLine("{0} {1}", j, value);
        j++;
    }
    Console.ReadKey();
}

答案 3 :(得分:0)

在迭代它们之前简单地对前k个元素进行排序可能是值得的。如果k足够小(比小于元素总数的一半),那么这将比维护一个你永远不会查询的排序列表便宜。

查看预先构建的部分排序方法,例如STL的部分排序。

创建所有元素的排序列表将在O(n log n)中完成,以进行基于比较的排序,无论它是否在运行中。部分排序会略好一些。

答案 4 :(得分:0)

这是我的第一个类似的数据结构,这次使用内部链接列表来提高插入速度。你失去了一些速度,因为你不能使用二进制搜索来找到插入点,但插入和删除(项目&gt; n)是O(1)并且应该平衡缺少二进制搜索。此结构使用更多内存,因为链接列表有2个额外的指针。

public class TopNStructureLinkedList<T> : IEnumerable<T> where T : IComparable<T>
{
    private const int SizeForLinearOrBinaryInsert = 20;

    private int _maxSize;
    private int _currentSize;
    private LinkedList<T> _items;
    private IComparer<T> _comparer;
    private LinkedListNode<T> _largestItemNode;

    /// <summary>
    /// The number of items
    /// </summary>
    public int Count { get { return _currentSize; } }

    public TopNStructureLinkedList(int maxSize, IComparer<T> comparer)
    {
        _maxSize = maxSize;
        _currentSize = 0;
        _items = new LinkedList<T>();
        _comparer = comparer;
        _largestItemNode = null;
    }

    public TopNStructureLinkedList(int maxSize)
        : this(maxSize, Comparer<T>.Default) { }

    /// <summary>
    /// Adds an item to this structure
    /// </summary>
    /// <param name="item">The item to add</param>
    /// <returns>True if the item was added, false otherwise</returns>
    public bool Add(T item)
    {
        if (_currentSize == 0)
        {
            _largestItemNode = _items.AddFirst(item);               
        }
        else if (_currentSize == 1)
        {
            if (_comparer.Compare(_largestItemNode.Value, item) <= 0)
            {
                _largestItemNode = _items.AddAfter(_largestItemNode, item);                   
            }
            else
            {
                _items.AddBefore(_largestItemNode, item);                   
            }
        }
        else if (_currentSize == _maxSize)
        {
            if (_comparer.Compare(_largestItemNode.Value, item) <= 0)
            {
                return false;
            }
            else
            {
                Insert(item);
                _largestItemNode = _items.Last.Previous;
                _items.RemoveLast();
                return true;
            }
        }
        else
        {
            if (_comparer.Compare(_largestItemNode.Value, item) <= 0)
            {
                _largestItemNode = _items.AddAfter(_largestItemNode, item);       
            }
            else
            {
                Insert(item);
            }
        }
        _currentSize++;
        return true;
    }

    /// <summary>
    /// Insert the item into the data structure
    /// </summary>
    /// <param name="item">The item to insert</param>
    private void Insert(T item)
    {
        LinkedListNode<T> node = _largestItemNode.Previous;
        while (node != null)
        {              
            if(_comparer.Compare(node.Value, item) <= 0) {
                _items.AddAfter(node, item);
               return;
            }
            node = node.Previous;               
        }
        _items.AddFirst(item);

    }

    public IEnumerator<T> GetEnumerator()
    {
        return ((IEnumerable<T>)_items).GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return _items.GetEnumerator();
    }
}

答案 5 :(得分:0)

如果内存不是问题,最好还是对整个数组进行局部排序。即使您使用O(log n)插入数据结构,您可以通过这种方式实现的最佳复杂性是O(n log k):n插入成本为O(log k)。

使用Selection Algorithm查找数组的k个顶部元素会给出O(k log n)的复杂度。 k小于n,所以它更好。

维基百科上的QuickSelect文章有一个实现。此外,使用纯PriorityQueue(以不同的方式,这里提到的大多数人)更容易。一目了然:

create_heap(array) // O(n)
for(int i=0; i<k; i++)
    sorted[i] = heap_pop(array) //O(log n)