趣味算法:字符串反转的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长度的情况)
是的,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种方法(转)的更多相关文章
- Java实现字符串反转的8种方法
/** * */ package com.wsheng.aggregator.algorithm.string; import java.util.Stack; /** * 8 种字符串反转的方法, ...
- Python实现字符串反转的几种方法
面试遇到的一个特无聊的问题--- 要求:在Python环境下用尽可能多的方法反转字符串,例如将s = "abcdef"反转成 "fedcba" 第一种:使用字符 ...
- [转]Python实现字符串反转的几种方法
#第一种:使用字符串切片 result = s[::-1] #第二种:使用列表的reverse方法 l = list(s) l.reverse() result = "".join ...
- Java将字符串反转的7种方法
/方法1 递归方法 public static String reverse1(String s) { int length = s.length(); if(length <= 1){ ret ...
- javascript 实现字符串反转的两种方法
第一种方法:利用数组方法 //先split将字串变成单字数组,然后reverse()反转,然后将数组拼接回字串 var str = "abcdef"; str.split(&quo ...
- c++中字符串反转的3种方法
第一种:使用algorithm中的reverse函数 #include <iostream> #include <string> #include <algorithm& ...
- C/C++字符串反转的N种方法
0x00 自己写一个 // 第一种 std::string reverse(std::string str) { std::string res(""); for (int i = ...
- Python字符串连接的5种方法
总结了一下Python字符串连接的5种方法: 加号 第一种,有编程经验的人,估计都知道很多语言里面是用加号连接两个字符串,Python里面也是如此直接用 "+" 来连接两个字符串: ...
- Js 字符串拼接的两种方法
字符串拼接的两种方法 用数组的方法的好处是:避免变量重新定义.赋值 <!DOCTYPE html> <html lang="en"> <head> ...
随机推荐
- docker -v挂载数据卷网络异常的问题
docker 删除容器并重新运行容器时报如下异常: docker: Error response from daemon: failed to create endpoint tomcat001 on ...
- mysql日常语句总结
#删除mysql的二进制日志文件 #将删除mysql-bin.*****1之前的日志文件 purge binary logs to 'mysql-bin.*****1'; #重新生成一个二进制日志文件 ...
- Python2.7<-------->Python3.x
版本差异 from __future__ Python2.7 Python3.x 除法 / // Unicode u'' ...
- js事件监听器用法实例详解
这篇文章主要介绍了js事件监听器用法,以实例形式较为详细的分析了javascript事件监听器使用注意事项与相关技巧,需要的朋友可以参考下本文实例讲述了js事件监听器用法.分享给大家供大家参考.具体分 ...
- 为Kindeditor控件添加图片自动上传功能
Kindeditor是一款功能强大的开源在线HTML编辑器,支持所见即所得的编辑效果.它使用JavaScript编写,可以无缝地与多个不同的语言环境进行集成,如.NET.PHP.ASP.Java等.官 ...
- 【QT】C++ GUI Qt4 学习笔记5
折腾了好几天,终于把这本书的第三章和第四章给看了个大概. 里面的函数调用关系可谓是复杂. 整理了一部分的函数关系如下: cell关系清理 data(role) 返回应该显示的值 或者对齐方式 或者公式 ...
- GCD的使用
什么是 GCD Grand Central Dispatch (GCD) 是 Apple 开发的一个多核编程的解决方法.该方法在 Mac OS X 10.6 雪豹中首次推出,并随后被引入到了 iOS4 ...
- August 23rd 2016 Week 35th Tuesday
The very essence of romance is uncertainty. 浪漫的精髓就在于它充满种种可能. And the uncertainty of life may be also ...
- 解药还是毒药(codevs 2594)
2594 解药还是毒药 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题解 查看运行结果 题目描述 Description Smart研制出对 ...
- Linux 压缩系列常用命令
tar 命令: http://man.linuxde.net/tar zip 命令: http://man.linuxde.net/zip unzip 命令: http://man.linuxde.n ...