题目描述:
定义字符串的左旋转操作:把字符串前面的k个字符移动到字符串的尾部。如把字符串abcdef左旋转k=2位得到字符串cdefab。请实现字符串左旋转的函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1)。
大家开始可能会有这样的潜在假设,K<N。事实上,很多时候也的确是这样的。但严格来说,我们不能用这样的“惯性思维”来思考问题。尤其在编程的时候,全面地考虑问题是很重要的,K可能是一个远大于N的整数。
仔细观察循环左移的特点,不难发现:每个元素左移N位后都会回到自己的位置上。因此,左移K位之后的情形,跟左移K’= K % N位之后的情形一样。
有三种方法可以实现线性时间内的左旋操作:
1、三次翻转,直接线性
2、两个指针逐步翻转,线性
3、stl的rotate算法,线性
1:三次翻转
将一个字符串分成两部分,X和Y两个部分,在字符串上定义反转的操作: ,即把X的所有字符反转(如,X="abc",那么 ="cba"),那么我们可以得到下面的结论:
= YX。这就可以转化为字符串的反转的问题了。就拿abcdef 这个例子来说,若要让def翻转到abc的前头,那么只要按下述3个步骤操作即可:
a、首先分为俩部分,X: abc,Y: def;
b、X-> ,abc->cba,Y-> ,def->fed;
c、 = YX,cbafed->defabc,即整个翻转。
代码如下:
char *invert(char *start, char *end)
{
char tmp, *ptmp = start;
while (start != NULL && end != NULL && start < end)
{
tmp = *start;
*start = *end;
*end = tmp;
start ++;
end --;
}
return ptmp;
}
char *leftrotate(char *s, int pos) //pos为要旋转的字符个数,或长度。
{
int len = strlen(s);
invert(s, s + (pos - 1)); //即 abc->cba
invert(s + pos, s + (len - 1)); //即 def->fed
invert(s, s + (len - 1)); //即cbafed->defabc。
return s;
}
2、两个指针逐步翻转
先来看个例子,如下,
abcdefghi,若要abc移动至最后:abcdef ghi->def abc ghi->def ghi abc
设m表示左旋的字符个数,n表示字符串长度。所以可定义俩指针,p1指向ch[0],p2指向ch[m],交换p1和p2所指元素,然后p1++, p2++。
但是上面的例子是一种特殊情况,更普遍的情况是最后一次交换时会留有尾巴,比如如果abc def ghi j要变成def ghi j abc:
abc def ghi j -> def abc ghi j-> def ghi abc j
此时abc j就是最后的尾巴,已经不能按照前面的处理方法了,所以需要如下的步骤: def ghi abc j -> def ghi abjc-> def ghi aj bc-> def ghi j abc。
总结如下:
a、首先让p1=ch[0],p2=ch[m],即让p1,p2相隔m的距离;
b、判断p2+m-1是否越界,如果没有越界转到c,否则转到d
c、不断交换*p1与*p2,然后p1++,p2++,循环m次,然后转到2。
d、此时p2+m-1已经越界,在此只需处理尾巴。过程如下:
d.1 通过n-p2得到p2与尾部之间元素个数r,即我们要前移的元素个数。
d.2 以下过程执行r次:
ch[p2]<->ch[p2-1],ch[p2-1]<->ch[p2-2],....,ch[p1+1]<->ch[p1];p1++;p2++;
代码如下:
void rotate(string &str, int m)
{
if (str.length() == 0 || m <= 0)
return;
int n = str.length();
if (m % n <= 0)
return;
int p1 = 0, p2 = m;
int k = n - n % m - m; //k表示尾巴之前的字符个数
// 交换p1,p2指向的元素,然后移动p1,p2
while (k --)
{
swap(str[p1], str[p2]);
p1++;
p2++;
}
// 重点,都在下述几行。处理尾巴,r为尾部左移次数
int r = n - p2;
while (r--)
{
int i = p2;
while (i > p1)
{
swap(str[i], str[i-1]);
i--;
}
p2++;
p1++;
}
//比如一个例子,abcdefghijk
// p1 p2
//当执行到这里时,defghi a b c j k
//p2+m出界了,r=n-p2=2,所以以下过程,要执行循环俩次。
//第一次:j 步步前移,abcjk->abjck->ajbck->jabck
//然后,p1++,p2++,p1指a,p2指k。
// p1 p2
//第二次:defghi j a b c k
//同理,此后,k步步前移,abck->abkc->akbc->kabc。
}
这里的尾巴的处理,其实可以采用递归的方法,递归的进行左旋或者右旋即可,不再赘述。
3:stl的rotate算法
考虑下面两个例子:
a: 将abcdef左旋2位,所以m=2, n=6,所以可以这样模拟:abcdef|ab
从第0位开始:0 <- 2 <- 4 <- 0(6%n == 0),得到cbedaf。接下来从第1位开始:1 <- 3 <- 5 <- 1(7%n == 1),得到cdefab。此时,从0到5都已经移动了一次,所以得到了最终结果。
b:将abcdefg左移2位,所以m=2, n=7,所以可以这样模拟:abcdefg|ab
从第0位开始:0 <- 2 <- 4 <- 6 <- 1(8%n == 1) <- 3 <- 5 <-0(7%n == 0)。至此,从0到6都已经移动了一次,所以得到了最终结果。
以上就是stl中的rotate算法的思想,具体描述如下:根据循环链来进行移动,所谓的循环链,就是所有序号为(j+ i*m) % n的元素构成的“环”,j为循环链的开始序号。循环链上的所有元素都移动到循环链中的前一位置。
一个循环链上共有多少个元素?从j开始,下一个元素是(j+m)%n,然后依次是(j+2m)%n, (j+3m)%n,..., (j+km)%n。所谓的循环链,就是因为链中所有序号构成了环,所以j = (j+km)%n。k就是使该等式成立的最小值。那么k等于多少,循环链上就有多少个元素。
j = (j+km)%n,意味着n|km (|表示整除),设t = gcd(n, m)表示n和m的最大公约数,所以n = xt, m = yt,因为t是最大公约数,所以x和y互质。所以: xt|kyt -> x|ky,因为x,y互质,所以k = x = n/t。所以,循环链中的元素个数为。所以,一共有gcd(n, m)个循环链。
这样,每个循环链上的元素移动一次,所有循环链完成移动之后,也就达到了左旋的效果。
gcd是求最大公约数的算法:给定俩个正整数m,n(m>=n),求它们的最大公约数。(注意,一般要求m>=n,若m<n,则要先交换m<->n。下文,会具体解释)。
用数学定理表示即为:“定理:gcd(a,b) = gcd(b, a % b) (a>b 且a % b 不为0)”。以下,是此算法的具体流程:
a、[求余数],令r=m%n,r为n除m所得余数(0<=r<n);
b、[余数为0?],若r=0,算法结束,此刻,n即为所求答案,否则,继续,转到3;
c、[重置],置m<-n,n<-r,返回步骤1.
代码如下:
void my_rotate(char *begin, char *mid, char *end)
{
int n = end - begin; //n表示字符串长度
int k = mid - begin; //k表示左旋元素的个数
int d = gcd(n, k); //d表示循环链个数
int i, j;
for (i = 0; i < d; i ++) //i表示循环链的起始序号
{
int tmp = begin[i];
int last = i;
//j为循环链的下一个元素
for (j = (i + k) % n; j != i; j = (j +k) % n)
{
begin[last] = begin[j];
last = j;
}
begin[last] = tmp;
}
}
http://blog.csdn.net/v_JULY_v/article/details/6322882