これが私のコードにとって何を意味するのかをもっと尋ねています。私は概念を数学的に理解していますが、概念が意味するものを頭で包むのに苦労しています。たとえば、データ構造に対してO(1)操作を実行する場合、アイテムが多いため、実行する必要のある操作の数は増えないことを理解しています。 O(n)操作は、各要素に対して一連の操作を実行することを意味します。誰かがここに空白を埋めることができますか?
それについて考える1つの方法はこれです:
O(N ^ 2)は、すべての要素について、それらを比較するなど、他のすべての要素で何かしていることを意味します。バブルソートはその一例です。
O(N log N)は、すべての要素について、要素のlog Nを見るだけでよいことを意味します。これは通常、効率的な選択を可能にする要素について何かを知っているためです。最も効率的な並べ替えは、マージ並べ替えなどの例です。
O(N!)は、N個の要素のすべての可能な順列に対して何かを行うことを意味します。巡回セールスマンはこの例です。N!ノードを訪問する方法、およびブルートフォースソリューションは、最適なものを見つけるために可能なすべての順列の合計コストを調べることです。
Big-O表記法がコードに意味する大きなことは、操作対象の「もの」の量を2倍にした場合にどのようにスケーリングされるかです。具体例を次に示します。
Big-O | 10の計算| 100個の計算 ----------------------------------------- ----------------------------- O(1)| 1 | 1 O(log(n))| 3 | 7 O(n)| 10 | 100 O(n log(n))| 30 | 700 O(n ^ 2)| 100 | 10000
したがって、O(n log(n))であるクイックソートとO(n ^ 2)であるバブルソートを使用します。 10個のものを並べ替える場合、クイック並べ替えはバブル並べ替えよりも3倍高速です。しかし、100個のものを並べ替えると、14倍高速になります。その場合、最速のアルゴリズムを明確に選択することが重要です。 100万行のデータベースに到達するとき、0.2秒で実行するクエリと数時間かかるクエリの違いを意味します。
考慮すべきもう1つのことは、悪いアルゴリズムはムーアの法則では解決できないことです。たとえば、O(n ^ 3)の科学的な計算があり、1日に100個の計算ができる場合、プロセッサー速度を2倍にすると、1日で125個しか取得できません。ただし、その計算をO(n ^ 2)にノックすると、1日に1000のことをしています。
説明:実際、Big-Oは、同じ特定のサイズポイントでの異なるアルゴリズムの比較パフォーマンスについてではなく、異なるサイズポイントでの同じアルゴリズムの比較パフォーマンスについては述べていません。
計算計算計算 Big-O | 10のこと| 100のこと| 1000個の場合 ------------------------------------------ ---------------------------- O(1)| 1 | 1 | 1 O(log(n))| 1 | 3 | 7 O(n)| 1 | 10 | 100 O(n log(n))| 1 | 33 | 664 O(n ^ 2)| 1 | 100 | 10000
あなたはそれを視覚化するのに役立つかもしれません:
また、LogY/LogXで関数をスケーリングします n1/2、n、n2 すべては 直線 のように見えますが、オンLogY/Xスケール 2n、en、10n は直線であり、n!は線形である(n log nのように見える)。
これは数学的すぎるかもしれませんが、ここに私の試みがあります。 (私am数学者。)
何かがO(f)(n))である場合、n要素は[〜#〜] a [〜#〜]f(n)+[〜#〜] b [〜#〜](たとえば、クロックサイクルまたはCPU動作で測定)。これらの定数[〜#〜] a [〜#〜]および[〜#〜] b [〜# 〜]、特定の実装から発生します。 [〜#〜] b [〜#〜]は、基本的に操作の「一定のオーバーヘッド」を表します。たとえば、サイズに依存しない前処理の一部コレクション。 [〜#〜] a [〜#〜]は、実際のアイテム処理アルゴリズムの速度を表します。
ただし、重要なのは、大きなO表記を使用して(どれだけうまくスケーリングできるか)を把握することです。したがって、これらの定数は実際には重要ではありません。10から10000アイテムまでスケールする方法を考えている場合、定数のオーバーヘッド[〜#〜] b [〜#〜] ?同様に、他の懸念事項(下記参照)は、乗法定数[〜#〜] a [〜#〜]の重みを確実に上回ります。
したがって、実際の取引はf(n)です。 fがnでまったく成長しない場合、例えばf(n)= 1の場合、素晴らしくスケーリングします---実行時間は常に[〜#〜] a [〜#〜]+[〜#〜] b [〜#〜]。 fがnと線形に増加する場合、つまりf(n)=n、実行時間は予想される限り最適にスケーリングされます---ユーザーが10秒間10 ns待機している場合要素の場合、10000要素を10000 ns待機します(加算定数は無視します)。しかし、nのように速く成長する場合2、それからあなたは困っている。より大きなコレクションを取得すると、物事が遅くなりすぎます。 f(n)=nlog(n)は適切な妥協案です。通常、線形スケーリングを行うほど操作を単純にすることはできませんが、f(n)=n2。
実際には、ここにいくつかの良い例があります:
don.neufeldの答えは非常に良いですが、おそらく2つの部分で説明します。最初に、ほとんどのアルゴリズムが該当するO()の大まかな階層があります。それから、それらのそれぞれを見て、その時間の複雑さのアルゴリズム標準アルゴリズムのスケッチを思いつくことができます。
実用的な目的のために、これまで重要と思われる唯一のO()は次のとおりです。
以上です。これらの間に収まる(またはO(2 ^ n)よりも大きい)他の多くの可能性がありますが、実際には頻繁に発生することはなく、これらの1つと質的にそれほど違いはありません。キュービックアルゴリズムは、すでに少しばかり拡張されています。言及する価値があるほど頻繁にそれらに遭遇したので、それらを含めただけです(例えば、行列乗算)。
これらのクラスのアルゴリズムで実際に何が起こっていますか?さて、これらの特徴に合わない例はたくさんありますが、良いスタートを切ったと思います。しかし、上記の場合、通常は次のようになります。
これらのどれも厳密ではありません。特に線形時間アルゴリズムではありません(O(n)):すべての入力、次に半分、次に半分などを調べる必要があるいくつかの例を考え出すことができます。 -入力のペアをまとめて折り返し、出力を再帰します。これらは上記の説明には当てはまりません。各入力を一度は見ていませんが、それでも直線的な時間で出てくるからです。それでも、時間の99.2%、線形時間は各入力を1回見ることを意味します。
これらの多くは、カードのシャッフルなど、プログラミングではないもので簡単に実証できます。
デッキ全体を介してスペードのエースを見つけることによってカードのデッキをソートし、次にデッキ全体を介してスペードの2つを見つけるなど、デッキが既に後方にソートされている場合は最悪の場合n ^ 2になります。 52枚すべてのカードを52回見ました。
一般に、本当に悪いアルゴリズムは必ずしも意図的なものではなく、同じセットで線形に繰り返される他のメソッド内で線形であるメソッドを呼び出すなど、他の何かの誤用です。
OK-ここには非常に良い答えがいくつかありますが、それらのほとんどすべてが同じ間違いを犯しているようで、一般的な使用法に浸透しているものです。
非公式には、f(n) = O(g(n))である場合、スケーリング係数まで、すべてのnがn0より大きい場合、g(n)は大きい f(n)より。つまり、f(n) grows no quicker than、or is bounded above above by、g(n)。これは、f(n)がどれだけ速く成長するかについては何も伝えません。g(n)より悪くないことが保証されているという事実を除いて。
具体例:n = O(2 ^ n)。 nは2 ^ nよりもはるかに速く成長しないことを知っているので、指数関数によって上に制限されると言うことができます。 nと2 ^ nの間には十分なスペースがあるので、非常にtightバウンドではありませんが、それでも正当なバウンドです。
なぜ私たち(コンピューター科学者)は正確ではなく境界を使用するのですか? a)境界は証明するのが簡単な場合が多く、b)アルゴリズムの特性を表現するための短縮形を提供するためです。私の新しいアルゴリズムがO(n.log n)であると言うと、最悪の場合、その実行時間はn入力でn.log nによって上から制限され、十分に大きいn(以下のコメントを参照してください)最悪の場合を意味しないかもしれません)。
代わりに、関数が他の関数とまったく同じくらい速く成長すると言いたい場合は、thetaを使用してそのポイントを作成します(Tと書く(f(n))は、マークダウンのf(n)の\ Thetaを意味します)。 T(g(n))は、上下からg(n)によって、再びスケーリングファクターまで、漸近的に境界付けられることの省略形です。
つまり、f(n) = T(g(n))<=> f(n) = O(g(n))およびg(n) = O(f(n))。この例では、2 ^ n!= O(n)であるため、n!= T(2 ^ n)であることがわかります。
なぜこれを心配するのですか?あなたの質問に「誰かがO(x!)を書くためにクラックを吸わなければならないでしょうか?」答えはノーです。基本的に、あなたが書くすべてのものは上から階乗関数によって制限されるからです。クイックソートの実行時間はO(n!)です-厳密な制限はありません。
また、ここには繊細さの別の側面があります。通常、O(g(n))表記を使用するときに最悪の場合の入力について話しているため、複合ステートメントを作成しています。最悪の場合、実行時間はg(n)のステップをとるアルゴリズムよりも悪化します。この場合も、モジュロスケーリングで、十分に大きいnになります。ただし、averageおよびさらにbestケース。
バニラクイックソートは、相変わらず良い例です。最悪の場合はT(n ^ 2)です(実際には少なくともn ^ 2ステップかかりますが、それ以上ではありません)が、平均的なケースではT(n.log n)です。ステップはn.log nに比例します。最良の場合はT(n.log n)でもありますが、たとえば、配列が既にソートされているかどうかを確認することで改善できます。その場合、最適な実行時間はT(n)になります。
これは、これらの境界の実際の実現についてのあなたの質問にどのように関係しますか?さて、残念ながら、O()表記は、現実の実装が処理しなければならない定数を隠します。したがって、たとえば、T(n ^ 2)操作の場合は、考えられるすべての要素のペアを訪問する必要がありますが、それらを訪問する必要がある回数はわかりません(ただし、 n)。そのため、すべてのペアを10回、または10 ^ 10回アクセスする必要があり、T(n ^ 2)ステートメントは区別しません。低次関数も非表示です。n^ 2 + 100n = T(n ^ 2)であるため、要素のすべてのペアを1回、個々の要素を100回すべて訪問する必要があります。 O()表記の背後にある考え方は、nが十分に大きい場合、n ^ 2が100nよりも大きくなりすぎて実行時間に100nの影響が見られないため、まったく問題ではないということです。ただし、定数因子などが実際の有意差を生むように、「十分に小さい」nを扱うことがよくあります。
たとえば、クイックソート(平均コストT(n.log n))とヒープソート(平均コストT(n.log n))は、どちらも同じ平均コストのソートアルゴリズムですが、通常、クイックソートはヒープソートよりもはるかに高速です。これは、ヒープソートがクイックソートよりも要素ごとにいくつかの比較を行うためです。
これは、O()表記法が役に立たないと言うことではなく、単に不正確です。小さなnを振るうには、かなり鈍いツールです。
(この論文の最後のメモとして、O()表記は関数の成長を説明するだけであることに注意してください-必ずしも時間である必要はなく、メモリ、分散システムで交換されるメッセージ、または必要なCPUの数である可能性があります並列アルゴリズム。)
C#で簡単なコード例を示して説明しようと思います。
ために List<int> numbers = new List<int> {1,2,3,4,5,6,7,12,543,7};
O(1)は次のようになります
return numbers.First();
O(n)は次のようになります
int result = 0;
foreach (int num in numbers)
{
result += num;
}
return result;
O(n log(n))は次のようになります
int result = 0;
foreach (int num in numbers)
{
int index = numbers.length - 1;
while (index > 1)
{
// yeah, stupid, but couldn't come up with something more useful :-(
result += numbers[index];
index /= 2;
}
}
return result;
O(n ^ 2)は次のようになります
int result = 0;
foreach (int outerNum in numbers)
{
foreach (int innerNum in numbers)
{
result += outerNum * innerNum;
}
}
return result;
O(n!)のように見えます。
しかし、あなたは一般的なポイントを得ることを望みますか?
私が技術者でない友人に説明する方法は次のとおりです。
複数桁の加算を検討してください。古き良き、鉛筆と紙の追加。あなたが7-8歳のときに学んだ種類。 2桁の3桁または4桁の数字を考えると、それらの合計がかなり簡単にわかります。
2つの100桁の数字を渡し、それらの合計を尋ねると、鉛筆と紙を使用しなければならない場合でも、それを理解することは非常に簡単です。明るい子供なら、ほんの数分でこのような追加を行うことができます。これには、約100回の操作しか必要ありません。
次に、複数桁の乗算を検討します。あなたはおそらく8歳か9歳くらいでそれを学んだでしょう。あなたは(願わくば)その背後にある仕組みを学ぶために、たくさんの反復訓練をしました。
ここで、同じ2つの100桁の数字を与えて、それらを掛け合わせるように言ったと想像してください。これは非常に多く、muchより困難な作業であり、何時間もかかるでしょう。そして間違いなくそうすることはないでしょう。この理由は、(このバージョンの)乗算がO(n ^ 2)であるためです。一番下の数字の各数字に一番上の数字の各数字を掛けて、合計で約n ^ 2回の操作を行う必要があります。 100桁の数値の場合、10,000回の乗算です。
私が考えているのは、Nを選んだ悪役Vによって引き起こされた問題を片付けるタスクがあり、彼がNを増やしたときに問題を完了するのにどれくらいかかるかを見積もる必要があるということです。
O(1)-> Nを増やしても実際にはまったく違いはありません
O(log(N))-> VがNを2倍するたびに、タスクを完了するために余分な時間Tを費やす必要があります。 Vは再びNを2倍にし、同じ金額を使用します。
O(N)-> VがNを2倍するたびに、2倍の時間を費やします。
O(N^2) -> every time V doubles N, you spend 4x as much time. (it's not fair!!!)
O(N log(N))-> VがNを2倍するたびに、2倍の時間ともう少し多くの時間を費やします。
これらはアルゴリズムの境界です。コンピューター科学者は、Nの値が大きい場合にどれくらいかかるかを説明したいと思います(暗号化で使用される数値を因数分解するときに重要になります-コンピューターが10倍になった場合、さらに多くのビットが実行されます暗号化を破るのに1年ではなく100年かかることを確認するために使用する必要がありますか?)
関係する人々に違いをもたらす場合、境界の一部に奇妙な表現が含まれることがあります。 O(N log(N)log(log(N)))のようなものを、いくつかのアルゴリズムのKnuthのArt of Computer Programmingのどこかで見ました。 (私の頭の上のどれが覚えていない)
いいえ、O(n)アルゴリズムは、各要素で演算を実行することを意味しません。Big-O表記法は、実機。
O(n)は、入力が増加するにつれてアルゴリズムにかかる時間が線形に増加することを意味します。 O(n ^ 2)は、アルゴリズムにかかる時間が入力の2乗に比例して大きくなることを意味します。などなど。
何らかの理由でまだ触れられていないこと:
O(2 ^ n)やO(n ^ 3)などの厄介な値を含むアルゴリズムを見ると、許容できるパフォーマンスを得るためには、問題に対する不完全な答えを受け入れなければならないことがよくあります。
最適化の問題に対処する場合、このように爆発する正しい解決策が一般的です。妥当な時間内に配信されるほぼ正しい回答は、マシンがほこりになってからずっと後に配信される正しい回答よりも優れています。
チェスを考えてください:正しい解決策が何であると考えられるのか正確にはわかりませんが、おそらくO(n ^ 50)のようなものか、さらに悪いものです。理論上、どのコンピューターでも正しい答えを実際に計算することはできません-宇宙のすべての粒子を計算要素として使用して、まだ多くのゼロが残っている宇宙の寿命の可能な限り短い時間で操作を実行する場合でも。 (量子コンピューターがそれを解決できるかどうかは別問題です。)
いいえ、Prologを使用してください。各要素を前の要素よりも大きくする必要があることを説明するだけでPrologにソートアルゴリズムを記述し、バックトラッキングでソートを行うと、O(x!)になります。 「順列ソート」とも呼ばれます。
Big-Oの背後にある「直感」
Xが無限に近づくにつれて、x上の2つの関数間の「競合」を想像してください:f(x) and g(x))。
さて、ある関数(あるx)からある関数が常に他の関数よりも高い値を持っている場合、この関数を他の関数よりも「高速」に呼び出しましょう。
したがって、たとえば、x> 100ごとにf(x)> g(x)と表示される場合、f(x)はより速い"g(x)より。
この場合、g(x) = O(f(x)).. f(x)は、ある種の「速度制限」をもたらすg(x)をソートします。最終的にはそれをパスし、永久に残します。
これは big-O表記 の定義とは異なり、f(x)はC * g(x)よりも大きい必要があるだけです。いくつかの定数C(これは、g(x)に一定の係数を乗じて競争に勝つことはできないという別の言い方です-f(x)が常に勝ちます。形式的な定義でも絶対値を使用しますが、なんとか直観的なものにしたいと思います。
私はドン・ノイフェルドの答えが好きですが、O(n log n)について何か追加できると思います。
単純な分割統治戦略を使用するアルゴリズムは、おそらくO(log n)になります。これの最も簡単な例は、ソートされたリストで何かを見つけることです。最初から始めてスキャンしないでください。真ん中に行って、次に戻るか進むかを決め、探していた最後の場所に半分ジャンプして、探しているアイテムが見つかるまでこれを繰り返します。
クイックソートまたはマージソートアルゴリズムを見ると、リストを半分に分割し、各半分を(同じアルゴリズムを再帰的に使用して)並べ替えてから、2つの半分を再結合するアプローチを採用していることがわかります。この種の再帰分割統治戦略はO(n log n)になります。
慎重に考えてみると、クイックソートはO(n) n個のアイテム全体に対してパーティションアルゴリズムを実行し、次にO(n) = n/2個のアイテムで2回、次にn/4個のアイテムで4回、等... 1個のアイテムでn個のパーティションに到達するまで(これは縮退しています)。 1はおよそlog nであり、各ステップはO(n)であるため、再帰的な分割統治はO(n log n)です。 、2つの並べ替えられたリストの再結合はO(n)です。
O(n!)アルゴリズムを記述する喫煙クラックに関しては、選択の余地がない限り、あなたはあなたです。上記の巡回セールスマンの問題は、そのような問題の1つであると考えられています。
レゴブロック(n)を垂直に積み重ねてジャンプするものと考えてください。
O(1)は、各ステップで何もしないことを意味します。高さは変わりません。
O(n)は、各ステップでcブロックをスタックすることを意味します。c1は定数です。
O(n ^ 2)は、各ステップでc2 x nブロックをスタックすることを意味します。ここで、c2は定数で、nはスタックされたブロックの数です。
O(nlogn)は、各ステップで、c3 x n x log nブロックをスタックすることを意味します。c3は定数で、nはスタックされたブロックの数です。
カメとウサギ(カメとウサギ)のf話を覚えていますか?
長期的にはカメが勝ちますが、短期的にはウサギが勝ちます。
O(logN)(カメ)vs. O(N)(hare)。
2つの方法のbig-Oが異なる場合、そのうちの1つが勝つNレベルがありますが、big-OはNの大きさについては何も言いません。
特定のサイズの問題を解決できるコンピューターがあるとします。ここで、パフォーマンスを数倍にできると想像してください。倍増ごとにどれだけ大きな問題を解決できますか?
サイズが2倍になる問題を解決できる場合、それはO(n)です。
1でない乗数がある場合、それはある種の多項式の複雑さです。たとえば、2倍にするごとに問題のサイズを約40%増加できる場合、それはO(n ^ 2)であり、約30%はO(n ^ 3)になります。
問題のサイズを大きくすると、指数関数的またはさらに悪化します。たとえば、2倍にするごとに1より大きい問題を解決できる場合、それはO(2 ^ n)です。 (これが、適度なサイズのキーでは暗号キーのブルートフォースが事実上不可能になる理由です。128ビットのキーは、64ビットの約16倍の処理を必要とします。)
8歳のlog(n)は、サイズn = 1:pに達するために、長さn logを2つに切る必要がある回数を意味します
O(n log n)は通常ソートされますO(n ^ 2)は通常要素のすべてのペアを比較します
上記の投稿に対するいくつかのコメントに答えるためだけに:
国内-私はこのサイトにいます。教訓のためではありませんが、私たち-プログラマーとして-通常は精度を重視しているためです。ここで行ったスタイルでO()表記を誤って使用すると、意味がなくなります。ここで使用されている規則では、何かがO(n ^ 2)のようにn ^ 2単位時間かかると言うこともできます。 O()を使用しても何も追加されません。私が話しているのは、一般的な使用法と数学的な精度のわずかな違いではなく、意味のあるものとそうでないものとの違いです。
私は、これらの用語を正確に使用する多くの優秀なプログラマーを知っています。 「ああ、私たちはプログラマーだから気にしない」と言うと、企業全体が安くなります。
onebyone-さて、そうではありませんが、私はあなたの主張を引き受けます。これは、任意の大きなnに対してO(1)ではありません。これは、O()の定義の一種です。むしろ、実際にその数の限界ではなく、実行されたステップの数について話してください。
O(n log n)を理解するには、log nはnのlog-base-2を意味することに注意してください。次に、各部分を見てください。
O(n)は、セット内の各アイテムを操作するときの多かれ少なかれです。
O(log n)は、項目の数を取得するために、操作の数が2を累乗する指数と同じ場合です。たとえば、バイナリ検索では、セットをn倍の半分のログにカットする必要があります。
O(n log n)は組み合わせです。セット内の各アイテムのバイナリ検索の行に沿って何かを実行しています。効率的な並べ替えは、多くの場合、アイテムごとに1つのループを実行し、各ループで適切な検索を実行して、問題のアイテムまたはグループを配置する適切な場所を見つけます。したがって、n * log n。
質問に誠実であり続けるために、私は8歳の子供に答えるような方法で質問に答えます
アイスクリームの売り手が、整然と配置されたさまざまな形状のアイスクリーム(たとえばN)を多数用意しているとします。真ん中にあるアイスクリームを食べたい
ケース1:-アイスクリームを食べることができるのは、それよりも小さいアイスクリームをすべて食べた場合のみです。準備したすべてのアイスクリームの半分を食べる必要があります(入力)。回答は、入力のサイズに直接依存します。次数o(N)
ケース2:-途中でアイスクリームを直接食べることができます
ソリューションはO(1)になります
ケース3:アイスクリームを食べることができるのは、それより小さいアイスクリームをすべて食べており、アイスクリームを食べるたびに別の子供(毎回新しい子供)がすべてのアイスクリームを食べることを許可する合計所要時間はN + N + N .......(N/2)回解はO(N2)になります
技術用語や数学的な概念は別として、実際の8歳の少年の説明を実際に書いてみます。
O(n^2)
操作は正確に何をしますか?
あなたがパーティーに参加していて、あなたを含むn
人がパーティーにいる場合。ある時点で誰が握手したかを忘れてしまう可能性があるため、全員が他の全員と握手するために必要なハンドシェイクの数。
注:これは、_n^2
_に十分近いn(n-1)
を生成するシンプレックスに近似しています。
操作が
O(n log(n))
である場合、一体何を意味するのでしょうか?
お気に入りのチームが勝ち、並んでいます。チームにはn
人のプレーヤーがいます。各プレイヤーを複数回、何回、プレイヤー数n
の桁数でハンドシェイクするかを考えると、各プレイヤーとハンドシェイクするのに何回必要でしょうか。
注:これにより_n * log n to the base 10
_が生成されます。
誰かが
O(x!)
を書くためにクラックを吸う必要がありますか?
あなたは裕福な子供であり、あなたのワードローブにはたくさんの布があり、各タイプの衣類用のx
引き出しがあり、引き出しは隣同士にあり、最初の引き出しには1つのアイテムがあり、各引き出しには引き出しのように布を左に1枚、つまり_1
_帽子、_2
_かつら、.. _(x-1)
_ズボン、次にx
シャツのようなものがあります。これで、各引き出しから1つのアイテムを使用して、いくつの方法でドレスアップできます。
注:この例は、_number of children = depth
_を介して行われる決定ツリー内のリーフの数を表します(_1 * 2 * 3 * .. * x
_)。
log(n)は対数成長を意味します。例としては、アルゴリズムの分割と征服があります。配列に1000個のソートされた番号(例:3、10、34、244、1203 ...)があり、リスト内の番号を検索(その位置を検索)する場合、値のチェックから開始できます。インデックス500の数値。シークよりも低い場合は750にジャンプします。シークよりも高い場合は250にジャンプします。その後、値(およびキー)が見つかるまでプロセスを繰り返します。サーチスペースの半分をジャンプするたびに、数値3004が数値5000を超えることはできないことがわかっているため、他の多くの値をテストすることはできません(ソートされたリストであることに注意してください)。
n log(n)は、n * log(n)を意味します。