反转字符串的最佳方法

570

我刚刚不得不在C# 2.0中编写一个字符串反转函数(即LINQ不可用),并得出了以下结果:

public string Reverse(string text)
{
    char[] cArray = text.ToCharArray();
    string reverse = String.Empty;
    for (int i = cArray.Length - 1; i > -1; i--)
    {
        reverse += cArray[i];
    }
    return reverse;
}

就我个人而言,我并不热衷于这个函数,并且确信有更好的方法来完成它。有吗?


63
如果您想获得适当的国际支持,翻译可能会非常棘手。例如,克罗地亚语/塞尔维亚语有两个字符的字母“lj”、“nj”等。 “ljudi”的正确反转应该是“idulj”,而不是“idujl”。当涉及到阿拉伯语、泰语等语言时,情况可能更加复杂。 - dbkk
2
我想知道将字符串连接起来是否比初始化临时数组并将结果存储在其中再将其转换为字符串更慢? - The Muffin Man
4
较新的相关帖子:如何反转带有重音字符的字符串? - Jeppe Stig Nielsen
15
请问您所说的“最好”是指什么?速度最快?可读性最高?在多种情况下(如空值检查、多语言等)最可靠?在不同版本的C#和.NET中最易维护? - hypehuman
5
为什么没有内置的直接方法来完成这个简单的任务? - Krishnadas PC
53个回答

813
public static string Reverse( string s )
{
    char[] charArray = s.ToCharArray();
    Array.Reverse(charArray);
    return new string(charArray);
}

19
不需要提及Unicode:在C#中,字符是Unicode字符而不是字节。异或可能更快,但除了可读性差之外,这甚至可能是Array.Reverse()内部使用的方法。 - Nick Johnson
31
@Arachnid:实际上,C#中的字符是UTF-16代码单元;表示补充字符需要两个代码单元。请参见http://www.jaggersoft.com/csharp_standard/9.4.1.htm。 - Bradley Grainger
6
是的,sambo99,我想你是正确的,但使用UTF-32是相当罕见的情况。而且,XOR只在非常少量的值范围内才更快,我想正确的答案是为不同长度实现不同的方法。但这篇文章清晰简洁,我认为这是一个优点。 - PeteT
27
Unicode控制字符使得这种方法对于非拉丁字符集来说是无用的。请参考Jon Skeet使用袜子偶像的解释:http://codeblog.jonskeet.uk/2009/11/02/omg-ponies-aka-humanity-epic-fail/(在文章1/4处),或者观看视频:http://vimeo.com/7516539。 - Callum Rogers
24
希望您不会遇到代理字符或组合字符。 - dalle
显示剩余12条评论

215
这里提供了一种正确反转字符串"Les Mise\u0301rables"的解决方案,结果为"selbare\u0301siM seL"。该结果应该像selbarésiM seL一样呈现,而不是像大多数基于代码单元(Array.Reverse等)或甚至代码点(对代理对进行特殊处理的反转)的实现所产生的结果,例如selbaŕesiM seL(请注意重音符号的位置)。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

public static class Test
{
    private static IEnumerable<string> GraphemeClusters(this string s) {
        var enumerator = StringInfo.GetTextElementEnumerator(s);
        while(enumerator.MoveNext()) {
            yield return (string)enumerator.Current;
        }
    }
    private static string ReverseGraphemeClusters(this string s) {
        return string.Join("", s.GraphemeClusters().Reverse().ToArray());
    }

    public static void Main()
    {
        var s = "Les Mise\u0301rables";
        var r = s.ReverseGraphemeClusters();
        Console.WriteLine(r);
    }
}

(这里还有一个实时运行的示例:https://ideone.com/DqAeMJ

它简单地使用了 .NET 用于字形簇迭代的 API,这个 API 一直存在,但似乎有点“隐藏”。


1
这在某些依赖于本地设置的东西上会失败。 - R. Martinho Fernandes
1
嗯,我猜它仍然是未来可靠的(假设这是BCL实现的限制?对此进行修复将自动受益于使用这些API)。 - sehe
3
实际上,通过实例化StringInfo(s),然后通过SubstringByTextElements(x, 1)进行迭代,并使用StringBuilder构建新字符串,速度会显着更快。 - user502255
5
你使用了Jon Skeet多年前给出的例子有点奇怪 https://codeblog.jonskeet.uk/2009/11/02/omg-ponies-aka-humanity-epic-fail/,虽然Jon没有提到解决方案,只是列举了问题。很好,你想出了一个解决方案。也许Jon Skeet发明了一台时光机,回到了2009年,并发布了你在解决方案中使用的问题例子。 - barlop
1
此外,如果你使用“lés”这个词(只有三个字母和一个e),就可以让你的例子变得简单得多了。这样做很容易看出发生了什么。不要再包含“miserables”这个词了,因为有些人可能会错误地看到那里的“e”,而“miserables”中的“e”两边都有一个“s”。 - barlop
显示剩余4条评论

131

这似乎是一个出乎意料的棘手问题。

我建议在大多数情况下使用Array.Reverse,因为它是本地编码的,并且非常简单易懂,易于维护。

根据我测试的所有情况看来,它似乎胜过StringBuilder。

public string Reverse(string text)
{
   if (text == null) return null;

   // this was posted by petebob as well 
   char[] array = text.ToCharArray();
   Array.Reverse(array);
   return new String(array);
}

有第二种方法,对于某些字符串长度可以更快,它使用了 Xor

    public static string ReverseXor(string s)
    {
        if (s == null) return null;
        char[] charArray = s.ToCharArray();
        int len = s.Length - 1;

        for (int i = 0; i < len; i++, len--)
        {
            charArray[i] ^= charArray[len];
            charArray[len] ^= charArray[i];
            charArray[i] ^= charArray[len];
        }

        return new string(charArray);
    }

注意:如果您想支持完整的Unicode UTF16字符集,请阅读此文。并使用那里的实现代替。在字符被反转后,可以通过使用上述算法之一并运行字符串来清理它以进一步优化。

这是StringBuilder、Array.Reverse和Xor方法之间性能比较的结果。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication4
{
    class Program
    {
        delegate string StringDelegate(string s);

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

        public static string ReverseXor(string s)
        {
            char[] charArray = s.ToCharArray();
            int len = s.Length - 1;

            for (int i = 0; i < len; i++, len--)
            {
                charArray[i] ^= charArray[len];
                charArray[len] ^= charArray[i];
                charArray[i] ^= charArray[len];
            }

            return new string(charArray);
        }

        public static string ReverseSB(string text)
        {
            StringBuilder builder = new StringBuilder(text.Length);
            for (int i = text.Length - 1; i >= 0; i--)
            {
                builder.Append(text[i]);
            }
            return builder.ToString();
        }

        public static string ReverseArray(string text)
        {
            char[] array = text.ToCharArray();
            Array.Reverse(array);
            return (new string(array));
        }

        public static string StringOfLength(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 Main(string[] args)
        {

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

            foreach (int l in lengths)
            {
                int iterations = 10000;
                string text = StringOfLength(l);
                Benchmark(String.Format("String Builder (Length: {0})", l), ReverseSB, iterations, text);
                Benchmark(String.Format("Array.Reverse (Length: {0})", l), ReverseArray, iterations, text);
                Benchmark(String.Format("Xor (Length: {0})", l), ReverseXor, iterations, text);

                Console.WriteLine();    
            }

            Console.Read();
        }
    }
}

这是结果:

26251 Ticks String Builder (Length: 1) : called 10000 times.
33373 Ticks Array.Reverse (Length: 1) : called 10000 times.
20162 Ticks Xor (Length: 1) : called 10000 times.

51321 Ticks String Builder (Length: 10) : called 10000 times.
37105 Ticks Array.Reverse (Length: 10) : called 10000 times.
23974 Ticks Xor (Length: 10) : called 10000 times.

66570 Ticks String Builder (Length: 15) : called 10000 times.
26027 Ticks Array.Reverse (Length: 15) : called 10000 times.
24017 Ticks Xor (Length: 15) : called 10000 times.

101609 Ticks String Builder (Length: 25) : called 10000 times.
28472 Ticks Array.Reverse (Length: 25) : called 10000 times.
35355 Ticks Xor (Length: 25) : called 10000 times.

161601 Ticks String Builder (Length: 50) : called 10000 times.
35839 Ticks Array.Reverse (Length: 50) : called 10000 times.
51185 Ticks Xor (Length: 50) : called 10000 times.

230898 Ticks String Builder (Length: 75) : called 10000 times.
40628 Ticks Array.Reverse (Length: 75) : called 10000 times.
78906 Ticks Xor (Length: 75) : called 10000 times.

312017 Ticks String Builder (Length: 100) : called 10000 times.
52225 Ticks Array.Reverse (Length: 100) : called 10000 times.
110195 Ticks Xor (Length: 100) : called 10000 times.

2970691 Ticks String Builder (Length: 1000) : called 10000 times.
292094 Ticks Array.Reverse (Length: 1000) : called 10000 times.
846585 Ticks Xor (Length: 1000) : called 10000 times.

305564115 Ticks String Builder (Length: 100000) : called 10000 times.
74884495 Ticks Array.Reverse (Length: 100000) : called 10000 times.
125409674 Ticks Xor (Length: 100000) : called 10000 times.

好像对于短字符串,异或操作可能更快。


3
这不会返回一个字符串 - 你需要用 "new String(...)" 将其包裹起来。 - Greg Beech
顺便说一句,我刚刚查看了Array.Reverse的实现方式,它是针对字符本地化的...这应该比StringBuilder选项快得多。 - Sam Saffron
好帖子,我认为我会坚持使用Array.Reverse而不仅仅是因为它在各种字符串长度上都有良好的性能,而且因为代码简洁。让我们面对现实,维护是问题的一半。此外,所有这些using语句的性能惩罚是什么? - PeteT
10
这些方法不能处理包含基本多文种平面以外的字符的字符串,即Unicode字符>= U+10000,这些字符用两个C#字符表示。我已经发布了一个正确处理这些字符串的答案。 - Bradley Grainger

88

如果您可以使用 LINQ (.NET Framework 3.5+),那么下面的一行代码将为您提供简短的代码。别忘了添加 using System.Linq; 以便访问Enumerable.Reverse

public string ReverseString(string srtVarable)
{
    return new string(srtVarable.Reverse().ToArray());
}

注意:

  • 本版本并非最快的 - 根据Martin Niederl的说法,比此处最快选项慢5.7倍。
  • 与许多其他选项一样,此代码完全忽略了各种多字符组合,请将使用限制在家庭作业和不包含这些字符的字符串上。有关正确处理此类组合的实现,请参见此问题中的另一个答案

这大约比最受欢迎的版本慢了5.7倍,所以我不建议使用它! - Martin Niederl
当我在2008年写下原始问题时,我当时正在使用C# 2.0,并且无法使用LINQ - 正如问题开头的评论所述。 - Guy

52

如果字符串包含Unicode数据(严格来说是非BMP字符),那么已发布的其他方法会破坏它,因为在反转字符串时无法交换高代理项和低代理项代码单元的顺序。(有关此更多信息,请参见我的博客。)

以下代码示例将正确地反转包含非BMP字符的字符串,例如"\U00010380\U00010381"(乌加里特字母阿尔法,乌加里特字母贝塔)。

public static string Reverse(this string input)
{
    if (input == null)
        throw new ArgumentNullException("input");

    // allocate a buffer to hold the output
    char[] output = new char[input.Length];
    for (int outputIndex = 0, inputIndex = input.Length - 1; outputIndex < input.Length; outputIndex++, inputIndex--)
    {
        // check for surrogate pair
        if (input[inputIndex] >= 0xDC00 && input[inputIndex] <= 0xDFFF &&
            inputIndex > 0 && input[inputIndex - 1] >= 0xD800 && input[inputIndex - 1] <= 0xDBFF)
        {
            // preserve the order of the surrogate pair code units
            output[outputIndex + 1] = input[inputIndex];
            output[outputIndex] = input[inputIndex - 1];
            outputIndex++;
            inputIndex--;
        }
        else
        {
            output[outputIndex] = input[inputIndex];
        }
    }

    return new string(output);
}

30
实际上,C# 中的字符是由 16 位 UTF-16 编码单元组成的;使用其中两个编码单元来编码补充字符,所以这是必要的。 - Bradley Grainger
15
似乎System.String应该为包含Unicode补充字符的字符串公开一个HereBeDragons属性。 - Robert Rossney
4
没问题:这种方法只是翻转字符串中的码点。总体而言,翻转字形群集可能更有“用处”(但首先翻转任意字符串有什么“用处”呢?),但仅使用.NET框架中内置的方法实现并不容易。 - Bradley Grainger
2
@Richard:打破字形簇的规则比仅仅检测组合代码点要复杂一些;有关更多信息,请参阅UAX#29中的Grapheme Cluster Boundaries文档。 - Bradley Grainger
1
@Andrei 请尝试使用"悲惨世界" - R. Martinho Fernandes
显示剩余6条评论

31

好的,为了“不要重复自己”的原则,我提供以下解决方案:

public string Reverse(string text)
{
   return Microsoft.VisualBasic.Strings.StrReverse(text);
}

我的理解是,这个实现在VB.NET中默认可用,可以正确处理Unicode字符。


12
这个只能正确处理代理项。它会弄乱组合标记:http://ideone.com/yikdqX。 - R. Martinho Fernandes
4
在NET .6中正确处理组合标记! - Adam Calvet Bohl
太棒了,简单明了! - chikega

22

请看维基百科条目这里。他们实现了String.Reverse扩展方法。这使您可以编写如下代码:

string s = "olleh";
s.Reverse();

他们还使用其他答案提到的ToCharArray/Reverse组合。源代码如下:

public static string Reverse(this string input)
{
    char[] chars = input.ToCharArray();
    Array.Reverse(chars);
    return new String(chars);
}

太棒了,只是扩展方法在C# 2.0中没有被引入。 - Kobi

22

从.NET Core 2.1开始,有一种新的方法可以使用string.Create方法来反转字符串。

请注意,此解决方案无法正确处理Unicode组合字符等,例如“Les Mise\u0301rables”会被转换为“selbarésiM seL”。请参见其他答案以获得更好的解决方案。

public static string Reverse(string input)
{
    return string.Create<string>(input.Length, input, (chars, state) =>
    {
        state.AsSpan().CopyTo(chars);
        chars.Reverse();
    });
}

这实际上是将input的字符复制到一个新字符串中,并在原地反转该新字符串。

string.Create有什么用处?

当我们从现有数组创建字符串时,会分配一个新的内部数组并复制值。否则,在创建字符串后(在安全环境中)可能会对其进行变异。也就是说,在以下片段中,我们必须两次分配长度为10的数组,一次作为缓冲区,一次作为字符串的内部数组。

var chars = new char[10];
// set array values
var str = new string(chars);

string.Create实质上允许我们在创建字符串时操作内部数组。也就是说,我们不再需要缓冲区,因此可以避免分配那个char数组。

Steve Gordon在这里详细介绍了它。 MSDN上也有一篇文章(点击链接)

如何使用string.Create

public static string Create<TState>(int length, TState state, SpanAction<char, TState> action);

该方法需要三个参数:
  1. 要创建的字符串长度,
  2. 您想要使用的数据以动态创建新字符串,
  3. 一个委托,从数据创建最终字符串,在其中第一个参数指向新字符串的内部char数组,第二个参数是您传递给string.Create的数据(状态)。

在委托中,我们可以指定如何从数据创建新字符串。在这种情况下,我们只需将输入字符串的字符复制到新字符串使用的Span中。然后,我们反转Span,因此整个字符串被反转。

基准测试

为了比较我提出的反转字符串的方式与接受的答案,我使用BenchmarkDotNet编写了两个基准测试。

public class StringExtensions
{
    public static string ReverseWithArray(string input)
    {
        var charArray = input.ToCharArray();
        Array.Reverse(charArray);
        return new string(charArray);
    }

    public static string ReverseWithStringCreate(string input)
    {
        return string.Create(input.Length, input, (chars, state) =>
        {
            state.AsSpan().CopyTo(chars);
            chars.Reverse();
        });
    }
}

[MemoryDiagnoser]
public class StringReverseBenchmarks
{
    private string input;

    [Params(10, 100, 1000)]
    public int InputLength { get; set; }


    [GlobalSetup]
    public void SetInput()
    {
        // Creates a random string of the given length
        this.input = RandomStringGenerator.GetString(InputLength);
    }

    [Benchmark(Baseline = true)]
    public string WithReverseArray() => StringExtensions.ReverseWithArray(input);

    [Benchmark]
    public string WithStringCreate() => StringExtensions.ReverseWithStringCreate(input);
}

这是我电脑上的结果:
| Method           | InputLength |         Mean |      Error |    StdDev |  Gen 0 | Allocated |
| ---------------- | ----------- | -----------: | ---------: | --------: | -----: | --------: |
| WithReverseArray | 10          |    45.464 ns |  0.4836 ns | 0.4524 ns | 0.0610 |      96 B |
| WithStringCreate | 10          |    39.749 ns |  0.3206 ns | 0.2842 ns | 0.0305 |      48 B |
|                  |             |              |            |           |        |           |
| WithReverseArray | 100         |   175.162 ns |  2.8766 ns | 2.2458 ns | 0.2897 |     456 B |
| WithStringCreate | 100         |   125.284 ns |  2.4657 ns | 2.0590 ns | 0.1473 |     232 B |
|                  |             |              |            |           |        |           |
| WithReverseArray | 1000        | 1,523.544 ns |  9.8808 ns | 8.7591 ns | 2.5768 |    4056 B |
| WithStringCreate | 1000        | 1,078.957 ns | 10.2948 ns | 9.6298 ns | 1.2894 |    2032 B |

从上面可以看出,使用ReverseWithStringCreate方法只分配了ReverseWithArray方法所用内存的一半。


它比Linq的reverse快得多。 - code4j
我刚在 .Net 6 上尝试了这种方法,它返回了“selbaŕesiM seL”,看起来是正确的,而另一种提到的 Grapheme 方法返回了“selbarésiM seL”,这是不正确的。因此,这种方法不仅正确,而且速度快约100倍。 - wertzui
这是我找到的最快速反转字符串的方法。它也非常简单易读。 - Thanos Kyprianos

19

Greg Beech发布了一个unsafe选项,它确实是最快的(它是一种就地反转); 但是,正如他在答案中指出的那样,这是一个完全灾难性的想法

话虽如此,我很惊讶有这么多人一致认为Array.Reverse是最快的方法。仍然有一种unsafe方法可以返回反转后的字符串副本(没有就地反转的花招),对于小字符串来说Array.Reverse方法快得多

public static unsafe string Reverse(string text)
{
    int len = text.Length;

    // Why allocate a char[] array on the heap when you won't use it
    // outside of this method? Use the stack.
    char* reversed = stackalloc char[len];

    // Avoid bounds-checking performance penalties.
    fixed (char* str = text)
    {
        int i = 0;
        int j = i + len - 1;
        while (i < len)
        {
            reversed[i++] = str[j--];
        }
    }

    // Need to use this overload for the System.String constructor
    // as providing just the char* pointer could result in garbage
    // at the end of the string (no guarantee of null terminator).
    return new string(reversed, 0, len);
}

以下是一些基准测试结果

你可以看到,随着字符串变得越来越大,性能增益逐渐缩小并最终消失在 Array.Reverse 方法中。但对于小到中等大小的字符串来说,很难超越这种方法。


3
StackOverflow 关于大字符串的问题。 - Rezo Megrelidze
@rezomegreldize: 是的,那会发生的 ;) - Dan Tao

16

简单明了的答案是使用扩展方法:

static class ExtentionMethodCollection
{
    public static string Inverse(this string @base)
    {
        return new string(@base.Reverse().ToArray());
    }
}

这是输出结果:

string Answer = "12345".Inverse(); // = "54321"

您的代码示例中,Reverse()ToArray()的顺序不正确。 - Chris Walsh
@符号有什么作用? - user5389726598465
2
@user5389726598465 请查看此链接:https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/tokens/verbatim由于“base”是C#中的关键字,必须在其前面加上@符号,以便C#编译器将其解释为标识符。 - Dyndrilliac
Reverse 返回的是 IEnumerable<TSource>,而不是像代码中所示的字符串类型。string Answer = "12345".Inverse(); // = "54321" - Urasquirrel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接