Your SlideShare is downloading. ×
PFDS 10.2.1 lists with efficient catenation
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×

Saving this for later?

Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime - even offline.

Text the download link to your phone

Standard text messaging rates apply

PFDS 10.2.1 lists with efficient catenation

280
views

Published on

Published in: Technology

0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
280
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
0
Comments
0
Likes
0
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. PFDS #11 10.2.1 Lists With Efficient Catenation @yuga 2012-11-03Copyright © 2012 yuga 1
  • 2. 目次 Structural Abstractionを用いた1つめの実装例として、 Catenable List を取り上げます。  定義: 何を作る  実装: どう作る  解析: どんなものなのかCopyright © 2012 yuga 2
  • 3. 定義 何を作るCopyright © 2012 yuga 3
  • 4. 実現する操作 以下の定義を実装したリストを作ります。 定義(CATENABLELISTシグネチャ): module type CATENABLELIST = sig type ‘a t val empty : ‘a t val isEmpty : ‘a t -> bool val cons : ‘a * ‘a t -> ‘a t val snoc : ‘a t * ‘a -> ‘a t val (++) : ‘a t -> ‘a t -> ‘a t val head : ‘a t -> ‘a val tail : ‘a t -> ‘a t end = Output Restricted Queues + (++)Copyright © 2012 yuga 4
  • 5. 実行時間はO(1) CatenableListはすべての操作をO(1)時間で実現します。  通常のリスト同士の連結操作(++)はO(n)時間かかるのに対し、 Catenable ListsではO(1)時間で可能になるのが特徴です。  Persistentな使い方をしても問題ないものにします。Copyright © 2012 yuga 5
  • 6. 実行時間はAmortized time でも、ごめんなさい。 Worst-Case timeではなくて、Amortized timeです。  Worst-CaseなCatenable Listsは11章に出てくるRecursive Slow-Downというテクニックを使って実現できます。 – Persistent Lists with Catenation via Recursive Slow-Down – こっちの方が先行して世に登場  今回扱うCatenable Listsは後発ですが、Worst-Caseなリスト より実装が簡単になっています。Copyright © 2012 yuga 6
  • 7. 実装 どう作るCopyright © 2012 yuga 7
  • 8. 実装のアイデア 効率の良い連結関数を作るため、リスト内部にqueueを設け て、その中に相手リストを格納します。 1 6 連結 2 4 ++ 7 8 ○はリストの 要素 3 9 青枠は queue 1 6 1 2 4 7 8 2 4 6 3 9 3 7 8 註: 空のqueueを省略しています 9Copyright © 2012 yuga 8
  • 9. 実装のアイデア Queueを新たに実装するのは本節の対象外です。以下の要件 を満たしていれば何でも良いので、既存のものを利用します。  QUEUEシグネチャを満たしている module type QUEUE = sig type ‘a t val empty : ‘a t val isEmpty : ‘a t -> bool val snoc : ‘a t * ‘a -> ‘a t val head : ‘a t -> ‘a val tail : ‘a t -> ‘a t end  すべての操作が Worst-Case / Amortized 関係なくO(1)時間で実行可能  Persistentな使い方をしても問題ないCopyright © 2012 yuga 9
  • 10. PFDSに出てきたqueue これまでに登場したqueueには以下のものがありました。 Section Name Cost Persistent 5.2 BatchedQueue O(n) worst-case time NG 6.3.2, BankersQueue O(1) amortized time OK 8.3 6.4.2 PhysicistsQueue O(1) amortized time OK 7.2 RealTimeQueue O(1) worst-case time OK 8.3 HoodMelvilleQueue O(1) worst-case time OK 10.1.3 BootStrappedQueue O(1) amortized time OK ⇒ BatchedQueue以外なら良さそうです。Copyright © 2012 yuga 10
  • 11. Structural Abstractionテンプレートを使って実装開始 10.2節に登場したテンプレートを参考に進めます。 ‘a c : Primitive type Queue ’a b : Bootstrapped type CatenableList type ‘a b = E | B of ‘a * ‘a b c let unit_b x = B (x, empty_b) let insert_b = function | (x, E) -> B (x, empty_c) | (x, B (y, c)) -> B (x, insert_c (unit_b y, c)) let join_b = function | (b, E) -> b | (E, b) -> b _c は ‘a c | (B (x, c), b) -> B (x, insert_c (b, c)) _b は ‘a b の関数Copyright © 2012 yuga 11
  • 12. 実装: empty / isEmpty 最初に、データ構造として空リストだけ定義します。 ここではCatenableListの型を t とします。 module CatenableList : CATENABLELIST = struct type ‘a t = E | … … end emptyとisEmptyの実装はデータコンストラクタを見るだけです。 let empty = E let isEmpty = function | E -> true | _ -> falseCopyright © 2012 yuga 12
  • 13. 実装: ++ rev.1 QUEUEシグネチャを実装したモジュールをQという名前で受け取 ります。ここではqueueの型を t として、リストのデータ構造を定 義します。 module CatenableList (Q : QUEUE) : CATENABLELIST = struct type ‘a t = E | C of ‘a * ‘a t Q.t … end (++)は、1つめのリストのqueueに2つめのリストを格納します。 let (++) xs ys = match (xs, ys) with | (E, ys) -> ys | (xs, E) -> xs | (C (x, q), ys) -> C (x, Q.snoc (q, ys))Copyright © 2012 yuga 13
  • 14. 実装: ++ rev.2 4行目は、あとで他の関数からも利用するので、ヘルパー関数link にくくりだします。 module CatenableList (Q : QUEUE) : CATENABLELIST = struct type ‘a t = E | C of ‘a * ‘a t Q.t let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let (++) xs ys = match (xs, ys) with | (E, ys) -> ys | (xs, E) -> xs | (xs, ys) -> link (xs, ys) … endCopyright © 2012 yuga 14
  • 15. 実装: cons / snoc consとsnocは、さきほど実装した(++)を使えば簡単です。 let cons (x, xs) = C (x, Q.empty) ++ xs let snoc (xs, x) = xs ++ C (x, Q.empty)Copyright © 2012 yuga 15
  • 16. 図解: cons / snoc consとsnocをそれぞれ3回繰り返した結果です。 3 cons cons 2 cons 1 2 E 1 1 1 2 3 snoc snoc snoc 1 1 1 E 2 2 3Copyright © 2012 yuga 16
  • 17. 実装: head headは先頭を取り出すだけです。 exception Empty let head = function | E -> raise Empty | C (x, _) -> xCopyright © 2012 yuga 17
  • 18. 実装: tail rev.1 tailは先頭をすてて、queueの中身をリスト状につなぎなおします。 exception Empty let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let linkAll q = linkは再掲 let x = Q.head q in let q’ = Q.tail q in if Q. isEmpty q’ then x else link (t, linkAll q’) let tail = function | E -> raise Empty | C (_, q) -> if Q.isEmpty q then q else linkAll qCopyright © 2012 yuga 18
  • 19. 図解: tail tailによりqueueの中身がつなぎなおされる過程です。 最初にheadが取り除かれます。 1 2 5 6 2 5 6 3 4 7 8 3 4 7 8 9 9Copyright © 2012 yuga 19
  • 20. 図解: tail 続いてqueueをほどいていきます。 2 5 6 2 5 6 3 4 7 8 3 4 7 8 9 9Copyright © 2012 yuga 20
  • 21. 図解: tail ほどき終わったらリスト状につなぎなおします。 5 2 5 6 2 6 3 4 7 8 3 4 7 8 9 9Copyright © 2012 yuga 21
  • 22. 実装: tail rev.2 tailの完了です。 2 3 4 5 6 7 8 9Copyright © 2012 yuga 22
  • 23. 実装: 同じデータを何度もtailしたとき しかし、今の実装では、tailするたびに最悪O(n)回linkを実行 することになり 、CatenableListをpersistentなデータ構造と して使うことができません。 2 1 tail O(n) 3 2 3 4 … 99 … 99 2 2 3 3 … … ならし解析が意味をなさない tail tail (参考: 5.6節 The Bad News) O(n) O(n) 99 99Copyright © 2012 yuga 23
  • 24. 実装: tail rev.2 そこで、linkAllの再帰実行を遅延データに包んで、先頭要素から queueをほどく処理を、順次必要になるまで遅延させます。 これによりtailがincremental関数になります。 exception Empty let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let linkAll q = let lazy t = Q.head q in let q’ = Q.tail q in if Q. isEmpty q’ then t else link (t, lazy (linkAll q’)) let tail = function | E -> raise Empty | C (_, q) -> if Q.isEmpty q then q else linkAll q 型: ‘a t (参考: 6章) 型: ‘a t Lazy.tCopyright © 2012 yuga 24
  • 25. 実装: ++ rev.3 その結果、queueに格納するデータ型が変化するので修正します。 module CatenableList (Q : QUEUE) : CATENABLELIST = struct type ‘a t = E | C of ‘a * ‘a t Lazy.t Q.t let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let (++) xs ys = match (xs, ys) with | (E, ys) -> ys | (xs, E) -> xs | (xs, ys) -> link (xs, lazy ys) … endCopyright © 2012 yuga 25
  • 26. 実装: 完成 module CatenableList (Q : QUEUE) : CATENABLELIST = struct type ‘a t = E | C of ‘a * ‘a t Lazy.t Q.t exception Empty let empty = E let isEmpty = function | E -> true | _ -> false let link (C (x, q), ys) -> C (x, Q.snoc (q, ys)) let (++) xs ys = match (xs, ys) with | (E, ys) -> ys | (xs, E) -> xs | (xs, ys) -> link (xs, lazy ys) let cons (x, xs) = C (x, Q.empty) ++ xs let snoc (xs, x) = xs ++ C (x, Q.empty) let head = function | E -> raise Empty | C (x, _) -> x let linkAll q = let lazy t = Q.head q in let q’ = Q.tail q in if Q. isEmpty q’ then t else link (t, lazy (linkAll q’)) let tail = function | E -> raise Empty | C (_, q) -> if Q.isEmpty q then q else linkAll q endCopyright © 2012 yuga Ocamlで実際に動かしてみたやつ: https://github.com/yuga/readpfds/blob/master/OCaml/catenableList.ml 26
  • 27. 解析 どんなものなのかCopyright © 2012 yuga 27
  • 28. 解析: ならし解析 実行コストの考え方:  ++ / cons / snoc / head 関数の実行コストは、実装からあき らかにO(1) worst-case timeです。  tail関数のO(n) worst-case timeな実行コストを、他の関数と の間でならして、CatenableListのすべての操作がO(1) amortized timeであることを証明します。  CatenableListのデータ構造に影響を与えるのは ++ / cons / snoc / tail 関数ですが、cons / snoc 関数は ++ 関数に依存し ています。ならし解析は++ 関数と tail 関数の2つに注目して 行います。Copyright © 2012 yuga 28
  • 29. 解析: Banker’s method ならし解析にあたり、Banker’s methodを採用します。  tail 関数の実行コストはサスペンションとして負債(debits)にし linkAll 関数が link 関数を呼ぶときの1番目の引数のノードに割り当 てます。  ++ 関数の実行コストもサスペンションとして負債(debits)にし、 ++ 関数が link 関数を呼ぶときの1番目の引数のノードに割り当て ます。  各Nodeが tail 関数によって取り除かれるとき、そのノードに 割り当てられたdebitsがすべて支払い済み(残サスペンション 数=0)であるようにします。Copyright © 2012 yuga 29
  • 30. 解析: 定義(ツリー) CatenableListのデータ構造を、ノードによって構成され たツリーが、階層状になっているものと考えます。 0 1 2 7 8 𝑡0 𝑡2 𝑡3 3 4 5 6 𝑡 𝑡1Copyright © 2012 yuga 30
  • 31. 解析: 定義(ノード識別子, degree, depth) このツリーにラベルと関数を定義します。 0 root (0 th) node of t 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 0 = 4 4th node of t 1 2 3 4 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 4 = 4 1st 2nd 3 rd 𝑑𝑒𝑝𝑡ℎ 𝑡 4 = 1 node of t 0 th node of 𝑡1 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 𝑗 0 = 4 5 6 7 8 1st 𝑑𝑒𝑝𝑡ℎ 𝑡 8 = 2 node of 𝑡 𝑗 9 10 11 12 𝑡 𝑡𝑗Copyright © 2012 yuga 31
  • 32. 解析: 各ノードに割り当てるdebitsの考え方 各ノードに割り当てるdebitsは以下のようになります。  ならし解析の目的はlinkAllのコストを配分すること  queueの中の子ノード数 (linkAllのコスト) = queueに含まれるサスペンション数 = そのノードに割り当てるdebits数  デビット数を表す関数を定義します。 𝑑 𝑡 𝑖 = ツリー 𝑡 の 𝑖 𝑡ℎ ノード上の𝑑𝑒𝑏𝑖𝑡𝑠数 𝑖 𝐷𝑡 𝑖 = 𝑗=0 𝑑 𝑡 (𝑗) = ツリー 𝑡 のルートノードから 𝑖 𝑡ℎ ノードまでの合計𝑑𝑒𝑏𝑖𝑡𝑠数Copyright © 2012 yuga 32
  • 33. 解析: Debit Invariant #1 ある1つのノード上に割り当てられるdebits数の上限を、 以下の不変式で表します。  あるノードのqueueに含まれるサスペンション数の上限は、 そのノードの出次数(out degree) ⇒ 𝑑 𝑡 𝑖 ≤ 𝑑𝑒𝑔𝑟𝑒𝑒 𝑡 (𝑖) … (1) 0 ツリーの全ノードの出次数の合計は ノード数よりも1小さいので、 0th node <= 4 1 2 7 1st node <= 0 ⇒ 𝐷 𝑡 ≤ |𝑡| 2nd 7th node node <= <= 3 2 8th node <= 2 12th node <= 0 8 12Copyright © 2012 yuga 33
  • 34. 解析: Debit Invariant #2 あるノードが、全体のツリー t のルートノードとなり tail 関数によって取り除かれるまでに返済しなければなら ないdebits数の上限を、以下の不変式であらわします。  そのノードへのルートノードからのpath数 (= depth) + そのノードより先に tail 関数で取り除かれるノード数 ⇒ 𝐷 𝑡 𝑖 ≤ 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑖 0 … (2) Left linear debit invariant 0 + 0 = 0 if i = 0 1 2 7 1 + 1 = 2 if i = 1 ルートノードは返済が 2 + 1 = 3 if i = 2 済んだ状態になる … 7 + 1 = 8 if i = 7 8 + 2 = 10 if i = 8 8 12 12 + 2 = 14 if i = 12Copyright © 2012 yuga 34
  • 35. 解析: 定理10.1 定理10.1 ++ 関数と tail 関数は、それぞれ 1 debit、3 debits ず つ返済することでDebit Invariantを維持する。Copyright © 2012 yuga 35
  • 36. 解析: ++ 関数は定理10.1を満たしているか ++ 関数が定理10.1を満たすことを証明します。 ++ 関数は、2つのツリー𝑡1 と𝑡2 を連結することで新たなツリー𝑡を作るものとします。ツリー𝑡の ノード数を|𝑡| 、 𝑡1 を|𝑡1 | とします。当然ながら𝑡1 とt2はそれぞれ不変式(1)と(2)を満たしています。 ++ 関数の実行の結果、𝑡1 のルートノードの子としてt2のルートノードが加わり新しいツリーtがう まれます。 新規に発生するdebitとしては、t2のルートノードを格納するサスペンションが作られた結果、𝑡の ルートノード(元𝑡1 のルートノード)に割り当てられるdebitが1増加します。 debit数の上限に影響するデータ構造の変化としては、𝑡1 のルートノードの出次数が1増え、𝑡2 の各 ノードのインデックスが|𝑡1 |増加しdepthも1増加します。Copyright © 2012 yuga 36
  • 37. 解析: ++ 関数は定理10.1を満たしているか まず新規debitについて考えます。不変式(1)によると、tの総出次数=𝑡1 の総出次数+𝑡2 の総出次数 + 1であるので、このdebitの返済は必要ありません。しかし不変式(2)よれば、ルートノードはdebitを 持てないため、すぐに1 debit返済する必要があります。 次にデータ構造の変化によるdebit数の上限の変化です。ルートノードの出次数増加は、不変式(1)に よればdebitの許容数を増やすものなので、既存のdebitに対する影響はありません。不変式(2)につ いては、𝑡1 に含まれていた任意のノードiは連結による影響を受けないため、i < |𝑡1 | に対し、 𝐷 𝑡 𝑖 = 𝐷 𝑡1 𝑖 ≤ 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡1 𝑖 = 𝑑𝑒𝑝𝑡ℎ 𝑡 (𝑖)です。ツリーt2に含まれていた任意のノード𝑖は𝑡の中でイ ンデックスが|𝑡1 |増加し、またdepthが1増加するので、 𝐷 𝑡 𝑡1 + 𝑖 = 𝐷 𝑡1 + 𝐷 𝑡2 𝑖 ≤ 𝑡1 + 𝐷 𝑡2 𝑖 ≤ 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡2 𝑖 = 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑡1 + 𝑖 − 1 < 𝑡1 + 𝑖 + 𝑑𝑒𝑝𝑡ℎ 𝑡 𝑡1 + 𝑖 となりこちらも不変式を維持しています。 以上から「++関数は1 debitの返済でDebit Invariantを維持し定理10.1を満たす」ことを証明でき ました。Copyright © 2012 yuga 37
  • 38. 解析: tail 関数は定理10.1を満たしているか tail 関数が定理10.1を満たすことを証明します。 ツリーtに tail 関数を適用してツリーt’を作るものとします(let t’ = tail t)。tのルートノードはm 個の子ノードを持っています。tail 関数はtのルートノードを取り除いた後、その子ノードとして queueに格納されていたツリーt0からtm-1を右から左へリスト状につなぎます。 𝑡′ 1 𝑡 0 2 3 4 5 𝑡0 𝑡𝑗 𝑡 𝑚−1 1 5 … x … 6 7 8 x 2 3 4 6 7 8 … … … … … …Copyright © 2012 yuga 38
  • 39. 解析: tail 関数は定理10.1を満たしているか 新規に発生するdebit ツリーt’jを、ツリーtjからtm-1までをリンクした部分結果とします。したがってツリーt’=t’0となり ます。一番外側を除いたすべてのリンクはサスペンションを作ります。一番外側が除かれるのは、 tail 関数からの linkAll 関数の呼び出しは遅延されてないからです。link関数の実行だけに注目して大 雑把に式にすると、 let tail = link (tj, lazy (link (tj+1, lazy (link (tm-2, lazy (tm-1)))))) となっています。このようにサスペンションの作成によってもたらされるdebitsを、ツリーtj ただし 0 < j <= m-1 の各ルートノードに割り当てます。 𝑡′0 𝑡′ 𝑗 debit数の上限に影響するデータ構造の変化 1 5 ツリーt0からツリーtm-2はそれぞれ1ずつ出次数が増 加します。また、ツリーt1からtm-1は、それぞれ1か らm-1だけdepthが増加します。 … 2 3 4 6 7 8 x 𝑡′ 𝑚−1 … … …Copyright © 2012 yuga 39
  • 40. 解析: tail 関数は定理10.1を満たしているか まず新規debitについて考えます。ツリーt1からtm-2までは、それぞれリンクによって出次数が1増 加しているので、新規debitを1割り当てても不変式(1)を維持していますが、tm-1についてはリンク を行わないので出次数が変化していません。したがって、tm-1に割り当てられる予定だった1 debit はすぐに返済する必要があります。 次にデータ構造の変化によるdebit数の上限の変化です。ツリーt0からtm-2までの出次数増加は、不 変式(1)によればdebitの許容数を増やすものなので、既存のdebitに対する影響はありません。不変 式(2)については、tjの中に含まれるtのi番目のノードをとりあげます。不変式(2)から Dt(i)<=i+deptht(i)であることがわかっています。これがtailによって、どのように各項の値がどの ように変化するかを見ます。tのルートノードが取り除かれるので、iは1減少します。tjの各ノードの depthはj-1増加します。一方でtjの各ノードのDt(i)は新規debitにより累積debitがj増加します。し たがって、 Dt’(i-1)=Dt(i)+j<=i+deptht(i)+j=i+(deptht’(i-1)-(j-1))+j=(i-1)+deptht’(i-1)+2 となり、2 debitsを返済すれば不変式(2)を維持できます。よって tail 関数が返済すべきdebitは合計 3となります。 以上から「tail 関数は3 debitの返済でDebit Invariantを維持し定理10.1を満たす」ことを証明でき ました。Copyright © 2012 yuga 40
  • 41. 参考文献 • Chris Okasaki, “10.2.1 Lists With Efficient Catenation”, Purely Functional Data Structures, Cambridge University Press (1999) • Chris Okasaki, “Amortization, lazy evaluation, and persistence: Lists with catenation via lazy linking”, In IEEE Symposium on Foundations of Computer Science, pages 646-654, October 1995 • Haim Kaplan and Robert E. Tarjan, “Persistent lists with catenation via recursive slow-down”, In ACM Symposium on Theory of Computing, pages 93-102, May 1995.Copyright © 2012 yuga 41