我正在寻找一种保留前n 个元素的数据结构,类似于这个问题,但增加了维护排序顺序的要求。显然我可以在最后进行排序,但可能有一种更有效的方法可以即时进行。只有插入,从不删除,然后在最后遍历前n 个元素。
这个问题与语言无关,但它将在 C# 中,因此使用本机 .NET 集合的答案是可取的。
编辑:我应该澄清一下,排序顺序仅在迭代前n 个元素时才最重要。在插入的过程中,只要保留前n 个元素,排序顺序就无关紧要。
我正在寻找一种保留前n 个元素的数据结构,类似于这个问题,但增加了维护排序顺序的要求。显然我可以在最后进行排序,但可能有一种更有效的方法可以即时进行。只有插入,从不删除,然后在最后遍历前n 个元素。
这个问题与语言无关,但它将在 C# 中,因此使用本机 .NET 集合的答案是可取的。
编辑:我应该澄清一下,排序顺序仅在迭代前n 个元素时才最重要。在插入的过程中,只要保留前n 个元素,排序顺序就无关紧要。
您必须向我们提供有关n的顺序和要插入的项目数的线索。
我认为排序顺序是相关的,你怎么知道哪些元素是 top n的一部分?仅仅因为您只希望所有插入末尾 的前n 个可能会产生对结构或算法的偏见。
为什么保留任何不在 top n中的项目?您可以使用大小为 n+1 的排序集(考虑 Python 的双端队列)。添加时,遍历列表并将新项目插入集合中的正确位置。当集合达到大小 (n+1) 时,每次插入后都会删除最后一项。这样,您始终拥有前n个项目,而不会使用永远不会检索到的项目来增加数据结构。
此外,如果您保留底部元素的值(n的最后一个,称为b),那么您可以使用它来完全跳过插入。当新项目x大于b时,这会将比较次数限制为 O(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();
}
如果内存不是问题,最后最好对整个数组进行部分排序。即使您使用 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)
这是与我的第一个类似的数据结构,这次使用内部链表来提高插入速度。您会损失一些速度,因为您不能使用二进制搜索来查找插入点,但是(项目 > 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();
}
}
在迭代它们之前简单地对最后的前 k 个元素进行排序可能是值得的。如果 k 足够小(比如少于元素总数的一半),那么这将比维护一个您从不查询的排序列表便宜。
查看预构建的部分排序方法,如 STL 的部分排序。
为所有元素创建排序列表将在 O(n log n) 中完成,以进行基于比较的排序,无论它是否处于运行状态。部分排序会稍微好一些。