我知道大部分编程语言都是图灵完备的,但我想知道是否可以用任何编程语言(尤其是任何编程范式)的算法以相同复杂度解决问题。
为了更明确说明我的问题,举个例子:是否存在一些问题,可以使用时间复杂度为x
(例如O(n)
)的命令式算法解决,但无法通过具有相同复杂度的函数式算法解决(或反之亦然)?
编辑:算法本身可以不同,问题在于解决问题的复杂度——使用语言中可用的任何方法。
我知道大部分编程语言都是图灵完备的,但我想知道是否可以用任何编程语言(尤其是任何编程范式)的算法以相同复杂度解决问题。
为了更明确说明我的问题,举个例子:是否存在一些问题,可以使用时间复杂度为x
(例如O(n)
)的命令式算法解决,但无法通过具有相同复杂度的函数式算法解决(或反之亦然)?
编辑:算法本身可以不同,问题在于解决问题的复杂度——使用语言中可用的任何方法。
一般而言,不是所有的算法都可以在所有语言中以相同的复杂度实现。例如,假设有一种语言禁止对数组进行O(1)的访问,那么这一点就变得微不足道了。然而,在函数式语言中没有任何算法(据我所知)无法以最优复杂度实现。算法伪代码的复杂度分析对哪些操作合法、哪些操作为O(1)做出了某些假设。如果打破其中一个假设,即使该语言是图灵完备的,您也可以改变算法实现的复杂度。图灵完备性不能保证任何操作的复杂度。
算法的运行时间可以通过O(n)等方式进行衡量,就像你所说的那样,算法的实现必须遵循相同的运行时间,否则它们就不能实现该算法。语言或实现本身并不会改变算法,因此也不会改变渐近运行时间。
尽管如此,某些语言和技术可能会使表达算法更加容易,并且由于语言的编译或执行方式而提供恒定的加速(或减速)。
log n
时间找到给定的数字,但在brainfuck中却不能。 - sepp2k[-]
这样的标准操作。你知道,在一些更古老的 RAM 设计中,RAM 探针可能需要 O(n) 的时间从位 0x0000 移动到 0x000f,在这种情况下它不再是 O(1) 时间了。当然,您可以争辩说它不再是“随机访问”内存,但任何内存都不是真正的随机访问。 - SOFe我认为一种语言可以有不同的基本操作,其成本为O(1),例如数学运算(+、-、*、/)、变量/数组访问(a[i])、函数调用以及您能想到的所有操作。
如果一种语言没有这些O(1)操作之一(例如脑弯曲不具有O(1)数组访问),它就无法像C语言一样以相同的复杂度完成所有任务,但如果一种语言具有更多的O(1)操作(例如具有O(1)数组搜索的语言),它就可以完成比C语言更多的任务。
我认为所有“严肃”的语言都具有相同的基本O(1)操作,因此它们可以以相同的复杂度解决问题。
我认为你的第一段是错误的。而且我认为你的编辑并没有改变它。
假设您要求实现的观察行为符合算法的时间复杂度,那么...
计算算法复杂度时会做出关于哪些操作是恒定时间的假设。这些假设是你将找到线索的地方。
一些更常见的假设是恒定时间数组访问、函数调用和算术操作等。
如果您无法在常量时间内使用语言提供这些操作,则无法以保留时间复杂度的方式重现算法。
合理的编程语言可以打破这些假设,并且有时必须这样做,比如处理不可变数据结构共享状态、并发等问题。
例如,Clojure使用树来表示向量。这意味着访问时间不是恒定的(我认为它是数组大小的log32,但即使它也可能是恒定的)。
您可以轻松想象一种语言在调用函数时必须在运行时执行复杂的操作。例如,决定指的是哪一个。
曾经有一段时间,浮点数和多字节整数乘除法不幸地不是常数时间(它们是在软件中实现的)。在语言过渡到使用硬件的时期,非常合理的语言实现表现出了非常不同的行为。如果你考虑Brainfuck或图灵机本身,那么有一个基本操作需要O(n)的时间,尽管在大多数其他语言中可以在O(1)的时间内完成——索引数组。
我不完全确定这一点,但我认为在函数式编程中也不能真正拥有数组(具有O(1)“获取位置元素”和O(1)“设置位置元素”)。由于不可变性,你可以拥有一个可以快速更改的结构,但访问它需要时间,或者你将不得不在每次更改时复制大量结构以获得快速访问。但我猜你可以使用单子来欺骗它。
从函数式编程和命令式编程的角度来看,我怀疑你不会发现任何真正的区别。
然而,如果单独看每种语言和实现方式,情况就不同了。暂且不考虑Brainfuck等语言的例子,仍然可以找到一些相当不错的例子。
我仍然记得很多年前在主机上写APL时的一个例子。任务是在一个已排序的数字数组中查找(并消除)重复项。当时,我所做的大部分编程工作都是用Fortran完成的,还有一些用Pascal(当时最新最伟大的东西)或BASIC完成的小片段。我做了一个看起来很明显的事情:编写了一个循环,遍历数组,比较array[i]
和array[i+1]
,并跟踪一些索引,将每个唯一的元素复制回适当数量的位置,具体取决于已经消除的元素数量。
虽然在我熟悉的语言中这种方法可能效果很好,但在APL中却几乎是一场灾难。更好的解决方案基于APL中易于操作而不是计算复杂性。具体来说,您需要将数组的第一个元素与旋转一个元素后的数组的第一个元素进行比较。然后,您可以将数组保持原样或删除最后一个元素。重复此过程直到整个数组都被遍历(据我回忆,当第一个元素小于旋转数组中的第一个元素时检测到)。
区别相当简单:像大多数APL实现一样(至少在当时),这个实现是一个纯解释器。单个操作(即使是相当复杂的操作)通常非常快,但解释输入文件需要相当长的时间。改进后的版本要短得多,解释速度也更快(例如,APL提供“旋转数组”作为单个原始操作,因此只需解释一个字符或两个字符,而不是一个循环)。