std::list
C ++標準ライブラリのクラスの逆関数に線形ランタイムがあるのはなぜですか?二重にリンクされたリストの場合、逆関数はO(1)である必要があると思います。
二重にリンクされたリストを反転するには、ヘッドポインターとテールポインターを切り替えるだけです。
Reverse
関数がO(1)に実装されることをどのように期待するかを示していただけますか
std::list
C ++標準ライブラリのクラスの逆関数に線形ランタイムがあるのはなぜですか?二重にリンクされたリストの場合、逆関数はO(1)である必要があると思います。
二重にリンクされたリストを反転するには、ヘッドポインターとテールポインターを切り替えるだけです。
Reverse
関数がO(1)に実装されることをどのように期待するかを示していただけますか
回答:
仮説的には、O(1)であったreverse
可能性があります。(ここでも仮説的に)リンクされたリストの方向が、リストが作成された元のリストと現在同じか反対かを示すブールリストメンバーがあった可能性があります。
残念ながら、これは基本的に他の操作のパフォーマンスを低下させます(ただし、漸近ランタイムは変更しません)。各操作で、リンクの「次の」ポインタまたは「前の」ポインタのどちらに従うかを検討するためにブール値を調べる必要があります。
これはおそらく比較的頻度の低い操作であると考えられていたため、標準(実装を指示せず、複雑さのみ)は、複雑さを線形にできると規定しました。これにより、「次の」ポインタは常に明確に同じ方向を意味し、一般的なケースの処理を高速化できます。
reverse
とO(1)
複雑他の操作のビッグOに影響を与えることなく、このブールフラグトリックを使用して、。しかし、実際には、たとえそれが技術的にO(1)であっても、すべての操作で追加の分岐はコストがかかります。対照的に、sort
O(1)であるリスト構造を作成することはできず、他のすべての操作は同じコストです。問題のポイントは、どうやら、O(1)
大きなOだけを気にかければ、無料で逆転できるということです。なぜそうしなかったのでしょうか。
std::uintptr_t
。最初にそれらを整数に変換する必要があります(明らかにtype です。その後、xorできます。)
std::uintptr_t
キャストしてからchar
、コンポーネントのXOR を実行できます。速度は遅くなりますが、100%ポータブルです。おそらく、これらの2つの実装から選択し、2番目の実装uintptr_t
が欠落している場合にのみ2番目の実装をフォールバックとして使用できます。いくつかのそれはこの答えで説明されている場合:stackoverflow.com/questions/14243971/...
それは可能性があることOリスト「の意味スワップ可能フラグ格納なる場合(1)prev
」および「next
各ノードが持つ」ポインタ。リストを逆にすることが頻繁な操作である場合、そのような追加は実際に役立つ可能性があり、現在の標準でそれを実装することが禁止される理由はわかりません。ただし、このようなフラグを指定すると、リストの通常のトラバーサルが(定数係数によってのみ)コストが高くなります。
current = current->next;
でoperator++
、リストのイテレータ、あなたはなるだろう
if (reversed)
current = current->prev;
else
current = current->next;
これは、簡単に追加することを決めたものではありません。リストは通常、逆にされるよりもずっと頻繁にトラバースされることを考えると、標準がこのテクニックを義務付けることは非常に賢明ではありません。したがって、逆演算は線形の複雑さを持つことができます。それは、しかし、ノートを行い、T ∈ O(1)⇒ T ∈ O(nは先に述べたように)ので、あなたの「最適化」を実施し、技術的に認められます。
Javaなどのバックグラウンドを持っている場合は、イテレータが毎回フラグをチェックする必要があるのか疑問に思うかもしれません。代わりに、2つの異なるイテレータタイプがあり、どちらも共通の基本タイプから派生していて、適切なイテレータstd::list::begin
をstd::list::rbegin
ポリモーフィックに返すことができませんか?可能ではありますが、イテレータを進めると、間接的な(インライン化が困難な)関数呼び出しになるため、全体がさらに悪化します。Javaでは、とにかくこの価格を定期的に支払っていますが、これも、パフォーマンスが重要な場合に多くの人がC ++を利用する理由の1つです。
コメントでベンジャミンリンドリーが指摘したように、reverse
イテレータを無効にすることは許可されていないため、標準で許可されている唯一のアプローチは、ポインタをイテレータ内のリストに戻し、二重間接メモリアクセスを引き起こすことです。
std::list::reverse
イテレータを無効にしません。
next
とprev
ポインターを配列に保存し、方向を0
またはとして保存し1
ます。順方向に反復するには、次の手順に従いpointers[direction]
、逆方向に反復しますpointers[1-direction]
(またはその逆)。これはまだわずかなオーバーヘッドを追加しますが、おそらくブランチほどではありません。
swap()
一定の時間に指定され、イテレータを無効にしません。
双方向のイテレータをサポートするすべてのコンテナに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。
それは、すべてのノード(n
合計)をトラバースし、それらのデータを更新する必要があるためです(更新ステップは確かにですO(1)
)。これは全体の操作になりO(n*1) = O(n)
ます。