[C语言]数组循环位移三种算法–实现与原理
一、问题引入
我们目前有一个一维的数组序列,其中有n个整数(并非绝对,同样可以为字符串,例如基因序列的分析),要求把下标从0到p(包含p,p小于等于n-1)的数组元素平移到数组最后。

这是一道很明显的数组循环位移问题,其实际应用非常广泛,例如在基因序列分析的特定问题时,我们也要对其中的部分碱基序列进行平移处理。其中实现方法有三种:
- 1.三次反转法(标准高效)
- 2.临时数组法
- 3.暴力排序法
二、算法实现和原理
1.三次反转法
三次反转法是标准的组合数学问题,对一个序列的反转操作是排列的一种基本变换,三次反转法展示了如何将循环移位分解为三个反转的复合。我们先看这个方法的数学原理,再进行原理解释和算法实现。
数学原理
对一一整个序列中的一个片段右移 k 位即是将数组最后 k 个元素与之前的元素整体交换位置。问题的关键是不用辅助空间在线性的时间内利用算法实现这一交换过程。
假设 X, Y 为两个序列,X^T 代表其反转序列(即 reverse),XY 代表两个序列的拼接,则有以下两式:
(X^T)^T = X
(XY)^T = Y^T X^T
由此可见,欲得到目标序列YX只需要对(XY)^T 再次反转即可,由此可得:
YX = (X^T Y^T)^T

算法实现
我们首先需要一个反转函数,反转函数需要接收三个参数:1.欲反转的数组、2.序列片段左侧下标、3.序列片段右侧下标。
反转函数
void reverse(int* w, int left_index,int right_index) {
//使用for循环来实现交换
int k = right_index;
for (int i = left_index; i < k;i++) {
int temp = w[i];
w[i] = w[k];
w[k] = temp;
k--;
}
}这里for循环的条件是i < k而不是 i <= k,因为当i等于k时,已经完成了交换,不需要再进行一次交换,并且不要判断条件设置为i < = left_index或者i < left_index ,因为left_index 作为判断条件,left_index是常量, 这样进行交换相当于对数组进行两次交换,第一次逆序交换,第二次正序交换,结果不变。
或者我们使用更简便的while循环来实现,笔者这里刚开始的思路是for循环,后面进行优化时考虑到while循环更加简便,所以大家可以自行选择方式。
void reverse(int* w, int left_index,int right_index) {
//使用while循环来实现交换
while (left_index < right_index) {
int temp = w[left_index];
w[left_index] = w[right_index];
w[right_index] = temp;
right_index--;
left_index++;
}
}三次反转函数
三次反转函数就相对比较简单了,调用我们上面写的反转函数即可。
void thirdReverse(int* w, int p, int n) {
reverse(w, 0, p);
printArr(w, n);
reverse(w, p + 1, n - 1);
reverse(w, 0, n - 1);
}2.临时数组法
临时数组法,即定义一个临时的数组来放置序列片段,将另一个序列片段前移后,在将临时数组中的序列片段插入序列后面。
原理
设原始序列为 XY,其中 X 表示前 p+1 个元素,Y 表示剩余元素。我们想要得到 YX。
临时数组法的步骤:
将
X复制到临时数组T,即T = X。将
Y整体前移到原序列的前部,此时原序列变为Y后面跟空位。将
T中的元素复制到原序列的末尾,最终得到YX = Y \cdot T,其中\cdot表示拼接。
因此,临时数组法本质上是借助额外空间 T 实现了 YX = Y \cdot X。

算法实现
临时数组法我们只需要实现上面的三步即可,对w中的元素进行操作,因此需要三个参数:1.欲循环位移的数组,2.前p位中最后
void tempArr(int* w, int p, int n) {
//取出前p+1个元素,存储在临时数组中
int temp[N];
for (int i = 0; i <= p; i++) {
temp[i] = w[i];
}
//将后n-p-1个元素前移
int j = 0;
for (int i = p + 1; i <= n - 1; i++) {
w[j] = w[i];
j++;
}
//将临时数组中的元素放回原数组的后面
for (int i = j; i <= j + p + 1; i++) {
w[i] = temp[i - j];
}
}3.暴力法
暴力法,即最直观的模拟方法:重复将第一个元素移动到数组末尾,共执行 p+1 次,每次移动一个元素。
原理
设原始序列为 XY,其中 X 表示前 p+1 个元素,Y 表示剩余元素。目标是将 XY 变换为 YX。
暴力法的步骤等价于对序列反复执行单步左移操作:
保存当前第一个元素。
将后续所有元素依次前移一位。
将保存的元素放到数组末尾。
重复上述过程 p+1 次,即完成 X 中所有元素的平移。
因此,暴力法本质上是通过多次单步移动来模拟整体平移,不借助额外数组,但时间复杂度较高。

算法实现
void bruteForce(int* w, int p, int n) {
int temp;
// 外层循环:需要将前 p+1 个元素逐个移到末尾
for (int step = 0; step <= p; step++) {
temp = w[0]; // 保存当前第一个元素
// 内层循环:将后续所有元素前移一位
for (int i = 0; i < n - 1; i++) {
w[i] = w[i + 1];
}
w[n - 1] = temp; // 将保存的元素放到末尾
}
}外层循环控制移动次数,共 p+1 次。内层循环从下标 0 到 n-2,将每个元素替换为后一个元素,实现整体前移。最后将临时保存的元素赋给 w[n-1],完成一次移动。
总结
| 我们可以对比一下这三种算法的优劣,在实际应用中选择合适的算法,三次反转法是最推荐实际应用的算法。 | 算法名称 | 时间复杂度 | 空间复杂度 | 思路简述 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| 暴力法 | O(p \cdot n),最坏 O(n^2) | O(1) | 重复执行 p+1 次单步左移:保存首个元素,其余元素前移,再将保存元素放末尾 | 思路最直观,易于理解和实现 | 效率低,大量重复移动,适合小规模数据 | |
| 临时数组法 | O(n) | O(p) 或 O(n) | 用临时数组保存前 p+1 个元素,将剩余元素前移,再将临时数组元素放回末尾 | 时间复杂度线性,实现简单 | 需要额外空间,当 p 接近 n 时空间开销大 | |
| 三次反转法 | O(n) | O(1) | 通过三次反转数组片段实现循环移位:反转前 p+1 个,反转剩余,再整体反转 | 线性时间,原地操作,无需额外空间 | 需要理解反转原理,代码稍显技巧性 |



