旋转序列的两种算法的速度。 (摘自《编程珍珠》一书)

Posted

技术标签:

【中文标题】旋转序列的两种算法的速度。 (摘自《编程珍珠》一书)【英文标题】:Speed of two algorithms rotating a sequence. (from the book Programming Pearls) 【发布时间】:2014-08-16 14:41:07 【问题描述】:

Programming Pearls一书的第 2 列中,有一个问题要求你设计一个算法来将字符串向左旋转 k 个位置。例如字符串为“12345”,k=2,则结果为“34512”。

第一种算法是模拟交换过程,即将x[(i + k) % n]放入x[i],重复直到完成。

第二种算法使用我们只需要交换 a="12" 和 b="345" 的观察,即前 k 个字符和最后 n - k 个字符。我们可以先将 a 反转为 a'="21",然后将 b 反转为 b'="543',然后将 (a'b')' 反转为 ba。

以下是我的代码:

算法一:

#define NEXT(j) ((j + k) % n)
#define PREV(j) ((j + n - k) % n)

#include "stdio.h"
#include "stdlib.h"

int gcd(int a, int b) 
    return (a % b == 0 ? b : gcd(b, a % b));


void solve(int *a, int n, int k) 
    int len = gcd(n, k);
    for (int i = 0; i < len; i++) 
        int x = a[i];
        int j = i;
        do 
            a[j] = a[NEXT(j)];
            j = NEXT(j);
         while (j != i);
        a[PREV(j)] = x;
    


int main(int argc, char const *argv[])

    int n, k;
    scanf("%d %d", &n, &k);

    int *a = malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) a[i] = i;

    solve(a, n, k);

    free(a);

    return 0;

算法2:

#include "stdio.h"
#include "stdlib.h"

void swap(int *a, int *b) 
    int t = *a;
    *a = *b;
    *b = t;


void reverse(int *a, int n) 
    int m = n / 2;
    for (int i = 0; i < m; i++) 
        swap(a + i, a + (n - 1 - i));
    


void solve(int *a, int n, int k) 
    reverse(a, k);
    reverse(a + k, n - k);
    reverse(a, n);


int main(int argc, char const *argv[])

    int n, k;
    scanf("%d %d", &n, &k);

    int *a = malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) a[i] = i;

    solve(a, n, k);

    free(a);

    return 0;

其中n是字符串的长度,k是要旋转的长度。

我使用 n=232830359 和 k=80829 来测试这两种算法。结果是,算法1耗时6.199s,算法2耗时1.970s。

但是,我认为这两种算法都需要计算 n 次交换。 (算法1很明显,算法2需要k/2 + (n-k)/2 + n/2 = n次交换)。

我的问题是,为什么他们的速度差异如此之大?

【问题讨论】:

可能是缓存未命中。第二个版本以一种很好的线性方式访问内存,而第一个版本则四处跳跃。如果您想进行更多调查,可以在这里查看一些想法:***.com/questions/2486840/…。 【参考方案1】:

这两种算法都比 CPU 更受内存限制。这就是为什么在分析基本操作(如交换或循环迭代)的数量时会给出与实际运行时间完全不同的结果的原因。所以我们将使用外部存储器模型而不是 RAM 模型。也就是说,我们将分析缓存未命中的数量。假设N 是一个数组大小,M 是缓存中的块数,B 是一个块大小。只要N 在您的测试中很大,就可以安全地假设N >M(即所有数组都不能在缓存中)。

1)第一种算法:访问数组元素的方式如下i(i + k) mod N(i + 2 * k) mod N等。如果k 很大,则两个连续访问的元素不在同一个块中。所以在最坏的情况下,两次访问会产生两次缓存未命中。 这两个块将被加载到缓存中,但之后可能很长一段时间都不会使用它们!因此,当再次访问它们时,它们可能已经被其他块替换(因为缓存比数组小)。这将再次错过。可以看出,该算法在最坏的情况下可以有O(N)缓存未命中。

2)第二种算法有非常不同的数组访问模式:l, r, l + 1, r - 1, ...。 如果访问l-th 元素导致未命中,则将其整个块加载到缓存中,因此访问l + 1l + 2,...直到块的末尾不会导致任何未命中。 rr - 1 等也是如此(实际上只有当 lr 块可以同时保存在缓存中时才成立,但这是一个安全的假设,因为缓存通常不直接映射)。所以这个算法在最坏的情况下有O(N / B)缓存未命中。

考虑到实际缓存的块大小大于一个整数大小,为什么第二种算法明显更快。

P.S 这只是实际情况的模型,但在这种特殊情况下,外部存储器模型比 RAM 模型效果更好(而且 RAM 模型也只是一个模型)。

【讨论】:

以上是关于旋转序列的两种算法的速度。 (摘自《编程珍珠》一书)的主要内容,如果未能解决你的问题,请参考以下文章

S7-1200对V90 PN进行速度控制的两种方法

QT 实现图片旋转的两种方法

Android实现旋转动画的两种方式

二维数组的两种遍历方式左右旋转左右逆序上下逆序 (kotlin实现)

伪随机数生成算法-梅森旋转(Mersenne Twister/MT)

编程之法:面试和算法心得(最大连续乘积子串)