【问题标题】:Find top N elements in an Array查找数组中的前 N ​​个元素
【发布时间】:2010-11-03 05:47:57
【问题描述】:

在无序列表(例如 100 个)中查找前 N 个(例如 10 个)元素的最佳解决方案是什么。

我想到的解决方案是 1. 使用快速排序对其进行排序,2. 获得前 10 名。

但是还有更好的选择吗?

【问题讨论】:

  • 如果 N 是小运行,找到 N 次的最大值
  • 这给了你最多 N 次,而不是前 N 个元素。
  • 你如何定义'top'?
  • 降序排列数组中的前 n 个元素
  • 请在此线程中查看答案stackoverflow.com/questions/251781/…

标签: java algorithm sorting


【解决方案1】:

时间可以减少到线性时间:

  1. 使用selection algorithm,它可以有效地在线性时间内找到未排序数组中的第 k 个元素。您可以使用快速排序的变体或更稳健的算法。

  2. 使用步骤 1 中获得的枢轴获取前 k 个。

【讨论】:

  • 这是一个非常好的方法。但我要提一下,线性(确定性)选择实际上非常慢 - 使用带有随机选择的枢轴的快速选择在典型问题规模上可能要快得多,但它可能需要二次时间。
【解决方案2】:

如果您正在处理固定长度整数等简单元素,那么如果您可以腾出与输入数据大小相同的内存缓冲区,则可以使用桶或基数排序在 O(n) 时间内完成排序,这将是最快的。

虽然有线性时间选择算法,the hidden constant is very high -- around 24。这意味着对于少于几百万个元素,O(nlog n) 算法通常会更快。

否则,在一般情况下,当您只能比较 2 个元素并确定哪个更大时,最好通过 heap data structure 解决问题。

假设您想要 n 个项目中的前 k 个。所有基于数据完全排序的解决方案都需要 O(nlog n) 时间,而使用堆只需要 O(nlog k) 时间——只需在前 k 个元素上构建一个堆,然后继续添加一个元素并删除最大值。这将为您留下一个包含最小 k 个元素的堆。

【讨论】:

    【解决方案3】:

    好吧,您可以在 O(n) 时间内从未排序的数组中创建一个堆,并且您可以在 O(log(n)) 时间内从堆中获取顶部元素。所以你的总运行时间是 O(n + k*log(n))。

    【讨论】:

      【解决方案4】:

      您可以使用List 和guava 的Comparators 类来获得想要的结果。这是一个高度优化的解决方案。请参阅下面的示例,该示例获得前 5 个数字。 api可以找到here

      import java.util.Comparator;
      import java.util.List;
      import java.util.stream.Collector;
      
      import org.junit.Test;
      
      import com.google.common.collect.Comparators;
      import com.google.common.collect.Lists;
      
      public class TestComparator {
      
          @Test
          public void testTopN() {
              final List<Integer> numbers = Lists.newArrayList(1, 3, 8, 2, 6, 4, 7, 5, 9, 0);
              final Collector<Integer, ?, List<Integer>> collector = Comparators.greatest(5,
                      Comparator.<Integer>naturalOrder());
              final List<Integer> top = numbers.stream().collect(collector);
              System.out.println(top);
          }
      
      }
      

      输出:[9, 8, 7, 6, 5]

      【讨论】:

        【解决方案5】:

        最好的解决方案是使用您选择的语言提供的任何设施,这将使您的生活更轻松。

        但是,假设这是一个与您应该选择哪种算法更相关的问题,我将在这里提出一种不同的方法。如果您说的是 100 中的 10,那么您通常不应该过多担心性能,除非您想每秒执行 很多次 次。

        例如,这段 C 代码(它的效率与我能做到的一样低效)仍然需要不到十分之一秒的时间来执行。我什至没有足够的时间考虑去喝杯咖啡。

        #include <stdio.h>
        #include <stdlib.h>
        #include <time.h>
        
        #define SRCSZ 100
        #define DSTSZ 10
        
        int main (void) {
            int unused[SRCSZ], source[SRCSZ], dest[DSTSZ], i, j, pos;
        
            srand (time (NULL));
            for (i = 0; i < SRCSZ; i++) {
                unused[i] = 1;
                source[i] = rand() % 1000;
            }
        
            for (i = 0; i < DSTSZ; i++) {
                pos = -1;
                for (j = 0; j < SRCSZ; j++) {
                    if (pos == -1) {
                        if (unused[j]) {
                            pos = j;
                        }
                    } else {
                        if (unused[j] && (source[j] > source[pos])) {
                            pos = j;
                        }
                    }
                }
                dest[i] = source[pos];
                unused[pos] = 0;
            }
        
            printf ("Source:");
            for (i = 0; i < SRCSZ; i++) printf (" %d", source[i]);
            printf ("\nDest:");
            for (i = 0; i < DSTSZ; i++) printf (" %d", dest[i]);
            printf ("\n");
        
            return 0;
        }
        

        通过time 运行它会给你(我已经对输出进行了一些格式化以使其可读,但不影响结果):

        Source: 403 459 646 467 120 346 430 247 68 312 701 304 707 443
                753 433 986 921 513 634 861 741 482 794 679 409 145 93
                512 947 19 9 385 208 795 742 851 638 924 637 638 141
                382 89 998 713 210 732 784 67 273 628 187 902 42 25
                747 471 686 504 255 74 638 610 227 892 156 86 48 133
                63 234 639 899 815 986 750 177 413 581 899 494 292 359
                60 106 944 926 257 370 310 726 393 800 986 827 856 835
                66 183 901
        Dest: 998 986 986 986 947 944 926 924 921 902
        
        real    0m0.063s
        user    0m0.046s
        sys     0m0.031s
        

        只有当数字变大时,您通常才需要担心。不要误会我的意思,我并不是说您不应该考虑性能。你不应该做的是花太多时间优化无关紧要的东西 - YAGNI 和所有爵士乐。

        与所有优化问题一样,衡量不要猜测!

        【讨论】:

        • 但它是一个合适的面试答案吗??
        • 是的,我相信会的。到目前为止,雇用的最危险的人是过度工程师。当被要求提供一个计算项目列表总和的函数时,这个人会给你一个 900 行的怪物,它可以配置为处理整数、浮点数、复数,并且还可以对每个项目执行预计算一个回调函数。这不是那种你希望从事有明确交付日期的工作的人 :-) 此外,它还提供了候选人可以自己思考的宝贵见解。任何猴子都可以剪代码。
        • 我会当场雇用这样的人,只要他们解释为什么这样做以及后果是什么。另请参阅 stackoverflow.com/questions/903572/…stackoverflow.com/questions/445425/… 虽然有......我们应该说,动画......关于优点的讨论:-)
        • 呵呵,实用性+1...但是回复:什么样的答案是“最好的”我认为关键是受访者要问:(1)典型的问题大小是多少? (2) 它需要多快? (顺便说一句,如果在大问题上的表现并不重要,那么最好的答案是“使用该语言内置的 sort() :-P)
        【解决方案6】:

        把一切都委托给 Java 怎么样 ;)

        function findTopN(Array list, int n)
        {
            Set sortedSet<Integer> = new TreeSet<>(Comparators.naturalOrder());
        
            // add all elements from list to sortedSet
        
            // return the first n from sortedSet
        }
        

        我并不是说这是最好的方法。我还是觉得尹竹的求第k大元素的方法是最好的答案。

        【讨论】:

        • TreeSet 将删除可能不需要的重复项。
        • 一个简单而干净的解决方案,但与可以在 O(n) 平均情况下选择前 k 个元素的选择算法(例如快速选择)相比,这将是 O(nlogn) 平均/最坏情况和 o 时间,其中 n 是输入数组的大小。
        【解决方案7】:

        是的,您可以在 O(n) 中做到这一点,只需保留前 N 个(排序的)运行列表。您可以使用常规库函数或 sorting network 对运行列表进行排序。例如。一个使用 3 的简单演示,并显示运行列表中的哪些元素每次迭代都会发生变化。

        5 2 8 7 9

        i = 0
        top[0] <= 5
        
        i = 1
        top[1] <= 2
        
        i = 2
        top[2] <= top[1] (2)
        top[1] <= top[0] (5)
        top[0] <= 8
        
        i = 3
        top[2] <= top[1] (5)
        top[1] <= 7
        
        i = 4
        top[2] <= top[1] (7)
        top[1] <= top[0] (8)
        top[0] <= 9
        

        【讨论】:

        • 如果所选项目的数量很少,这很好。要从一百万个中选择 10000 个顶级元素,这不再是最佳选择。
        • 这有点像Insertion sort
        【解决方案8】:

        写在选择排序和插入排序实现的下方。对于更大的数据集,我建议插入排序比选择排序更好

        public interface FindTopValues
        {
          int[] findTopNValues(int[] data, int n);
        }
        

        插入排序实现:

        public class FindTopValuesInsertionSortImpl implements FindTopValues {  
        
        /**
         * Finds list of the highest 'n' values in the source list, ordered naturally, 
         * with the highest value at the start of the array and returns it 
         */
        @Override
        public int[] findTopNValues(int[] values, int n) {
        
            int length = values.length;
            for (int i=1; i<length; i++) {
                int curPos = i;
                while ((curPos > 0) && (values[i] > values[curPos-1])) {
                    curPos--;
                }
        
                if (curPos != i) {
                    int element = values[i];
                    System.arraycopy(values, curPos, values, curPos+1, (i-curPos));
                    values[curPos] = element;
                }
            }       
        
            return Arrays.copyOf(values, n);        
        }   
        
        }
        

        选择排序实现:

        public class FindTopValuesSelectionSortImpl implements FindTopValues {
        
        /**
         * Finds list of the highest 'n' values in the source list, ordered naturally, 
         * with the highest value at the start of the array and returns it 
         */
        @Override
        public int[] findTopNValues(int[] values, int n) {
            int length = values.length;
        
            for (int i=0; i<=n; i++) {
                int maxPos = i;
                for (int j=i+1; j<length; j++) {
                    if (values[j] > values[maxPos]) {
                        maxPos = j;
                    }
                }
        
                if (maxPos != i) {
                    int maxValue = values[maxPos];
                    values[maxPos] = values[i];
                    values[i] = maxValue;
                }           
            }
            return Arrays.copyOf(values, n);        
        }
        }
        

        【讨论】:

          【解决方案9】:

          是的,有一种方法比快速排序做得更好。正如殷朱所指出的,您可以先搜索第 k 个最大元素,然后使用该元素值作为您的枢轴来拆分数组

          【讨论】:

            【解决方案10】:

            面试时我被要求使用相同的算法。 我做到了,如果有人可以将其与 Java 中最快的算法进行比较 - 将非常有用。

                public int[] findTopNValues(int[] anyOldOrderValues, int n) {
                    if (n < 0) {
                        return new int[]{};
                    }
                    if (n == 1) {
                        return new int[]{findMaxValue(anyOldOrderValues)};
                    }
            
                    int[] result = new int[n + 1];
                    for (int i = 0; i < Math.min(n, anyOldOrderValues.length); i++) {
                        result[i] = anyOldOrderValues[i];
                    }
                    Arrays.sort(result);
            
                    int max = result[0];
                    for (int i = n - 1; i < anyOldOrderValues.length; i++) {
                        int value = anyOldOrderValues[i];
                        if (max < value) {
                            result[n] = value;
                            Arrays.sort(result);
                            int[] result1 = new int[n + 1];
                            System.arraycopy(result, 1, result1, 0, n);
                            result = result1;
                            max = result[0];
                        }
                    }
                    return convertAndFlip(result, n);
                }
            
                public static int[] convertAndFlip(int[] integers, int n) {
                    int[] result = new int[n];
                    int j = 0;
                    for (int i = n - 1; i > -1; i--) {
                        result[j++] = integers[i];
                    }
                    return result;
                }
            

            并对此进行测试:

            public void testFindTopNValues() throws Exception {
                final int N = 100000000;
                final int MAX_VALUE = 100000000;
                final int returnArray = 1000;
                final int repeatTimes = 5;
            
                FindTopValuesArraySorting arraySorting = new FindTopValuesArraySorting();
            
                int[] randomArray = createRandomArray(N, MAX_VALUE);
                for (int i = 0; i < repeatTimes; i++) {
            
                    long start = System.currentTimeMillis();
                    int[] topNValues = arraySorting.findTopNValues(randomArray, returnArray);
                    long stop = System.currentTimeMillis();
            
                    System.out.println("findTopNValues() from " + N + " elements, where MAX value=" + (MAX_VALUE - 1) + " and return array size " + returnArray + " elements : " + (stop - start) + "msec");
                    // System.out.println("Result list = " + Arrays.toString(topNValues));
                }
            }
            
            private static int[] createRandomArray(int n, int maxValue) {
                Random r = new Random();
                int[] arr = new int[n];
                for (int i = 0; i < n; i++) {
                    arr[i] = r.nextInt(maxValue);
                }
                return arr;
            }
            

            结果类似于:

            findTopNValues() from 100000000 elements, where MAX value=99999999 and return array size 1000 elements : 395msec
            findTopNValues() from 100000000 elements, where MAX value=99999999 and return array size 1000 elements : 311msec
            findTopNValues() from 100000000 elements, where MAX value=99999999 and return array size 1000 elements : 473msec
            findTopNValues() from 100000000 elements, where MAX value=99999999 and return array size 1000 elements : 380msec
            findTopNValues() from 100000000 elements, where MAX value=99999999 and return array size 1000 elements : 406msec
            

            ~400msc 平均结果,用于从 100.000.000 个初始元素的数组中获取 1000 个最大整数。 不错!

            刚刚尝试了上面的那一套:

            findTopNValues() from 101 elements and return array size 10 elements : 1msec
            Result list = [998, 986, 986, 986, 947, 944, 926, 924, 921, 902]
            Original list = [403, 459, 646, 467, 120, 346, 430, 247, 68, 312, 701, 304, 707, 443, 753, 433, 986, 921, 513, 634, 861, 741, 482, 794, 679, 409, 145, 93, 512, 947, 19, 9, 385, 208, 795, 742, 851, 638, 924, 637, 638, 141, 382, 89, 998, 713, 210, 732, 784, 67, 273, 628, 187, 902, 42, 25, 747, 471, 686, 504, 255, 74, 638, 610, 227, 892, 156, 86, 48, 133, 63, 234, 639, 899, 815, 986, 750, 177, 413, 581, 899, 494, 292, 359, 60, 106, 944, 926, 257, 370, 310, 726, 393, 800, 986, 827, 856, 835, 66, 183, 901]
            

            【讨论】:

              【解决方案11】:

              最好的算法很大程度上取决于 K 的大小。 如果 K 很小,那么只需遵循 BubbleSort 算法并迭代外循环 K 次 将给出前 K 值。 复杂度为 O(n*k)。

              但是,对于接近 n 的 K 值,复杂度将接近 O(n^2)。在这种情况下,快速排序可能是一个不错的选择。

              【讨论】:

              • BubbleSort 本身有 O(nlogn)。所以复杂度不会是 O(n*k)。更多订单不取决于因素的大小,即这里的 n 和 k!
              【解决方案12】:
              public class FindTopValuesSelectionSortImpl implements FindTopValues {
              
              /**
               * Finds list of the highest 'n' values in the source list, ordered naturally, 
               * with the highest value at the start of the array and returns it 
               */
              @Override
              public int[] findTopNValues(int[] values, int n) {
                  int length = values.length;
              
                  for (int i=0; i<=n; i++) {
                      int maxPos = i;
                      for (int j=i+1; j<length; j++) {
                          if (values[j] > values[maxPos]) {
                              maxPos = j;
                          }
                      }
              
                      if (maxPos != i) {
                          int maxValue = values[maxPos];
                          values[maxPos] = values[i];**strong text**
                          values[i] = maxValue;
                      }           
                  }
                  return Arrays.copyOf(values, n);        
              }
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2023-03-09
                • 1970-01-01
                • 1970-01-01
                • 2013-10-27
                相关资源
                最近更新 更多