1
1
# Heckel
2
2
ある配列Oからある配列Nへの差分を取ることを考えましょう。
3
3
4
- Heckel Algorithmでは、以下の様に3つのdata structureを考えます 。
4
+ Heckel Algorithmでは、以下の様に3つのデータを考えます 。
5
5
1 . symbol table
6
6
2 . old element references
7
7
3 . new element references
8
8
9
- まずsymbol tableから説明します。symbol tableは配列O, Nの各要素をkeyとするテーブルです。以下の様に実装的には、配列O, Nの各要素(のハッシュ値)をkey 、symbol table entryをvalueとする辞書型のデータです 。
10
- 要素をKeyとしますので、同じ要素であれば、同じValueが返ってきます。そのため、symbol tableは、全登場人物を管理するdata structureとなります 。
9
+ まずsymbol tableから説明します。symbol tableは配列O, Nの各要素をkeyとするテーブルです。以下の様に実装的には、配列O, Nの各要素のハッシュ値をKey 、symbol table entryをValueとする辞書型のデータです 。
10
+ 要素をKeyとしますので、同じ要素であれば、同じValueが返ってきます。そのため、symbol tableは、全登場人物を管理する辞書型データとなります 。
11
11
12
- symbol table entryは配列O, N内、 ** それぞれのkey要素の数(カウンター )** と** key要素の配列O内でのインデックス ** を持つ値です。カウンターは配列O , Nそれぞれに対して管理するので2つ必要で、インデックスと合わせると 、symbol table entryは3つのプロパティを持つことになります。以下のコードのSymbolTableEntryがそれに該当します 。
12
+ symbol table entryは ** 配列O, N内におけるそれぞれのKey要素の数(Counter )** と** Key要素の配列O内でのインデックス(OLNO) ** を持つ値です。Counterは配列O , Nそれぞれに対して管理するので2つ必要で、OLNOと合わせると 、symbol table entryは3つのデータを持つことになります 。
13
13
14
- 実は、このカウンターが持つ値としては * 0, 1 or many(.zero, .one, .many)* の3つだけを考えれば十分です。これは、Heckel Algorithmが配列O, Nそれぞれで重複しない要素、もしくはユニークな要素を起点として、差分を取ることを考えるからです。詳細については後ほどの [ 6-Steps] ( #6steps ) で説明します。
14
+ 実は、このカウンターが持つ値としては * 0, 1 or many(.zero, .one, .many)* の3つだけを考えれば十分です。これは、Heckel Algorithmが配列O, Nそれぞれで、共通でユニークな要素(shared unique element)を起点として、差分を取ることを考えるからです。そのため、実装的にはenum Counterにassociated valueを持たせれば大丈夫です。以下のコードのSymbolTableEntryがそれに該当します。
15
+
16
+ 詳細については後ほどの [ 6-Steps] ( #6steps ) で説明します。
15
17
16
18
``` swift
17
19
let O: [Int ] = [1 , 2 , 3 , 3 ] // 1 and 2: unique, 3: not unique
18
20
let N: [Int ] = [1 , 2 , 2 , 3 ] // 1 and 3: unique, 2: not unique
19
21
```
20
22
21
- カウンターに加えてもう一つ、` key要素の配列O内でのインデックス ` がありますが、これはそのままの意味ですね。専門的には ` OLNO ` と呼ばれます。
22
- さらに、この ` OLNO ` は、カウンターが.oneの場合のみ必要です。
23
-
24
23
``` swift
25
24
< E: Hashable >
26
25
@@ -47,13 +46,17 @@ class SymbolTableEntry {
47
46
var newCounter: Counter
48
47
}
49
48
```
50
- ` 1. symbol table ` をまとめると、** 配列O, Nの各要素が全体で考えてどのくらいの数(Counter)含まれているのか?そして、それは配列Oのどこに(OLNO)含まれているのか?を管理するdata structureです。**
49
+ ` 1. symbol table ` をまとめると、
50
+ - 配列O, Nの各要素がそれぞれでどのくらいの数(Counter)含まれているのか
51
+ - それは配列Oのどこに(OLNO)含まれているのか
52
+
53
+ を管理する辞書型データです。
51
54
52
- それでは、` 2, 3: old element references, new element references ` についてです。まず前提として、これら2つは、配列O, Nの各要素と` 1:1対応する ` 別の配列です。慣習的に配列OA, NAとします。` 各要素と1:1対応する ` とありますが、配列OA, NAにはそれぞれ、どのような値が入るのでしょうか。
55
+ それでは、` 2, 3: old element references, new element references ` についてです。まず前提として、これら2つは、配列O, Nの各要素と` 1:1対応する ` 別の配列です。慣習的に配列OA, NAとします。
53
56
54
- 今、元々の配列O, Nの要素の情報を持っているdata structureは 、配列O, Nに加えて先ほどの ` symbol table ` (keyとして情報を持っている) がありますよね 。` old, new element references ` では、** 自分自身以外のdata structureの要素を参照して自分自身を再構成すること ** を考えます 。
57
+ 今、元々の配列O, Nの要素の情報を持っているデータは 、配列O, Nに加えて先ほどの ` symbol table ` (Keyとして情報を持っている) があります 。` old, new element references ` では、** 自分が持っている要素が自分以外のどこにあるのか ** ということを考えます 。
55
58
56
- 配列OA, NAにはその参照が格納されます。全体で3つある参照元の内 、自分自身以外を考えるので、参照元は ` .symbolTable ` と ` .theOther ` の2つだけです。実装的には以下のようになります。
59
+ 配列OA, NAにはその参照が格納されます。全体で3つある配列O, 配列N, 辞書SymbolTable参照元の内 、自分自身以外を考えるので、参照元は ` .symbolTable ` と ` .theOther ` の2つだけです。実装的には以下のようになります。
57
60
58
61
``` swift
59
62
enum ElementReference {
@@ -62,35 +65,29 @@ enum ElementReference {
62
65
}
63
66
```
64
67
65
- さて、 一度ここまでの登場人物をまとめます。
68
+ 一度ここまでの登場人物をまとめます。
66
69
- symbol table :
67
- - 2つの配列O, Nの各要素をkeyとする辞書型データ
70
+ - 2つの配列O, Nの各要素をKeyとする辞書型データ
68
71
- 配列O, Nで共通
69
72
- symbol table entry: key-valueのvalueを管理
70
- - その要素がそれぞれの配列に何個含まれてるのか 。
71
- - その要素が古い方の配列O内で何番目のインデックスなのか 。
73
+ - その要素がそれぞれの配列にどのぐらい含まれてるのか 。
74
+ - その要素が配列O内で何番目のインデックスなのか 。
72
75
- Old
73
76
- O: 配列 (oldArray)
74
77
- OA: ElementReferenceを管理する配列 (oldElementReferences)
75
78
- New
76
79
- N: 配列 (newArray)
77
80
- NA: ElementReferenceを管理する配列 (newElementReferences)
78
81
79
- ()内はswift化した時の変数名とします 。
80
- これ以降、他の登場人物は登場せず、これらを ` うまく組み合わせて ` 行くことで差分を取ることが出来ます 。差分を取るまでに6つのStepが必要であり、その組み合わせ方と手順がHeckel Algorithmの核です。
82
+ ()内はswiftで書いた時の変数名とします 。
83
+ これ以降、他の登場人物は登場せず、これらをうまく組み合わせることで差分を取ることが出来ます 。差分を取るまでに6つのStepが必要であり、その組み合わせ方と手順がHeckel Algorithmの核です。
81
84
82
85
## <a name =" 6steps " > 6-Steps
83
86
84
87
### Step-1
85
88
86
89
それでは、1-6 Stepの内 Step-1から見て行きましょう。手順は以下の通りです。
87
- ```
88
- 1. 配列Nの各要素をキーとして、symbol table entryを作成。(ただし、そのキーのentryが存在している時は、そのentryを渡す。)
89
- 2. その要素のnewCounterをインクリメントする
90
- 3. NA[i]に.symbolTable(entry:)をセットする。(iはその要素のインデックス)
91
- 4. symbol tableに[key: value] = [N[i], NA[i]]として、entryを登録する。
92
- ```
93
- Step-1は比較前の準備と言ったところです。
90
+ 配列Nの各要素を、SymbolTableに登録していく作業です。
94
91
``` swift
95
92
newArray.forEach { element in
96
93
let entry = symbolTable[element.hashValue ] ?? SymbolTableEntry ()
@@ -103,7 +100,7 @@ newArray.forEach { element in
103
100
104
101
### Step-2
105
102
106
- Step-2はStep-1と同じ操作をOldに対して行うだけです。ただし、Oldの場合、SymbolTableEntry.indicesInOldの管理も必要でしたね 。
103
+ Step-2はStep-1と同じ操作をOldに対して行うだけです。ただし、Oldの場合、OLNOの管理も必要でした 。
107
104
``` swift
108
105
oldArray.enumerated ().forEach { index, element
109
106
let entry = symbolTable[element.hashValue ] ?? TableEntry ()
@@ -112,23 +109,25 @@ oldArray.enumerated().forEach { index, element
112
109
symbolTable[element.hashValue ] = entry
113
110
}
114
111
```
112
+ oldCounter.incrementに渡しているindexは、case .oneのassociated valueとして管理されます。
113
+
115
114
すでに、6つの内、2つのStepが完了しました。
116
115
117
116
これからStep-3, 4, 5に移りますが、その前にこの3つのStepを大まかに説明します。
118
117
119
- Step-1, 2では ` newElementReferences, oldElementReferences ` に ` .symbolTable ` だけを登録していました。これは一旦全て ` .symbolTable ` を参照させることで、配列の比較のための準備をしているのです。
120
-
121
- これから、それらの参照を可能な限り ` .theOther ` に変えて行きます。つまり、symbol tableからもう片方の配列に参照を変えるということです。しかし、その要素がもう片方の配列に存在していなければ、参照はできません。その場合はそのまま ` .symbolTable ` を参照したままにします。
118
+ Step-1, 2では ` newElementReferences, oldElementReferences ` に ` .symbolTable ` だけを登録していました。この段階では一旦全て ` .symbolTable ` 参照になっています。
122
119
123
- そうなると、最終的には ` .symbolTable ` は共通で持たない要素を ` . theOther` は共通の要素を示しそうな雰囲気がします。ということは、、 ` .symbolTable ` が ` delete, insert ` を、 ` .theOther ` が ` move ` になるのか??結論は一旦おいておきましょう 。
120
+ これから、それらの参照を可能な限り ` .theOther ` に変えて行きます。しかし、その要素がもう片方の配列に存在していなければ、切り替えはできません。その場合はそのまま ` .symbolTable ` を参照したままにします 。
124
121
125
- ### Step-3
122
+ そうなると、最終的には ` .symbolTable ` は共通で持たない要素を ` .theOther ` は共通の要素を示すことになります。結論としては
123
+ - ` .symbolTable ` -> ` delete, insert `
124
+ - ` .theOther ` -> ` move `
126
125
127
- Step-3では、 ` oldCounter == newCounter == .one ` の場合のみ計算を行います 。
126
+ に変換されることになります 。
128
127
129
- Heckelアルゴリズムでは、Counter { .zero, .one, .many } でした。.zeroは初期値だとして、.manyは無視するということは、先ほど出てきた ` ユニークな要素 ` をうまく使って計算するということです。 ` oldCounter == newCounter == .one ` の条件は、各配列でそのユニークな要素がただ一つの共通要素になっていることになります。
128
+ ### Step-3
130
129
131
- それでは、ユニークな要素に対して参照先を ` .symbolTable ` から ` .theOther ` に変えて行きましょう 。
130
+ Step-3では、 ` oldCounter == newCounter == .one ` の場合のみ、つまり、shared unique elementに対して計算を行います 。
132
131
133
132
``` swift
134
133
newElementReferences.enumerated ().forEach { newIndex, reference in
@@ -140,11 +139,11 @@ newElementReferences.enumerated().forEach { newIndex, reference in
140
139
oldElementReferences[oldIndex] = .theOther (index : newIndex)
141
140
}
142
141
```
143
- ** 本来、共通する2つの要素を見つけるためには配列Nをループしてその中で配列Oをループ、またはその反対をする必要がありそうです。しかし、Heckelアルゴリズムでは2つの配列で共通のsymbolTable(keyは各要素)を持ち、そのvalue内でindicesInOldを持つことで、片方のループだけで共通の要素を見つけられる様にしているわけです。( 前者の様な方法の場合、計算量がO(NxM)となってしまうので、それを避けている点はとても重要かつ大きなポイントです。) **
142
+ 本来、共通する2つの要素を見つけるためには配列Nをループしてその中で配列Oをループ、またはその反対をする必要がありそうです。しかし、Heckelアルゴリズムでは2つの配列で共通のsymbolTable(keyは各要素)を持ち、そのvalue内でOLNOを持つことで、片方のループだけで共通の要素を見つけられる様にしています。 前者の様な方法の場合、計算量がO(NxM)となってしまうので、それを避けている点はとても重要かつ大きなポイントです。
144
143
145
144
### Step-4
146
145
147
- Step-4に移りましょう。Step-3はユニークな要素に対してのみ 、参照元を` .symbolTable ` から` .theOther ` に変えました。しかし、ユニークでなくても、もしくは被った要素でも2つの配列で共通部分を持つ場合はもちろん考えられますよね? ここでは、それを計算します。 Step-3で計算した、ユニークな要素のインデックスを起点にして計算するわけです 。
146
+ Step-4に移りましょう。Step-3はshared unique elementに対してのみ 、参照元を` .symbolTable ` から` .theOther ` に変えました。しかし、ユニークでなくても、2つの配列で共通部分を持つ場合はもちろん考えられます。 ここでは、Step-3の結果を起点として参照を変えます 。
148
147
``` swift
149
148
newElementReferences.enumerated ().forEach { newIndex, _ in
150
149
guard case let .theOther (index : oldIndex) = newElementReferences[newIndex],
@@ -157,14 +156,11 @@ newElementReferences.enumerated().forEach { newIndex, _ in
157
156
oldElementReferences[oldIndex + 1 ] = .theOther (index : newIndex + 1 )
158
157
}
159
158
```
160
- .symbolTable(entry: SymbolTableEntry)は配列O, Nで共通のsymbolTableへの参照で、このassociated valueのSymbolTableEntryは` symbol table ` からは要素をkeyとして得られました。
161
- つまり、` newEntry === oldEntry ` 、すなわち、entryが同じオブジェクトであればそのreferenceは同じ要素を指していることになります。
162
- 上の式で、無事に参照先を ` .theOther ` に変えられそうですね。
159
+ Step-3で、.theOther参照になった要素の一つ隣の要素が同じ要素である場合、それを.theOther参照に変えます。
163
160
164
161
### Step-5
165
162
166
- Step-5です。Step-4ではユニークな要素を起点にして、その次の要素を対象にしていました。しかし、ユニークな要素は疎らに存在していることも十分考えられる訳です。その次の要素は勿論、その一つ前の要素に対しても同じことをする必要があります。Step-5ではそれを計算して行きます。
167
- Step-4ではascending orderで問題ありませんが、Step-5ではdescending orderにすることに注意しましょう。
163
+ Step-4と同じことを、descending loopで行います。
168
164
``` swift
169
165
newElementReferences.enumerated ().reversed ().forEach { newIndex, _ in
170
166
guard case let .theOther (index : oldIndex) = newElementReferences[newIndex],
@@ -177,15 +173,16 @@ newElementReferences.enumerated().reversed().forEach { newIndex, _ in
177
173
oldElementReferences[oldIndex - 1 ] = .theOther (index : newIndex - 1 )
178
174
}
179
175
```
180
- さて 、6つのStepの内、5つが完了しました。
176
+ これで 、6つのStepの内、5つが完了しました。
181
177
182
178
### Step-6
183
179
184
- ここまでで、配列O, Nに対応した配列OA, NAが決定されました。配列OA, NA内の各referenceが ` .symbolTable ` を指しているのか、` .theOther ` を指しているのかによって、* 共通の要素・共通で無い要素を判別すること* ができます。 先ほど少しだけ触れましたが、
180
+ ここまでで、配列O, Nに対応した配列OA, NAが決定されました。配列OA, NA内の各referenceが ` .symbolTable ` を指しているのか、` .theOther ` を指しているのかによって、* 共通の要素・共通で無い要素を判別すること* ができます。
185
181
- 共通で無い要素で配列Oに含まれているものは、配列Nに編集するためには削除しなければなりません。よって ` delete ` 。
186
182
- 共通で無い要素で配列Nに含まれているものは、配列Oに加えなければなりません。よって ` insert ` 。
187
183
- 共通要素の場合は、順番を変える編集が必要です。よって ` move ` 。
188
184
185
+ になります。
189
186
Step-6では、これらの計算を行います。
190
187
191
188
``` swift
@@ -214,7 +211,9 @@ newElementReferences.enumerated().forEach { newIndex, reference in
214
211
```
215
212
共通でなければ` .symbolTable ` を参照するしかなく、共通であれば` .theOther ` を参照します。よって、この様な計算で、delete, insert, moveのdiffが得られます。
216
213
217
- ここで、moveに関しては注意が必要です。元々の配列Oの各要素に対応して、配列OAがありました。もし配列Oと配列Nが同じであった場合、この配列は全ての参照先が ` .theOther ` になります。これをそのまま ` move ` としてしまうと、あるインデックスから同じインデックスにmoveするという冗長なコマンドになってしまいます。これはあまり嬉しくないですね。その冗長なmoveを無くすことを考えましょう。
214
+ ここで、moveに関しては注意が必要です。元々の配列Oの各要素に対応して、配列OAがありました。もし配列Oと配列Nが同じであった場合、この配列は全ての参照先が ` .theOther ` になります。これをそのまま ` move ` としてしまうと、あるインデックスから同じインデックスにmoveするという冗長なコマンドになってしまいます。そのため、その条件の時は、moveとして検知しないなどの工夫は必要です。
215
+
216
+ このように、Heckel Algorithmでは、必ずしも最短の編集距離には成り得ません。しかし、線形時間で差分が取れるということは最大の特徴では無いでしょうか。
218
217
219
218
220
219
0 commit comments