如何以允许快速插入任何索引的方式表示一系列音乐笔记?

时间:2014-06-10 02:27:29

标签: algorithm data-structures clojure functional-programming

为了“有趣”,并学习函数式编程,我正在开发一个Clojure中的程序,它使用来自这种音乐理论的思想称为“Westergaardian Theory”进行算法组合。它产生一系列音乐(其中一行只是一个由一系列音符组成的音符,每个音符都有音高和持续时间)。它基本上是这样的:

  1. 从包含三个音符的行开始(选择这些音符的具体细节并不重要)。
  2. 在此行上随机执行多个“操作”之一。该操作从满足特定标准的所有相邻音符对中随机选取(对于每对,标准仅取决于该对并且独立于该行中的其他音符)。它在所选对之间插入一个或多个音符(取决于操作)。每项操作都有自己独特的标准。
  3. 继续在线上随机执行这些操作,直到该线为所需长度。
  4. 我遇到的问题是我的实现速度很慢,我怀疑它可以更快。我是Clojure和函数式编程的新手(虽然我对OO有经验),所以我希望有更多经验的人可以指出我是不是在考虑功能范例还是错过某些FP技术

    我目前的实现是每一行都是一个包含地图的矢量。每张地图都有:note和a:dur。 :note的值是表示音符的关键字,如:A4或:C#3。 :dur的值是一个分数,表示音符的持续时间(1是整个音符,1/4是四分音符等等)。因此,例如,表示从C3开始的C大调的线看起来像这样:

    [
    {:note :C3 :dur 1}
    {:note :D3 :dur 1}
    {:note :E3 :dur 1}
    {:note :F3 :dur 1}
    {:note :G3 :dur 1}
    {:note :A4 :dur 1}
    {:note :B4 :dur 1}
    ]
    

    这是一个有问题的表示,因为实际上没有一种快速的方法可以插入到矢量的任意索引中。但插入是这些线路上最常执行的操作。我当前用于将音符插入一行的可怕功能基本上在插入时使用子集来分割矢量,使用conj来连接第一部分+音符+最后部分,然后使用flatten和vec使它们都是一维的向量。例如,如果我想将C3和D3插入索引3处的C大调(F3所在的位置),它会这样做(我将使用音符名称代替:note和:dur map):

    1. (conj [C3 D3 E3] [C3 D3] [F3 G3 A4 B4]),产生[C3 D3 E3 [C3 D3] [F3 G3 A4 B4]]
    2. (vec(flatten previous-vector))给出[C3 D3 E3 C3 D3 F3 G3 A4 B4]
    3. 运行时间是O(n),AFAIK。

      我正在寻找一种方法来加快插入速度。我已经搜索了有关Clojure数据结构的信息,这些数据结构有快速插入但没有找到任何可行的方法。我发现了“手指树”,但它们只允许在列表的开头或结尾快速插入。

      编辑:我把它分成两个问题。 The other part is here.

2 个答案:

答案 0 :(得分:5)

你遗漏的一件事是,理论上无论如何,手指树确实可以快速插入任何指数。它们只有直接允许你在任何一端插入,但它们也提供快速分割和快速连接,因此快速插入 - 任意位置功能可以被框定为"分成两个序列,附加到其中之一,然后再将它们连在一起"。

我说"理论上"因为手指树依赖于恒定时间内存访问,但是它们比简单的向量产生更多的缓存未命中,并且通常不会像你期望的那样执行。手指树很有趣,但在clojure中并不常用,我也不建议真正使用它们。

一种可能性是继续使用慢速操作。如果你的向量永远不会很长,并且性能不是很重要,那么O(n)插入操作就会非常重要。

如果这不好,那么有一个你想要的O(log(n))插入的解决方案,尽管它并不是很有趣。答案是......模拟可变指针!这是一种常常有效的方法:如果指针是可变的,你可以只有一个链表,每个单元知道它的两个邻居,并在插入时根据需要更新它们。但是你不能在这里,因为循环引用对于功能数据来说并不是很好。但是,您可以添加一个间接级别:为每个单元格提供一个唯一的标签",并让它只存储其邻居的标签。那么你没有循环引用,你可以便宜地进行本地更新。这是我所描述的布局的一个例子,你的C大调比例:

{:cell-data {0 {:left nil :right 1, :note :C3 :dur 1}
             1 {:left 0 :right 2, :note :D3 :dur 1}
             2 {:left 1 :right 3, :note :E3 :dur 1}
             3 {:left 2 :right 4, :note :F3 :dur 1}
             4 {:left 3 :right 5, :note :G3 :dur 1}
             5 {:left 4 :right 6, :note :A4 :dur 1}
             6 {:left 5 :right nil, :note :B4 :dur 1}}
 :first-node 0, :last-node 6}

此处的数字是连续的,但您可以看到如何在5到6之间添加节点,方法是创建一个{:left 5 :right 6}的新节点,并更改节点5的:right,以及节点6的:left

这个组织有点麻烦,但它确实满足了你的需求。

答案 1 :(得分:0)

如何在地图中使用比率键?这样,插入就可以使用键来执行所选对的平均键。

如果您需要在施工时遍历它,您甚至可以使用有序地图。

使用您的示例

编辑

  • C大调:
(def line {0 {:note :C3 :dur 1}
           1 {:note :D3 :dur 1}
           2 {:note :E3 :dur 1}
           3 {:note :F3 :dur 1}
           4 {:note :G3 :dur 1}
           5 {:note :A4 :dur 1}
           6 {:note :B4 :dur 1}})
  • 插入C3的位置(在E3和F3之间)
(def between-E3-and-F3 [2 3])
  • 插入C3:
(let [[pos-E3 pos-F3] between-E3-and-F3
      C3              {:note :C3 :dur 1}
      pos-C3          (/ (+ pos-E3 pos-F3) 2) ;; 5/2
      line            (accoc line pos-C3 C3)]
 ...)
  • 然后插入D3(在新插入的C3和F3之间):
(let [[pos-E3 pos-F3] between-E3-and-F3
      C3              {:note :C3 :dur 1}
      pos-C3          (/ (+ pos-E3 pos-F3) 2) ;; 5/2
      line            (accoc line pos-C3 C3)
      D3              {:note :D3 :dur 1}
      pos-D3          (/ (+ pos-C3 pos-F3) 2) ;; 11/4
      line            (accoc line pos-D3 D3)]
 ...)

如果pos1pos2是不同的比率(或整数或大数字),则可以确定pos-C3与两者不同(/将产生比率确切的,不是浮点数)。这样,您始终可以插入新笔记(不替换现有笔记)。要按顺序生成笔记列表,您只需对其进行排序:

(map second (sort-by first line))

或者:

(vals (sorted-map line)) ;; you can also initialize line as a sorted map
                         ;; before inserting the notes

你按顺序记下你的笔记。