zoukankan      html  css  js  c++  java
  • 趣味算法:字符串反转的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();
    }

    希望本文对你有所帮助。

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

  • 相关阅读:
    《SeleniumBasic 3.141.0.0
    《SeleniumBasic 3.141.0.0
    《SeleniumBasic 3.141.0.0
    MQ详解及四大MQ对比
    Locust的使用
    pytest测试框进阶(二)
    pytest测试框进阶(一)
    pytest测试框架简介(二)
    pytest测试框架简介(一)
    弱网测试简介
  • 原文地址:https://www.cnblogs.com/YangBinChina/p/4186441.html
Copyright © 2011-2022 走看看