9

我正在寻找一种保留前n 个元素的数据结构,类似于这个问题,但增加了维护排序顺序的要求。显然我可以在最后进行排序,但可能有一种更有效的方法可以即时进行。只有插入,从不删除,然后在最后遍历前n 个元素。

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

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

4

6 回答 6

2

您必须向我们提供有关n的顺序和要插入的项目数的线索。

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

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

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

于 2013-02-20T17:41:29.043 回答
1

如果你真的需要让它们一直保持排序,你必须使用自平衡二叉搜索树。但要考虑到这(保持元素排序)不是一种优化,而是一种有成本的奢侈品。

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

您想如何访问已排序的元素?如果你只是想遍历排序后的序列,一个普通的自平衡二叉搜索树就足够了。

如果您想随时按排序序列中的位置访问任何元素(另一种奢侈......),那么您需要扩充树。基本上,每个节点都会有一个额外的字段来计算其子树中的节点数,包括它自己。

于 2013-02-20T03:23:25.687 回答
1

这个答案类似于凯利的答案,但有一个经过测试的代码示例。由于 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();
}
于 2013-02-20T19:59:00.580 回答
0

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

使用选择算法查找数组的前 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)
于 2013-02-20T21:29:18.107 回答
0

这是与我的第一个类似的数据结构,这次使用内部链表来提高插入速度。您会损失一些速度,因为您不能使用二进制搜索来查找插入点,但是(项目 > 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();
    }
}
于 2013-02-20T20:50:19.637 回答
0

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

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

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

于 2013-02-20T17:27:54.250 回答