数组循环移位算法(左旋字符串)【总结】

时间:2023-01-03 21:21:45

问题:定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。如把字符串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;
}
}

虽然这个算法可以实现数组的循环右移,但是算法复杂度为OK * N),不符合题目的要求,需要继续往下探索。

但是在实际操作过程中K未必小于N,也就是说有肯那个时间复杂度超过N^2。通过观察可以可知,循环移动K位和循环移动K%N是一样的,这就将时间复杂度降下来了。

解法二:(三次反转算法)假设原数组序列为abcd1234,要求变换成的数组序列为1234abcd,即循环右移了4位。比较之后,不难看出,其中有两段的顺序是不变的:1234和abcd,可把这两段看成两个整体。右移K位的过程就是把数组的两部分交换一下。变换的过程通过以下步骤完成:数组循环移位算法(左旋字符串)【总结】

  1. 逆序排列abcd:abcd1234 → dcba1234;
  2. 逆序排列1234:dcba1234 → dcba4321;
  3. 全部逆序: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

参考网址

http://www.cnblogs.com/JCSU/archive/2009/03/09/1321582.html

http://blog.csdn.net/jcwKyl/article/details/3874629