旋转序列的两种算法的速度。 (摘自《编程珍珠》一书)
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 + 1
,l + 2
,...直到块的末尾不会导致任何未命中。 r
、r - 1
等也是如此(实际上只有当 l
和 r
块可以同时保存在缓存中时才成立,但这是一个安全的假设,因为缓存通常不直接映射)。所以这个算法在最坏的情况下有O(N / B)
缓存未命中。
考虑到实际缓存的块大小大于一个整数大小,为什么第二种算法明显更快。
P.S 这只是实际情况的模型,但在这种特殊情况下,外部存储器模型比 RAM 模型效果更好(而且 RAM 模型也只是一个模型)。
【讨论】:
以上是关于旋转序列的两种算法的速度。 (摘自《编程珍珠》一书)的主要内容,如果未能解决你的问题,请参考以下文章
二维数组的两种遍历方式左右旋转左右逆序上下逆序 (kotlin实现)