我主要使用mathematica作为数学工作台,并编写相对较小的ad-hoc程序。然而,我正在设计一个我打算在Mathematica中编程的系统。我需要将数据存储在树中,并搜索和遍历树。虽然我知道如何实现树,但我更喜欢标准的,经过测试的代码。我在Mathematica用户维基上查看了基本数据结构的类型。我没有找到,尽管Mathematica文档中有一个小例子。
现在问我的问题:
是否有适用于某处的数据结构的(开源)软件包?
您对数据结构使用了哪种方法?逐步开发自己的util包?
(不是问题,只是一个评论。也许......缺少(很多可用的)开源软件包是Mathematica没有应有的动力的原因。鸡/蛋问题,我害怕。)
答案 0 :(得分:41)
在Mathematica中,你所做的大部分工作都是基于表达式。表达式自然具有树形结构。对于深度优先遍历(可能是最常见的),您可以使用Scan
,Map
,Cases
等函数。与更传统的语言不同的是,没有保存表达式树中单个节点的标识的简单方法,因为Mathematica中没有指针。此外,对于Mathematica中惯用的表达式的许多操作,只需要在几个地方修改它就会复制整个表达式,因为表达式是不可变的。
将不可变的Mathematica表达式用作树仍然有几个优点。一个是因为它们是不可变的,只要看一下它们就很容易理解它们存储的东西(状态和行为并不混合)。另一个原因是,有{... 1}},Map
或MapIndexed
等高效且通用的功能可以遍历它们。例如,访问者设计模式为invisible - 它只是Scan
,内置于语言中。此外,还有内置函数,如Map[f,tree,Infinity]
,Cases
,Replace
等,它们允许编写非常简洁和声明性的代码来解构树,找到具有一定数量的树。因为树不仅限于从列表构建并且是从不同的头构建的,所以可以有效地使用它来编写非常简洁的树处理代码。最后,根据exploratory and bottom-up programming的精神,可以非常轻松地构建您想要的任何树结构,从而更轻松地执行实验和原型设计,从而缩短开发周期并最终实现更好的设计。
也就是说,你当然可以实现“有状态”(可变)树数据结构。我怀疑,它通常尚未完成的真正原因是,与构建,修改和遍历此类树相关的性能损失,因为它将在每个步骤进行完整的符号评估过程(请参阅this帖子关于那个的更多细节)。有关如何在Mathematica上下文中使用二进制搜索树以获得高效代码的2个示例,请参阅我的帖子here(通用符号设置)和here(在编译代码的上下文中) )。对于在Mathematica中以惯用方式构建数据结构的一般方法,我推荐Roman Maeder的书籍:“Mathematica中的编程”,“Mathematica程序员I& II”,特别是“Mathematica中的计算机科学”。在后者中,他详细讨论了如何在Mathematica中实现二叉搜索树。 EDIT 正如@Simon所提到的,@Daniel Lichtblau的谈话也是一个很好的资源,它展示了如何构建数据结构并使其高效。
关于在Mathematica中实现数据结构的一般方法,它将包含一些状态,这是一个从this Mathgroup线程中的帖子中提取的简单示例 - 它实现了一个“对”数据结构。
ReplaceAll
以下是如何使用它:
Unprotect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
ClearAll[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
Module[{first, second},
first[_] := {};
second[_] := {};
pair /: new[pair[]] := pair[Unique[]];
pair /: pair[tag_].delete[] := (first[tag] =.; second[tag] =.);
pair /: pair[tag_].setFirst[value_] := first[tag] = value;
pair /: pair[tag_].getFirst[] := first[tag];
pair /: pair[tag_].setSecond[value_] := second[tag] = value;
pair /: pair[tag_].getSecond[] := second[tag];
Format[pair[x_Symbol]] := "pair[" <> ToString[Hash[x]] <> "]";
];
Protect[pair, setFirst, getFirst, setSecond, getSecond, new, delete];
创建新对对象列表:
pr = new[pair[]];
pr.setFirst[10];
pr.setSecond[20];
{pr.getFirst[], pr.getSecond[]}
{10, 20}
设置字段:
pairs = Table[new[pair[]], {10}]
{"pair[430427975]", "pair[430428059]", "pair[430428060]", "pair[430428057]",
"pair[430428058]", "pair[430428063]", "pair[430428064]", "pair[430428061]",
"pair[430428062]", "pair[430428051]"}
检查字段:
Module[{i},
For[i = 1, i <= 10, i++,
pairs[[i]].setFirst[10*i];
pairs[[i]].setSecond[20*i];]]
在我提到的帖子中有一个更详细的讨论。以这种方式创建的“对象”的一个大问题是它们没有自动垃圾收集,这可能是在顶级Mathematica中实现的OOP扩展本身没有真正起飞的主要原因之一。
Mathematica有几个OOP扩展,例如Roman Maeder的#.getFirst[] & /@ pairs
{10, 20, 30, 40, 50, 60, 70, 80, 90, 100}
#.getSecond[] & /@ pairs
{20, 40, 60, 80, 100, 120, 140, 160, 180, 200}
包(源代码在他的“Mathematica Programmer”一书中),classes.m
商业包,以及其他几个。但是,直到Mathematica本身提供有效的机制(可能基于某种指针或参考机制)来构建可变数据结构(如果这种情况发生),可能会出现与此类数据结构的顶级实现相关的大量性能损失在mma。此外,由于mma基于不变性作为核心思想之一,因此将可变数据结构与Mathematica编程的其他习惯用法很好地结合起来并不容易。
编辑
以下是上述示例中的一个简单的有状态树实现:
Objectica
一些使用示例:
Module[{parent, children, value},
children[_] := {};
value[_] := Null;
node /: new[node[]] := node[Unique[]];
node /: node[tag_].getChildren[] := children[tag];
node /: node[tag_].addChild[child_node, index_] :=
children[tag] = Insert[children[tag], child, index];
node /: node[tag_].removeChild[index_] :=
children[tag] = Delete[children[tag], index];
node /: node[tag_].getChild[index_] := children[tag][[index]];
node /: node[tag_].getValue[] := value[tag];
node /: node[tag_].setValue[val_] := value[tag] = val;
];
有关使用此可变树数据结构的一个非常重要的示例,请参阅我的this帖子。它还面对这个方法,重新使用Mathematica原生数据结构和函数,并很好地说明了本文开头讨论的要点。
答案 1 :(得分:8)
我主要使用mathematica作为数学工作台,并编写相对较小的临时程序。
Mathematica对此非常擅长。
您对数据结构使用了什么方法?逐步开发自己的util包?
我避免在Mathematica中创建自己的数据结构,因为它无法有效地处理它们。具体而言,Mathematica中的一般数据结构往往比其他地方慢10-1,000倍,这极大地限制了它们的实际用途。例如,Mathematica is 100× slower than F# at computing the range of depths in a red-black tree。
使用列表进行逻辑编程是Mathematica通常比其他编译语言慢几个数量级的一个示例。以下Mathematica程序使用链表来解决n-queens问题:
safe[{x0_, y0_}][{x1_, y1_}] :=
x0 != x1 && y0 != y1 && x0 - y0 != x1 - y1 && x0 + y0 != x1 + y1
filter[_, {}] := {}
filter[p_, {h_, t_}] := If[p[h], {h, filter[p, t]}, filter[p, t]]
search[n_, nqs_, qs_, {}, a_] := If[nqs == n, a + 1, a]
search[n_, nqs_, qs_, {q_, ps_}, a_] :=
search[n, nqs, qs, ps,
search[n, nqs + 1, {q, qs}, filter[safe[q], ps], a]]
ps[n_] :=
Fold[{#2, #1} &, {}, Flatten[Table[{i, j}, {i, n}, {j, n}], 1]]
solve[n_] := search[n, 0, {}, ps[n], 0]
这是等效的F#:
let safe (x0, y0) (x1, y1) =
x0<>x1 && y0<>y1 && x0-y0<>x1-y1 && x0+y0<>x1+y1
let rec filter f = function
| [] -> []
| x::xs -> if f x then x::filter f xs else filter f xs
let rec search n nqs qs ps a =
match ps with
| [] -> if nqs=n then a+1 else a
| q::ps ->
search n (nqs+1) (q::qs) (filter (safe q) ps) a
|> search n nqs qs ps
let ps n =
[ for i in 1..n do
for j in 1..n do
yield i, j ]
let solve n = search n 0 [] (ps n) 0
solve 8
使用Mathematica解决8-queens问题需要10.5s,使用F#需要0.07s。因此,在这种情况下,F#比Mathematica快150倍。
Stack Overflow问题Mathematica "linked lists" and performance给出了一个更极端的例子。将Mathematica代码简单地转换为F#,可以提供比Mathematica快4,000到200,000倍的等效程序:
let rand = System.Random()
let xs = List.init 10000 (fun _ -> rand.Next 100)
Array.init 100 (fun _ ->
let t = System.Diagnostics.Stopwatch.StartNew()
ignore(List.length xs)
t.Elapsed.TotalSeconds)
具体来说,Mathematica需要0.156s到16s来执行单次迭代,而F#需要42μs到86μs。
如果我真的想留在Mathematica,那么我就把Mathematica的一些内置数据结构中所做的一切都搞定了,例如: Dispatch
。