【问题标题】:What is the fastest algorithm to determine if any number in a sorted array is multiple of `x`?确定排序数组中的任何数字是否是“x”的倍数的最快算法是什么?
【发布时间】:2017-12-27 04:50:24
【问题描述】:

给定一个正整数 x 和一个已排序的正整数数组 A

是否有比O(N) 更快的算法来确定A 中的任何元素是否是x 的倍数? A中没有负面元素。

到目前为止,我唯一的想法是天真的循环A,我不知道是否有任何方法可以利用A 已排序这一事实来加快速度。

【问题讨论】:

  • 不,没有。查看数组A 中的一个值一般不会为您提供任何关于左侧或右侧是否存在这样一个倍数的线索。它可能在任何地方。即使您只查看了数组中的一个值,您仍然不会知道最后一个剩余值是否是x 的倍数。所以它将是 O(n).
  • 我认为这很大程度上取决于x 的大小和A 的长度。如果xA 很大,那么在A 的第一个和最后一个元素之间对x 的所有倍数进行二分搜索可能会有所回报。
  • @tobias_k,为了提高效率,您还需要 A 中的两个极值相距不远。如果 A 有 10000 个从 1 到 100000000x+1 的值,最好扫描 A 中的所有值。
  • 我猜你需要线性时间。考虑这种特殊情况,给定一个已排序的正整数数组,它是否包含偶数(2 的倍数)?我认为不查看每个元素就无法找到 ans。
  • 如果阵列很大,您可以通过 SIMD/多线程获得恒定因子加速。对于特定的输入模式,您可以使用恒定时间。但我认为在一般情况下没有比 O(n) 更快的东西。另外,它可以包含重复项吗?

标签: algorithm math


【解决方案1】:

这似乎很大程度上取决于x 的大小和A 中的元素数量,尤其是Ax 的候选倍数的数量。

A 中的特定数字进行二进制搜索需要O(log(n)) 时间(n 是A 中的元素数),因此如果k 之间存在x 的可能倍数A 的第一个和最后一个元素,需要O(k * log(N)) 来检查它们。如果该数字小于n,则可以使用此算法,否则只需进行线性搜索。

(另外,上面的算法可能有一些小的优化。例如,一旦你检查了x*i(但没有找到它),你可以使用x*i应该是的位置作为下限搜索 x*(i+1) 而不是数组的第一个元素。)

【讨论】:

  • 你也可以让它成为一个递归算法,一次搜索 x 的所有倍数,在每一轮将它们拆分为下半部分和上半部分,并且每次再次检查是否 k*logn
【解决方案2】:

HT @tobias_k 的评论。

您可以在 ~O(n/x) 中解决它(更新这实际上可能是O(N*log(N)/X^2))。您同时对 x 的所有倍数进行二分搜索。在每次迭代中细分每个搜索空间以及当搜索空间不能包含x的倍数,你中止那个分支。因此,不是对每个值进行二分搜索,而是对所有值进行二分搜索,但只搜索那些仍然在其范围内包含有效倍数的分支。最好的事情是它完全阻止了对相同空间的研究两次,这使得最坏的情况 x=1 或 O(n/1) O(n)。在最好的情况下,它会知道范围不能包含倍数并在 O(1) 中中止。

由于保证了 O(n) 的更坏情况,在这种情况下,您基本上会错过每一次该死的缓存查找(请记住,在现实世界中,这可能最终比时间复杂度更重要,因此请测试此类事情)。你会得到理论上的时间复杂度,它可能比 O(n) 更好,但永远不会比这更糟(除了在数组周围跳跃会错过缓存,因为这就是计算机最终在现实世界中实际工作的方式)。

正如预测的那样,速度增加很大程度上取决于 k (x) 的值。

这开始比 k = ~128 处的原始循环更快。 (除数)

截断分支设法使现实世界超越原始循环。我假设 n 计数并不重要,因为它的规模似乎大致相同,但也许直接检查会更好。

注意:根据这段代码的性质,它会跳过双打,这是计数的差异。

public class MultipleSearch {

    public static void main(String[] args) {
        Random random = new Random();
        int[] array = new int[500000000];
        for (int i = 0, m = array.length; i < m; i++) {
            array[i] = Math.abs(random.nextInt());
        }
        Arrays.sort(array);
        for (int k = 1; k < 16777216; k *= 2) {
            long time;
            time = System.currentTimeMillis();
            binaryFactorLocator(array, k);
            System.out.println("Factors found multi: " + (System.currentTimeMillis() - time) + " @" + k);
            time = System.currentTimeMillis();
            loopFactorLocator(array, k);
            System.out.println("Factors found loop: " + (System.currentTimeMillis() - time) + " @" + k);
        }
    }

    public static void loopFactorLocator(int[] array, int k) {
        int count = 0;
        for (int i = 0, m = array.length; i < m; i++) {
            if (array[i] % k == 0) {
                count++;
                //System.out.println("loop: " + array[i] + " value at index " + i + " is a proven multiple of " + k);
            }
        }
        System.out.println(count + " items found.");
    }

    public static void binaryFactorLocator(int[] array, int k) {
        int count = binaryFactorLocator(0, array, k, 0, array.length);
        System.out.println(count + " items found.");
    }

    public static int binaryFactorLocator(int count, int[] array, int k, int start, int end) {
        if (start >= end) { //contains zero elements. (End is exclusive)
            return count;
        }
        int startValue = array[start]; //first value
        int endValue = array[end - 1]; //last value;
        if (startValue / k == endValue / k) { //if both values are contained within the same factor loop.
            if (startValue % k == 0) { //check lower value for being perfect factor.
                //System.out.println("multi-binary: " + startValue + " value at index " + start + " is a proven multiple of " + k);
                return count + 1;
            }
            return count; //There can be no other factors within this branch.
        }
        int midpoint = (start + end) / 2; //subdivide
        count = binaryFactorLocator(count, array, k, start, midpoint); //recurse.
        count = binaryFactorLocator(count, array, k, midpoint, end); //recurse.
        return count;
    }
}

这个实现应该是相当可靠的,因为它截断了 start/k == end/k 元素内的循环,它应该跳过 double (有时,它可能会在两个 double 值之间切割)。显然,像这样的递归可能不会是最优的,或许应该用更少的调用堆栈堆栈重写。

474682772 items found.
Factors found multi: 21368 @1
500000000 items found.
Factors found loop: 5653 @1
236879556 items found.
Factors found multi: 21573 @2
250000111 items found.
Factors found loop: 7782 @2
118113043 items found.
Factors found multi: 19785 @4
125000120 items found.
Factors found loop: 5445 @4
58890737 items found.
Factors found multi: 16539 @8
62500081 items found.
Factors found loop: 5277 @8
29399912 items found.
Factors found multi: 12812 @16
31250060 items found.
Factors found loop: 5117 @16
14695209 items found.
Factors found multi: 8799 @32
15625029 items found.
Factors found loop: 4935 @32
7347206 items found.
Factors found multi: 5886 @64
7812362 items found.
Factors found loop: 4815 @64
3673884 items found.
Factors found multi: 3441 @128
3906093 items found.
Factors found loop: 4479 @128
1836857 items found.
Factors found multi: 2100 @256
1953038 items found.
Factors found loop: 4592 @256
918444 items found.
Factors found multi: 1335 @512
976522 items found.
Factors found loop: 4361 @512
459141 items found.
Factors found multi: 959 @1024
488190 items found.
Factors found loop: 4447 @1024
229495 items found.
Factors found multi: 531 @2048
243961 items found.
Factors found loop: 4114 @2048
114715 items found.
Factors found multi: 295 @4096
121964 items found.
Factors found loop: 3894 @4096
57341 items found.
Factors found multi: 195 @8192
61023 items found.
Factors found loop: 4061 @8192
28554 items found.
Factors found multi: 106 @16384
30380 items found.
Factors found loop: 3757 @16384
14282 items found.
Factors found multi: 65 @32768
15207 items found.
Factors found loop: 3597 @32768
7131 items found.
Factors found multi: 35 @65536
7575 items found.
Factors found loop: 3288 @65536
3678 items found.
Factors found multi: 17 @131072
3883 items found.
Factors found loop: 3281 @131072
1796 items found.
Factors found multi: 13 @262144
1900 items found.
Factors found loop: 3243 @262144
873 items found.
Factors found multi: 6 @524288
921 items found.
Factors found loop: 2970 @524288
430 items found.
Factors found multi: 3 @1048576
456 items found.
Factors found loop: 2871 @1048576
227 items found.
Factors found multi: 2 @2097152
238 items found.
Factors found loop: 2748 @2097152
114 items found.
Factors found multi: 1 @4194304
120 items found.
Factors found loop: 2598 @4194304
48 items found.
Factors found multi: 0 @8388608
51 items found.
Factors found loop: 2368 @8388608

【讨论】:

  • 这个算法的复杂度不是O(n/x)。它通常不依赖于x 的值。在最坏的情况下是O(n)。但这并不重要,因为它在实际数据上比 for 循环要快得多。
  • 你能用一个简单的例子解释更多或演示吗?我不太了解它是如何工作的,我得到的是你试图减少 x 的多个连续二进制搜索的潜在范围?
  • @shole,如果我理解正确的话,这个答案和 Zen 的答案是一样的。
  • @Tata​​rize,在最坏的情况下,您的binaryFactorLocator 的调用次数大约为1 + 2 + 4 + .. + n,小于2*n。没有log(n) 乘数。并且没有/X^2 部分,因为对于每个X,我们可以构建一个ai = x*i+1 数组,这将导致O(n) 操作。
  • 作为旁注,尝试使用System.nanoTime() 来测量经过的时间。这是一个计时器,currentTimeMillis() 是一个时钟。为什么这很重要:stackoverflow.com/a/351571
【解决方案3】:

在之前的尝试中,我尝试了一个简单的二分搜索,但正如所指出的,这实际上并没有导致任何结果。

这是我最好的尝试。我怀疑这是否值得麻烦,对于现实世界的案例来说它甚至可能会更慢,但是就这样吧。

如果你有一个排序好的正整数数组 A[0..n],并且你想检查 A[i..j] 中是否存在正整数 X 的倍数,其中 0≤i

If i>j then A[i..j] ist empty and thus contains no multiple of X
Else if A[i] % X = 0 or A[j] % X = 0 then A[i..j] contains a multiple of X
Else if A[i] / X = A[j] / X then A[i..j] contains no multiple of X
Else A[i..j] contains a multiple of X iff A[i+1..(i+j)/2] or A[(i+j)/2+1..j-1] contains a multiple of X

我的猜测是复杂度会是 O(n/X) 左右,因此,在总体方案上并没有真正的改进。

编辑添加: 如果您的数据真的很奇特,这实际上可能会有所帮助。在许多情况下,它实际上可能会受到伤害:

  • 管理返回堆栈会产生开销(第一个递归调用是非终端的)
  • 因为我们在数据中来回跳过而不是遍历它,所以我们破坏了缓存局部性
  • 我们使处理器的分支预测更加困难

【讨论】:

  • 这看起来好多了,我真的在研究它。情况 3 和 A[i] = A[j] 一样吧?
  • @Shale:其实不一样,因为是整数除法。例如,11/5 = 14/5 = 2。这就是让我们跳过值块的情况。
  • 回家后让我试试这个方法,至少目前对我来说听起来很棒
  • 看起来不错,在某些情况下比简单的迭代要快得多。但是这个算法的复杂度还是O(n)
  • 同意,但这个想法本身很聪明
【解决方案4】:

我相信一般问题的答案是否定的。假设A 具有以下结构:A ai 的第i'th 元素由i*x +1 给出,因此A 中没有元素是x 的倍数。但是,您永远不会通过使用上述任何“诡计”来节省任何时间……您基本上必须根据您对Ax 的了解做出选择。您基本上可以在 O(N) 和 O(K) 之间进行选择,其中 K 是 Ax 的可能倍数,您可以通过使用散列来实现 O(K),这样 i*x in A 就变成了恒定时间操作(平均...)。

【讨论】:

    【解决方案5】:

    尝试两件事 - 首先找到数组中大于 x 的第一个元素。无需搜索下半部分。二进制搜索可以做到这一点。在某些情况下这会减少时间,但如果第一个元素大于 x,那么显然不需要。然后,在确定了可能是 x 倍数的部分后,如果运行单个线程,或者如果您可以运行多个线程,则将上半部分分成多个段,并进行单独的线程搜索,然后进行二进制搜索。我认为这可能是最有效的方法,但需要注意以下几点。

    1. 如果数组中第一个大于 x 的元素相当低,那么设置二分查找比线性扫描要花费更多时间。

    2. 进行二分搜索也是有代价的。如果您的数组不是很大,那么使用它的效率将低于线性搜索。我不是在说算法的顺序。我只考虑执行时间。

    3. 如果您可以执行多个线程,那么设置这些线程的成本也相当高。除非您的阵列非常长,否则您也可能不会从中获得任何好处。但是,如果它有数百万个项目长,您可以通过将其拆分成更小的块来受益。我想说这种场景很少有用。

    【讨论】:

      【解决方案6】:

      迭代地从A 中减去x。如果任何结果等于 0,则停止。

      关于减法,您在最坏的情况下这样做1 + {max(A) DIV x} 次,因为您必须在所有其他人都已通过检查失败后从A 的最大元素中减去x,然后再一次(因此是 1)到最大元素本身,它也没有通过检查,比如 7 DIV 3 = 2,所以在三个迭代中:

      1. 7 - 3 = 4
      2. 4 - 3 = 1
      3. 1 - 3 = -2 , != 0 因此不是 可分的

      这仍然符合O(n) 的条件,但速度很快,对数组进行整数减法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-09-22
        • 2020-08-09
        • 2012-03-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-10-19
        相关资源
        最近更新 更多