问题:定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。如把字符串abcdef左旋转2位得到字符串cdefab。请实现字符串左旋转的函数。要求时间对长度为n的字符串操作的复杂度为O(n),辅助内存为O(1)。
解法一:(循环换位算法)不考虑时间和空间的限制。设移动的位数为k。则循环k次,每次移动1位。这样的空间复杂度是O(1),时间复杂度是O(n*k)。如果k小于n,则O(n^2)。
void RightShift(char *arr, int N, int k)
{
k %= N ; //缩小k的范围
while(k--)
{
char t = arr[N-1];
for(int i = N-1; i > 0; i--)
arr[i] = arr[i-1];
arr[0] = t;
}
}
虽然这个算法可以实现数组的循环右移,但是算法复杂度为O(K * N),不符合题目的要求,需要继续往下探索。
但是在实际操作过程中K未必小于N,也就是说有肯那个时间复杂度超过N^2。通过观察可以可知,循环移动K位和循环移动K%N是一样的,这就将时间复杂度降下来了。
解法二:(三次反转算法)假设原数组序列为abcd1234,要求变换成的数组序列为1234abcd,即循环右移了4位。比较之后,不难看出,其中有两段的顺序是不变的:1234和abcd,可把这两段看成两个整体。右移K位的过程就是把数组的两部分交换一下。变换的过程通过以下步骤完成:
- 逆序排列abcd:abcd1234 → dcba1234;
- 逆序排列1234:dcba1234 → dcba4321;
- 全部逆序:dcba4321 → 1234abcd。
Reverse(char *arr, int b, int e)
{
for(; b < e; b++, e--)
{
char temp = arr[e];
arr[e] = arr[b];
arr[b] = temp;
}
}
RightShift(char *arr, int N, int k)
{
k %= N;
Reverse(arr, 0, k-1);
Reverse(arr, k, N-1);
Reverse(arr, 0, N-1);
}
解法三:(排列循环算法)对于给定数组A[0..N-1]向后循环换位N-K位运算,可分解为恰好gcd(K,N-K)个循环置换,且0,...,gcd(K,N-K)-1中的每个数恰属于一个循环置换。其中gcd(x,y)表示x和y的最大公因数。
我们从头开始分析这个问题,对于数组A[0..n-1],要将其向后循环移动k位元素。因为每个元素右移n位后又回到了原来的位置上,所以右移k位等于右移k mod n位。考虑每个元素右移k位后的最终位置,比如对于A[0],右移k位后在k mod n位置上,原来在k mod n位置上的元素右移k位后到了2*k mod n的位置上,把如此因为A[0]的移动而受到连环影响必须移动的位置列出来,就是下面这样一个位置序列:0,k,2*k,...,(t-1)k。其中每一项都是在模n的意义下的位置。t*k mod n 的结果是0。t是使得t*k mod n的结果为0的最小正整数。
这个位置序列实质上是模n加法群中由元素k生成的一个循环子群。由群论中的结论(该结论的证明见最后)知,循环子群(k)的周期为n / gcd(k,n),元数为n / gcd(k,n),其陪集个数为gcd(k,n)。换句话说,A[0..n-1]循环右移k位的结果是循环子群(k)以及它的所有陪集循环右移一位。例如,将A[0..5] = {1,2,3,4,5,6}循环右移4位,这里n = 6, k = 4, gcd(k, n) = 2。A[0]的最终位置是4,A[4]的最终位置是2,A[2]的最终位置是0,这样,位置0,4,2便是由k=4生成的循环群,周期为6 / gcd(4,6) = 6 / 2 = 3,这样的循环子群共有gcd(4,6) = 2个。
// 数组的循环移位
#include <cstdio>
int gcd(int m, int n) {
int r;
while(r = m % n) {
m = n; n = r;
}
return n;
}
void shiftArray(int A[], int n, int k) {
// 因为左移的代码比右移的代码好实现的多,而右移k位
// 等价于左移-k位,-k = n - k。以下代码是通过左移-k位来实现右移k位
k = n - (k % n);
for(int i = 0, cnt = gcd(n, k); i < cnt; i++) {
int t = A[i], p = i, j = (k+i) % n;
while(j != i) {
A[p] = A[j]; p = j; j = (k + p) % n;
}
A[p] = t;
}
}
void printArray(int A[], int n) {
for(int i = 0; i < n; i++) {
printf("%-3d", A[i]);
if((i+1)%10 == 0) printf("/n");
}
}
int main() {
int A[] = {1,2,3,4,5,6, 7};
shiftArray(A, 7, 4);
printArray(A, 7);
return 0;
}
另一种实现方法:
char* string_cyclicshift_v2( char* string, int i )
{
char ch;
int exchange;
int len;
exchange = 0;
len = strlen( string );
i = i%len;
if ( 0 == i )
return string;
int start_pos=0;
while ( exchange<len )
{
char ch = string[start_pos];
int currpos = start_pos;
int nextpos = (len+currpos+i)%len;
while ( nextpos != start_pos )
{
string[currpos] = string[nextpos];
++exchange;
currpos = nextpos;
nextpos = (len+currpos+i)%len;
}
string[currpos] = ch;
++exchange;
++start_pos;
}
return string;
}
上述所用到的那个群论结论的证明:
结论:设G是一个循环群,其中一个生成元素为a,若r和n的最大公约数为d,则a^r的周期为n / d。
在模n加法群中,1是一个生成元素,任意元素k=1*k,所以任意元素k生成的循环子群(k)的周期为n / gcd(k,n)。因为gcd(k,n)=gcd(k,n-k),所以也可以写成n / gcd(k, n-k)。
解法四:(部分逆转)将整个数组分成两部分,然后将较小的一部分与另一部分对换,对于剩下的数组继续执行当前算法,直到数组长度相等两段对换.
原始字符串为:abcdefgh,循环右移三位的结果为defghabc。其操作过程可以分解如下:
abc defgh-->def abc gh --> defgh c ab--> defgha c b --> defghabc
参考网址: