【问题标题】:Is this method to get the closest number in a sorted List most effective?这种获取排序列表中最接近数字的方法是否最有效?
【发布时间】:2019-08-05 01:00:59
【问题描述】:

我有大量的整数数组(大小在 10'000 和 1'400'000 之间)。我想让第一个整数更大的值。该值永远不会在数组中。

我已经寻找了各种解决方案,但我只找到了:

  1. 估计每个值的方法,不是为排序列表或数组设计的(具有 O(n) 时间复杂度)。
  2. 递归方法和/或不是为非常大的列表或数组设计的方法(时间复杂度为 O(n) 或更高,但在其他语言中,所以我不确定)。

我设计了自己的方法。这里是:

int findClosestBiggerInt(int value, int[] sortedArray) {
    if( sortedArray[0]>value ||
            value>sortedArray[sortedArray.length-1] )   // for my application's convenience only. It could also return the last.
        return sortedArray[0];

    int exp = (int) (Math.log(sortedArray.length)/Math.log(2)),
        index = (int) Math.pow(2,exp);
    boolean dir; // true = ascend, false = descend.
    while(exp>=0){
        dir = sortedArray[Math.min(index, sortedArray.length-1)]<value;
        exp--;
        index = (int)( index+ (dir ? 1 : -1 )*Math.pow(2,exp) );
    }

    int answer = sortedArray[index];
    return answer > value ? answer : sortedArray[index+1];
}

它的时间复杂度为 O(log n)。对于长度为 1'400'000 的数组,它将在 while 块内循环 21 次。不过,我不确定它是否无法改进。

有没有更有效的方法来做到这一点,而不需要外部包的帮助?节省的任何时间都很棒,因为这种计算非常频繁。

【问题讨论】:

  • 简单的二分搜索有什么问题?
  • 我不确定。不是对数组中的特定值进行二分查找吗?
  • 更多关于代码审查的话题。你应该在那里问。
  • 但是,二分查找可用于解决更广泛的问题,例如在数组中找到相对于目标的下一个最小或下一个最大元素,即使它不存在来自数组。来自维基百科。
  • 另外,你需要保留重复的能力吗? TreeSet 已内置此功能。

标签: java arrays algorithm


【解决方案1】:

有没有更有效的方法来做到这一点,而不需要外部包的帮助?节省的任何时间都很棒,因为这种计算非常频繁。

这是一种使用地图而不是数组的方法。

      int categorizer = 10_000;
      // Assume this is your array of ints.
      int[] arrayOfInts = r.ints(4_000, 10_000, 1_400_000).toArray();

您可以像这样将它们分组在地图中。

       Map<Integer, List<Integer>> ranges =
            Arrays.stream(arrayOfInts).sorted().boxed().collect(
                  Collectors.groupingBy(n -> n / categorizer));

现在,当您想查找更高的下一个元素时,您可以获得包含该数字的列表。

假设你想要下一个大于 982,828 的数字

      int target = 982,828;
      List<Integer> list = map.get(target/categorizer); // gets the list at key = 98

现在只需使用您喜欢的方法处理列表。一注。在某些情况下,您的最高数字可能会出现在该列表之后的其他列表中,具体取决于差距。您可能需要通过调整数字的分类方式或搜索后续列表来考虑这一点。但这可以大大减少您正在使用的列表的大小。

【讨论】:

  • 我做了一些测试。 Gene 和 Ruakh 的答案有相当快的方法,但你的方法比 TreesSet#higher 快得多。
【解决方案2】:

正如 Gene 的回答所示,您可以通过二分搜索来做到这一点。内置的java.util.Arrays 类提供了a binarySearch method 来为你做这件事:

int findClosestBiggerInt(final int value, final int[] sortedArray) {
    final int index = Arrays.binarySearch(sortedArray, value + 1);
    if (index >= 0) {
        return sortedArray[index];
    } else {
        return sortedArray[-(index + 1)];
    }
}

你会发现它比你写的方法快得多;它仍然是 O(log n) 时间,但常数因子会低得多,因为它不会执行像 Math.logMath.pow 这样的昂贵操作。

【讨论】:

    【解决方案3】:

    二分搜索很容易修改为您想要的。

    与目标完全匹配的标准二分搜索维护一个[lo,hi] 整数括号,其中目标值(如果存在)始终在其中。每一步都会使支架变小。如果括号的大小为零(hi

    对于这个新问题,除了目标值的定义之外,不变量是完全一样的。我们必须注意不要以可能消除下一个更大元素的方式缩小括号。

    这是“标准”二分搜索:

    int search(int tgt, int [] a) {
      int lo = 0, hi = a.length - 1;
      // loop while the bracket is non-empty
      while  (lo <= hi) {
        int mid = lo + (hi - lo) / 2;
        // if a[mid] is below the target, ignore it and everything smaller
        if (a[mid] < tgt) lo = mid + 1;
        // if a[mid] is above the target, ignore it and everything bigger
        else if (a[mid] > tgt) hi = mid - 1;
        // else we've hit the target
        else return mid;
      }
      // The bracket is empty. Return "nothing."
      return -1;
    }
    

    在我们的新案例中,显然需要更改的部分是:

        // if a[mid] is above the target, ignore it and everything bigger
        else if (a[mid] > tgt) hi = mid - 1;
    

    那是因为a[mid] 可能是答案。我们无法将其从括号中删除。显而易见的尝试是保留a[mid]

        // if a[mid] is above the target, ignore everything bigger
        else if (a[mid] > tgt) hi = mid;
    

    但是现在我们在一个案例中引入了一个新问题。如果lo == hi,即括号缩小到1个元素,这个if没有进展!它设置hi = mid = lo + (hi - lo) / 2 = lo。括号的大小保持为 1。循环永远不会终止。

    因此,我们需要的另一个调整是循环条件:当括号达到大小 1 或更少时停止:

      // loop while the bracket has more than 1 element.
      while  (lo < hi) {
    

    对于大小为 2 或更大的括号,lo + (hi - lo) / 2 始终小于 hi。设置hi = mid 取得进展。

    我们需要的最后一个修改是在循环终止后检查括号。原来的算法现在是三种情况,而不是一种:

    1. 空或
    2. 包含一个元素,即答案,
    3. 或者不是。

    在返回之前很容易解决这些问题。总之,我们有:

    int search(int tgt, int [] a) {
      int lo = 0, hi = a.length - 1;
      while  (lo < hi) {
        int mid = lo + (hi - lo) / 2;
        if (a[mid] < tgt) lo = mid + 1;
        else if (a[mid] > tgt) hi = mid;
        else return mid;
      } 
      return lo > hi || a[lo] < tgt ? -1 : lo;
    }
    

    正如您所指出的,对于 140 万个元素的数组,此循环将执行不超过 21 次。我的 C 编译器为整个事情生成了 28 条指令;循环是 14。21 次迭代应该是几微秒。它只需要很小的常量空间,并为 Java 垃圾收集器生成零工作。很难看出你会如何做得更好。

    【讨论】:

    • 感谢您的精彩回答。经过一些测试,Arrays#binarySearch 方法似乎快了 40% 左右。我想这与较低级别有关,但这只能证明您的方法经过了令人印象深刻的优化而优雅。
    • @WholeBrain 是的,我认为 Arrays.binarySearch 是在本机代码中实现的以提高速度。我只发布了这个,因为我遇到了许多不知道二进制搜索可以适应“下一个最大/最小”的人,或者尝试这样做并弄错或错误的 impl。另一个有趣的变化是精确搜索以找到任意数量的重复中的第一个/最后一个,仍然在 log(n) 时间内。
    猜你喜欢
    • 1970-01-01
    • 2022-01-16
    • 2020-11-16
    • 2015-10-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-26
    • 2022-11-24
    相关资源
    最近更新 更多