【问题标题】:ReverseString, a C# interview-questionReverseString,一个 C# 面试问题
【发布时间】:2010-11-03 19:50:22
【问题描述】:

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

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] 不能随机访问,所以它可能很慢。和杂项。但我肯定说过我从来不需要优化 C# 代码,所以我的想法可能没有让我在面试中失败。

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

-编辑-

我将其更改为 wiki,因为可以有几个正确的答案。 我也很高兴我明确地说我从来不需要优化 C# 程序并提到了其他的杂项。哎呀。我一直认为 C# 在这类事情上没有任何性能问题。哎呀。

【问题讨论】:

  • 请记住,这与其说是一个实际问题,不如说是一个谜题。在现实生活中,您通常可以以最方便的方式反转字符串,然后继续。只有在您确定它会导致性能问题后才回来(通常不会)
  • 这不是现实世界,它的工作面试。

标签: c#


【解决方案1】:
 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;
    }

【讨论】:

    【解决方案2】:

    亡灵术。
    作为一项公共服务,这就是您实际正确反转 string
    的方式(反转字符串 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 后跟一个用于 透析;但还有一个替代的、遗留的、单一的代码点 代表这个字形)。某些代码点从不属于任何代码点 字形(例如零宽度非连接符或方向覆盖)。

    字形是图像,通常存储在字体中(这是一个集合 字形),用于表示字形或其部分。字体可能 将多个字形组合成一个表示,例如,如果 上面的 ä 是单个代码点,字体可以选择将其呈现为 两个独立的、空间重叠的字形。对于 OTF,字体的 GSUB 和 GPOS 表包含替换和定位信息,以使 这项工作。一个字体可能包含多个相同的替代字形 字形也是。

    【讨论】:

      【解决方案3】:

      这种方法将迭代次数减少了一半。它不是从头开始,而是从头开始并交换字符直到它到达中心。必须将字符串转换为 char 数组,因为字符串上的索引器没有设置器。

          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);
          }
      

      【讨论】:

        【解决方案4】:

        x 是要反转的字符串。

                Stack<char> stack = new Stack<char>(x);
        
                string s = new string(stack.ToArray());
        

        【讨论】:

          【解决方案5】:

          我更喜欢这样的:

          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());
                  }
              }
          }
          

          【讨论】:

            【解决方案6】:

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

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

            【讨论】:

            • (即使它有效,也不是很理想。Enumerable.Reverse() 必须建立一个元素缓冲区,需要定期调整大小。然后是迭代它的问题等。使用 Array.Reverse 效率更高。是的,它需要多几行代码 - 但它更好,IMO。)
            • 您是否在 Reverse 的结果上调用了 ToArray,也许? return new String(s.ToCharArray().Reverse().ToArray());
            【解决方案7】:

            到目前为止给出的一些答案:

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

            哪种解决方案更有效?好吧,我必须对其进行基准测试才能有任何想法——但即便如此,这也不能说明全部情况。您是否在内存压力高的情况下使用它,额外的垃圾真的很痛苦?您的内存与 CPU 等相比有多快?

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

            【讨论】:

            • 如果我写过一门语言,我会实现 string.Reverse() 只是为了避免这样愚蠢的面试问题!
            • 如果你这样做了,他们就不得不想出更愚蠢的问题来问人们。
            • 关于“Array.Reverse 对我来说听起来好多了。(好吧,所以它在代理等方面仍然失败,但是嘿......)”。什么是代理?我相信我曾经看过一个视频,你说反转“悲惨世界”会得到不正确的结果。但是,我在您说的那一刻就尝试过,但没有(我相信这是一年前的事情,并且与此相关。您还谈到了日期/时间和数字)。虽然它没有显示出来,但我几乎在使用 .NET 3.5 ideone.com/3ZzPg -edit- 的 winform 应用程序中做到了这一点 - 也许这段代码更好。它说的是真的ideone.com/SSNfN
            • @acidzombie24:您的代码使用锐角 e 的单个字符表示。如果您使用 两个 字符来表示它,则会发生故障:一个普通的 e 和一个组合字符。代理对略有不同,但相似——这就是将两个 UTF-16 代码单元组合起来创建一个代表 Unicode 代码点的地方。
            • @JonSkeet:什么字符串包含两个字符来表示一个字符?之前我试图找到一个,并且在那个视频之后一直睁大眼睛,但我从来没有找到一个带有这样字符的字符串
            【解决方案8】:

            问题在于字符串连接的成本很高,因为字符串在 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 个字符)。一旦内容试图超过该容量,它就会(我认为)自己的容量加倍并继续。这比使用普通字符串每次都分配内存要好得多,但如果你也能避免这种情况,那就更好了。

            【讨论】:

            • 我与它无关,但考虑一下这个人是否不小心撞到然后撞到了。从逻辑上讲,它不应该出现在最近的活动中,但这是可能的。我之所以想到这一点,是因为当我第一次进入这个网站时(5 个月前),我测试了向上然后向下然后向上投票。只是看看我能不能做到。
            • 加里:习惯吧。很多时候,人们不加评论就对正确答案投了反对票。
            【解决方案9】:

            您应该使用 StringBuilder 类来创建结果字符串。字符串是不可变的,因此当您在循环的每个交互中附加一个字符串时,必须创建一个新字符串,这不是很有效。

            【讨论】:

            • 出现字符串问题时不要立即自动急于使用 StringBuilder。可能还有其他更简单的解决方案——Marc 的代码既漂亮又优雅。
            【解决方案10】:

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

            【讨论】:

            • 啊,+= 生成了一个新对象!那太疯狂了。我一直认为'='迫使这是一个内部操作。为什么允许字符串更新自身以指向新字符串!?!
            • 字符串不允许自我更新 - 字符串 variable 允许重新分配以引用不同的字符串。
            【解决方案11】:

            最重要的是?这会降低性能 - 它必须创建 很多 字符串(每个字符一个)。最简单的方法是:

            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);
            }
            

            【讨论】:

              【解决方案12】:

              由于字符串是不可变的,每个+= 语句都会通过复制最后一步中的字符串以及单个字符来创建一个新字符串以形成一个新字符串。实际上,这将是一个 O(n2) 算法而不是 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);
              }
              

              【讨论】:

              • n² 在“大字符串”上尤为重要。
              • 这是我见过的最常见的 .NET 问题。字符串分配可能是一个瓶颈,因为临时字符串会影响 GC 性能。这是一个特别好的面试题,可以测试 .NET 体验与“我是上周读过 C# 书籍的 C++ 程序员”
              • 附带说明,分代 GC(如 .NET GC)非常擅长分配和释放短期对象。
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-09-14
              • 2020-09-20
              相关资源
              最近更新 更多