老赵反对北大青鸟的随笔中提到了数组反转。这的确是一道非常基础的算法题,然而也是一道很不平常的算法题(也许所有的算法深究下去都会很不平常)。因为我写着写着,就写出来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长度的情况)

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

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

可见,直接使用指针的效率是最高的。而类库自带的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();
}

希望本文对你有所帮助。

转自 http://www.cnblogs.com/kirinboy/archive/2010/04/23/reverse-a-string.html

趣味算法:字符串反转的N种方法(转)的更多相关文章

  1. Java实现字符串反转的8种方法

    /** * */ package com.wsheng.aggregator.algorithm.string; import java.util.Stack; /** * 8 种字符串反转的方法, ...

  2. Python实现字符串反转的几种方法

    面试遇到的一个特无聊的问题--- 要求:在Python环境下用尽可能多的方法反转字符串,例如将s = "abcdef"反转成 "fedcba" 第一种:使用字符 ...

  3. [转]Python实现字符串反转的几种方法

    #第一种:使用字符串切片 result = s[::-1] #第二种:使用列表的reverse方法 l = list(s) l.reverse() result = "".join ...

  4. Java将字符串反转的7种方法

    /方法1 递归方法 public static String reverse1(String s) { int length = s.length(); if(length <= 1){ ret ...

  5. javascript 实现字符串反转的两种方法

    第一种方法:利用数组方法 //先split将字串变成单字数组,然后reverse()反转,然后将数组拼接回字串 var str = "abcdef"; str.split(&quo ...

  6. c++中字符串反转的3种方法

    第一种:使用algorithm中的reverse函数 #include <iostream> #include <string> #include <algorithm& ...

  7. C/C++字符串反转的N种方法

    0x00 自己写一个 // 第一种 std::string reverse(std::string str) { std::string res(""); for (int i = ...

  8. Python字符串连接的5种方法

    总结了一下Python字符串连接的5种方法: 加号 第一种,有编程经验的人,估计都知道很多语言里面是用加号连接两个字符串,Python里面也是如此直接用 "+" 来连接两个字符串: ...

  9. Js 字符串拼接的两种方法

    字符串拼接的两种方法 用数组的方法的好处是:避免变量重新定义.赋值 <!DOCTYPE html> <html lang="en"> <head> ...

随机推荐

  1. docker -v挂载数据卷网络异常的问题

    docker 删除容器并重新运行容器时报如下异常: docker: Error response from daemon: failed to create endpoint tomcat001 on ...

  2. mysql日常语句总结

    #删除mysql的二进制日志文件 #将删除mysql-bin.*****1之前的日志文件 purge binary logs to 'mysql-bin.*****1'; #重新生成一个二进制日志文件 ...

  3. Python2.7<-------->Python3.x

    版本差异 from __future__   Python2.7 Python3.x 除法 / // Unicode u''                                       ...

  4. js事件监听器用法实例详解

    这篇文章主要介绍了js事件监听器用法,以实例形式较为详细的分析了javascript事件监听器使用注意事项与相关技巧,需要的朋友可以参考下本文实例讲述了js事件监听器用法.分享给大家供大家参考.具体分 ...

  5. 为Kindeditor控件添加图片自动上传功能

    Kindeditor是一款功能强大的开源在线HTML编辑器,支持所见即所得的编辑效果.它使用JavaScript编写,可以无缝地与多个不同的语言环境进行集成,如.NET.PHP.ASP.Java等.官 ...

  6. 【QT】C++ GUI Qt4 学习笔记5

    折腾了好几天,终于把这本书的第三章和第四章给看了个大概. 里面的函数调用关系可谓是复杂. 整理了一部分的函数关系如下: cell关系清理 data(role) 返回应该显示的值 或者对齐方式 或者公式 ...

  7. GCD的使用

    什么是 GCD Grand Central Dispatch (GCD) 是 Apple 开发的一个多核编程的解决方法.该方法在 Mac OS X 10.6 雪豹中首次推出,并随后被引入到了 iOS4 ...

  8. August 23rd 2016 Week 35th Tuesday

    The very essence of romance is uncertainty. 浪漫的精髓就在于它充满种种可能. And the uncertainty of life may be also ...

  9. 解药还是毒药(codevs 2594)

    2594 解药还是毒药  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 钻石 Diamond 题解  查看运行结果     题目描述 Description Smart研制出对 ...

  10. Linux 压缩系列常用命令

    tar 命令: http://man.linuxde.net/tar zip 命令: http://man.linuxde.net/zip unzip 命令: http://man.linuxde.n ...