为什么这段代码会产生指数循环? .Net,Lehvenstein距离

时间:2016-04-06 14:00:53

标签: c# .net recursion infinite-loop levenshtein-distance

所以最近我开始编写一个编码项目来尝试创建一些代码,以数学方式创建一种描述两个字符串相似之处的方法。在我的研究中,我在网上找到了很多例子来帮助我创建我想要的代码。 我发现了一个错误,在我的运行中它正在创建,我在调用,指数循环。它不是一个无限循环,它运行并且它解决了问题,但是我传入方法的字符串越长,方法运行的时间就越长。代码如下所示

public static int LevenshteinDistance(this string source, string target)
    {
        Console.WriteLine("Start of method");
        if (source.Length == 0) { return target.Length; }
        if (target.Length == 0) { return source.Length; }

        int distance = 0;

        Console.WriteLine("Distance creation");
        if (source[source.Length - 1] == target[target.Length - 1]) { distance = 0; }
        else { distance = 1; }

        Console.WriteLine("Recursive MethodCall");
        return Math.Min(Math.Min(LevenshteinDistance(source.Substring(0, source.Length - 1), target) + 1,
                                 LevenshteinDistance(source, target.Substring(0, target.Length - 1))) + 1,
                                 LevenshteinDistance(source.Substring(0, source.Length - 1), target.Substring(0, target.Length - 1)) + distance);
    }

因此,对于较小的字符串,这种方法运行得很好,但是当我开始传递地址或长名称等内容时需要很长时间。事实上,我完全解散了这个方法并写了另一个(我会提供这个,如果有人想要它,但这对问题并不重要),这符合我的目的,但为了解决问题和学习,我试图弄清楚究竟为什么这个在递归编码时需要这么长时间。我在调试模式下用笔和纸逐步完成了我的代码,当我在这里进行递归调用时

return Math.Min(Math.Min(LevenshteinDistance(source.Substring(0, source.Length - 1), target) + 1,
                                 LevenshteinDistance(source, target.Substring(0, target.Length - 1))) + 1,
                                 LevenshteinDistance(source.Substring(0, source.Length - 1), target.Substring(0, target.Length - 1)) + distance);
    }

我可以找出这些部分正在发生的事情

return Math.Min(***Math.Min(LevenshteinDistance(source.Substring(0, source.Length - 1), target) + 1,
                                 LevenshteinDistance(source, target.Substring(0, target.Length - 1))) + 1,***
                                 LevenshteinDistance(source.Substring(0, source.Length - 1), target.Substring(0, target.Length - 1)) + distance);
    }

我试图将我所说的部分用粗体和斜体字表示,它是围绕它的'***'部分。到达那个部分我理解但接下来的部分,第三个LevenshteinDistance调用和它的第一次迭代我开始失去对递归的关注以及它是如何工作的以及我的混乱开始的地方。这部分

LevenshteinDistance(source.Substring(0, source.Length - 1), target.Substring(0, target.Length - 1)) + distance);
    }

我收集的代码一旦到达此调用,然后开始重新进行第一次LevenshteinDistance调用并重复它刚刚执行的调用。这就是我对此感到困惑的原因。为什么它再次调用递归调用的第一部分,然后是第二部分,是什么导致时间完成代码以指数方式延长?

注意:在字面意义上,时间可能不是指数级的,运行短字符串比较的次数是亚秒级的,一旦字符串得到更长的时间,它就会从亚秒级跳出 - > ~15秒 - > 2:30分钟 - > 35分钟

第二注意:标记为无限循环,因为指数循环不存在,这有点接近它。

4 个答案:

答案 0 :(得分:4)

因为它是递归的,而不仅仅是单个递归(就像你用于树视图一样),这只小狗传递了自己3个递归调用的返回结果!

这就是为什么你看到指数时间随着字符串越长而增加的原因。

答案 1 :(得分:1)

对于大小为nm的一对字符串(源,目标),您正在对该函数进行3次递归调用。

LevenshteinDistance(source[0..n - 1], target)
LevenshteinDistance(source, target[0..m - 1])
LevenshteinDistance(source[0..n - 1], target[0..m - 1])

因此,您要为每个节点创建一个包含3个子节点的树,其最小深度为min(n,m),最大深度为max(m,n)

因此,在此树的每个级别中,节点数量是前一级别的3倍:

0
|- 1
    |- 2
    |- 2
    |- 2
|- 1
    |- 2
    |- 2
    |- 2
|- 1
    |- 2
    |- 2
    |- 2

等等。

因此,对于树中的每个级别k,您有3个 k 节点。 因此,算法的复杂度为O(3 max(n,m)),这是指数级的。

答案 2 :(得分:1)

标准递归算法多次计算值。

以下是两个大小为3的字符串的小例子,计算顺序为

D[2, 2] = min(recurse(1, 2), recurse(2, 1), recurse(1, 1) + eq(1, 1))

在3次递归调用之后,你得到

//first recursive call
D[1, 2] = min(recurse(0, 2), recurse(1, 1), recurse(0, 1))

//second recursive call
D[2, 1] = min(recurse(1, 1), recurse(2, 0), recurse(1, 0))

//third recursive call
D[1, 1] = min(recurse(0, 1), recurse(1, 0), recurse(0, 0))

您已经在这里看到我们有多个相同值的计算。

你已经想出了复杂性,指数。更精确Θ(3^min(m, n))Here is a good answer that explains and calculates the complexity.

但是,这可以通过对计算值使用缓存来克服,如果已经计算了值,则检查缓存。此方法也称为Memoization,然后复杂性变为Θ(nm)

答案 3 :(得分:1)

请注意,您正在为每个呼叫进行3次递归调用。我的数学略有偏差,但大致你正在为每个级别进行3次调用(在递归调用树中)。级别对应于2输入字符串之间的最小字符数。

对于LevenshteinDistance("a", "x")电话,您最终会拨打4个电话(第一个电话+3个递归电话)

对于LevenshteinDistance("ab", "xy")通话,您最终会拨打19个电话(首先拨打+ 3个递归通话,每次递归通话会产生3个以上的通话,其中2个通话会再次递响)

(ab, xy)
        (a, xy)
                (<empty>, xy)
                (a, x)
                        (<empty>, x)
                        (a, <empty>)
                        (<empty>, <empty>)
                (<empty>, x)
        (ab, x)
                (a, x)
                        (<empty>, x)
                        (a, <empty>)
                        (<empty>, <empty>)
                (ab, <empty>)
                (a, <empty>)
        (a, x)
                (<empty>, x)
                (a, <empty>)
                (<empty>, <empty>)

请注意,调用树中的每个体面(处理字符串中的最后一个字符)都不会将n减少1,导致总数在(3 ^(n + 1)-1)/ 2到(3)之间^(n + 2)-1)/ 2调用

希望能够充分了解代码的性能

我还没有对你的算法或实现进行太多分析,但是我可以告诉你一些提高性能的事情

  1. 在这种情况下,使用字符数组而不是字符串作为参数。并将指针传递给正在考虑的数组的最后一个字符。这不仅会减少大量不需要的分配,还会删除所有子字符串调用
  2. 使用动态编程,即存储调用结果并在启动递归调用之前查看,因为相同的递归调用已经进行了很多次