誰もが知っているわけではない、std :: listの不快な機能

二重リンクリストは、誰もが知っているすべての場所で使用される基本的なデータ構造です。 誰もが理由を知っており、どの場合にベクトルよりも効果的であり、どの演算が線形の複雑さを持ち、どの演算が一定であるか...



ちょっと待ってくださいが、size()関数の複雑さを知っていますか?

「もちろん私は知っています-O(1)!」多くの人が答えます。



size_type size() const { return _size; }
      
      







些細で、効果的で、安全ですね。

私はこの方法でこの機能を実装します、あなたのほとんどは同じことをするでしょう。



ただし、GNU STL実装を書いた人は異なる意見を持っています。





gcc 4.xでのこの関数の実装は次のようになります。

 /** Returns the number of elements in the %list. */ size_type size() const { return std::distance(begin(), end()); }
      
      





あなたが私を信じていないなら、あなたのディストリビューションをチェックしてください。



ここで何が見えますか? サイズを取得するなどの簡単な操作を実行するために、リストではカウントを伴う完全なパスを実行します!



これは、この関数をループに入れてリストを調べた場合、アルゴリズムの複雑さが自動的にN倍になることを意味します。





 for (std::list <int>::const_iterator I = my_list.begin (); I != my_list.end (); ++I) { if (*I > my_list.size ()) { std::cout << "Haha! "<< *I << std::endl; } }
      
      





一見明らかなO(N)の代わりに、ここでO(N ^ 2)を取得します。 リストに100個のアイテムがあると便利です。 しかし、1,000,000の場合はどうでしょうか?



また、マルチスレッド環境ではsize()の呼び出しが安全でないことも意味します。





 std::list <int> my_list; // Thread 1 void thread1_proc () { while (!stop) { std::cout << "List size: " << my_list.size () << std::endl; } } // Thread 2 void thread2_proc () { while (!stop) { int n = rand () my_list.push_back (n); if (n % 2) my_list.pop_front (); } }
      
      





最初のスレッドでクラッシュする深刻なリスクがあります。



それはまた







では、GNUプログラマーがこの方法でリストを実装したのはなぜですか?



_size内部変数を維持する必要がないため、スプライスをO(1)で実装できます。そうしないと、O(N)が最悪の場合になります。



spliceは、あるリストから別のリストに連続した要素を転送する操作です。 リストの新しいサイズを数える必要がないので、あるノードから別のノードにポインターを「切り替える」だけで十分です。 一定時間。



_size内部変数があるため、転送する要素の数を計算し、両方のリストでそれに応じて更新する必要があります。



これがその理由です。 ところで、この操作はどのくらいの頻度で使用しますか? そして、上記のすべて?



一般的に、注意してください。 また、他のSTL実装では、変数_sizeはO(1)に対しても適切に機能することに注意してください。 異なるプラットフォームで異なるSTL実装を使用してプロジェクトを構築する場合は、2回注意してください。



私はこれにお辞儀をします。 金曜日の植物の記事でごめんなさい。



UPD

新しいC ++ 11標準では、すべてのSTLコンテナのサイズがO(1)で機能する必要があり、これは非常に優れています。

ただし、GNU STL実装に変更を加える試みは、これまでのところバイナリ互換性の問題のために失敗しています。 詳細はこちらgcc.gnu.org/bugzilla/show_bug.cgi?id=49561



All Articles