えびちゃんの日記

えびちゃん(競プロ)の日記です。

データ構造の操作に関する動詞

人には人の直感。

データ構造同士をくっつけるとき、なんでも「マージ」と呼んでしまう人がいる気がします。 それはそれでいい気もしますが、どういう性質を持つデータ構造か・どういうくっつけ方かによって適切に感じる動詞は異なる気がします。

マージ以外にも、よくある操作についてニュアンスの違いみたいなのを書きたくなりました。 他の人の直感も知りたいです。

データ構造同士をくっつける操作

型で言うとこんな感じの。

impl<T> Ds<T> {
    fn merge(&mut self, other: Self);
}

merge

一般的・抽象的な感じっぽい。

くっつける操作一般に対して言及したいときは適切な感じがする一方、特定の操作に対して言うにはふわっとしているような気がする。 「データ構造をマージする一般的なテク」などは適切な用例な気がする。

append

「後ろにくっつける」感がある。

可変長配列の末尾に別の配列の要素をくっつけるときに使いそう。

concat

concatenate が正しいかも。

「連結する」感があり、順序としては append と同じそう。文字列感があるかもないかも。

prepend

「前にくっつける」感がある。

append, concat, prepend は、ユーザの決めた順序を保つデータ構造で、順序を指定してくっつける感を感じる。

meld

「融合する」

ヒープ(優先度つきキュー)で使う印象がある。meldable heap 以外の文脈であんまり見ない感。 ユーザが決めた順序ではなく、型で決められた大小関係を管理するので、ユーザからすると順序を意識しない名前がいいのかなあ。 二分木ベースの meldable heap などでは、くっつける際に各要素のポインタが混ざり合う感があるので、融合っぽいニュアンスになるのかなあ。

平衡二分探索木とかみたいに、全部の要素を大小関係で整列するわけではなく、最大値(最小値)だけ管理できるようにしている部分も違うかも? 順序が入り乱れた平衡二分探索木をくっつけるのは(計算量的にたぶん)難しい気がするから、そういう操作はあんまり見ないかもで、比較しにくいかも。

unite, unify

んーあんまり使ってる人見ないかも?

union-find におけるくっつけ操作用かも。集合(数学)の union (\(\cup)\) だと immutable っぽく見えるので、それに対応する動詞が欲しかった。 Pythonset だと、intersection (\(\cap\)) での更新は intersection_update なのに対して、union での更新は update だったのでびっくりした。

splice

「継ぎ合わせる」。

連結リスト (linked list) で、隣り合うポインタを継ぎ接ぎしてくっつける感がある。

insert

「挿入する」。

可変長配列の特定の位置に別の配列を入れる場合など。連結リストのように継ぎ接ぎするわけではなく、押し退けて入れる感が splice との違いかも。概念・実装上の違いで名前が違っていて、インタフェース自体は似ているかも? 添字を渡すかカーソルを渡すかの違いがある気がするからやっぱり違うかも。

interleave

「綴じ込む」?

データ構造というよりはイテレータ操作で頻出かも。

[a, b, c] と [x, y, z] に対して交互に混ぜて [a, x, b, y, c, z] のようにするイメージ。

extend

まとめて入れる感?

「追加する操作」が一意であるような文脈(可変長配列の末尾に入れる状況や、平衡二分探索木に入れる状況など?)で、その追加操作を繰り返す感?

その他

mix, coalesce, combine, blend, mingle などが類義語らしいものの、あんまり見ない気がする。 coalesce は binomial heap の説明 のときには見た。

connect というのもしっくり来ないかも。

要素を追加する操作

型で言うと以下。

impl<T> Ds<T> {
    fn add(&mut self, elem: T);
}

add

加える。

内部実装や詳細な部分には踏み込まず、単に「追加するよー」感がある気がする。よくも悪くも。

push

入れる?(?)

stack や queue 感があるかも。deque 系だと {front, back} や {left, right} が付加されたりするかも。

enqueue とかもあるかも。

insert

挿入する。

特定の位置に(必要に応じて押し退けて)入れる場合?

可変長配列への insert と、平衡二分探索木への insert をうまく説明できないかも。 後者は push ではだめ? push は端に押し入れるみたいなイメージがある?(だから不適?)

ユーザが指定した順序・型での大小関係での順序とかは別として、単に間に入れるみたいなイメージ?

ユーザから見て位置が一意なとき(末尾だけとか、型で定まるとか)は push か?と思ったけどそれだと平衡二分探索木は push なんだよね。 ヒープに入れるのが push なのは? 間に挿入するというよりは、末尾にとりあえず追加して修正 (sift((最初、shift の typo なのかと思ってました。))) するから insert とは違う感ある?

emplace

据え付ける?

C++ 感(?)。in-place に値を構築するみたいな感がある?

その他

配列 [a, b, c, d] があって間に x を混ぜて [a, x, b, x, c, x, d] にする操作は intersperse(点在させる)っぽい。

join は、(文字列の配列をカンマ区切りでくっつけるとかのように)[[T]]T から [T] を作る感がある。[[a, b], [c], [d, e, f]] と x から [a, b, x, c, x, d, e, f] みたいな。型が違うから immutable に新しいものを作る状況しかないっぽい。

put も push に近い感あるかも?

削除

型だと以下?

impl<T> Ds<T> {
    fn delete(&mut self, elem: &T);
    fn pop(&mut self);
}

delete, erase, remove, discard

消す。

あんまり違いがなくて、どれも「指定した値」「指定した場所」にあるものを消すよー感。

名詞形が deletion, erasure, removal, discard と、どれも作り方が違うの面白くてすき。

pop

取り除く。

stack や queue のように、除くものが一意だと delete とかよりもこちらの印象がある。deque だと {front, back} や {left, right} がつくかも。

dequeue とかも近いかも。stack だと使いにくそう。

queue は「待ち行列に入れる」「自分の番が来る」感のあるイメージだけど、stack を「待ち行列に入れる」「(最後尾の人が)諦めて立ち去る」と見なされることはなさそう。そういう方針で命名しても面白いか?と一瞬思ったけど、自分の番が来ることのない待ち行列に入る状況が不自然すぎて無理だった。

extract

抽出する。

ヒープで最大値(最小値)を抜き出すときに言いそう。実際の実装でこういう命名になることは稀で、抽象データ構造としてのインタフェースを話すときにしか言われない感ある。

その他

destroy, eliminate とかはあんまり言われなさそう。

集合操作だと subtract もあるかも。

take とかもあるかも。これも pop みたいに一意っぽさあるかも。extend の逆みたいに、何個除くみたいなときは .take(n) みたいにしたくなるかも。

検索

impl<T> Ds<T> {
    fn search<K>(&self, key: K) -> Option<T>;
    fn contains<K>(&self, key: K) -> bool;
}

「この要素はある?」とか「これに応じた値が欲しい」とか。

search

調べる。

抽象的っぽい。返り値の型が曖昧な感じがするので、概念の説明とかの文脈で「実装次第で bool を返すか値自体を返すかは文脈に応じてよしなに〜」とかのときには適していそうだけど、実装で見るとうれしくなさそう。

正規表現マッチとかで出てくると、言語ごとに仕様が違って嫌になる系のやつ。

find

探す。

これもややふわっとしていそう? でも bool を返す感じはしなさそう。

look-up

調べる、探す。

ハッシュテーブル感ある(?)。

contains, has, exists

「含む」「存在する」とか。

bool を返す系のやつ。

あんまりニュアンスの違いはない気がするけど、has は(短いので)スクリプト言語寄り感があったり、exists は(なんとなく)素朴に命名しがちなエンジニアが好みそう感を感じたりするかも。contains が一番安心する(??)。

関係ないけど、(it.)isFooBar(fooBar であるとき true、でないとき false)みたいな命名で「動詞を先頭に持ってくる」感があるのか、(it.)existsFooBar(fooBar が存在するとき true、しないとき false)みたいな命名を見かけることがややあって、えびちゃん的には違和感がある。それなら (it.)containsFooBar とかのが語順として自然そう。

includes

「含む」。

こっちは、なんとなく、sub-array として含むか?の検索のイメージがあって、[1, 2, 3] includes [1, 2] とか、[1, 2, 3] includes [1, 3] ではないとかっぽさを感じていた。JavaScript では [1, 2, 3] includes 2 とかの意味合いで使われており、そっかーとなった。

その他

ヒープの一番上とかを見るような、見たいものが一意のときは peek とかを使うかも。

consists とかはあんまり見ないかも。is-there (.isThere?(elem)) とかは見たことないけどあってもよさそう。よくないかも。

かなり素朴に命名する人だと .query(elem) みたいにしそう。「クエリ」、検索以外にも「更新クエリ」のように使われたりしてよくわからないがち。「質問」っぽい見た目だから immutable 感あるけど、「(これこれの操作を)していただけますか?」のような要求だったりもしそう。複数種類のクエリがあるとき query って名前はとてもしんどい。

まとめて除く・分ける系の操作

impl<T> Ds<T> {
    fn split<I>(&mut self, position: I) -> Self;
    fn partition(&mut self, pred: impl Fn(&T) -> bool) -> (Self, Self);
}

split

特定の位置の前後で分ける感? Rust だと split_off となってそう。抽象的感もある気はする。

partition

特定の条件を満たすかどうかで分けるときに使われそう。ある値より大きいかどうかとか、偶数か奇数かとか。

その他

purge とかはあんまり見ない感ある。

特定の条件を満たさない要素を消すのは retain とか。 filter とかもあるけど、フィルタで残すのか消すのかわからなくて怒られそう。イテレータ処理の文脈だとそれは合意が取れてそう。 retain も filter も順序は保たれそう。

remove-if みたいな名前はありそう。

感想

merge 関連は状況に応じていろんな単語があってニュアンスが違うのに対し、add・remove 関連は(一つを出し入れするのは単純だから?)ニュアンスの違いが比較的少なそう?な感じがした。そもそも、データ構造として実装できるような操作がある程度固定されているから、そこまではたくさんの種類の単語が必要になりにくいのかも?と思ったりした。 検索のときは、返り値の型が boolT かに応じて適切に命名できたらうれしいなと思った。

いろんな動詞を思い通りに使えるとうれしいね。

既存の名前と似た操作ならそれに倣って命名したいけど、既存のものとは違う操作をするときに、既存の語にこだわらずに適切に選べたらもっとうれしい。たとえば、CHT の「与えた \(x\) 座標において、今ある直線たちのうちの最小の \(y\) 座標は?」みたいな操作は、よくあるデータ構造にはないので命名が難しい(.min_at(x) とかにしていた記憶があるけど、改善の余地がありそう)。

おわり

me.meow();