【问题标题】:What is the best data structure for keeping the top n elements in sort order?保持前 n 个元素按排序顺序排列的最佳数据结构是什么?
【发布时间】:2013-02-04 20:49:04
【问题描述】:

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

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

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

【问题讨论】:

  • 优先队列怎么样?如果由堆支持,它将非常有效。不确定它是否是原生 .NET 集合
  • 如果 N 比较小,只有插入,并且你想保持它们的排序顺序,每次插入的冒泡排序还不错。
  • 是否可以跟踪更新时实际改变价值的项目?它们是否每次迭代只更新一次(在最终的 top-N 排序之前)?

标签: data-structures language-agnostic


【解决方案1】:

如果您确实需要始终保持它们排序,则必须使用自平衡二叉搜索树。但请注意,这(保持元素排序)并不是一种优化,而是一种需要付出代价的奢侈品。

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

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

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

    • 请原谅我的无知,但是您如何确保保留顶部 n 元素而不保持它们的排序?
    • @CraigW,你会保留所有东西,然后在最后进行排序并扔掉你不需要的东西。如果内存是个问题,那么维护平衡的二叉树可能是最好的方法。
    • 如果你没有足够的内存来存储整个列表,平衡二叉树也无济于事,但它的内存效率很高。此外,保留所有元素会导致所有不在顶部 n 中的项目的开销,无论是存储它们还是最终排序它们。
    • 内存绝对是个问题。一次会有数千个这样的数据结构,所以我只能保留前 n
    • @CraigW:你在这里进行了时空权衡。如果您的案例足够独特,您可能希望将自己的数据结构组合在一起。例如,为 2n 个元素分配空间。当它装满时,重新分类并切下下半部分。您可以保证仅在 n 次插入后进行排序,并且您正在对部分排序的数据进行排序。这可能比一直维护排序的数据结构要快。
    【解决方案3】:

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

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

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

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

    【讨论】:

    • n 大约为 1-100,通常在 10 左右。平均会有几百到几千次插入或尝试插入。跨度>
    • 只跟踪底部元素的问题是最坏情况复杂度,n次插入后,每次新插入都会在顶部n,迫使你摆脱当前底部并搜索新底部以 O(n) 为代价。那么,我们并没有真正节省多少。
    • 除了插入的项目以相反的顺序预先排序的情况外,它不会平均到 O(n/2) 吗?对于任何 > b 的项目,我们仍然有 O(1)。将这两个群体结合起来需要更多关于潜在插入集的知识。
    • 我喜欢 Kelly 的索引最后一个元素的想法,如果你看看我的回答,如果你使用 T[] 数组和指向最后一个元素的点 (int),这很简单。然后添加一个新的最后一个元素是 O(1)。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

        如果内存不是问题,最后还是对整个数组进行部分排序。即使您使用 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)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-01-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-12-11
          • 1970-01-01
          相关资源
          最近更新 更多