Skip to content

Commit 39fbf11

Browse files
authored
Merge pull request #21 from horita-yuya/update-readme
update
2 parents dcdf320 + 8c39b45 commit 39fbf11

File tree

1 file changed

+43
-44
lines changed

1 file changed

+43
-44
lines changed

README.md

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,25 @@
11
# Heckel
22
ある配列Oからある配列Nへの差分を取ることを考えましょう。
33

4-
Heckel Algorithmでは、以下の様に3つのdata structureを考えます
4+
Heckel Algorithmでは、以下の様に3つのデータを考えます
55
1. symbol table
66
2. old element references
77
3. new element references
88

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は、全登場人物を管理する辞書型データとなります
1111

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つのデータを持つことになります
1313

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) で説明します。
1517

1618
```swift
1719
let O: [Int] = [1, 2, 3, 3] // 1 and 2: unique, 3: not unique
1820
let N: [Int] = [1, 2, 2, 3] // 1 and 3: unique, 2: not unique
1921
```
2022

21-
カウンターに加えてもう一つ、`key要素の配列O内でのインデックス` がありますが、これはそのままの意味ですね。専門的には `OLNO` と呼ばれます。
22-
さらに、この `OLNO` は、カウンターが.oneの場合のみ必要です。
23-
2423
```swift
2524
<E: Hashable>
2625

@@ -47,13 +46,17 @@ class SymbolTableEntry {
4746
var newCounter: Counter
4847
}
4948
```
50-
`1. symbol table` をまとめると、**配列O, Nの各要素が全体で考えてどのくらいの数(Counter)含まれているのか?そして、それは配列Oのどこに(OLNO)含まれているのか?を管理するdata structureです。**
49+
`1. symbol table` をまとめると、
50+
- 配列O, Nの各要素がそれぞれでどのくらいの数(Counter)含まれているのか
51+
- それは配列Oのどこに(OLNO)含まれているのか
52+
53+
を管理する辞書型データです。
5154

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とします。
5356

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`では、**自分が持っている要素が自分以外のどこにあるのか**ということを考えます
5558

56-
配列OA, NAにはその参照が格納されます。全体で3つある参照元の内、自分自身以外を考えるので、参照元は `.symbolTable``.theOther` の2つだけです。実装的には以下のようになります。
59+
配列OA, NAにはその参照が格納されます。全体で3つある配列O, 配列N, 辞書SymbolTable参照元の内、自分自身以外を考えるので、参照元は `.symbolTable``.theOther` の2つだけです。実装的には以下のようになります。
5760

5861
```swift
5962
enum ElementReference {
@@ -62,35 +65,29 @@ enum ElementReference {
6265
}
6366
```
6467

65-
さて、一度ここまでの登場人物をまとめます。
68+
一度ここまでの登場人物をまとめます。
6669
- symbol table :
67-
- 2つの配列O, Nの各要素をkeyとする辞書型データ
70+
- 2つの配列O, Nの各要素をKeyとする辞書型データ
6871
- 配列O, Nで共通
6972
- symbol table entry: key-valueのvalueを管理
70-
- その要素がそれぞれの配列に何個含まれてるのか
71-
- その要素が古い方の配列O内で何番目のインデックスなのか
73+
- その要素がそれぞれの配列にどのぐらい含まれてるのか
74+
- その要素が配列O内で何番目のインデックスなのか
7275
- Old
7376
- O: 配列 (oldArray)
7477
- OA: ElementReferenceを管理する配列 (oldElementReferences)
7578
- New
7679
- N: 配列 (newArray)
7780
- NA: ElementReferenceを管理する配列 (newElementReferences)
7881

79-
()内はswift化した時の変数名とします
80-
これ以降、他の登場人物は登場せず、これらを`うまく組み合わせて`行くことで差分を取ることが出来ます。差分を取るまでに6つのStepが必要であり、その組み合わせ方と手順がHeckel Algorithmの核です。
82+
()内はswiftで書いた時の変数名とします
83+
これ以降、他の登場人物は登場せず、これらをうまく組み合わせることで差分を取ることが出来ます。差分を取るまでに6つのStepが必要であり、その組み合わせ方と手順がHeckel Algorithmの核です。
8184

8285
## <a name="6steps"> 6-Steps
8386

8487
### Step-1
8588

8689
それでは、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に登録していく作業です。
9491
```swift
9592
newArray.forEach { element in
9693
let entry = symbolTable[element.hashValue] ?? SymbolTableEntry()
@@ -103,7 +100,7 @@ newArray.forEach { element in
103100

104101
### Step-2
105102

106-
Step-2はStep-1と同じ操作をOldに対して行うだけです。ただし、Oldの場合、SymbolTableEntry.indicesInOldの管理も必要でしたね
103+
Step-2はStep-1と同じ操作をOldに対して行うだけです。ただし、Oldの場合、OLNOの管理も必要でした
107104
```swift
108105
oldArray.enumerated().forEach { index, element
109106
let entry = symbolTable[element.hashValue] ?? TableEntry()
@@ -112,23 +109,25 @@ oldArray.enumerated().forEach { index, element
112109
symbolTable[element.hashValue] = entry
113110
}
114111
```
112+
oldCounter.incrementに渡しているindexは、case .oneのassociated valueとして管理されます。
113+
115114
すでに、6つの内、2つのStepが完了しました。
116115

117116
これからStep-3, 4, 5に移りますが、その前にこの3つのStepを大まかに説明します。
118117

119-
Step-1, 2では `newElementReferences, oldElementReferences``.symbolTable`だけを登録していました。これは一旦全て `.symbolTable` を参照させることで、配列の比較のための準備をしているのです。
120-
121-
これから、それらの参照を可能な限り `.theOther` に変えて行きます。つまり、symbol tableからもう片方の配列に参照を変えるということです。しかし、その要素がもう片方の配列に存在していなければ、参照はできません。その場合はそのまま `.symbolTable`を参照したままにします。
118+
Step-1, 2では `newElementReferences, oldElementReferences``.symbolTable`だけを登録していました。この段階では一旦全て `.symbolTable` 参照になっています。
122119

123-
そうなると、最終的には `.symbolTable` は共通で持たない要素を `.theOther` は共通の要素を示しそうな雰囲気がします。ということは、、 `.symbolTable``delete, insert`を、`.theOther``move` になるのか??結論は一旦おいておきましょう
120+
これから、それらの参照を可能な限り `.theOther` に変えて行きます。しかし、その要素がもう片方の配列に存在していなければ、切り替えはできません。その場合はそのまま `.symbolTable`を参照したままにします
124121

125-
### Step-3
122+
そうなると、最終的には `.symbolTable` は共通で持たない要素を `.theOther` は共通の要素を示すことになります。結論としては
123+
- `.symbolTable` -> `delete, insert`
124+
- `.theOther` -> `move`
126125

127-
Step-3では、`oldCounter == newCounter == .one` の場合のみ計算を行います
126+
に変換されることになります
128127

129-
Heckelアルゴリズムでは、Counter { .zero, .one, .many } でした。.zeroは初期値だとして、.manyは無視するということは、先ほど出てきた `ユニークな要素`をうまく使って計算するということです。`oldCounter == newCounter == .one` の条件は、各配列でそのユニークな要素がただ一つの共通要素になっていることになります。
128+
### Step-3
130129

131-
それでは、ユニークな要素に対して参照先を`.symbolTable` から `.theOther` に変えて行きましょう
130+
Step-3では、`oldCounter == newCounter == .one` の場合のみ、つまり、shared unique elementに対して計算を行います
132131

133132
```swift
134133
newElementReferences.enumerated().forEach { newIndex, reference in
@@ -140,11 +139,11 @@ newElementReferences.enumerated().forEach { newIndex, reference in
140139
oldElementReferences[oldIndex] = .theOther(index: newIndex)
141140
}
142141
```
143-
**本来、共通する2つの要素を見つけるためには配列Nをループしてその中で配列Oをループ、またはその反対をする必要がありそうです。しかし、Heckelアルゴリズムでは2つの配列で共通のsymbolTable(keyは各要素)を持ち、そのvalue内でindicesInOldを持つことで、片方のループだけで共通の要素を見つけられる様にしているわけです。(前者の様な方法の場合、計算量がO(NxM)となってしまうので、それを避けている点はとても重要かつ大きなポイントです。)**
142+
本来、共通する2つの要素を見つけるためには配列Nをループしてその中で配列Oをループ、またはその反対をする必要がありそうです。しかし、Heckelアルゴリズムでは2つの配列で共通のsymbolTable(keyは各要素)を持ち、そのvalue内でOLNOを持つことで、片方のループだけで共通の要素を見つけられる様にしています。前者の様な方法の場合、計算量がO(NxM)となってしまうので、それを避けている点はとても重要かつ大きなポイントです。
144143

145144
### Step-4
146145

147-
Step-4に移りましょう。Step-3はユニークな要素に対してのみ、参照元を`.symbolTable`から`.theOther`に変えました。しかし、ユニークでなくても、もしくは被った要素でも2つの配列で共通部分を持つ場合はもちろん考えられますよね?ここでは、それを計算します。Step-3で計算した、ユニークな要素のインデックスを起点にして計算するわけです
146+
Step-4に移りましょう。Step-3はshared unique elementに対してのみ、参照元を`.symbolTable`から`.theOther`に変えました。しかし、ユニークでなくても、2つの配列で共通部分を持つ場合はもちろん考えられます。ここでは、Step-3の結果を起点として参照を変えます
148147
```swift
149148
newElementReferences.enumerated().forEach { newIndex, _ in
150149
guard case let .theOther(index: oldIndex) = newElementReferences[newIndex],
@@ -157,14 +156,11 @@ newElementReferences.enumerated().forEach { newIndex, _ in
157156
oldElementReferences[oldIndex + 1] = .theOther(index: newIndex + 1)
158157
}
159158
```
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参照に変えます。
163160

164161
### Step-5
165162

166-
Step-5です。Step-4ではユニークな要素を起点にして、その次の要素を対象にしていました。しかし、ユニークな要素は疎らに存在していることも十分考えられる訳です。その次の要素は勿論、その一つ前の要素に対しても同じことをする必要があります。Step-5ではそれを計算して行きます。
167-
Step-4ではascending orderで問題ありませんが、Step-5ではdescending orderにすることに注意しましょう。
163+
Step-4と同じことを、descending loopで行います。
168164
```swift
169165
newElementReferences.enumerated().reversed().forEach { newIndex, _ in
170166
guard case let .theOther(index: oldIndex) = newElementReferences[newIndex],
@@ -177,15 +173,16 @@ newElementReferences.enumerated().reversed().forEach { newIndex, _ in
177173
oldElementReferences[oldIndex - 1] = .theOther(index: newIndex - 1)
178174
}
179175
```
180-
さて、6つのStepの内、5つが完了しました。
176+
これで、6つのStepの内、5つが完了しました。
181177

182178
### Step-6
183179

184-
ここまでで、配列O, Nに対応した配列OA, NAが決定されました。配列OA, NA内の各referenceが `.symbolTable`を指しているのか、`.theOther`を指しているのかによって、*共通の要素・共通で無い要素を判別すること*ができます。 先ほど少しだけ触れましたが、
180+
ここまでで、配列O, Nに対応した配列OA, NAが決定されました。配列OA, NA内の各referenceが `.symbolTable`を指しているのか、`.theOther`を指しているのかによって、*共通の要素・共通で無い要素を判別すること*ができます。
185181
- 共通で無い要素で配列Oに含まれているものは、配列Nに編集するためには削除しなければなりません。よって `delete`
186182
- 共通で無い要素で配列Nに含まれているものは、配列Oに加えなければなりません。よって `insert`
187183
- 共通要素の場合は、順番を変える編集が必要です。よって `move`
188184

185+
になります。
189186
Step-6では、これらの計算を行います。
190187

191188
```swift
@@ -214,7 +211,9 @@ newElementReferences.enumerated().forEach { newIndex, reference in
214211
```
215212
共通でなければ`.symbolTable`を参照するしかなく、共通であれば`.theOther`を参照します。よって、この様な計算で、delete, insert, moveのdiffが得られます。
216213

217-
ここで、moveに関しては注意が必要です。元々の配列Oの各要素に対応して、配列OAがありました。もし配列Oと配列Nが同じであった場合、この配列は全ての参照先が `.theOther` になります。これをそのまま `move` としてしまうと、あるインデックスから同じインデックスにmoveするという冗長なコマンドになってしまいます。これはあまり嬉しくないですね。その冗長なmoveを無くすことを考えましょう。
214+
ここで、moveに関しては注意が必要です。元々の配列Oの各要素に対応して、配列OAがありました。もし配列Oと配列Nが同じであった場合、この配列は全ての参照先が `.theOther` になります。これをそのまま `move` としてしまうと、あるインデックスから同じインデックスにmoveするという冗長なコマンドになってしまいます。そのため、その条件の時は、moveとして検知しないなどの工夫は必要です。
215+
216+
このように、Heckel Algorithmでは、必ずしも最短の編集距離には成り得ません。しかし、線形時間で差分が取れるということは最大の特徴では無いでしょうか。
218217

219218

220219

0 commit comments

Comments
 (0)