Sentinel Node wikipedia page では、センチネルノードのNULLに対する利点は次のとおりです。
センチネルノードに対するチェックがどのように高速になるか(またはリンクリストまたはツリーに適切に実装する方法)がよくわからないので、これは2つの部分からなる質問のほうが多いと思います。
単純な反復を行い、要素内のデータを調べないだけの場合、センチネルには利点がありません。
ただし、「検索」タイプのアルゴリズムに使用すると、いくつかの本当の利点があります。たとえば、特定の値x
を検索するリンクリストリスト_std::list
_を想像してください。
歩哨なしで何をするか:
_for (iterator i=list.begin(); i!=list.end(); ++i) // first branch here
{
if (*i == x) // second branch here
return i;
}
return list.end();
_
しかし、歩哨では(もちろん、endは実際にはこのための実際のノードでなければなりません...):
_iterator i=list.begin();
*list.end() = x;
while (*i != x) // just this branch!
++i;
return i;
_
リストの最後をテストするために追加のブランチが必要ないことがわかります-値は常にそこにあることが保証されているため、x
が見つからない場合は自動的にend()
を返します「有効な」要素。
センチネルの別のクールで実際に役立つアプリケーションについては、「intro-sort」を参照してください。これは、ほとんどの_std::sort
_実装で使用されているソートアルゴリズムです。センチネルを使用していくつかのブランチを削除するパーティションアルゴリズムのクールなバリアントがあります。
理論的な議論よりも小さなコード例の方が良い説明になると思います。
以下は、ノードの二重リンクリストでのノード削除のコードです。ここで、NULL
はリストの終わりを示し、2つのポインターfirst
とlast
は最初と最後のノードのアドレスを保持するために使用されます:
// Using NULL and pointers for first and last
if (n->prev) n->prev->next = n->next;
else first = n->next;
if (n->next) n->next->prev = n->prev;
else last = n->prev;
これは同じコードですが、リストの終わりを示す特別なダミーノードがあり、リストの最初のノードのアドレスが特別なノードのnext
フィールドに格納され、最後のノードがリスト内のノードは、特別なダミーノードのprev
フィールドに格納されます。
// Using the dummy node
n->prev->next = n->next;
n->next->prev = n->prev;
同じ種類の簡略化がノード挿入にも存在します。たとえば、ノードn
をノードx
の前に挿入します(x == NULL
またはx == &dummy
は最後の位置への挿入を意味します)コードは次のようになります:
// Using NULL and pointers for first and last
n->next = x;
n->prev = x ? x->prev : last;
if (n->prev) n->prev->next = n;
else first = n;
if (n->next) n->next->prev = n;
else last = n;
そして
// Using the dummy node
n->next = x;
n->prev = x->prev;
n->next->prev = n;
n->prev->next = n;
ご覧のとおり、二重にリンクされたリストからダミーノードアプローチが削除され、すべての特殊なケースとすべての条件文が削除されています。
次の図は、メモリ内の同じリストに対する2つのアプローチを表しています...
あなたの質問への答え(1)は、リンクされたWikipediaエントリの最後の文にあります:"通常NULLにリンクするノードは、" nil "(nil自体を含む)にリンクするので、 NULLをチェックするための高価な分岐操作。 "
通常、ノードにアクセスする前に、ノードのNULLをテストする必要があります。代わりに有効なnilノードがある場合、この最初のテストを実行する必要はなく、比較と条件付きブランチを保存します。そうしないと、ブランチが誤っている場合に、最新のスーパースカラーCPUで高価になる可能性があります。予測した。
まず、歩哨を脇に置きましょう。コードの複雑さに関しては、ltjaxでの答えとして、彼はコードを提供してくれました
for (iterator i=list.begin(); i!=list.end(); ++i) // first branch here
{
if (*i == x) // second branch here
return i;
}
return list.end();
コードは次のように形成することができます。
auto iter = list.begin();
while(iter != list.end() && *iter != x)
++iter;
return iter;
乱雑な(グループ化された)ループ終了条件のため、ループ本体を通過して正確性を判断し、lessと入力すると、すべてのループ終了条件を覚えていなくてもループ終了条件を簡単に確認できます。ただし、ここではブール回路に注意してください。
ポイントは、ここで使用されるセンチネルはコードの複雑さを軽減するためのものではありませんが、各ループでのインデックスチェックを削減するのに役立ちます。線形検索の場合、インデックスが有効な範囲にあるかどうかのチェックから始め、範囲内にある場合は、センチネルを使用せずに、値が適切かどうかをチェックします。しかし、目的の値で最後に配置されたセンチネルを使用すると、インデックス境界のチェックを省略できますが、ループの終了が保証されているため、値のみをチェックします。これはセンチネル制御ループに属しています。目的の値が見つかるまで繰り返します。
読むことをお勧めします:アルゴリズムの紹介、第3版、そしてPDF形式をお持ちの場合は、キーワード「センチネル」を検索するだけですべてを入手できます。実際、この例は非常に簡潔で興味深いものです。カイロで象と象を狩る方法についての議論はあなたに興味があるかもしれません。もちろん象の狩猟の話はしていません。
標準テンプレートライブラリのコンテキストで回答してみます。
1)「next()」の呼び出しでは、NULLは必ずしもリストの終わりを示すわけではありません。メモリエラーが発生した場合はどうなりますか?センチネルノードを返すことは、リストの終わりが発生したことを示す決定的な方法であり、他の結果ではありません。つまり、NULLはリストの終わりだけではなく、さまざまなことを示します。
2)これは可能な方法の1つにすぎません。リストを作成するときに、クラスの外部で共有されないプライベートノード(たとえば、「lastNode」と呼ばれる)を作成します。リストの最後まで反復したことを検出したら、「next()」に「lastNode」への参照を返させます。また、「end()」というメソッドで「lastNode」への参照を返します。最後に、クラスの実装方法によっては、これを正しく機能させるために比較演算子をオーバーライドする必要がある場合があります。
例:
class MyNode{
};
class MyList{
public:
MyList () : lastNode();
MyNode * next(){
if (isLastNode) return &lastNode;
else return //whatever comes next
}
MyNode * end() {
return &lastNode;
}
//comparison operator
friend bool operator == (MyNode &n1, MyNode &n2){
return (&n1 == &n2); //check that both operands point to same memory
}
private:
MyNode lastNode;
};
int main(){
MyList list;
MyNode * node = list.next();
while ( node != list.end() ){
//do stuff!
node = list.next();
}
return 0;
}