您可以使用什么数据结构来删除和替换O(1)?还是在需要上述结构时如何避免出现这种情况?
ST
Haskell 的monad非常出色。
您可以使用什么数据结构来删除和替换O(1)?还是在需要上述结构时如何避免出现这种情况?
ST
Haskell 的monad非常出色。
Answers:
有大量的数据结构利用惰性和其他技巧来实现分摊的固定时间,甚至(对于某些有限的情况,例如队列)还可以对许多问题进行固定时间更新。Chris Okasaki的博士学位论文 “ Purely Functional Data Structures”和同名的书是一个很好的例子(也许是第一个主要的例子),但是自那以后,该领域取得了进步。这些数据结构通常不仅在界面上具有纯粹的功能,而且还可以用纯Haskell和类似语言实现,并且是完全持久的。
即使没有任何这些高级工具,简单的平衡二进制搜索树也可以提供对数时间更新,因此可以模拟可变存储器,而最坏情况下,对数速度会降低。
还有其他选项可能被视为作弊,但对于实现工作和实际性能非常有效。例如,线性类型或唯一性类型允许通过防止程序保留到先前的值(将被更改的内存)来就地更新,以作为概念上纯净语言的实现策略。这比持久性数据结构的通用性要小:例如,您不能通过存储状态的所有先前版本来轻松构建撤消日志。尽管AFAIK在主要功能语言中尚不可用,但它仍然是一个强大的工具。
将可变状态安全地引入功能设置的另一种选择是ST
Haskell中的monad。可以实现它而无需进行任何更改,并且不添加任何unsafe*
功能,它的行为就像只是一个花哨的包装,隐式地传递了持久性数据结构(参见参考资料State
)。但是由于某种类型的系统欺骗会强制执行求值顺序并防止转义,因此可以通过就地突变安全地实现它,并具有所有性能优势。
一种廉价的可变结构是参数堆栈。
看一看典型的SICP风格阶乘计算:
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
如您所见,to的第二个参数fac
用作包含快速变化的乘积的可变累加器n * (n-1) * (n-2) * ...
。但是,看不到可变变量,也没有办法无意间更改累加器,例如从另一个线程更改累加器。
当然,这是一个有限的例子。
您可以通过廉价地替换头节点(并且扩展为从头开始的任何部分)来获得不可变的链表:您只需将新头指向旧头所指向的下一个节点即可。这对许多列表处理算法(fold
基于任何东西)都适用。
您可以从基于HAMT的关联数组中获得相当不错的性能。从逻辑上讲,您会收到一个新的关联数组,其中某些键值对已更改。该实现可以在旧对象和新创建的对象之间共享大多数公共数据。不过这不是O(1);通常,至少在最坏的情况下,您会得到对数的东西。另一方面,与可变树相比,不可变树通常不会遭受任何性能损失。当然,这需要一些内存开销,通常远不是让人望而却步。
另一种方法基于这样的想法,即如果一棵树掉在森林中而没人听见,它就不需要发出声音。也就是说,如果您可以证明一点突变状态永远不会离开某些本地范围,则可以安全地对其内部的数据进行突变。
Clojure的瞬态是不可变数据结构的可变“影子”,不会泄漏到本地范围之外。Clean使用Uniques实现类似的目的(如果我没记错的话)。Rust通过静态检查的唯一指针来帮助做类似的事情。
ref
并将它们限制在一定范围内的方法。请参阅IORef
或STRef
。然后当然还有TVar
s和MVar
s相似,但是具有并发的语义(针对TVar
s的stm 和基于MVar
s的互斥体)
您要问的内容太宽泛了。O(1)从哪个位置拆卸和更换?序列的头?尾巴?一个任意的位置?使用的数据结构取决于这些细节。就是说,2-3棵手指树似乎是最灵活的持久数据结构之一:
我们提出了2-3个手指树,这是一种持久性序列的功能性表示,支持在摊销的恒定时间内访问末端,并且在较小块的大小上按时间对数进行级联和拆分。
(...)
此外,通过以通用形式定义拆分操作,我们获得了通用数据结构,该数据结构可以用作序列,优先级队列,搜索树,优先级搜索队列等。
通常,当更改任意位置时,持久性数据结构具有对数性能。这可能是问题,也可能不是问题,因为O(1)算法中的常数可能很高,并且对数减慢可能会“吸收”到整体速度较慢的算法中。
更重要的是,持久性数据结构使对程序的推理更加容易,并且这应该始终是您的默认操作模式。您应该尽可能支持持久性数据结构,并且在分析并确定持久性数据结构是性能瓶颈之后,才应使用可变数据结构。其他一切都是过早的优化。