【问题标题】:Sort a "sorted" array对“排序”数组进行排序
【发布时间】:2009-06-12 09:35:35
【问题描述】:
  1. 假设给定一个大小为 n 且具有排序值的数组。
  2. 在第 i 次迭代中,给出一个新的随机生成值,并插入到数组的末尾。
  3. 然后重新使用数组,并丢弃最小值项。
  4. 迭代 n 后,保留的数组将包含最大值的项。

例如,在 Java 语法中,它会是这样的:

List l = new ArrayList();
l.add(new Integer(2));
l.add(new Integer(3));
l.add(new Integer(6));
l.add(new Integer(9));

Random rand = new Random();
for (int i=0; i < n; i++) {
  l.add(new Integer(rand.nextInt(1000)));
}    
Collections.sort(l);
l.remove(0);

但它似乎效率低下。有更好的算法吗?

【问题讨论】:

  • @Bob Cross:我需要完全相同的东西,这不是功课
  • @nimcap,我很高兴你找到了这个问题。

标签: java algorithm arrays sorting


【解决方案1】:

对新值使用二进制插入(类似于二进制搜索)。丢弃最小的。应该很快。

顺便说一句——这可以作为一种方便的扩展方法来实现:

private static int GetSortedIndex( this IList list, IComparer comparer, object item, int startIndex, int endIndex )
{
  if( startIndex > endIndex )
  {
    return startIndex;
  }
  var midIndex = startIndex + ( endIndex - startIndex ) / 2;
  return comparer.Compare( list[midIndex], item ) < 0 ?
    GetSortedIndex( list, comparer, item, midIndex + 1, endIndex ) :
    GetSortedIndex( list, comparer, item, startIndex, midIndex - 1 );
}

public static void InsertSorted( this IList list, IComparer comparer, object item )
{
  list.Insert( list.GetSortedIndex( comparer, item ), item );
}

Java 等价物

public static void main(String[] args)
{
   List l = new ArrayList();
   l.add(new Integer(2));
   l.add(new Integer(3));
   l.add(new Integer(6));
   l.add(new Integer(9));

   Random rand = new Random();
   for (int i=0; i < 10; i++) {
       Integer rnd = new Integer(rand.nextInt(1000));
       int pos = Collections.binarySearch(l,rnd);
       if(pos < 0) pos = ~pos;
       l.add(pos,rnd);
   }    
   System.out.println(l);
}

【讨论】:

  • 如果'list'是一个ArrayList(如提问者的例子),这仍然是O(n)。
  • 很慢,因为插入列表很慢。
  • 问题被标记为“java”,因此没有扩展方法。
  • 没错-我在编辑答案时忘记了。也许它可以帮助一些 .Net 人 - 所以我不删除它。
  • 我已经添加了代码的 java 等价物...尽管 binarySearch 也有类似的行为 C#...也许您也可以添加该版本...
【解决方案2】:

使用TreeSet 而不是List,它将保持顺序,使得最大值始终位于SortedSet#last()。如果使用 1.6+,您可以使用 NavigableSet 方法; pollLast() 将返回并删除最大值。

NavigableSet<Integer> set = new TreeSet<Integer>();

//... setup data

Integer highest = set.pollLast();

set.add(rand.nextInt(1000));

Integer newHighest = set.pollLast();

【讨论】:

    【解决方案3】:

    使用最小堆来存储数据,每次插入一个新的随机值后,在O(1)时间内删除最小值。

    n次迭代后,执行n次extract-min得到排序列表。

    【讨论】:

    • 我将这个问题解释为“数组保持排序”和“最小元素被删除”是两个后置条件,而不仅仅是后者。
    • 对不起,我认为即使我们想要一个排序列表,我的方法也会起作用。更改了答案以反映这一事实。
    【解决方案4】:

    我很惊讶没有人提到这一点...您正在寻找的数据结构是priority queue。毫无疑问,这是完成这项任务的最有效方式。可以使用多种不同的方法来实现优先级队列(请参阅链接文章),但最常见的是基于 binary heap。在自二进制变体中(这很典型),插入和删除都需要O(log n) 时间。

    Java 库中似乎有一个built-in generic classPriorityQueue&lt;E&gt;,所以看起来你可以直接使用它。毫不奇怪,这种类型似乎基于堆数据结构,尽管比我不能说的更具体。无论如何,它应该非常适合您的使用。

    【讨论】:

    • 当然,要指出的是,二进制堆最后不会被排序(好吧,我猜它可能会被排序,但不太可能) .如果只有 N 个最大的项目就足够了,那么你就可以了。如果需要在最后进行排序,则必须制作一个副本,将每个项目拉出并插入一个新数组,最后需要 O(Nlog(N)) 额外的工作。
    • Suvesh 已经提到了这一点:*.com/questions/985843/sort-a-sorted-array/…
    • 是的,确实如此。但是,该问题似乎并未说明最终集合中项目的顺序很重要。即使最后额外“拉出每个项目”,我相信这种方法仍然是最有效的。
    • @erickson:看来他确实做到了——尽管很模糊。我认为我的答案提供的细节(除了给它一个“优先队列”的确切名称)是值得的。
    • a PriorityQueue 不允许从末尾删除元素
    【解决方案5】:

    一个非常简单的优化是在插入之前将排序数组中的最小值(因此应该是第一项)与新值进行比较。如果新值大于此值,则将元素替换为新值,然后重新使用数组。

    【讨论】:

      【解决方案6】:

      Collections.binarySearch()

      ArrayList.ensureCapcity()

      您的伪代码将一组新项目 N 插入到大小为 S 的排序列表 A 中,然后丢弃最小的项目。使用 Collections.binarySearch() 查找插入点。 [如果您的 List 不支持 RandomAccess,请阅读说明对性能的影响。 ArrayList 确实支持 RandomAccess。]

      List<Integer> l = new ArrayList<Integer>();
      l.add(new Integer(2));
      l.add(new Integer(3));
      l.add(new Integer(6));
      l.add(new Integer(9));
      
      l.ensureCapacity(l.size()+n);
      
      Random rand = new Random();
      for (int i=0; i < n; i++) {
        final Integer newInt = Integer.rand.nextInt(1000);
        int insertPoint = Collections.binarySearch(l, newInt);
        if (insertPoint < 0)  insertPoint = -(insertPoint + 1);
        l.add(insertPoint, newInt);
      }
      l.remove(0);
      

      但是,您确定要丢弃 1 件物品吗?还是您的意思是将一组新项目 N 插入大小为 S 的排序列表 A 并仅保留 S 最大的项目。在这种情况下,请跟踪最小值:

      int min = l.get(0);
      l.ensureCapacity(l.size()+n);
      
      Random rand = new Random();
      for (int i=0; i < n; i++) {
        final Integer newInt = Integer.rand.nextInt(1000);
        if (newInt > min) {
          int insertPoint = Collections.binarySearch(l, newInt);
          if (insertPoint < 0)  insertPoint = -(insertPoint + 1);
          l.add(insertPoint, newInt);
        }
      }
      

      但是,如果 N 很大,最好将 N 单独排序为一个排序数组,丢弃 N(0) 或 A(0) 中较小的一个,然后将两个排序数组合并在一起 [left as an读者练习]。

      如果您最终使用的是实际数组,请参阅 Arrays.binarySearchSystem.arraycopy

      【讨论】:

        【解决方案7】:

        如果需要,我能想到的最快算法是用新元素替换最小的元素,并通过重复与相邻元素交换将新元素推到适当的位置。

        编辑:代码假定数组是按降序排列的,因此最后一个元素是最小的。

        void Insert(int[] array, int newValue)
        {
            // If the new value is less than the current smallest, it should be
            // discarded
            if (new_value <= array[array.length-1])
                return;
        
            array[array.length-1] = newValue;
            for (int i = array.length-1; i > 0; --i)
            {
                if (newValue <= array[i-1])
                    break;
        
                // Swap array[i] with array[i-1]
                array[i] = array[i-1];
                array[i-1] = newValue;
            }
        }
        

        【讨论】:

        • 呃,这是最快的算法吗?如果我理解正确,这是 O(N)。您可以利用二分查找在 O(NlogN) 中执行此操作,这样会快得多。
        • O(NlogN) 比 O(N) 慢。如果你仔细想想,即使你做了一个二分查找来找到位置,你仍然需要移动所有的元素。所以,你有一个额外的步骤。
        • 是的。要么将数据保存为数组,在这种情况下,您必须移动所有内容以腾出空间,因此二进制搜索以 O(N + logN) = O(N) 结束,或者您具有链表类型的结构,在这种情况下,二进制搜索实际上不起作用。首先请注意,您必须测试新元素是否小于最小元素,在这种情况下您应该什么都不做。
        【解决方案8】:

        您可以使用二分搜索将值插入排序数组。

        【讨论】:

          【解决方案9】:

          如果您使用的是 ArrayList,则在对数组进行排序之前,如果新数字较大,则可以将数组中的最后一个数字替换为新数字。

          Java Collections.sort 使用归并排序,这在这种情况下并不是最有效的排序方式。您想使用二分搜索找到插入点,然后将所有后续数字移一位。

          编辑:这一切都可以用这样的数组来完成:

          public static int addDiscard(int[] list, int number)
          {
              if (number > list[list.length - 1])
              {
                  int index = findInsertionIndex(list, number); // use binary search
                  for (int i = list.length - 1; i > index; i--)
                  {
                      list[i] = list[i - 1];
                  }
                  list[index] = number;
              }
          }
          

          【讨论】:

            【解决方案10】:

            我不知道您是否可以更改数据结构,或者您需要支持哪些其他操作,但堆会更适合您描述的那种操作。

            【讨论】:

              【解决方案11】:

              这将使大小保持在 4 并按照我的理解做你想做的事情。

              SortedSet<Integer> set = new TreeSet<Integer>();
              set.add(2);
              set.add(3);
              set.add(6);
              set.add(9);
              Random rand = new Random();
              for (int i=0; i < n; i++) {
                int i = rand.nextInt(1000);
                set.remove(set.first());
                set.add(i);
              }    
              

              【讨论】:

                【解决方案12】:

                ShellSortNatural Mergesort 在大量预排序的数据上非常高效 (binary search 插入排序列表需要更多时间,因为无论如何一次更新都需要 O(n)。

                或者,您可以使用堆数据结构。

                【讨论】:

                  【解决方案13】:

                  您真的需要一次在线的一项算法吗?或者您实际上是在解析更大的数据集合并且只想要前 n 个项目?如果是后者,看partial qsort

                  【讨论】:

                    【解决方案14】:

                    我不确定上面的例子是否可行,n 是什么?如果您循环添加从 1 到 1,000 的随机 #,您将始终得到 1000、999、998 和 997 - 不是吗?我不认为添加 # 然后每次都使用是有效的 - 检查四个位置中的每一个并用更高的 # 替换可能会更快。

                    很大程度上取决于您将添加多少随机 #,到少数 # 添加并检查 4 个位置中的每一个 很多 # 添加只是假设您获得了范围内的最高值。

                    【讨论】:

                      【解决方案15】:

                      一个关键问题是,您是否需要在每个新项目生成后都知道前 4 个项目,还是在所有项目都生成后只需要前 4 个项目。此外,它是字面上的 4 个*项目,还是只是一个示例或插图?

                      因为如果您真的要生成数千个值并且只想要前 4 个值,我认为将每个新值与现有 4 个值进行比较并丢弃如果少于所有值会比做很多更快排序。这只是对每个新项目进行 4 次比较,而不是进行重复排序的可能更大的数量。

                      类似地,如果您只需要在流程结束时的前 N ​​个,将它们全部收集、排序,然后取前 N 个可能会更快。但同样,如果大多数值都被消除,排序“失败者”的相对位置可能会浪费大量时间。如果我们只想要前 4 名,那么一个项目是 #5 还是 #10,382,842 无关紧要。

                      【讨论】:

                        【解决方案16】:

                        这是另一个解决方案,它将操作合并为一个搜索、一个数组副本和一个值集。这避免了排序或循环的需要。

                        public static <T extends Comparable<T>> 
                                void insertAndRemoveSmallest(T[] array, T t) {
                            int pos = Arrays.binarySearch(array, t);
                            if (pos < 0) pos = ~pos;
                            // this is the smallest entry so no need to add it and remove it.
                            if (pos == 0) return;
                            pos--;
                            // move all the entries down one.
                            if (pos > 0) System.arraycopy(array, 1, array, 0, pos);
                            array[pos] = t;
                        }
                        

                        这个程序

                        public static void main(String... args) {
                            Integer[] ints = {2, 3, 7, 6, 9};
                            System.out.println("Starting with " + Arrays.toString(ints));
                            for (int i : new int[]{5, 1, 10, 8, 8}) {
                                insertAndRemoveSmallest(ints, i);
                                System.out.println("After adding " + i + ": " + Arrays.toString(ints));
                            }
                        }
                        

                        打印

                        Starting with [2, 3, 7, 6, 9]
                        After adding 5: [3, 5, 7, 6, 9]
                        After adding 1: [3, 5, 7, 6, 9]
                        After adding 10: [5, 7, 6, 9, 10]
                        After adding 8: [7, 6, 8, 9, 10]
                        After adding 8: [6, 8, 8, 9, 10]
                        

                        【讨论】:

                          最近更新 更多