趣味算法:字符串反转的N种方法

老赵反对北大青鸟的随笔中提到了数组反转。这的确是一道非常基础的算法题,然而也是一道很不平常的算法题(也许所有的算法深究下去都会很不平常)。因为我写着写着,就写出来8种方法……现在我们以字符串的反转为例,来介绍这几种方法并对它们的性能进行比较。

使用Array.Reverse方法

对于字符串反转,我们可以使用.NET类库自带的Array.Reverse方法

public static string  ReverseByArray(this string  original)
{
    char[] c = original.ToCharArray();
    Array.Reverse(c);
    return new string(c);
}

使用字符缓存

在面试或笔试中,往往要求不用任何类库方法,那么有朋友大概会使用类似下面这样的循环方法

public static string ReverseByCharBuffer(this string original)
{
    char[] c = original.ToCharArray();
    int l = original.Length;
    char[] o = new char[l];
    for (int i = 0; i < l ; i++)
    {
        o[i] = c[l - i - 1];
    }
    return new string(o);
}

当然,聪明的同学们一定会发现不必对这个字符数组进行完全遍历,通常情况下我们会只遍历一半

public static string ReverseByCharBuffer2(this string original)
{
    char[] c = original.ToCharArray();
    int l = original.Length;
    for (int i = 0; i < l / 2; i++)
    {
        char t = c[i];
        c[i] = c[l - i - 1];
        c[l - i - 1] = t;
    }
    return new string(c);
}

ReverseByCharBuffer使用了一个新的数组,而且遍历了字符数组的所有元素,因此时间和空间的开销都要大于ReverseByCharBuffer2。

在Array.Reverse内部,调用了非托管方法TrySZReverse,如果TrySZReverse不成功,实际上也是调用了类似ReverseByCharBuffer2的方法。

if (!TrySZReverse(array, index, length))
{
    int num = index;
    int num2 = (index + length) - 1;
    object[] objArray = array as object[];
    if (objArray == null)
    {
        while (num < num2)
        {
            object obj3 = array.GetValue(num);
            array.SetValue(array.GetValue(num2), num);
            array.SetValue(obj3, num2);
            num++;
            num2--;
        }
    }
    else
    {
        while (num < num2)
        {
            object obj2 = objArray[num];
            objArray[num] = objArray[num2];
            objArray[num2] = obj2;
            num++;
            num2--;
        }
    }
}

大致上我能想到的算法就是这么多了,但是我无意间发现了StackOverflow上的一篇帖子,才发现这么一个看似简单的反转算法实现起来真可谓花样繁多。

使用StringBuilder

使用StringBuilder方法大致和ReverseByCharBuffer一样,只不过不使用字符数组做缓存,而是使用StringBuilder。

public static string ReverseByStringBuilder(this string original)
{
    StringBuilder sb = new StringBuilder(original.Length);
    for (int i = original.Length - 1; i >= 0; i--)
    {
        sb.Append(original[i]);
    }
    return sb.ToString();
}

当然,你可以预见,这种算法的效率不会比ReverseByCharBuffer要高。

我们可以像使用字符缓存那样,对使用StringBuilder方法进行优化,使其遍历过程也减少一半

public static string ReverseByStringBuilder2(this string original)
{
    StringBuilder sb = new StringBuilder(original);
    for (int i = 0, j = original.Length - 1; i <= j; i++, j--)
    {
        sb[i] = original[j];
        sb[j] = original[i];
    }
    return sb.ToString();
}

以上这几种方法按算法角度来说,其实可以归结为一类。然而下面的几种算法就完全不是同一类型的了。

使用栈

栈是一个很神奇的数据结构。我们可以使用它后进先出的特性来对数组进行反转。先将数组所有元素压入栈,然后再取出,顺序很自然地就与原先相反了。

public static string ReverseByStack(this string original)
{
    Stack<char> stack = new Stack<char>();
    foreach (char ch in original)
    {
        stack.Push(ch);
    }
    char[] c = new char[original.Length];
    for (int i = 0; i < original.Length; i++)
    {
        c[i] = stack.Pop();
    }
    return new string(c);
}

两次循环和栈的开销无疑使这种方法成为目前为止开销最大的方法。但使用栈这个数据结构的想法还是非常有价值的。

使用XOR

使用逻辑异或也可以进行反转

public static string ReverseByXor(this string original)
{
    char[] charArray = original.ToCharArray();
    int l = original.Length - 1;
    for (int i = 0; i < l; i++, l--)
    {
        charArray[i] ^= charArray[l];
        charArray[l] ^= charArray[i];
        charArray[i] ^= charArray[l];
    }
    return new string(charArray);
}

在C#中,x ^= y相当于x = x ^ y。通过3次异或操作,可以将两个字符为止互换。对于算法具体的解释可以参考这篇文章

使用指针

使用指针可以达到最快的速度,但是unsafe代码不是微软所推荐的,在这里我们就不多做讨论了

public static unsafe string ReverseByPointer(this string original)
{
    fixed (char* pText = original)
    {
        char* pStart = pText;
        char* pEnd = pText + original.Length - 1;
        for (int i = original.Length / 2; i >= 0; i--)
        {
            char temp = *pStart;
            *pStart++ = *pEnd;
            *pEnd-- = temp;
        }

        return original;
    }
}

使用递归

对于反转这类算法,都可以使用递归方法

public static string ReverseByRecursive(this string original)
{
    if (original.Length == 1)
        return original;
    else
        return original.Substring(1).ReverseByRecursive() + original[0];
}

使用委托,还可以使代码变得更加简洁

public static string ReverseByRecursive2(this string original)
{
    Func<string, string> f = null;
    f = s => s.Length > 0 ? f(s.Substring(1)) + s[0] : string.Empty;
    return f(original);
}

但是委托开销大的弊病在这里一点也没有减少,以至于我做性能测试的时候导致系统假死甚至内存益处。

使用LINQ

System.Enumerable里提供了默认的Reverse扩展方法,我们可以基于该方法来对String类型进行扩展

public static string ReverseByLinq(this string original)
{
    return new string(original.Reverse().ToArray());
}

性能比较

接下来让我们来对以上8种方法的11个扩展方法来进行性能比较。

影响字符串反转算法性能的因素主要就是字符串的长度。让我们分别取1、10、15、25、50、75、100、1000、10000作为字符串长度来进行测试。用下面的方法来随机生成不同长度的字符串

static string GenerateStringByLength(int length)
{
    Random random = new Random();
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < length; i++)
    {
        sb.Append(Convert.ToChar(Convert.ToInt32(
            Math.Floor(26 * random.NextDouble() + 65))));
    }
    return sb.ToString();
}

用下面的方法来计算时间

static void Benchmark(string description, Func<string> func, int times)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int j = 0; j < times; j++)
    {
        func();
    }
    sw.Stop();
    Debug.WriteLine("{0} Ticks {1} : called {2} times.", 
        sw.ElapsedTicks, description, times);
}

测试的主方法如下

static void Main(string[] args)
{
    // 预热
    "abcde".ReverseByArray();
    "abcde".ReverseByCharBuffer();
    "abcde".ReverseByCharBuffer2();
    "abcde".ReverseByLinq();
    "abcde".ReverseByPointer();
    "abcde".ReverseByRecursive();
    "abcde".ReverseByRecursive2();
    "abcde".ReverseByStack();
    "abcde".ReverseByStringBuilder();
    "abcde".ReverseByStringBuilder2();
    "abcde".ReverseByXor();

    int[] lengths = new int[] { 1, 10, 15, 25, 50, 75, 100, 1000, 100000 };

    foreach (int l in lengths)
    {
        int iterations = 100;
        string text = GenerateStringByLength(l);
        Benchmark(String.Format("ReverseByArray (Length: {0})", l),
            text.ReverseByArray, iterations);
        Benchmark(String.Format("ReverseByCharBuffer (Length: {0})", l), 
            text.ReverseByCharBuffer, iterations);
        Benchmark(String.Format("ReverseByCharBuffer2 (Length: {0})", l), 
            text.ReverseByCharBuffer2, iterations);
        Benchmark(String.Format("ReverseByStringBuilder (Length: {0})", l),
            text.ReverseByStringBuilder, iterations);
        Benchmark(String.Format("ReverseByStringBuilder2 (Length: {0})", l),
            text.ReverseByStringBuilder2, iterations);
        Benchmark(String.Format("ReverseByStack (Length: {0})", l), 
            text.ReverseByStack, iterations);
        Benchmark(String.Format("ReverseByXor (Length: {0})", l), 
            text.ReverseByXor, iterations);
        Benchmark(String.Format("ReverseByPointer (Length: {0})", l), 
            text.ReverseByPointer, iterations);
        Benchmark(String.Format("ReverseByRecursive (Length: {0})", l), 
            text.ReverseByRecursive, iterations);
        Benchmark(String.Format("ReverseByRecursive2 (Length: {0})", l), 
            text.ReverseByRecursive2, iterations);
        Benchmark(String.Format("ReverseByLinq (Length: {0})", l), 
            text.ReverseByLinq, iterations);

        Debug.WriteLine(Environment.NewLine);
    }
}

好了,来看看结果吧。(由于递归算法与其他算法的开销不在一个数量级上,因此忽略了对该算法的比较)

197602 Ticks ReverseByArray (Length: 1) : called 100 times.
75773 Ticks ReverseByCharBuffer (Length: 1) : called 100 times.
111833 Ticks ReverseByCharBuffer2 (Length: 1) : called 100 times.
134535 Ticks ReverseByStringBuilder (Length: 1) : called 100 times.
148598 Ticks ReverseByStringBuilder2 (Length: 1) : called 100 times.
192435 Ticks ReverseByStack (Length: 1) : called 100 times.
63098 Ticks ReverseByXor (Length: 1) : called 100 times.
51945 Ticks ReverseByPointer (Length: 1) : called 100 times.
587865 Ticks ReverseByLinq (Length: 1) : called 100 times.


185325 Ticks ReverseByArray (Length: 10) : called 100 times.
189712 Ticks ReverseByCharBuffer (Length: 10) : called 100 times.
100155 Ticks ReverseByCharBuffer2 (Length: 10) : called 100 times.
216232 Ticks ReverseByStringBuilder (Length: 10) : called 100 times.
209497 Ticks ReverseByStringBuilder2 (Length: 10) : called 100 times.
669832 Ticks ReverseByStack (Length: 10) : called 100 times.
163237 Ticks ReverseByXor (Length: 10) : called 100 times.
74303 Ticks ReverseByPointer (Length: 10) : called 100 times.
1058348 Ticks ReverseByLinq (Length: 10) : called 100 times.


215437 Ticks ReverseByArray (Length: 15) : called 100 times.
206610 Ticks ReverseByCharBuffer (Length: 15) : called 100 times.
168180 Ticks ReverseByCharBuffer2 (Length: 15) : called 100 times.
260542 Ticks ReverseByStringBuilder (Length: 15) : called 100 times.
296153 Ticks ReverseByStringBuilder2 (Length: 15) : called 100 times.
785857 Ticks ReverseByStack (Length: 15) : called 100 times.
177915 Ticks ReverseByXor (Length: 15) : called 100 times.
84802 Ticks ReverseByPointer (Length: 15) : called 100 times.
1113262 Ticks ReverseByLinq (Length: 15) : called 100 times.


266167 Ticks ReverseByArray (Length: 25) : called 100 times.
260820 Ticks ReverseByCharBuffer (Length: 25) : called 100 times.
236025 Ticks ReverseByCharBuffer2 (Length: 25) : called 100 times.
380408 Ticks ReverseByStringBuilder (Length: 25) : called 100 times.
440430 Ticks ReverseByStringBuilder2 (Length: 25) : called 100 times.
1197593 Ticks ReverseByStack (Length: 25) : called 100 times.
262388 Ticks ReverseByXor (Length: 25) : called 100 times.
110453 Ticks ReverseByPointer (Length: 25) : called 100 times.
1611900 Ticks ReverseByLinq (Length: 25) : called 100 times.


258435 Ticks ReverseByArray (Length: 50) : called 100 times.
474135 Ticks ReverseByCharBuffer (Length: 50) : called 100 times.
341655 Ticks ReverseByCharBuffer2 (Length: 50) : called 100 times.
662242 Ticks ReverseByStringBuilder (Length: 50) : called 100 times.
587078 Ticks ReverseByStringBuilder2 (Length: 50) : called 100 times.
2116350 Ticks ReverseByStack (Length: 50) : called 100 times.
417375 Ticks ReverseByXor (Length: 50) : called 100 times.
177847 Ticks ReverseByPointer (Length: 50) : called 100 times.
9114592 Ticks ReverseByLinq (Length: 50) : called 100 times.


270022 Ticks ReverseByArray (Length: 75) : called 100 times.
488647 Ticks ReverseByCharBuffer (Length: 75) : called 100 times.
378225 Ticks ReverseByCharBuffer2 (Length: 75) : called 100 times.
1096148 Ticks ReverseByStringBuilder (Length: 75) : called 100 times.
772312 Ticks ReverseByStringBuilder2 (Length: 75) : called 100 times.
3069427 Ticks ReverseByStack (Length: 75) : called 100 times.
479092 Ticks ReverseByXor (Length: 75) : called 100 times.
234195 Ticks ReverseByPointer (Length: 75) : called 100 times.
3330945 Ticks ReverseByLinq (Length: 75) : called 100 times.


319717 Ticks ReverseByArray (Length: 100) : called 100 times.
584857 Ticks ReverseByCharBuffer (Length: 100) : called 100 times.
505470 Ticks ReverseByCharBuffer2 (Length: 100) : called 100 times.
1076715 Ticks ReverseByStringBuilder (Length: 100) : called 100 times.
942375 Ticks ReverseByStringBuilder2 (Length: 100) : called 100 times.
4390493 Ticks ReverseByStack (Length: 100) : called 100 times.
649725 Ticks ReverseByXor (Length: 100) : called 100 times.
293025 Ticks ReverseByPointer (Length: 100) : called 100 times.
6405082 Ticks ReverseByLinq (Length: 100) : called 100 times.


3262087 Ticks ReverseByArray (Length: 1000) : called 100 times.
5511607 Ticks ReverseByCharBuffer (Length: 1000) : called 100 times.
9097485 Ticks ReverseByCharBuffer2 (Length: 1000) : called 100 times.
10325760 Ticks ReverseByStringBuilder (Length: 1000) : called 100 times.
18120420 Ticks ReverseByStringBuilder2 (Length: 1000) : called 100 times.
40247490 Ticks ReverseByStack (Length: 1000) : called 100 times.
6837915 Ticks ReverseByXor (Length: 1000) : called 100 times.
2654011 Ticks ReverseByPointer (Length: 1000) : called 100 times.
84809355 Ticks ReverseByLinq (Length: 1000) : called 100 times.


368229982 Ticks ReverseByArray (Length: 100000) : called 100 times.
609454380 Ticks ReverseByCharBuffer (Length: 100000) : called 100 times.
507932685 Ticks ReverseByCharBuffer2 (Length: 100000) : called 100 times.
748738972 Ticks ReverseByStringBuilder (Length: 100000) : called 100 times.
732820133 Ticks ReverseByStringBuilder2 (Length: 100000) : called 100 times.
2249140177 Ticks ReverseByStack (Length: 100000) : called 100 times.
508241490 Ticks ReverseByXor (Length: 100000) : called 100 times.
192039592 Ticks ReverseByPointer (Length: 100000) : called 100 times.
2346782325 Ticks ReverseByLinq (Length: 100000) : called 100 times.

整理成表格如下

绘制成更直观的折线图(由于数量级差别太大,故舍去1000和10000长度的情况)

 image

是的,LINQ方法处理长度为50的数组时,效率比长度为75的数组还要低。我测试了很多次,都是这样的结果,感兴趣的朋友可以深入研究一下。

将耗时明显偏高的LINQ方法和Stack方法去掉,剩下各种算法在时间上的优劣就一目了然了。

image

可见,直接使用指针的效率是最高的。而类库自带的Array.Reverse尽管在面对长度较小的数组时没有明显优势,但面对大数组其算法效率却十分稳定。XOR方法在小数组面前效率很高,但面对大数组就败下阵来。遍历了数组一半元素的CharBuffer2表现优异,无论面对大数组还是小数组,排名都很靠前。

指针方法尽管高效,但其带来的问题也许会很严重,而且面对大数组时Array.Reverse也同样优秀,因此一般情况下还是推荐使用Array.Reverse。当然如果面试官希望你拿出一套不使用类库的高效方案,CharBuffer2将是最佳选择。

当然,你也可以去找一个数组长度的临界点,在临界点以下使用CharBuffer2,在临界点以上使用Array.Reverse。如

public static string Reverse(this string original)
{
    if (original.Length <= 25)
        return original.ReverseByCharBuffer2();
    else
        return original.ReverseByArray();
}

希望本文对你有所帮助。

posted @ 2010-04-23 13:16 麒麟.NET 阅读(8977) 评论(47) 编辑 收藏

 回复 引用 查看   
#1楼 2010-04-23 13:27 virus      
但使用栈这个数据结构的想法还是非常有价值的。

牛逼了,使用栈应该是这道题的本意,我面试了很多的地方,都是示意我使用数据结构解决这个问题

 回复 引用 查看   
#2楼 2010-04-23 13:29 槑槑      
沙发,,学习了
 回复 引用 查看   
#3楼[楼主] 2010-04-23 13:29 麒麟.NET      
@virus
是啊,还要通过题干分析出题者的本意:)

 回复 引用 查看   
#4楼[楼主] 2010-04-23 13:29 麒麟.NET      
希望这篇随笔不会让大家有“茴”字的4种写法的感觉
 回复 引用 查看   
#5楼 2010-04-23 13:45 GaryChen      
顶...很不错,真的
 回复 引用 查看   
#6楼[楼主] 2010-04-23 13:50 麒麟.NET      
@GaryChen
@槑槑
呵呵,我现在MSN上不了了。。。

 回复 引用 查看   
#7楼 2010-04-23 13:50 GaryChen      
@麒麟.NET
你是我们群的麒麟哥?

 回复 引用 查看   
#8楼 2010-04-23 13:51 SeaSunK      
我也是看了老赵的文章写了一下,其中就用了异或。原来差距是这样拉开的。不错,顶一下。。好好看!还想请教下那图是怎样弄的?
 回复 引用 查看   
#9楼[楼主] 2010-04-23 13:51 麒麟.NET      
@GaryChen
是啊,我昨天不是还问你们对数组反转有什么想法呢吗?

 回复 引用 查看   
#10楼 2010-04-23 13:52 GaryChen      
@麒麟.NET
额..看了你的文章,可见内功如此深厚...我很郁闷

 回复 引用 查看   
#11楼[楼主] 2010-04-23 13:53 麒麟.NET      
@SeaSunK
呵呵,我也是受StackOverflow里那个帖子的启发,总结了一下而已。
图就是用Excel生成的啊

 回复 引用 查看   
#12楼 2010-04-23 13:53 young40      
挺好的,写的,呵呵。
 回复 引用 查看   
#13楼 2010-04-23 13:56 飞林沙      
好文....
递归的方法之前想不到....

 回复 引用 查看   
#14楼[楼主] 2010-04-23 14:04 麒麟.NET      
@GaryChen
抬举了,不过是一个简单的算法而已,跟内功没啥关系。。。

 回复 引用 查看   
#15楼[楼主] 2010-04-23 14:04 麒麟.NET      
@young40
:)

 回复 引用 查看   
#16楼 2010-04-23 14:05 GaryChen      
@麒麟.NET
麒麟哥对CLR有很深入的理解啊..希望多在群里指教你一下哈

 回复 引用 查看   
#17楼 2010-04-23 14:05 Milo Yip      
內容不錯。不過之前說的是數組,應該更簡單,而且不會有C#字串的一些overhead(因為C#字串是immutable的)。

建議Benchmark時可以每個case執行多次,取其最少值。減少運行中受其他影響(context switch, interrupt, etc)。

 回复 引用 查看   
#18楼 2010-04-23 14:07 seiya027848      
非常棒,佩服
 回复 引用 查看   
#19楼[楼主] 2010-04-23 14:08 麒麟.NET      
@飞林沙
很多方法我事先都没想到,搜到那篇帖子以后才发现原来可以有这么多种方法

 回复 引用 查看   
#20楼[楼主] 2010-04-23 14:10 麒麟.NET      
引用Milo Yip:
內容不錯。不過之前說的是數組,應該更簡單,而且不會有C#字串的一些overhead(因為C#字串是immutable的)。

建議Benchmark時可以每個case執行多次,取其最少值。減少運行中受其他影響(context switch, interrupt, etc)。

哇,Milo大大光临,多谢指教:)

 回复 引用 查看   
#21楼[楼主] 2010-04-23 14:10 麒麟.NET      
引用GaryChen:
@麒麟.NET
麒麟哥对CLR有很深入的理解啊..希望多在群里指教你一下哈

- -||欢迎指教:)

 回复 引用 查看   
#22楼 2010-04-23 14:19 喵 喵      
很好
 回复 引用 查看   
#23楼 2010-04-23 14:38 贤达      
这种做法是值得大家学习的!
 回复 引用 查看   
#24楼 2010-04-23 14:42 码农SeraphWU      
长见识了!!
 回复 引用 查看   
#25楼 2010-04-23 15:01 张磊_larry.zhang      
楼主的精神非常值得学习
 回复 引用 查看   
#26楼 2010-04-23 15:02 张磊_larry.zhang      
老赵对北大青鸟的面试要求,估计只要写出第2种就算合格了吧
 回复 引用 查看   
#27楼 2010-04-23 15:13 HedgeHog      
有意思。楼主解释的很详细。
相信找这些解决方案也耗了不少时间。
支持你一下。

 回复 引用 查看   
#28楼 2010-04-23 15:35 ffffff      
支持!
 回复 引用 查看   
#29楼 2010-04-23 16:42 残香恨      
呵呵!
这两天流行玩反转了!

 回复 引用 查看   
#30楼 2010-04-23 17:10 田田的猪      
我就一句 哥 崇拜你 发自内心的 你的内功好强大
 回复 引用 查看   
#31楼 2010-04-23 17:15 炭炭      
说实话,测试的这段代码才显出搂住的内功。
顶一个!

 回复 引用 查看   
#32楼 2010-04-23 17:36 依落の守候      
好贴,顶了。
 回复 引用 查看   
#33楼 2010-04-23 17:39 kent06      
请问:Benchmark(string description, Func<string> func, int times)
中的
Func<string>是如何传值的?
自己找了半天也没解决,只好问问楼主了!


 回复 引用 查看   
#34楼 2010-04-23 17:39 幸运猴子      
委托的那个写法很赞!很优美。
 回复 引用 查看   
#35楼 2010-04-23 18:53 Sevendays      
厉害了,学习了~
 回复 引用 查看   
#36楼 2010-04-23 18:55 longware      
“回”字的四种写法!呵呵
 回复 引用 查看   
#37楼 2010-04-23 19:50 LindyBoy      
不错,好文,很清楚,good
 回复 引用 查看   
#38楼 2010-04-23 21:58 Paladin.lao --VSTO      
这文章牛逼,很好,我收藏了
 回复 引用 查看   
#39楼 2010-04-23 22:32 Frank Xu Lei      
不错,好文~
 回复 引用 查看   
#40楼 2010-04-23 22:38 Microshaoft      
面试
你应该自行实现栈
而不是用一个现成的栈

 回复 引用 查看   
#41楼 2010-04-23 22:40 aohan      
看似简单的问题能分析的这么透彻,非常好

有理有据,不管是用来理解概念还是要做决策,应该说都足够的清晰。

 回复 引用 查看   
#42楼 2010-04-25 12:51 深山老林      
佩服。
 回复 引用 查看   
#43楼 2010-04-26 13:08 Nickelzhang      
好文,其实递归的本质还是利用了栈
 回复 引用 查看   
#44楼 2010-04-26 23:36 StephenJu      
分析的很透彻!
记得没错的话以前面试时也遇到过异或(不通过引用第3个变量交换2者的值)
VERY GOOD!

 回复 引用 查看   
#45楼 2010-05-07 01:42 Franky      
学习了...
 回复 引用 查看   
#46楼 2010-05-14 21:43 张跃      
引用SeaSunK:我也是看了老赵的文章写了一下,其中就用了异或。原来差距是这样拉开的。不错,顶一下。。好好看!还想请教下那图是怎样弄的?

同问。。。
刚打了一个同问,发现后面回复已经给了结果。不过还是顶一下。
lz用的office2010?

 回复 引用 查看   
#47楼[楼主] 2010-05-17 09:49 麒麟.NET      
@张跃
是Office 2007