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


192

std::listC ++標準ライブラリのクラスの逆関数に線形ランタイムがあるのはなぜですか?二重にリンクされたリストの場合、逆関数はO(1)である必要があると思います。

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


18
なぜ人々がこの質問に反対票を投じているのか理解できません。これは完全に合理的な質問です。二重にリンクされたリストを元に戻すには、O(1)時間かかるはずです。
好奇心が

46
残念ながら、一部の人々は「質問は良い」という概念を「質問は良いアイデアを持っている」と混同しています。このような質問が大好きです。基本的に「私の理解は一般に認められている慣行とは異なっているようです。この矛盾を解決してください」という考えです。他の人たちは「それは99.9999%のケースで処理の無駄だ、それについて考えさえしない」というアプローチを取るように見えるでしょう。それが慰めであるなら、私ははるかに、はるかに少なく投票されてきました!
corsiKa 2016

4
ええ、この質問には、その質に対する非常に多くの反対票がありました。おそらくそれは、ブリンディの答えに賛成した人と同じだろう。公平に言うと、「二重リンクリストを元に戻すには、ヘッドポインタとテールポインタを切り替えるだけです」というのは、誰もが高校で学ぶ標準のリンクリストの実装、または人々が使用する多くの実装では一般に当てはまりません。SOの多くの場合、質問または回答に対する直感的な直感的な反応は、賛成/反対の決定を引き起こします。その文をもっと明確にしたり、省略したりした場合、反対票は少なくなったと思います。
Chris Beck、

3
または、あなたと一緒に証明の責任を負わせてください、@ Curious:二重リンクリストの実装をここで作成しました:ideone.com/c1HebOReverse関数がO(1)に実装されることをどのように期待するかを示していただけますか
CompuChip 2016

2
@CompuChip:実際には、実装によっては、そうでない場合があります。どのポインタを使用するかを知るために追加のブール値は必要ありません。あなたを指し示していないものを使用してください...ちなみに、XORされたリンクリストを使用すれば、自動的に行うことができます。したがって、はい、それはリストの実装方法に依存し、OPステートメントを明確にすることができます。
Matthieu M.16年

回答:


187

仮説的には、O(1)であったreverse可能性があります。(ここでも仮説的に)リンクされたリストの方向が、リストが作成された元のリストと現在同じか反対かを示すブールリストメンバーがあった可能性があります。

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

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


29
@MooseBoys:私はあなたのアナロジーに同意しません。違いは、リストの場合には、実装が提供することができる、ということであるreverseO(1)複雑他の操作のビッグOに影響を与えることなく、このブールフラグトリックを使用して、。しかし、実際には、たとえそれが技術的にO(1)であっても、すべての操作で追加の分岐はコストがかかります。対照的に、sortO(1)であるリスト構造を作成することはできず、他のすべての操作は同じコストです。問題のポイントは、どうやら、O(1)大きなOだけを気にかければ、無料で逆転できるということです。なぜそうしなかったのでしょうか。
Chris Beck

9
XORリンクリストを使用した場合、逆転は一定の時間になります。ただし、イテレータは大きくなり、それをインクリメント/デクリメントすると、計算コストがわずかに高くなります。これは、リンクされたリストの種類を問わず、避けられないメモリアクセスによって小さくなります。
Deduplicator '25

3
@IlyaPopov:すべてのノードが実際にこれを必要としますか?ユーザーはリストノード自体に質問することはなく、メインのリスト本文のみを質問します。したがって、ユーザーが呼び出すどのメソッドでもブール値にアクセスするのは簡単です。リストを逆にするとイテレータが無効になるというルールを作成したり、イテレータを使用してブール値のコピーを保存したりできます。ですから、ビッグOには必ずしも影響しないと思います。認めますが、仕様を1行ずつ説明しませんでした。:)
Chris Beck、

4
@ケビン:うーん、何?いずれにせよ、2つのポインタを直接xorすることはできませんstd::uintptr_t。最初にそれらを整数に変換する必要があります(明らかにtype です。その後、xorできます。)
Deduplicator

3
@Kevin、間違いなくC ++でXORリンクリストを作成できます。これは、実際にはこの種の事柄の子孫言語です。を使用する必要があるということは何もありません。配列にstd::uintptr_tキャストしてからchar、コンポーネントのXOR を実行できます。速度は遅くなりますが、100%ポータブルです。おそらく、これらの2つの実装から選択し、2番目の実装uintptr_tが欠落している場合にのみ2番目の実装をフォールバックとして使用できます。いくつかのそれはこの答えで説明されている場合:stackoverflow.com/questions/14243971/...
クリス・ベック

61

それは可能性があることOリスト「の意味スワップ可能フラグ格納なる場合(1)prev」および「next各ノードが持つ」ポインタ。リストを逆にすることが頻繁な操作である場合、そのような追加は実際に役立つ可能性があり、現在の標準でそれを実装することが禁止される理由はわかりません。ただし、このようなフラグを指定すると、リストの通常のトラバーサルが(定数係数によってのみ)コストが高くなります。

current = current->next;

operator++、リストのイテレータ、あなたはなるだろう

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

これは、簡単に追加することを決めたものではありません。リストは通常​​、逆にされるよりもずっと頻繁にトラバースされることを考えると、標準がこのテクニックを義務付けることは非常に賢明ではありません。したがって、逆演算は線形の複雑さを持つことができます。それは、しかし、ノートを行い、TO(1)⇒ TOnは先に述べたように)ので、あなたの「最適化」を実施し、技術的に認められます。

Javaなどのバックグラウンドを持っている場合は、イテレータが毎回フラグをチェックする必要があるのか​​疑問に思うかもしれません。代わりに、2つの異なるイテレータタイプがあり、どちらも共通の基本タイプから派生していて、適切なイテレータstd::list::beginstd::list::rbeginポリモーフィックに返すことができませんか?可能ではありますが、イテレータを進めると、間接的な(インライン化が困難な)関数呼び出しになるため、全体がさらに悪化します。Javaでは、とにかくこの価格を定期的に支払っていますが、これも、パフォーマンスが重要な場合に多くの人がC ++を利用する理由の1つです。

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


7
@galinette:std::list::reverseイテレータを無効にしません。
ベンジャミンリンドリー

1
@galinette申し訳ありませんが、私は「あたりフラグとして以前のコメント読みMIS イテレータ」あたりのフラグとは対照的に、「ノードあなたがそれを書いたように」。もちろん、ノードごとのフラグは、すべてを反転させるために線形トラバーサルを実行する必要があるので、逆効果になります。
5gon12eder 2016

2
@ 5gon12eder:非常に失われたコストで分岐を排除できます:nextprevポインターを配列に保存し、方向を0またはとして保存し1ます。順方向に反復するには、次の手順に従いpointers[direction]、逆方向に反復しますpointers[1-direction](またはその逆)。これはまだわずかなオーバーヘッドを追加しますが、おそらくブランチほどではありません。
Jerry Coffin

4
おそらく、イテレータ内のリストへのポインタを格納することはできません。 swap()一定の時間に指定され、イテレータを無効にしません。
Tavian Barnes、2016

1
@TavianBarnesくそー!ええと、トリプルインダイレクション…(つまり、実際にはトリプルではありません。動的に割り当てられたオブジェクトにフラグを格納する必要がありますが、イテレータのポインタはもちろん、リストを間接的に指定する代わりに、そのオブジェクトを直接指すことができます。)
5gon12eder

37

双方向のイテレータをサポートするすべてのコンテナに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。


18

それは、すべてのノード(n合計)をトラバースし、それらのデータを更新する必要があるためです(更新ステップは確かにですO(1))。これは全体の操作になりO(n*1) = O(n)ます。


27
すべてのアイテム間のリンクも更新する必要があるからです。一枚の紙を取り出し、反対投票する代わりにそれを引き出します。
Blindy 2016

11
なぜあなたがそう確信しているのかと尋ねるのですか?あなたは私たちの両方の時間を無駄にしています。
Blindy 2016

28
@Curious二重にリンクされたリストのノードには方向性があります。そこからの理由。
2016

11
@Blindy良い答えが完成しているはずです。「一枚の紙を取り、それを引き出す」ことは、良い答えの必須の要素ではないはずです。良い回答ではない回答は反対票の対象となります。
RM

7
@靴:彼らはする必要がありますか?XOR-linked-listなどを調べてください。
Deduplicator '25 / 02/25

2

また、すべてのノードの前のポインタと次のポインタを入れ替えます。それが線形を取る理由です。O(1)では、このLLを使用する関数がLLに関する情報も入力として受け取る場合、それが正常にアクセスしているか逆にアクセスしているかのように行うことができます。


1

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


1

それは単にリストを逆の順序でコピーする必要があるため、O(n)です。個々のアイテム操作はO(1)ですが、リスト全体にn個あります。

もちろん、新しいリスト用のスペースの設定やその後のポインターの変更などに伴う一定の時間操作があります。一次n因子を含めた場合、O表記は個々の定数を考慮しません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.