将以空格分隔的数字转换为整数数组的最有效方法是什么?

时间:2016-10-07 14:58:36

标签: c# .net algorithm optimization micro-optimization

我的问题很简单:在尝试进行转换时,我有一个真实的(未想象的)性能错误

"1273 912 84" --> {1273, 912, 84}

所以我想弄清楚如何尽可能快地完成它。

总有3个数字,前2个总是在[1,3000]范围内,最后2个总是在[1,100000]范围内。

背景:我正在研究HackerRank问题,我的解决方案是在其中一个测试用例上超时。我阅读了关于这个问题的讨论,一位老兄说,每个人都失败了最后一个测试用例的原因是因为他们解析了巨大的输入。

我尝试解决此性能错误的方法是创建类似

的类
class IntParser
{
    private Dictionary<string, int> _Map = new Dictionary<string, int>();

    public IntParser(int n)
    {
        for (int i = 1; i <= n; ++i)
        {
            _Map[i.ToString()] = i;
        }
    }

    public int[] ParseVals(string line)
    {
        return Array.ConvertAll(line.Split(' '), ParseVal);
    }

    public int ParseVal(string s)
    {
        int retval;
        try
        {
            retval = _Map[s]; 
        }
        catch(KeyNotFoundException)
        {
            retval = Int32.Parse(s);
            _Map[s] = retval;
        }
        return retval;
    }
}

并使用

初始化它
var parser = new IntParser(100000); 

然后像

一样使用它
int[] triplet = parser.ParseVals(Console.ReadLine());

令人惊讶的是,这仍然不够有效。

5 个答案:

答案 0 :(得分:2)

您可以在O(N)时间和O(N)空间中执行此操作。 你只需要创建一个迭代你的角色并实现一个小状态机的函数。你可以只有3个州:

1)我正在读一个空间 2)我正在读一个数字 3)到达列表的末尾

可能的转变是: 1)直接读取一个数字 1)从阅读空间到阅读数字 2)从阅读数字到阅读空间或到达列表末尾

您只需调整代码并记住进入状态1)。每当您退出州1)时,您必须添加一个您已阅读的数字列表。

答案 1 :(得分:2)

如果不进行剖析,很难说清楚,但要记住几点:

  1. String.Split生成垃圾。
  2. .NET中的例外代价很高,因此最好将TryGetValue用于缓存。
  3. 您可以通过简单地迭代数字来解析可能比Int32.Parse更快地解析整数。
  4. 与仅解析的成本相比,插入大量项目后,字典查找可能会增加。
  5. 然而,“3,000,000多行”对我来说似乎不是一个大问题,所以我不确定解析是否是你真正的罪魁祸首。

    为什么不配置文件? Visual Studio 2015社区捆绑了一个CPU分析器。

    (更新)

    我尝试通过完全删除String.Substring并在同一方法中进行所有解析来改进@ Jamiec的答案,并提出:

    static public int[] ParserInPlace(string s)
    {
        var result = new int[3];
        var x = 0;
        for (var i = 0; i < s.Length; i++)
        {
            if (s[i] == ' ')
            {
                x++;
                continue;
            }
    
            result[x] = result[x] * 10 + (s[i] - '0');
        }
    
        return result;
    }
    

    在我的机器上,这似乎比@ Jamiec的随机数解决方案快2倍,比OP的原始代码快了~10倍(发布模式,x86,Works On My Machine™):

    RANDOM INPUTS  
    ParserBasic:             2068.4759ms  
    OP's Parser:             1520.8422ms  
    ParserNoSplit:           1300.3933ms  
    ParserNoSplitNoIntParse:  322.271ms  
    ParserInPlace:            125.0064ms  
    
    SMALLEST INPUT (1 1 1)  
    ParserBasic:             1715.9653ms  
    OP's Parser:              702.8926ms  
    ParserNoSplit:           1006.344ms  
    ParserNoSplitNoIntParse:  203.4511ms  
    ParserInPlace:             59.2876ms  
    
    LARGEST INPUT (3000 3000 100000)  
    ParserBasic:             1971.8206ms  
    OP's Parser:              827.6612ms  
    ParserNoSplit:           1256.9101ms  
    ParserNoSplitNoIntParse:  274.5071ms  
    ParserInPlace:            111.802ms  
    

    如果你需要一次解析一行,那么你可能不需要在每次迭代中分配相同的int[] result数组(但你需要将它设置为零),这样你就可以减少垃圾甚至更进一步。

答案 2 :(得分:1)

我认为我能提出的最有效的方法就是不要使用String.Splitint.Parse

我比较了4种不同的方式

  1. 使用基本String.Split
  2. 使用您的IntParser
  3. 使用this fast string split answer
  4. 上的变体形式
  5. 添加@ CharlesMager的代码,用于将整数解析为3。
  6. 这是我如何进行比较的。

    我使用此algorythm创建了3,000,000个字符串以匹配您的输入

    static string CreateExampleString()
    {
        return String.Join(" ", new[] { rnd.Next(1, 3000), rnd.Next(1, 3000), rnd.Next(1, 100000) });
    }
    

    我创建了一个基准测试程序

    static TimeSpan Test(Func<string,int[]> parser, List<string> inputs)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        foreach (var input in inputs)
            parser(input);
        sw.Stop();
    
        return sw.Elapsed;
    }
    

    然后我定义了4个测试用例,第一个是

    static int[] ParserBasic(string input)
    {
        return input.Split(' ').Select(int.Parse).ToArray();
    }
    

    第二个就是我已经说过的IntParser,第三个是

    static public int[] ParserNoSplit(string s)
    {
        int[] result = new int[3];
        int x = 0;
        int b = 0;
        for(var i=0;i<s.Length;i++)
        {
            if(s[i] == ' ')
            {
                result[x++] = int.Parse(s.Substring(b, i - b));
                b = i + 1;
            }
        }
        result[x] = int.Parse(s.Substring(b));
        return result;
    }
    

    最后,我采用了上述内容并将int.Parse替换为@CharlesMager答案中解析的代码,并获得了最佳结果!

    static public int[] ParserNoSplitNoIntParse(string s)
    {
        int[] result = new int[3];
        int x = 0;
        int b = 0;
        for (var i = 0; i < s.Length; i++)
        {
            if (s[i] == ' ')
            {
                result[x++] = ParseVal(s.Substring(b, i - b));
                b = i + 1;
            }
        }
        result[x] = ParseVal(s.Substring(b));
        return result;
    }
    
    static int ParseVal(string s)
    {
        var result = 0;
    
        for (var i = 0; i < s.Length; i++)
        {
            result = result * 10 + (s[i] - '0');
        }
    
        return result;
    }
    

    实际测试代码是

    var allStrings = Enumerable.Range(0, 3000000).Select(x => CreateExampleString()).ToList();
    var result1 = Test(ParserBasic, allStrings);
    Console.WriteLine($"Result1: {result1.TotalMilliseconds}ms");
    
    var result2 = Test(parser.ParseVals, allStrings);
    Console.WriteLine($"Result2: {result2.TotalMilliseconds}ms");
    
    var result3 = Test(ParserNoSplit, allStrings);
    Console.WriteLine($"Result3: {result3.TotalMilliseconds}ms");
    
    var result4 = Test(ParserNoSplitNoIntParse, allStrings);
    Console.WriteLine($"Result4: {result4.TotalMilliseconds}ms");
    

    通过每个输入运行相同的300万个输入的结果如下:

      

    结果1:2491ms
      结果2:2132ms
      结果3:1579ms
      结果4:861毫秒

    哪个更好,当然。它可以在更多方面得到改进,但避免String.Split可以提高整体性能。

答案 3 :(得分:0)

试试这个(一定要使用Try Catch,以防你的字符串里面没有可转换的字符串:

    string myString = "1234 321 145";
    int[] myInts = Array.ConvertAll(myString.Split(' '), int.Parse);

在这里编辑: 我做了一个测试,看看花了多少时间:

    List<String> list = new List<String>();
    Random rnd = new Random();
    for (int i = 0; i < 3000; i++) {
        list.Add(String.Join(" ", new[] { rnd.Next(1, 3000), rnd.Next(1, 3000), rnd.Next(1, 100000) }));
    }
    Stopwatch sw = new Stopwatch();
    sw.Start();
    for (int i = 0; i < 3000; i++) {
        int[] myInts = Array.ConvertAll(list[i].Split(' '), int.Parse);
    }

    sw.Stop();

    MessageBox.Show(sw.Elapsed.ToString());

结果是:00:00:00.0016167

答案 4 :(得分:0)

使用此实现,我的速度提高了43%(每个dotTrace):

public int ParseVal(string s)
{
    var result = 0;

    for (var i = 0; i < s.Length; i++)
    {
        result = result*10 + (s[i] - '0');
    }

    return result;
}

大部分时间(70%)现在都在String.Split - 快速谷歌会提供可能有用的内容like this