【求职面试】左旋转字符串(C#版)

题目描述:

定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部。
如把字符串abcdef左旋转2位得到字符串cdefab。
请实现字符串左旋转的函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1)


首先第一种最直观的解法:把数组字符一个个的左移:

class LeftRotateString
{
public static void Run()
{
LeftRotateString lrs = new LeftRotateString();
string str = "abcdef";
var result = lrs.Method1a(str, 2);
Console.WriteLine("Source:{0}\r\nResult:{1}", str, result);
Console.Read();
}

public string Method1a(string str, int k)
{
var charArray = str.ToCharArray();
while (k-- > 0)
{
var firstChar = charArray[0];
for (int i = 0; i < charArray.Length - 1; i++)
{
charArray[i] = charArray[i + 1];
}
charArray[charArray.Length - 1] = firstChar;

}
return new string(charArray);
}
}

注意,string类型是不可变的,如果你用str[0] = str[1],不能编译通过。

虽然功能可以实现,但是算法复杂度为O(K * N),不符合要求。继续探索。

大家开始可能会有这样的潜在假设,K<N。事实上,很多时候也的确是这样的。但严格来说,我们不能用这样的“惯性思维”来思考问题。
尤其在编程的时候,全面地考虑问题是很重要的,K可能是一个远大于N的整数,在这个时候,上面的解法是需要改进的。
仔细观察循环右移的特点,不难发现:每个元素右移N位后都会回到自己的位置上。因此,如果K > N,右移K-N之后的数组序列跟右移K位的结果是一样的。 进而可得出一条通用的规律:
右移K位之后的情形,跟右移K’= K % N位之后的情形一样

        public string Method1b(string str, int k)
{
k = k % str.Length;//add this sentence.
var charArray = str.ToCharArray();
while (k-- > 0)
{
var firstChar = charArray[0];
for (int i = 0; i < charArray.Length - 1; i++)
{
charArray[i] = charArray[i + 1];
}
charArray[charArray.Length - 1] = firstChar;

}
return new string(charArray);
}

这只是代码技巧~~算法复杂度降为O(N^2),达不到O(n)的要求。

三次翻转法:
假设原数组序列为abcd1234,要求变换成的数组序列为1234abcd,即循环右移了4位。比较之后,不难看出,其中有两段的顺序是不变的:1234和abcd,可把这两段看成两个整体。左移K位的过程就是把数组的两部分交换一下。
变换的过程通过以下步骤完成:
 逆序排列abcd:abcd1234 → dcba1234;
 逆序排列1234:dcba1234 → dcba4321;
 全部逆序:dcba4321 → 1234abcd。

       public void Reverse(char[] charArray, int begin, int end)
{
for (; begin < end; begin++, end--)
{
var temp = charArray[begin];
charArray[begin] = charArray[end];
charArray[end] = temp;
}
}

public string LeftShift(string str, int k)
{
k = k % str.Length;
var charArray = str.ToCharArray();
Reverse(charArray, 0, k - 1);
Reverse(charArray, k, str.Length - 1);
Reverse(charArray, 0, str.Length - 1);
return new string(charArray);
}

OK, 到这里为止,总算满足条件了,算法复杂度为O(2N) = O(N)。
我在想,为什么能想出这样好的点子呢?这里用到了线性代数矩阵的知识。哈哈,这里体现到数学的力量了!

我们还是把字符串看成有两段组成的,记位XY。左旋转相当于要把字符串XY变成YX。我们先在字符串上定义一种翻转的操作,就是翻转字符串中字符的先后顺序。把X翻转后记为XT。显然有(XT)T=X。

我们首先对X和Y两段分别进行翻转操作,这样就能得到XTYT。接着再对XTYT进行翻转操作,得到(XTYT)T=(YT)T(XT)T=YX。

 

那么,到此结束了么?No,要是到此结束,那就失去乐趣了。


题目是限制空间复杂度O(1),那么如果不限制呢?哈哈,那就太简单了。

        public string Method2(string str, int k)
{
k = k % str.Length;
var charArray = str.ToCharArray();
var left = str.ToCharArray(0, k);
for (int i = 0; i < str.Length - k; i++)
{
charArray[i] = charArray[i + k];
}
for (int i = str.Length - k, j = 0; i < str.Length; i++, j++)
{
charArray[i] = left[j];
}
return new string(charArray);
}

时间复杂度就是O(N),但是空间复杂度是O(K)了。可惜了,不符合题目要求啊,不过这样以空间换时间的做法还是挺好的。

难道就没有更好的方法了么?难道就没有强大的数学公式解法之类的?

好吧,让我们反向从结果看看有木有什么规律。

对于"abcdefg" 左移3来说,你看结果,会发现这样神奇的规律:

       public string LeftShift2(string str, int k)
{
k = k % str.Length;
var charArray = str.ToCharArray();
var temp = charArray[0];
charArray[0] = charArray[3];
charArray[3] = charArray[6];
charArray[6] = charArray[2];
charArray[2] = charArray[5];
charArray[5] = charArray[1];
charArray[1] = charArray[4];
charArray[4] = temp;
return new string(charArray);
}

通过这一连串的赋值,就可以用O(n)的时间复杂度和O(1)的空间复杂度来解决。但是这是针对"abcdefg"左移3实现的。

我们需要扩展到一般应用。其实从结果我们可以看出来了,这一系列的赋值关键是index的计算。

聪明同学应该发现了规律——index是一个循环链,当index>str.Length时,指针又从左开始(取模运算),前一个end 就变成现在的start。

于是我很快的写出了这个程序:

        ///<summary>
/// 此代码有bug
///</summary>
public string LeftShiftWrong(string str, int k)
{
k = k % str.Length;
var charArray = str.ToCharArray();
var temp = charArray[0];
int begin = 0, end = 0;
for (int i = 0; i < str.Length - 1; i++)
{
begin = end;
end = (begin + k) % str.Length;
charArray[begin] = charArray[end];
}
charArray[end] = temp;
return new string(charArray);
}

我很高兴,它的结果对于"abcdefg" 左移3来说是正确的。

但是没过多久,我在午休小憩的时候,灵光一闪,这个程序有个严重的bug,对于"abcdef"左移2来说,结果是错的!(不知道你有没有灵光一闪呢?我发现自己在编程的时候总是没有,但是当我休息的时候却会有灵光一闪。看来还得多休息啊!!!)

原因在于对于"abcdefg" 左移3来说,3和7是互为质数的,除不尽,因此整个循环链一直串起来。

但是对于"abcdef"左移2来说,2和6不是互为质数的,有最大公约数2,循环链会在0的时候断掉。需要继续下一条循环链。

可以看看"abcdef"左移2的手工解法:

 

        public string LeftShift2(string str, int k)
{
k = k % str.Length;
var charArray = str.ToCharArray();
//开始一段子链0,2,4
var temp = charArray[0];
charArray[0] = charArray[2];
charArray[2] = charArray[4];
charArray[4] = temp;
//开始新的一段子链1,3,5
temp = charArray[1];
charArray[1] = charArray[3];
charArray[3] = charArray[5];
charArray[5] = temp;
return new string(charArray);
}

从中可以看出,这种情况,应该有多条子循环链。条数 = k 和 str.Length的 最大公约数。

于是,终于可以写成正确并且高效的程序了:

 

        ///<summary>
/// 左移字符串。时间复杂度O(n), 空间复杂度O(1)
///</summary>
public string LeftShiftCorrect(string str, int k)
{
if (String.IsNullOrEmpty(str) || str.Length < 2 || k < 1)
throw new InvalidOperationException("Can not left shift");

k = k % str.Length;
var charArray = str.ToCharArray();

var subLinkStartIndex = 0; //子链的开始index
var temp = charArray[subLinkStartIndex]; //子链的开始字符
int current = 0, next = 0;
for (int i = 0; i < str.Length; i++)
{
current = next;
next = (current + k) % str.Length;
if (next != subLinkStartIndex)
{
charArray[current] = charArray[next];
}
else
{
//子链结束
charArray[current] = temp;
//开始下一个子链
subLinkStartIndex++;
temp = charArray[subLinkStartIndex];
next = subLinkStartIndex;
}
}
return new string(charArray);
}

完了么?没完。接下来我扩展成通用的数组循环移位:

        ///<summary>
/// 数组循环移位。时间复杂度O(n), 空间复杂度O(1)
/// k>0为左移,k<0为右移
///</summary>
public void Shift<T>(T[] array, int k)
{
if (array == null || array.Length < 2)
throw new InvalidOperationException("Can not left shift");
if (k == 0) return;

k = k % array.Length;

var subLinkStartIndex = k > 0 ? 0 : array.Length - 1; //子链的开始index
var temp = array[subLinkStartIndex]; //子链的开始字符
int current = subLinkStartIndex, next = subLinkStartIndex;
for (int i = 0; i < array.Length; i++)
{
current = next;
next = (current + k) % array.Length;
//处理右移<0的情况
if (next < 0) next += array.Length;
if (next != subLinkStartIndex)
{
array[current] = array[next];
}
else
{
//子链结束
array[current] = temp;
//开始下一个子链
subLinkStartIndex = k > 0 ? subLinkStartIndex + 1 : subLinkStartIndex - 1;
temp = array[subLinkStartIndex];
next = subLinkStartIndex;
}
}
}

哎,你说了,这玩意有嘛用啊?花了这两天搞这玩意,有意思么?

嘿嘿,其实数组循环移位大有用处咯。比如说编辑文本的剪切功能:

”爱你一万年”--剪切成“一万年爱你”,就可以用上面数组的循环移位算法实现。



其实从结果分析,最终的字符串"
爱你一万年"内容没变,只是输出顺序变了,而我们上面的算法呢,不管怎么做,都是要移动字符串。时间复杂度最少为O(n)。那么能不能不移动字符串内容呢,用一个强大的公式,来输出新的顺序呢?

有时候,从纯算法角度走到头了,我们必须换个思路,以数据结构的方式来解决问题。

于是,我就构建了下面这个数据结构,来实现旋转后顺序的输出。公式很简单,就是自己计算什么时候是第一个要输出的index。

 

    class RotateStructure<T>
{
private T[] _data;
private int _beginIndex;

public RotateStructure(T[] data)
{
_data = data;
}

public void Shift(int k)
{
k = k % _data.Length;
_beginIndex = (_beginIndex + k) % _data.Length;
}

public T this[int index]
{
get
{
var offsetIndex = (index + _beginIndex) % _data.Length;
return _data[offsetIndex];
}
}

public IEnumerable<T> Output()
{
for (int i = _beginIndex; i < _beginIndex + _data.Length; i++)
{
var index = i % _data.Length;
yield return _data[index];
}
}

于是,这样就实现了时间复杂度O(1),空间复杂度O(1)的算法呢。

呵呵,差不多暂时到这边了。

那个,谁有好工作推荐啊?求南京地区的 .net开发,要求就一条,技术含量高的,团队气氛活跃的,有发展前景的啊!

有意者请联系我。

posted @ 2011-10-18 18:50  primeli  阅读(608)  评论(0编辑  收藏