CS derecesine sahip çoğu insan, Big O'nun ne anlama geldiğini kesinlikle bilecektir . Bir algoritmanın ne kadar iyi ölçeklendiğini ölçmemize yardımcı olur.
Ama nasıl yok, merak ediyorum sen hesaplamak veya algoritmaların karmaşıklığı yaklaşır?
CS derecesine sahip çoğu insan, Big O'nun ne anlama geldiğini kesinlikle bilecektir . Bir algoritmanın ne kadar iyi ölçeklendiğini ölçmemize yardımcı olur.
Ama nasıl yok, merak ediyorum sen hesaplamak veya algoritmaların karmaşıklığı yaklaşır?
Yanıtlar:
Burada basit terimlerle açıklamak için elimden geleni yapacağım, ancak bu konunun öğrencilerimin sonunda kavraması için birkaç ay sürdüğüne dikkat edin. Java kitabındaki Veri Yapıları ve Algoritmalar Bölüm 2 hakkında daha fazla bilgi bulabilirsiniz .
BigOh'u elde etmek için kullanılabilecek hiçbir mekanik prosedür yoktur .
Bir "yemek kitabı" olarak, BigOh'u bir kod parçasından elde etmek için, öncelikle bir boyutta bir girdi verildiğinde hesaplamaların kaç adımının yürütüldüğünü saymak için bir matematik formülü oluşturduğunuzu fark etmeniz gerekir.
Amaç basittir: kodu yürütmeye gerek kalmadan algoritmaları teorik bir bakış açısıyla karşılaştırmak. Adım sayısı ne kadar az olursa algoritma o kadar hızlı olur.
Örneğin, şu kod parçasına sahip olduğunuzu varsayalım:
int sum(int* data, int N) {
int result = 0; // 1
for (int i = 0; i < N; i++) { // 2
result += data[i]; // 3
}
return result; // 4
}
Bu işlev, dizinin tüm öğelerinin toplamını döndürür ve bu işlevin hesaplama karmaşıklığını saymak için bir formül oluşturmak istiyoruz :
Number_Of_Steps = f(N)
Yani f(N)
, hesaplama adımlarının sayısını saymak için bir fonksiyonumuz var . Fonksiyonun girişi, işlenecek yapının boyutudur. Bu fonksiyonun şöyle çağrıldığı anlamına gelir:
Number_Of_Steps = f(data.length)
Parametre değeri N
alır data.length
. Şimdi fonksiyonun gerçek tanımına ihtiyacımız var f()
. Bu, her ilginç satırın 1'den 4'e kadar numaralandırıldığı kaynak kodundan yapılır.
BigOh'u hesaplamanın birçok yolu vardır. Bu noktadan itibaren, giriş verilerinin boyutuna bağlı olmayan her cümlenin sabit bir C
sayı hesaplama adımları aldığını varsayacağız .
Fonksiyonun bireysel adım sayısını ekleyeceğiz ve ne yerel değişken bildirimi ne de return ifadesi data
dizinin boyutuna bağlı değildir .
Bu, 1. ve 4. satırların her birinin C miktarında adım attığı ve fonksiyonun şu şekilde olduğu anlamına gelir:
f(N) = C + ??? + C
Sonraki bölüm, for
ifadenin değerini tanımlamaktır . Hesaplama adımlarının sayısını saydığımızı, yani for
ifadenin gövdesinin yürütüldüğünü unutmayın N
. Yani eklemekle aynı şey C
, N
süreleri:
f(N) = C + (C + C + ... + C) + C = C + N * C + C
Kaç beden for
çalıştırıldığını saymak için mekanik bir kural yoktur, kodun ne işe yaradığına bakarak saymanız gerekir. Hesaplamaları basitleştirmek için, for
ifadenin değişken başlatma, koşul ve artış bölümlerini göz ardı ediyoruz .
Gerçek BigOh'u elde etmek için fonksiyonun Asimptotik analizine ihtiyacımız var . Bu kabaca şu şekilde yapılır:
C
.f()
olsun polynomium onun içinde standard form
.N
yaklaşımlar infinity
.Bizim f()
iki terim vardır:
f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1
Tüm C
sabitleri ve yedek parçaları almak:
f(N) = 1 + N ^ 1
Son terim, f()
sonsuzluğa yaklaştığında ( sınırlar üzerinde düşünün ) daha büyük büyüyen terim olduğundan , bu BigOh argümanıdır ve sum()
fonksiyonun bir BigOh'u vardır:
O(N)
Bazı zor olanları çözmek için birkaç püf noktası vardır: mümkün olduğunca özetlerini kullanın .
Örnek olarak, bu kod özetler kullanılarak kolayca çözülebilir:
for (i = 0; i < 2*n; i += 2) { // 1
for (j=n; j > i; j--) { // 2
foo(); // 3
}
}
Sorulmanız gereken ilk şey, infaz emridir foo()
. Her zamanki gibi olsa da O(1)
, profesörlerinize bunu sormanız gerekir. O(1)
(neredeyse, çoğunlukla) sabit C
, boyuttan bağımsız anlamına gelir N
.
Birinci for
cümle ile ilgili ifade yanıltıcıdır. Endeks biterken 2 * N
, artış iki ile yapılır. Bu, ilk for
işlemin yalnızca N
adımlar uygulandığı ve sayımı ikiye bölmemiz gerektiği anlamına gelir .
f(N) = Summation(i from 1 to 2 * N / 2)( ... ) =
= Summation(i from 1 to N)( ... )
Cümle sayısı iki o değerine bağlıdır beri hatta daha zordur i
. Bir göz atın: i dizini şu değerleri alır: 0, 2, 4, 6, 8, ..., 2 * N ve ikincisi for
idam edilir: N, birincinin N, 2 - 2'nin ikincisi, N - 4 üçüncüsü ... ikincisinin for
asla idam edilmediği N / 2 aşamasına kadar .
Formülde bu şu anlama gelir:
f(N) = Summation(i from 1 to N)( Summation(j = ???)( ) )
Yine, adım sayısını sayıyoruz . Ve tanım gereği, her toplama her zaman birinden başlamalı ve birden büyük veya eşit bir sayı ile bitmelidir.
f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )
(Bunun foo()
olduğunu O(1)
ve C
adımlar attığını varsayıyoruz .)
Burada bir sorunumuz var: i
değeri N / 2 + 1
yukarı çektiğinde , içsel Toplama negatif bir sayıyla biter! Bu imkansız ve yanlış. Anı iki önemli noktadan i
ayırmalıyız N / 2 + 1
.
f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )
Önemli andan beri, i > N / 2
iç for
idam yapılmayacak ve vücudunda sabit bir C yürütme karmaşıklığı olduğunu varsayıyoruz.
Şimdi özetlemeler bazı kimlik kuralları kullanılarak basitleştirilebilir:
w
)Cebir uygulamak:
f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )
f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )
f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )
=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )
f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )
=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 =
(N / 2 - 1) * (N / 2) / 2 =
((N ^ 2 / 4) - (N / 2)) / 2 =
(N ^ 2 / 8) - (N / 4)
f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )
f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)
f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)
f(N) = C * ( N ^ 2 / 4 ) + C * N
f(N) = C * 1/4 * N ^ 2 + C * N
Ve BigOh:
O(N²)
O(n)
burada n
elemanların veya sayısıdır O(x*y)
burada x
ve y
dizinin boyutları. Big-oh "girdiye göre" olduğundan girişinizin ne olduğuna bağlıdır.
Big O, bir algoritmanın zaman karmaşıklığı için üst sınırı verir. Genellikle veri setlerinin (listelerin) işlenmesi ile birlikte kullanılır, ancak başka yerlerde de kullanılabilir.
C kodunda nasıl kullanıldığına dair birkaç örnek.
Diyelim ki bir dizi n öğemiz var
int array[n];
Dizinin ilk öğesine erişmek istersek, dizinin ne kadar büyük olduğu önemli olmadığından O (1) olur, ilk öğenin alınması her zaman aynı sabit zamanı alır.
x = array[0];
Listede bir numara bulmak isteseydik:
for(int i = 0; i < n; i++){
if(array[i] == numToFind){ return i; }
}
Bu O (n) olur, çünkü en fazla numaramızı bulmak için tüm listeye bakmamız gerekir. Big-O hala O (n) olsa da, ilk denememizi bulabilir ve döngüden bir kez geçebiliriz, çünkü Big-O bir algoritmanın üst sınırını açıklar (omega alt sınır için ve teta sıkı sınır için) .
İç içe döngüler elde ettiğimizde:
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
array[j] += 2;
}
}
Bu, O (n ^ 2) 'dir, çünkü dış halkanın (O (n)) her geçişinde tekrar tüm listeyi gözden geçirmeliyiz, böylece n'nin bizi n kare ile çarpması gerekir.
Bu, yüzeyi zorlukla çiziyor, ancak daha karmaşık algoritmaları analiz etmeye başladığınızda, kanıtları içeren karmaşık matematik devreye giriyor. Umarım bu size en azından temel bilgileri tanır.
O(1)
. Örneğin C standart API'lerinde bsearch
, doğası gereği O(log n)
, strlen
öyle O(n)
ve qsort
öyle O(n log n)
(teknik olarak hiçbir garantisi yoktur ve çabuk sıralamanın kendisi en kötü durum karmaşıklığına sahiptir O(n²)
, ancak libc
yazarınızın bir moron olmadığını varsayarsak , ortalama vaka karmaşıklığı O(n log n)
ve kullanır O(n²)
davayı vurma olasılığını azaltan bir pivot seçim stratejisi ). Ve her ikisi de bsearch
ve qsort
karşılaştırma işlevi patolojik ise daha kötü olabilir.
Sorununuz için Big O zamanını nasıl anlayacağınızı bilmek yararlı olsa da, bazı genel durumları bilmek algoritmanızda karar vermenize yardımcı olabilir.
Http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions adresinden kaldırılan en yaygın durumlardan bazıları şunlardır :
O (1) - Bir sayının çift mi yoksa tek mi olduğunu belirleme; sabit boyutlu bir arama tablosu veya karma tablo kullanma
O (logn) - Sıralı bir dizide ikili aramayla bir öğe bulma
O (n) - Sıralanmamış listedeki bir öğeyi bulma; iki n basamaklı sayı ekleme
O (n 2 ) - İki n basamaklı sayının basit bir algoritma ile çarpılması; iki n × n matrisinin eklenmesi; kabarcık sıralama veya ekleme sıralama
O (n 3 ) - İki n × n matrisini basit algoritma ile çarpma
O (c n ) - Dinamik programlama kullanarak gezici satıcı problemine (kesin) çözüm bulma; kaba kuvvet kullanılarak iki mantıksal ifadenin eşdeğer olup olmadığını belirleme
O (n!) - Seyahat eden satıcı problemini kaba kuvvet aramasıyla çözme
O (n n ) - Genellikle asimptotik karmaşıklık için daha basit formüller elde etmek için O (n!) Yerine kullanılır
x&1==1
tuhaflığı kontrol etmek için kullanmıyorsunuz ?
x & 1
yeterli olacaktır, kontrol etmeye gerek yok == 1
; C'de operatör önceliği sayesindex&1==1
değerlendirilir , bu yüzden aslında testle aynıdır ). Sanırım cevabı yanlış okuyorsunuz; orada noktalı virgül var, virgül yok. Hem tek / çift test söylüyor, hatta / tek test için bir arama tablosu ihtiyacım olacağını söylemiyor ve bir arama tablosu vardır kontrol işlemleri. x&(1==1)
x&1
O(1)
Küçük hatırlatma: big O
gösterim asimtotik karmaşıklığı belirtmek için kullanılır (yani, sorunun boyutu sonsuza kadar büyüdüğünde) ve bir sabiti gizler.
Bir O (n) 'de algoritma ve O'da bir (n arasındaki bu araçlar , 2 ), hızlı ilk bir zaman aralığında bir değere vardır da (her zaman değil, n, ki burada n, birinci algoritma boyutta sorunlar> için en hızlı).
Gizli sabitin büyük ölçüde uygulamaya bağlı olduğuna dikkat edin!
Ayrıca, bazı durumlarda, çalışma zamanı girdinin n boyutunun belirleyici bir işlevi değildir . Örneğin hızlı sıralama kullanarak sıralama yapın: n öğenin bir dizisini sıralamak için gereken süre sabit değildir ancak dizinin başlangıç yapılandırmasına bağlıdır.
Farklı zaman karmaşıklıkları vardır:
Ortalama vaka (genellikle anlaşılması çok daha zor ...)
...
İyi bir giriş R. Sedgewick ve P. Flajolet tarafından Algoritma Analizine Giriş .
Dediğiniz gibi premature optimisation is the root of all evil
, ve (mümkünse) profilleme , kodu optimize ederken her zaman kullanılmalıdır. Hatta algoritmalarınızın karmaşıklığını belirlemenize yardımcı olabilir.
Buradaki cevapları gördüğümüzde, çoğumuzun gerçekten algoritmanın sırasına bakarak yaklaştığını ve örneğin üniversitede düşündüğümüz gibi ana yöntemle hesaplamak yerine sağduyuyu kullandığımız sonucuna varabiliriz . Bununla birlikte, profesörün bile bizi (daha sonra) sadece hesaplamak yerine düşünmeye teşvik ettiğini eklemeliyim .
Ayrıca özyinelemeli işlevler için nasıl yapıldığını eklemek istiyorum :
varsayalım ( şema kodu ) gibi bir fonksiyonumuz var :
(define (fac n)
(if (= n 0)
1
(* n (fac (- n 1)))))
verilen sayının faktöriyelini tekrarlayan hesaplar.
İlk adım, fonksiyonun gövdesi için performans özelliğini sadece bu durumda belirlemeye çalışmaktır , vücutta özel bir şey yapılmaz, sadece bir çarpma (veya 1 değerinin geri dönüşü).
Yani vücut için performans: O (1) (sabit).
Ardından , yinelemeli arama sayısı için bunu deneyin ve belirleyin . Bu durumda n-1 yinelemeli çağrılarımız var.
Yani özyinelemeli çağrıların performansı şöyledir: O (n-1) (önemsiz kısımları attığımız için sipariş n'dir).
Sonra bu ikisini bir araya getirin ve ardından tüm özyinelemeli fonksiyonun performansına sahipsiniz:
1 * (n-1) = O (n)
Peter , dile getirdiğiniz sorunları cevaplamak için; Burada tarif ettiğim yöntem aslında bunu oldukça iyi idare ediyor. Ancak bunun hala bir tahmin olduğunu ve tam olarak matematiksel olarak doğru bir cevap olmadığını unutmayın. Burada açıklanan yöntem aynı zamanda üniversitede öğretilen yöntemlerden biridir ve doğru hatırlamıyorsam bu örnekte kullandığım faktöriyelden çok daha gelişmiş algoritmalar için kullanılmıştır.
Tabii ki her şey, işlevin gövdesinin çalışma süresini ve özyinelemeli çağrıların sayısını ne kadar iyi tahmin edebileceğinize bağlıdır, ancak bu diğer yöntemler için de geçerlidir.
Maliyetiniz bir polinomsa, çarpanı olmadan en yüksek vadeli terimi saklayın. Örneğin:
O ((N / 2 + 1) * (n / 2)) = O (n 2 /4 + n / 2) = O (n 2 /4) = O (n 2 )
Bu sonsuz dizi için işe yaramıyor. Genel vaka için tek bir reçete yoktur, ancak bazı yaygın vakalar için aşağıdaki eşitsizlikler geçerlidir:
O (log N ) <O ( N ) <O ( N log N ) <O ( N 2 ) <O ( N k ) <O (e n ) <O ( n !)
Bunu bilgi açısından düşünüyorum. Herhangi bir problem belirli sayıda bit öğrenmekten ibarettir.
Temel aracınız karar noktaları ve entropi kavramlarıdır. Karar noktasının entropisi size vereceği ortalama bilgidir. Bir program iki kolu, bir karar noktası içeriyorsa, örneğin, 's entropi her dal kez günlük olasılık toplamı 2 bu dalın ters olasılık. Bu kararı uygulayarak ne kadar öğrenirsiniz.
Örneğin if
, her ikisinin de eşit olması muhtemel iki dalı olan bir deyim 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 entropisine sahiptir. = 1. Yani entropisi 1 bit.
N = 1024 gibi N öğeler içeren bir tablo aradığınızı varsayalım. Log (1024) = 10 bit olduğu için bu 10 bitlik bir sorundur. Dolayısıyla, eşit derecede olası sonuçları olan IF ifadeleriyle arama yapabiliyorsanız, 10 karar almalıdır.
İkili arama ile elde ettiğiniz budur.
Doğrusal arama yaptığınızı varsayalım. İlk elemana bakarsınız ve istediğiniz eleman olup olmadığını sorarsınız. Olasılıklar 1/1024, değil 1023/1024. Bu kararın entropisi 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * yaklaşık 0 = yaklaşık .01 bit'tir. Çok az şey öğrendiniz! İkinci karar daha iyi değil. Bu nedenle doğrusal arama çok yavaştır. Aslında öğrenmeniz gereken bit sayısında üsteldir.
Dizinleme yaptığınızı varsayalım. Tablonun çok sayıda bölmeye önceden sıralanmış olduğunu ve doğrudan tablo girişine dizin oluşturmak için anahtardaki tüm bitlerin bazılarını kullandığınızı varsayalım. 1024 kutu varsa, tüm 1024 olası sonuç için entropi 1/1024 * log (1024) + 1/1024 * log (1024) + ... 'dir. Bu 1/1024 * 1024 sonucun 10 katı veya bir indeksleme işlemi için 10 bit entropidir. Bu nedenle aramayı dizine ekleme hızlıdır.
Şimdi sıralamayı düşünün. N öğeniz var ve bir listeniz var. Her öğe için, öğenin listede nereye gittiğini aramanız ve ardından listeye eklemeniz gerekir. Bu nedenle sıralama, temel alınan aramanın adım sayısının kabaca N katını alır.
Dolayısıyla, kabaca eşit olasılıkla sonuçları olan ikili kararlara dayanan türlerin tümü O (N log N) adımlarını alır. Bir O (N) sıralama algoritması, indeksleme aramasına dayanıyorsa mümkündür.
Neredeyse tüm algoritmik performans sorunlarına bu şekilde bakılabileceğini buldum.
Hadi baştan başlayalım.
Her şeyden önce, veriler üzerinde belirli basit işlemlerin O(1)
zaman içinde, yani girdinin boyutundan bağımsız olarak zaman içinde yapılabileceği ilkesini kabul edin . C'deki bu ilkel işlemler aşağıdakilerden oluşur:
Bu prensip için gerekçe, tipik bir bilgisayarın makine talimatlarının (ilkel adımlar) ayrıntılı bir şekilde incelenmesini gerektirir. Açıklanan işlemlerin her biri az sayıda makine talimatı ile yapılabilir; genellikle sadece bir veya iki talimat gereklidir. Sonuç olarak, C'deki birkaç tür ifade O(1)
zaman içinde, yani girdiden bağımsız olarak belirli bir süre içinde yürütülebilir . Bunlar basit içerir
C'de, bir dizi değişkeni bir değere başlatarak ve bu değişkeni döngü etrafında her seferinde 1 oranında artırarak birçok for-loop oluşur. For-döngüsü, dizin bir sınıra ulaştığında sona erer. Örneğin, for-loop
for (i = 0; i < n-1; i++)
{
small = i;
for (j = i+1; j < n; j++)
if (A[j] < A[small])
small = j;
temp = A[small];
A[small] = A[i];
A[i] = temp;
}
dizin değişkeni i'yi kullanır. Döngünün etrafında her seferinde i artar ve n - 1'e ulaştığında yineleme durur.
Bununla birlikte, şimdilik, son ve başlangıç değerleri arasındaki farkın, dizin değişkeninin artırıldığı miktara bölünmesiyle elde edilen basit for-loop formuna odaklanın, döngüde kaç kez gittiğimizi gösterir . Bu sayı, döngüden atlama ifadesi aracılığıyla çıkmanın yolları olmadığı sürece kesindir; her durumda yineleme sayısında bir üst sınırdır.
Örneğin, for döngüsü yinelenir ((n − 1) − 0)/1 = n − 1 times
, çünkü 0 i'nin başlangıç değeri olduğundan, n - 1 i tarafından ulaşılan en yüksek değerdir (yani, n − 1'e ulaştığında döngü durur ve i = n− ile yineleme olmaz 1) ve 1, i'nin her yinelemesinde i'ye eklenir.
Döngü gövdesinde harcanan sürenin her yineleme için aynı olduğu en basit durumda, vücut için büyük oh oh üst sınırını döngü etrafındaki sayılarla çarpabiliriz . Kesin olarak, döngü indeksini başlatmak için O (1) zamanı ve döngü indeksinin limitle ilk karşılaştırması için O (1) süresini eklemeliyiz , çünkü döngüden daha fazla zaman test ediyoruz. Ancak, döngüyü sıfır kez yürütmek mümkün olmadığı sürece, döngüyü başlatma ve sınırı bir kez test etme süresi, toplama kuralı tarafından bırakılabilen düşük dereceli bir terimdir.
Şimdi bu örneği düşünün:
(1) for (j = 0; j < n; j++)
(2) A[i][j] = 0;
Bunu biliyoruz hattı (1) sürer O(1)
zaman. Açıkça, alt sınırı sınır (1) satırında bulunan üst sınırdan çıkararak ve sonra 1 ekleyerek belirleyebileceğimiz gibi döngü etrafında dolaşıyoruz. Beden, satır (2) O (1) zamanını alır, j'yi arttırma süresini ve j'yi n ile karşılaştırma süresini ihmal edebiliriz, her ikisi de O (1). Böylece, (1) ve (2) hatlarının çalışma süresi n ve O (1) 'in çarpımıdır O(n)
.
Benzer şekilde, (2) ila (4) çizgilerinden oluşan dış halkanın çalışma süresini de bağlayabiliriz.
(2) for (i = 0; i < n; i++)
(3) for (j = 0; j < n; j++)
(4) A[i][j] = 0;
Zaten hatlar (3) ve (4) 'ün O (n) zaman aldığını tespit ettik. Böylece, dış döngünün her yinelemesinin O (n) zaman aldığı sonucuna vararak, her bir yinelemede i'yi artırmak ve i <n'yi test etmek için O (1) süresini ihmal edebiliriz.
Dış ilmeğin i = 0'ı ve i <n koşulunun (n + 1) st testi de aynı şekilde O (1) zaman alır ve ihmal edilebilir. Son olarak, her döngü için O (n) zamanını alarak toplam O(n^2)
çalışma süresi vererek dış döngüde n kez dolaştığımızı gözlemliyoruz
.
Daha pratik bir örnek.
Kodu analiz etmek yerine kodunuzun sırasını tahmin etmek istiyorsanız, n ve kodunuzun zaman değerini artıran bir dizi yapıştırabilirsiniz. Zamanlamalarınızı bir günlük ölçeğinde çizin. Kod O (x ^ n) ise, değerler n eğim çizgisine düşmelidir.
Bunun sadece kodu incelemeye göre birçok avantajı vardır. Birincisi, çalışma süresinin asimptotik sırasına yaklaştığı aralıkta olup olmadığınızı görebilirsiniz. Ayrıca, O (x) sırası olduğunu düşündüğünüz bazı kodların, örneğin kütüphane çağrılarında harcanan zaman nedeniyle, gerçekten O (x ^ 2) sırası olduğunu görebilirsiniz.
Temelde zamanın% 90'ını oluşturan şey sadece döngüleri analiz etmektir. Tek, çift, üçlü iç içe döngüleriniz var mı? O (n), O (n ^ 2), O (n ^ 3) çalışma süreniz var.
Çok nadiren (geniş bir taban kütüphanesine sahip bir platform yazmıyorsanız (örneğin, .NET BCL veya C ++ 'ın STL'si gibi), döngülerinize bakmaktan daha zor olan herhangi bir şeyle karşılaşırsınız (ifadeler için, git, vb...)
Büyük O gösterimi yararlıdır, çünkü gereksiz komplikasyonlarla ve detaylarla çalışmak ve gizlemek kolaydır (gereksiz bazı tanımlar için). Böl ve fethet algoritmalarının karmaşıklığını çözmenin güzel bir yolu ağaç yöntemidir. Diyelim ki medyan prosedürü olan bir quicksort sürümünüz var, bu yüzden diziyi her seferinde mükemmel dengelenmiş alt dizilere ayırıyorsunuz.
Şimdi birlikte çalıştığınız tüm dizilere karşılık gelen bir ağaç oluşturun. Kökte orijinal diziye sahipsiniz, kökte alt diziler olan iki çocuk var. Altta tek eleman dizileri olana kadar bunu tekrarlayın.
Medyanı O (n) zamanında bulabildiğimiz ve diziyi O (n) zamanında iki parçaya böldüğümüz için, her düğümde yapılan iş O (k) 'dir, burada k dizinin büyüklüğüdür. Ağacın her seviyesi (en fazla) tüm diziyi içerir, böylece seviye başına çalışma O (n) olur (alt dizilerin boyutları n'ye kadar eklenir ve seviye başına O (k) olduğundan bunu ekleyebiliriz) . Girdiyi her yarıya indirdiğimizden beri ağaçta yalnızca log (n) seviyeleri vardır.
Bu nedenle iş miktarını O (n * log (n)) ile sınırlandırabiliriz.
Ancak, Big O bazen görmezden gelemeyeceğimiz bazı detayları gizler. Fibonacci dizisini şu şekilde hesaplamayı düşünün:
a=0;
b=1;
for (i = 0; i <n; i++) {
tmp = b;
b = a + b;
a = tmp;
}
ve a ve b'nin Java'daki BigInteger'lar veya keyfi olarak büyük sayıları işleyebilecek bir şey olduğunu varsayalım. Çoğu insan bunun titremeden bir O (n) algoritması olduğunu söylerdi. Bunun nedeni for döngüsünde n yinelemeye sahip olmanız ve O (1) döngüsünün yan tarafında çalışmanızdır.
Ancak Fibonacci sayıları büyüktür, n. Büyük tamsayılarla ekleme yapmak O (n) miktarını alacaktır. Yani bu prosedürde yapılan toplam iş miktarı
1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)
Bu algoritma kuadradik zamanda çalışır!
Genel olarak daha az kullanışlı olduğunu düşünüyorum, ancak bütünlük uğruna , bir algoritmanın karmaşıklığında bir alt sınır tanımlayan bir Büyük Omega Ω ve hem bir üst hem de alt sınır tanımlayan bir Büyük Teta Θ vardır.
Algoritmayı büyük O gösterimini bildiğiniz parçalara ayırın ve büyük O operatörleri ile birleştirin. Bilmemin tek yolu bu.
Daha fazla bilgi için konuyla ilgili Wikipedia sayfasını kontrol edin .
Kullandığım algoritmalara / veri yapılarına aşinalık ve / veya yineleme yuvalamasının hızlı bakış analizi. Zorluk, bir kütüphane işlevini, muhtemelen birden çok kez çağırdığınızda - işlevi gereksiz yere zaman zaman aradığınızdan veya hangi uygulamayı kullandıklarından emin olamayabilirsiniz. Belki kütüphane işlevleri, ister büyük O ister başka bir metrik olsun, belgelerde ve hatta IntelliSense'te bulunan bir karmaşıklık / verimlilik ölçüsüne sahip olmalıdır .
"Büyük O'yu nasıl hesaplıyorsunuz" ile ilgili olarak, bu Hesaplamalı karmaşıklık teorisinin bir parçasıdır . Bazı (çok) özel durumlar için bazı basit sezgisel taramalarla gelebilirsiniz (iç içe döngüler için döngü sayılarını çarpmak gibi), esp. tek istediğin herhangi bir üst sınır tahmini olduğunda ve çok kötümser olup olmadığını umursamıyorsun - sanırım muhtemelen sorunun ne olduğu budur.
Sorunuza herhangi bir algoritma için gerçekten cevap vermek istiyorsanız, yapabileceğiniz en iyi şey teoriyi uygulamaktır. Basit "en kötü durum" analizinin yanı sıra amortisman analizini uygulamada çok yararlı buldum .
1. durum için, iç döngü yürütülür n-i
süreler, bu nedenle toplam yürütme sayısı, i
' 0
dan n-1
(daha düşük, daha düşük veya eşit değil) gidiş toplamıdır n-i
. Sonunda n*(n + 1) / 2
anladın O(n²/2) = O(n²)
.
2. döngü için dış döngü i
arasında 0
ve n
dahil edilir; o zaman iç döngü j
kesinlikle daha büyük olduğunda yürütülür n
, bu da imkansızdır.
Ana yöntemi (veya uzmanlıklarından birini) kullanmaya ek olarak, algoritmalarımı deneysel olarak test ediyorum. Bu kanıtlayamaz herhangi bir karmaşıklık sınıfının elde edildiğini , ancak matematiksel analizin uygun olduğuna dair güvence sağlayabilir. Bu güvenceye yardımcı olmak için, tüm vakaları uyguladığımdan emin olmak için kodlarım deneylerimle birlikte kullanıyorum.
Çok basit bir örnek olarak, .NET framework'ün liste sıralamasının hızında bir akıl sağlığı kontrolü yapmak istediğinizi söyleyin. Aşağıdaki gibi bir şey yazabilir, ardından n * log (n) eğrisini aşmadığından emin olmak için sonuçları Excel'de analiz edebilirsiniz.
Bu örnekte karşılaştırma sayısını ölçüyorum, ancak her örnek boyutu için gereken gerçek zamanı incelemek de ihtiyatlı. Ancak, o zaman sadece algoritmayı ölçtüğünüze ve test altyapınızdaki yapay nesneleri içermediğinden daha dikkatli olmalısınız.
int nCmp = 0;
System.Random rnd = new System.Random();
// measure the time required to sort a list of n integers
void DoTest(int n)
{
List<int> lst = new List<int>(n);
for( int i=0; i<n; i++ )
lst[i] = rnd.Next(0,1000);
// as we sort, keep track of the number of comparisons performed!
nCmp = 0;
lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }
System.Console.Writeline( "{0},{1}", n, nCmp );
}
// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
DoTest(n);
Bellek kaynaklarının sınırlı olması durumunda endişe kaynağı olabilecek alan karmaşıklıklarına da izin vermeyi unutmayın. Örneğin, sabit bir uzay algoritması isteyen birinin, temel olarak algoritma tarafından alınan alan miktarının kod içindeki herhangi bir faktöre bağlı olmadığını söylemenin bir yolu olduğunu duyabilirsiniz.
Bazen karmaşıklık, bir şeyin kaç kez çağrıldığından, bir döngünün ne sıklıkta yürütüldüğünden, belleğin ne sıklıkta tahsis edildiğinden ve bu sorunun yanıtlanmasının başka bir bölümüdür.
Son olarak, büyük O, bir algoritmanın ne kadar kötü olabileceğini tanımlamak için kullanılan en kötü durum olan en kötü durum, en iyi durum ve amortisman durumları için kullanılabilir.
Sıklıkla gözden kaçan şey algoritmalarınızın beklenen davranışıdır. Algoritmanızın Big-O'sunu değiştirmez , ancak "erken optimizasyon. .. .." ifadesiyle ilgilidir.
Algoritmanızın beklenen davranışı - çok aptalca - algoritmanızın en çok göreceğiniz veriler üzerinde çalışmasını ne kadar hızlı bekleyebilirsiniz.
Örneğin, bir listede bir değer arıyorsanız, o (n) olur, ancak gördüğünüz listelerin çoğunun değerinizin ön planda olduğunu biliyorsanız, algoritmanızın tipik davranışı daha hızlıdır.
Gerçekten çivi çakmak için, "girdi alanınızın" olasılık dağılımını tanımlayabilmeniz gerekir (bir listeyi sıralamanız gerekiyorsa, bu liste ne sıklıkta sıralanacaktır? Ne sıklıkla tamamen tersine çevrilir? çoğu zaman sıralanır mı?) Bunu bilmek her zaman mümkün değildir, ancak bazen biliyorsunuzdur.
harika bir soru!
Feragatname: Bu cevap yanlış beyanlar içermektedir aşağıdaki açıklamalara bakınız.
Big O kullanıyorsanız, daha kötü durumdan bahsediyorsunuz (bunun daha sonra ne anlama geldiği hakkında daha fazla bilgi). Ayrıca, ortalama bir durum için sermaye teta ve en iyi durum için büyük bir omega vardır.
Big O'nun güzel bir resmi tanımı için bu siteye göz atın: https://xlinux.nist.gov/dads/HTML/bigOnotation.html
f (n) = O (g (n)), tüm n ≥ k için 0 ≤ f (n) ≤ cg (n) olacak şekilde c ve k pozitif sabitleri olduğu anlamına gelir. C ve k değerleri f fonksiyonu için sabitlenmeli ve n'ye bağlı olmamalıdır.
Peki, şimdi "en iyi durum" ve "en kötü durum" karmaşıklıklarıyla ne demek istiyoruz?
Bu muhtemelen en açık şekilde örneklerle gösterilmiştir. Örneğin, sıralı bir dizideki bir sayıyı bulmak için doğrusal arama kullanıyorsanız, en kötü durum , dizideki son öğe için arama yapmaya karar vereceğimizdir, çünkü bu, dizideki öğeler kadar çok adım atacaktır. En iyi durumda biz aradığınızda olacağını ilk elemanın biz ilk kontrolünden sonra yapılabilir olacağından.
Tüm bu sıfat- kasa karmaşıklıklarının amacı, bir varsayımsal programın tamamlanması için gereken süreyi belirli değişkenlerin büyüklüğü açısından grafik olarak göstermenin bir yolunu aramamızdır. Bununla birlikte, birçok algoritma için, belirli bir girdi boyutu için tek bir zaman olmadığını iddia edebilirsiniz. Bunun bir işlevin temel gereksinimiyle çeliştiğine dikkat edin, herhangi bir girdinin birden fazla çıktısı olmamalıdır. Bu nedenle , bir algoritmanın karmaşıklığını tanımlamak için birden fazla işlev buluyoruz. Şimdi, n büyüklüğünde bir dizi aramak, dizide aradığınız şeye bağlı olarak ve n ile orantılı olarak bağlı olarak değişen miktarlarda zaman alabilse de, en iyi durum, ortalama durum kullanarak algoritmanın bilgilendirici bir tanımını oluşturabiliriz ve en kötü durum sınıfları.
Üzgünüz, bu çok kötü yazılmış ve çok fazla teknik bilgiye sahip değil. Ama umarım zaman karmaşıklığı sınıflarının düşünülmesini kolaylaştıracaktır. Bunlarla rahat olduğunuzda, programınızla ayrıştırma ve dizi boyutlarına ve veri yapılarınıza dayalı akıl yürütme gibi şeylerin önemsiz durumlarda ne tür girdilerle sonuçlanacağı ve hangi girdinin sonuçlanacağı basit bir konu haline gelir. en kötü durumlarda.
Bunu programlı olarak nasıl çözeceğimi bilmiyorum, ama insanların yaptığı ilk şey, yapılan işlem sayısında belirli kalıplar için algoritmayı örneklememizdir, diyelim ki 4n ^ 2 + 2n + 1 2 kuralımız var:
F (x) 'i basitleştirirsek, burada f (x) yapılan işlem sayısının formülüdür (yukarıda açıklanan 4n ^ 2 + 2n + 1), buradaki büyük O değerini [O (n ^ 2) elde ederiz durum]. Ancak bunun, programda uygulanması zor olabilecek Lagrange enterpolasyonunu hesaba katması gerekir. Ve eğer gerçek big-O değeri O (2 ^ n) ise ve O (x ^ n) gibi bir şeye sahip olabilirsek, bu algoritma muhtemelen programlanamaz. Ama birisi beni yanlış kanıtlarsa, bana kodu ver. . . .
A kodu için, dış döngü n+1
kez çalıştırılacaktır , '1' süresi, i'nin hala gereksinimi karşılayıp karşılamadığını kontrol eden işlem anlamına gelir. Ve iç döngü n
çarpı, n-2
çarpı ... Böylece,0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)
.
B kodu için, iç döngü içeri girmez ve foo () işlemini yürütmezse de, iç döngü n kez yürütülür, dış döngü yürütme süresine bağlıdır, bu O (n)
Big-O'yu biraz farklı bir şekilde açıklamak istiyorum.
Big-O, programların karmaşıklığını karşılaştırmaktır, yani girdiler arttıkça ne kadar hızlı büyüyorlar, eylemi yapmak için harcanan tam zamanı değil.
Büyük O formüllerinde IMHO daha karmaşık denklemler kullanmamanız daha iyi olur (sadece aşağıdaki grafikteki formüllere bağlı kalabilirsiniz.) Ancak yine de daha kesin formül (3 ^ n, n ^ 3, vb.) Kullanabilirsiniz. .) ancak bundan daha fazlası bazen yanıltıcı olabilir! Mümkün olduğunca basit tutmak daha iyi.
Burada bir kez daha vurgulamak isterim ki burada algoritmamız için kesin bir formül elde etmek istemiyoruz. Sadece girdiler büyüdüğünde nasıl büyüdüğünü göstermek ve bu anlamda diğer algoritmalarla karşılaştırmak istiyoruz. Aksi takdirde, tezgah işaretleme gibi farklı yöntemler kullanmanız daha iyi olur.