web-dev-qa-db-ja.com

std :: list :: reverseにO(n)の複雑さがあるのはなぜですか?

C++標準ライブラリのstd::listクラスのリバース関数に線形ランタイムがあるのはなぜですか?二重リンクリストの場合、逆関数はO(1)である必要があります。

二重リンクリストを逆にするには、ヘッドポインターとテールポインターを切り替えるだけです。

187
Curious

仮に、reverseO(1)だったかもしれません。リンクされたリストの方向が、リストが作成された元のリストの方向と現在同じであるか反対であるかを示すブールリストメンバーが(再び)仮定されます。

残念ながら、それは基本的に他のすべての操作のパフォーマンスを低下させます(ただし、漸近的なランタイムは変更しません)。各操作では、リンクの「次の」または「前の」ポインターに従うかどうかを検討するためにブール値を調べる必要があります。

これはおそらく比較的まれな操作と考えられていたため、標準(実装を指示せず、複雑さのみ)が、複雑さは線形になる可能性があると指定しました。これにより、「次の」ポインタが常に同じ方向を明確に意味するようになり、一般的な操作が高速化されます。

182
Ami Tavory

それcould O(1)リストに、各ノードが持つ「prev」および「next」ポインターの意味を交換できるフラグが格納される場合。リストを逆にすることが頻繁な操作である場合、そのような追加は実際に有用である可能性があり、それを実装する理由がわからないprohibited現在の標準による。ただし、そのようなフラグを設定すると、リストの通常のトラバーサルがより高価になります(一定の要因による場合のみ)。

current = current->next;

リストイテレータのoperator++では、次のようになります

if (reversed)
  current = current->prev;
else
  current = current->next;

これは簡単に追加することにしたものではありません。リストは通常​​、逆にされるよりもずっと頻繁にトラバースされるため、標準ではこの手法をmandateすることは非常に賢明ではありません。したがって、逆の操作は線形の複雑さを持つことができます。ただし、注意してください t ∈ O(1)⇒ t ∈ On)そのため、前述のとおり、「最適化」を技術的に実装することは許可されます。

Javaまたは同様の背景から来た場合、反復子が毎回フラグをチェックする必要があるのはなぜかと思うかもしれません。代わりに、共通の基本型から派生し、std::list::beginstd::list::rbeginが適切な反復子を多相的に返す2つの異なる反復子型を使用することはできませんか?可能ですが、イテレーターを進めることは間接的な(インライン化が難しい)関数呼び出しになるため、これは全体をさらに悪化させます。 Javaでは、とにかくこの価格を定期的に支払っていますが、これも、パフォーマンスが重要な場合に多くの人がC++に到達する理由の1つです。

コメントの Benjamin Lindley で指摘されているように、reverseはイテレータを無効にすることが許可されていないため、標準で許可されている唯一のアプローチは、リスト内のリストへのポインタを格納することです二重間接メモリアクセスを引き起こすイテレータ。

59
5gon12eder

双方向イテレータをサポートするすべてのコンテナにはrbegin()とrend()の概念があるため、この質問は意味がありませんか?

イテレータを逆にし、それを介してコンテナにアクセスするプロキシを構築するのは簡単です。

この非操作は確かにO(1)です。

といった:

#include <iostream>
#include <list>
#include <string>
#include <iterator>

template<class Container>
struct reverse_proxy
{
    reverse_proxy(Container& c)
    : _c(c)
    {}

    auto begin() { return std::make_reverse_iterator(std::end(_c)); }
    auto end() { return std::make_reverse_iterator(std::begin(_c)); }

    auto begin() const { return std::make_reverse_iterator(std::end(_c)); }
    auto end() const { return std::make_reverse_iterator(std::begin(_c)); }

    Container& _c;
};

template<class Container>
auto reversed(Container& c)
{
    return reverse_proxy<Container>(c);
}

int main()
{
    using namespace std;
    list<string> l { "the", "cat", "sat", "on", "the", "mat" };

    auto r = reversed(l);
    copy(begin(r), end(r), ostream_iterator<string>(cout, "\n"));

    return 0;
}

期待される出力:

mat
the
on
sat
cat
the

これを考えると、標準委員会は、コンテナのO(1)の逆順序を義務付けるのに時間がかかっていないようです。重複を避けながら厳密に必要なものだけ。

ちょうど私の2c。

37
Richard Hodges

すべてのノード(n合計)を横断し、それらのデータを更新する必要があるため(更新手順は実際O(1)です)。これにより、操作全体がO(n*1) = O(n)になります。

18
Blindy

また、すべてのノードの前と次のポインターを交換します。それがリニアが必要な理由です。 O(1)で実行できますが、このLLを使用する関数が、正常にアクセスしているか逆にアクセスしているかなどの入力としてLLに関する情報も取得する場合.

2
techcomp

アルゴリズムの説明のみ。要素を含む配列があり、それを逆にする必要があると想像してください。基本的な考え方は、最初の位置の要素を最後の位置に変更し、2番目の位置の要素を最後から2番目の位置に変更するなど、各要素を繰り返すことです。配列の中央に到達すると、すべての要素が変更されるため、(n/2)回の反復でO(n)と見なされます。

1
danilobraga

リストを逆順でコピーする必要があるという理由だけで、O(n)です。個々のアイテム操作はO(1)ですが、リスト全体にn個あります。

もちろん、新しいリスト用のスペースのセットアップ、その後のポインターの変更などに関連する一定時間の操作があります。O表記では、1次nファクターを含めると個々の定数は考慮されません。

1
SDsolar