ReverseString,一个C#面试问题

时间:2009-06-17 21:45:33

标签: c#

我有一个面试问题,询问我对初级程序员写的一段代码的“反馈”。他们暗示可能存在问题,并表示将大量使用大字符串。

public string ReverseString(string sz)
{
    string result = string.Empty;
    for(int i = sz.Length-1; i>=0; i--)
    {
      result += sz[i]
    }
    return result;
}

我无法发现它。我没有看到任何问题。 事后我可以说用户应该调整大小,但看起来C#没有调整大小(我是C ++人)。

我最后编写的东西就像使用迭代器一样,如果可能,容器中的[x]不能随机访问,所以它可能很慢。和misc事情。但我绝对说我从来没有优化C#代码,所以我的想法可能没有让我在面试时失败。

我想知道,这段代码有什么问题,你们看到了吗?

-edit -

我将其更改为维基,因为可以有几个正确的答案。 此外,我很高兴我明确表示我从来没有优化过C#程序并且提到了misc其他的东西。哎呀。我一直认为C#对这些类型的东西没有任何性能问题。糟糕。

12 个答案:

答案 0 :(得分:57)

最重要的是?这样会很有效 - 它必须创建批次字符串(每个字符一个)。最简单的方法是:

public static string Reverse(string sz) // ideal for an extension method
{
    if (string.IsNullOrEmpty(sz) || sz.Length == 1) return sz;
    char[] chars = sz.ToCharArray();
    Array.Reverse(chars);
    return new string(chars);
}

答案 1 :(得分:36)

问题是字符串连接的代价很高,因为字符串在C#中是不可变的。给出的示例将在每次迭代时创建一个字符长一个字符的新字符串,效率非常低。为避免这种情况,您应该使用StringBuilder类,如下所示:

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

StringBuilder专门针对这样的场景编写,因为它使您能够连接字符串而不会有过多的内存分配。

你会注意到我为StringBuilder提供了一个你不经常看到的初始容量。如您所知,开始时的结果长度,这将删除不必要的内存分配。

通常会发生的事情是为StringBuilder分配一定量的内存(默认为16个字符)。一旦内容试图超过该容量,它就会使自己的能力增加一倍(并且继续)。这比每次分配内存要好得多,就像普通字符串一样,但如果你能避免这种情况,那就更好了。

答案 2 :(得分:21)

对目前给出的答案进行了一些评论:

  • 他们中的每一个(到目前为止!)都会在代理对和组合字符上失败。哦,Unicode的乐趣。反转字符串与反转字符序列不同。
  • 我喜欢Marc's optimisation的null,空和单字符输入。特别是,这不仅可以快速得到正确答案,而且还可以处理null(其他答案都没有)
  • 我原本以为ToCharArray后跟Array.Reverse会是最快的,但它会创建一个“垃圾”副本。
  • StringBuilder解决方案会创建一个字符串(而非char数组)并对其进行操作,直到您调用ToString为止。没有涉及额外的复制......但是还有更多工作要么保持长度等。

哪种解决方案更有效?好吧,我必须对它进行基准测试才能有任何想法 - 但即便这样也不会说出整个故事。你是否在内存压力很大的情况下使用它,额外的垃圾真的很痛苦?您的内存与CPU等有多快?

与以往一样,可读性通常是王< - 并且它在这方面并没有比Marc的答案好得多。特别是,对于一个一个错误,没有空间,而我实际上必须考虑验证其他答案。我不喜欢思考。它伤害了我的大脑,所以我尽量不去做。使用内置的Array.Reverse听起来对我来说要好得多。 (好吧,所以它仍然在代理等方面失败,但是嘿......)

答案 3 :(得分:7)

由于字符串是不可变的,因此每个+=语句将通过在最后一步中复制字符串以及单个字符来形成新字符串来创建新字符串。实际上,这将是O(n 2 )算法而不是O(n)。

更快的方法是(O(n)):

// pseudocode:
static string ReverseString(string input) {
    char[] buf = new char[input.Length];
    for(int i = 0; i < buf.Length; ++i)
       buf[i] = input[input.Length - i - 1];
    return new string(buf);
}

答案 4 :(得分:3)

您可以在.NET 3.5中执行此操作:

    public static string Reverse(this string s)
    {
        return new String((s.ToCharArray().Reverse()).ToArray());
    }

答案 5 :(得分:1)

更好的解决方法是使用StringBuilder,因为它不是不可变的,你不会得到你将获得的可怕的对象生成行为。在.net中,所有字符串都是不可变的,这意味着每次命中时,+ =运算符都会创建一个新对象。 StringBuilder使用内部缓冲区,因此可以在缓冲区中完成反转,无需额外的对象分配。

答案 6 :(得分:1)

您应该使用StringBuilder类来创建结果字符串。字符串是不可变的,所以当你在循环的每个迭代中追加一个字符串时,必须创建一个新字符串,这不是很有效。

答案 7 :(得分:1)

我更喜欢这样的东西:

using System;
using System.Text;
namespace SpringTest3
{
    static class Extentions
    {
        static private StringBuilder ReverseStringImpl(string s, int pos, StringBuilder sb)
        {
            return (s.Length <= --pos || pos < 0) ? sb : ReverseStringImpl(s, pos, sb.Append(s[pos]));
        }

        static public string Reverse(this string s)
        {
            return ReverseStringImpl(s, s.Length, new StringBuilder()).ToString();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("abc".Reverse());
        }
    }
}

答案 8 :(得分:1)

x是要反转的字符串。

        Stack<char> stack = new Stack<char>(x);

        string s = new string(stack.ToArray());

答案 9 :(得分:1)

此方法将迭代次数减半。它不是从最后开始,而是从头开始并交换字符直到它到达中心。不得不将字符串转换为char数组,因为字符串上的索引器没有setter。

    public string Reverse(String value)
    {
        if (String.IsNullOrEmpty(value)) throw new ArgumentNullException("value");

        char[] array = value.ToCharArray();

        for (int i = 0; i < value.Length / 2; i++)
        {
            char temp = array[i];
            array[i] = array[(array.Length - 1) - i];
            array[(array.Length - 1) - i] = temp;
        }

        return new string(array);
    }

答案 10 :(得分:0)

Necromancing。
作为公共服务,这就是你实际上正确地反转字符串的方式(反转一个字符串 NOT 等于反转一个字符序列

public static class Test
{

    private static System.Collections.Generic.List<string> GraphemeClusters(string s)
    {
        System.Collections.Generic.List<string> ls = new System.Collections.Generic.List<string>();

        System.Globalization.TextElementEnumerator enumerator = System.Globalization.StringInfo.GetTextElementEnumerator(s);
        while (enumerator.MoveNext())
        {
            ls.Add((string)enumerator.Current);
        }

        return ls;
    }


    // this 
    private static string ReverseGraphemeClusters(string s)
    {
        if(string.IsNullOrEmpty(s) || s.Length == 1)
             return s;

        System.Collections.Generic.List<string> ls = GraphemeClusters(s);
        ls.Reverse();

        return string.Join("", ls.ToArray());
    }

    public static void TestMe()
    {
        string s = "Les Mise\u0301rables";
        // s = "noël";
        string r = ReverseGraphemeClusters(s);

        // This would be wrong:
        // char[] a = s.ToCharArray();
        // System.Array.Reverse(a);
        // string r = new string(a);

        System.Console.WriteLine(r);
    }
}

请参阅: https://vimeo.com/7403673

顺便说一句,在Golang中,正确的方法是:

package main

import (
  "unicode"
  "regexp"
)

func main() {
    str := "\u0308" + "a\u0308" + "o\u0308" + "u\u0308"
    println("u\u0308" + "o\u0308" + "a\u0308" + "\u0308" == ReverseGrapheme(str))
    println("u\u0308" + "o\u0308" + "a\u0308" + "\u0308" == ReverseGrapheme2(str))
}

func ReverseGrapheme(str string) string {

  buf := []rune("")
  checked := false
  index := 0
  ret := "" 

    for _, c := range str {

        if !unicode.Is(unicode.M, c) {

            if len(buf) > 0 {
                ret = string(buf) + ret
            }

            buf = buf[:0]
            buf = append(buf, c)

            if checked == false {
                checked = true
            }

        } else if checked == false {
            ret = string(append([]rune(""), c)) + ret
        } else {
            buf = append(buf, c)
        }

        index += 1
    }

    return string(buf) + ret
}

func ReverseGrapheme2(str string) string {
    re := regexp.MustCompile("\\PM\\pM*|.")
    slice := re.FindAllString(str, -1)
    length := len(slice)
    ret := ""

    for i := 0; i < length; i += 1 {
        ret += slice[length-1-i]
    }

    return ret
}

这是不正确的方法(ToCharArray.Reverse):

func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

请注意,您需要了解
之间的区别 - 角色和字形
- 一个字节(8位)和一个码点/符文(32位)
- 一个代码点和一个GraphemeCluster [32+位](又名Grapheme / Glyph)

Reference:

  

字符是一个重载的术语,可能意味着许多事情。

     

代码点是信息的原子单位。文字是一系列的   代码点。每个代码点都是一个数字,由...给出   Unicode标准。

     

字素是显示的一个或多个代码点的序列   作为单个图形单元,读者可识别为单个单元   书写系统的要素。例如,a和ä都是   字形,但它们可能由多个代码点组成(例如ä可能是   两个代码点,一个用于基本字符a,后跟一个用于   diaresis;但也有一个替代的遗留单一代码点   代表这个字形)。一些代码点永远不属于任何代码点   字形(例如零宽度非连接或方向覆盖)。

     

字形是一种图像,通常以字体(即集合)存储   字形),用于表示字素或其部分。字体可以   将多个字形组合成单个表示,例如,if   上面的ä是单个代码点,字体可以选择将其渲染为   两个独立的,空间重叠的字形。对于OTF,字体的GSUB和   GPOS表包含要进行的替换和定位信息   这项工作。字体可能包含多个替代字形   字形也是。

答案 11 :(得分:0)

 static string reverseString(string text)
    {
        Char[] a = text.ToCharArray();
        string b = "";
        for (int q = a.Count() - 1; q >= 0; q--)
        {
            b = b + a[q].ToString();
        }
        return b;
    }