Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/.net/20.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
C# 为什么这段代码会产生指数循环。净Lehvenstein距离_C#_.net_Recursion_Infinite Loop_Levenshtein Distance - Fatal编程技术网

C# 为什么这段代码会产生指数循环。净Lehvenstein距离

C# 为什么这段代码会产生指数循环。净Lehvenstein距离,c#,.net,recursion,infinite-loop,levenshtein-distance,C#,.net,Recursion,Infinite Loop,Levenshtein Distance,所以最近我开始了一个编码项目,试图创建一些代码,以数学方式创建一种描述两个字符串有多相似的方法。在我的研究中,我在网上找到了大量的例子来帮助我创建我想要的代码。 我发现了一个错误,在我运行它的过程中,它正在创建,我称之为,指数循环。它不是一个无限循环,它运行并解决问题,但是我传递给方法的字符串越长,方法运行的时间就越长。代码如下 public static int LevenshteinDistance(this string source, string target) {

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

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);
    }
我试着用黑体字和斜体字标出这个部分,我的意思是,它周围有“***”的部分。说到这一部分,我理解了,但是接下来的部分,第三个levenshteinsistance调用,这是第一次迭代,我开始失去对递归的关注,它是如何工作的,以及我的困惑从哪里开始的。这部分

LevenshteinDistance(source.Substring(0, source.Length - 1), target.Substring(0, target.Length - 1)) + distance);
    }
我收集的代码一旦到达这个调用,就开始再次执行第一个LevenshteinDistance调用,并在刚刚执行的调用上重复。这就是我困惑的地方。为什么它会再次调用递归调用的第一部分,然后调用第二部分?是什么导致完成代码的时间呈指数级增长

注意:从字面意义上讲,时间可能不是指数级的,运行短字符串比较的时间是亚秒,一旦字符串稍微长一点,它就会从亚秒->约15秒->2:30分钟->35分钟跳转


第二个注意事项:被标记为无限循环的指数循环并不存在,这有点接近它。

因为它是递归的,而不仅仅是单一递归(就像你在树视图中使用的那样),这只小狗会传递自己3次递归调用的返回结果

这就是为什么看到指数计时随着字符串的延长而增加

对于大小为
n
m
的一对字符串(源、目标),您正在对函数进行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
,您有3k个节点。
因此,算法的复杂度是O(3max(n,m)),这是指数型的。

标准递归算法多次计算值

下面是两个大小为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))


但是,可以通过对计算值使用缓存并检查缓存(如果已经计算了值)来克服这一问题。这个方法也会被调用,然后复杂度会变成
Θ(nm)

请注意,每个调用都要进行3次递归调用。我的数学有点不对劲,但大致上每个级别(在递归调用树中)调用3次。级别对应于2个输入字符串之间的最小字符数

对于
levenshteindication(“a”,“x”)
调用,您将最终进行4次调用(第一次调用+3次递归调用)

对于
LevenshteinDistance(“ab”,“xy”)
调用,您将进行19次调用(第一次调用+3次递归调用,每次递归调用将导致3次以上调用,其中2次调用将在最后一次递归)

(ab,xy)
(a,xy)
(,xy)
(a,x)
(,x)
(a,)
(, )
(,x)
(ab,x)
(a,x)
(,x)
(a,)
(, )
(ab,)
(a,)
(a,x)
(,x)
(a,)
(, )
请注意,调用树中的每一次删除(处理字符串中的最后一个字符)都不会将n减少1,从而导致调用总数介于(3^(n+1)-1)/2到(3^(n+2)-1)/2之间

希望这能充分说明代码的性能

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

  • 在本例中,使用字符数组而不是字符串作为参数。并将指针传递给正在考虑的数组的最后一个字符。这不仅会减少大量不必要的分配,而且会删除所有子字符串调用
  • 使用动态编程,即存储调用的结果,并在启动递归调用之前进行查找,因为相同的递归调用会进行很多次

  • “错误”不是递归本身,一些最有效的代码是递归方法,但它是您创建的递归类型,它是递归时间越长,输入字符串越长。最终您将遇到一个;)谢谢你,杰里米,我在哪里可以找到一个地方学习如何画一个程序的“树视图”?也许下次遇到类似的问题时,我可以尝试使用这种策略,只要使用调用堆栈,Ctrl+Alt+C,就可以了
    (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>)