Permalänk
Medlem

Big O notation

Hej,

har vid 3-4 tillfällen stött på big-O notation i litteratur och ej lyckats förstå mig på det hitentills. Detta är frustruerande eftersom det hade varit väldigt trevligt att själv kunna uppskatta hur snabb kod man skrivit.
Jag "vet" typ att nested for-loop, 2st, där man i inre loopen accessar en array har big-O av O(n^2). Och det är ju jäkligt informativt *not*
Och big-O är worst case om jag förstått det rätt, dvs. om man söker igenom en array med 50 element men hittar det man söker efter 25, är det ändå O(50) eftersom elementet kunde legat där (n=50).

Först undrar jag vad "n" egentligen är? i boken jag läste senaste är det antal gånger man går in i en array och hämtar, och varför just man syftar till detta förstår jag ej.

Jag tror jag håller där och fortsätter fråga utifrån det jag förhoppningsvis får veta

Tack

Permalänk

Ordo som det kallas är ett sätt att mäta komplexiteten hos en algoritm. Så om du har två algoritmer en med O(n) och en med O(n^2) så är den första mindre komplex och alltså oftast snabbare. n är storleken på problemet, om du t.ex ska sortera en lista är det antalet element i listan.

Visa signatur

Asrock Z68 Extreme 7 gen 3 | EVGA GTX580 SLI | Intel i7 2600k | Corsair Vengeance 16GB
Corsair H80 | Corsair Force 3 120gb | HX1050 |NZXT Phantom | Samsung 22" SA300

Sony VAIO pro 13
i7 8GB 256GB

Permalänk

Jag förstår att det inte är helt lätt att förstå utan att detta är något som kräver att man tänker några gånger. Ett sätt som fungerade bra för mig att lära mig O-notation är att analysera sorteringsalgoritmer.

För att ta ett enkelt exempel, säg att du har en lista med 4 element: [8, 2, 5, 4] som du vill sortera. Det finns en enkel algoritm som heter Bubble Sort. Den går igenom hela listan och jämför två element i taget som är jämte varandra. Är det vänstra elementet större än det högra så byter vi plats på dom. När vi når slutet på listan börjar vi om så vida vi inte har bytt plats på något element, har vi inte det är vi klara.

Algoritmen fungerar då så här:

Iteration 1: => [8, 2, 5, 4] Kontrollera element på plats 0 och 1 i listan, d.v.s. jämför 8 och 2. Vi ser att 8 > 2, därför byter vi plats på dom. => [2, 8, 5, 4] Kontrollera element på plats 1 och 2 i listan, d.v.s. 8 > 5. Byt plats. => [2, 5, 8, 4] Kontrollera element på plats 2 och 3 i listan. 8 > 4. Byt plats. => [2, 5, 4, 8] Iteration 1 klar. Bytte vi plats på något under iteration, ja flera gånger. Kör en iteration till: Iteration 2: [2, 5, 4, 8] => kolla 2 > 5. Byt inte plats. [2, 5, 4, 8] => kollar 5 > 4. Byt plats! [2, 4, 5, 8] => kolla 5 > 8. Byt inte plats. Iteration 2 klar. Bytte vi plats på något. Ja - en iteration till. Iteration 3: [2, 4, 5, 8] => 2 > 4? Byt inte plats. [2, 4, 5, 8] => 4 > 5? Byt inte plats. [2, 4, 5, 8] => 5 > 8? Byt inte plats. Iteration 3 klar. Bytte vi plats på något? Nej. Klar!

Så, vad säger oss nu detta? För att kunna prata om algoritmens effektivitet måste vi analysera hur den beter sig i generella fall. Vi kan göra detta på flera sätt genom att t.ex. betrakta hur algoritmen beter sig för olika input. Är listan nästan sorterad behöver vi bara ett litet antal iterationer. I andra fall behöver vi dock många fler.

Låt oss nu anta att vi har en lista med n element som är så dåligt sorterad den kan vara, d.v.s. alla element ligger på ett sådant sätt att just vår Bubble Sort-algoritm kommer flytta runt alla element ett maximalt antal gånger. Vi har byggt ett vorst case scenario helt enkelt.

Vi kan då härleda (går inte in på det här för att fatta mig kort) att vi måste köra maximalt n stycken iterationer - och vi vet ju att i varje iteration går vi igenom hela listan en gång och den innehåller n st element. Alltså kräver vår algoritm att vi går igenom vår lista med n st element, n st gånger => n*n = n^2.

Vi kan då säga att vår Bubble Sort sorterar vår lista inom O(n^2), där n syftar på antalet element i vår lista. I olika algoritmer kan n syfta på olika saker men ofta handlar det om inputens storlek.

När man pratar om big-O eller ordo förenklar vi ofta. Säg att vi gör ett extra steg som resulterar i att vi istället får ett uttryck O(2*n^2) så är vi inte intresserade av konstanten framför för i det stora hela är 2*n^2 ungefär n^2, speciellt när n är stort, därför slänger vi ofta bort alla konstanter inom O-uttrycket. Det är dock enormt stor skillnad på n^2 och n^3, speciellt när vår input n är stor. På så sätt blir vi jätteglada om vi kan lösa ett problem inom O(n^2) istället för O(n^3) men det spelar mindre roll om vi löser det inom O(100*n^2) eller O(n^2) - tänk själv skillnaden om n är en miljard.

I ditt exempel säger vi att din algoritm är linjär, d.v.s. O(50) = O(2 * 25) = O(25) = O(n). Ändrar vi storleken på n (t.ex. dubblar n i detta fallet) ökar tiden för vår algoritm linjärt, medan om vi skulle ha O(n^2) skulle tiden öka kvadratiskt (d.v.s. mycket mer!).

Nu har jag bara skrapat på ytan och försökt förklara enkelt men detta ämnet är otroligt intressant men ofta överväldigande att börja med.

Utöver O (Big-O eller Ordo) kan vi även mäta tidskomplexiteten på "tightare" sätt, vi kan också vilja ta hänsyn till annat än inputens storlek - kanske kräver vår algoritm väldigt mycket minne, men jag tror jag nöjer mig här så jag inte bara skriver en wall of text som förvirrar mer än den hjälper.

En bra bok för den intresserade är Introduction to Algorithms: http://en.wikipedia.org/wiki/Introduction_to_Algorithms
Tror även den finns som online-kurs någonstans. Det är dock som sagt ett tungt ämne som är svårt att greppa utan handledning.

Fråga gärna om du vill veta mer!

Visa signatur

"Knowledge amplification. What he learns, we all learn. What he knows, we all benefit from."

Permalänk
Hedersmedlem

Jag ska försöka mig på en lite kortare förklaring.
Ordo beskriver en komplexitet som sagt. Vad det innebär är att man vill beskriva antingen i tid eller minne hur mycket av det(tid eller minne) som kommer krävas av algoritmen om problemet växer. Så om man har en algoritm som har komplexitet O(n)(linjär komplexitet) så betyder det att tidåtgång kommer ökas lika mycket som indatat ökar i storlek. Är det så att man får indata av en storlek som kommer det ta en viss tid, om man sen skickar in dubbelt så mycket data så kommer det ta dubbelt så mycket tid. Är det O(n^2) så kommer det ta kvadratiskt så mycket tid om indatat dubblas eller generellt ökas.

Permalänk
Datavetare

Du frågar vad "n" är. Om du ser uttrycket

f(N) ∈ O(g(N))

så betyder det att antal steg, "compute units", du behöver för att beräkna f(N) med N element/objekt är proportionellt mot g(count(N)) för stora värden på N, där count(N) skulle ge antalet element.

T.ex.

merge_sort(N) ∈ O(log(N))

Däremot säger O(...) inget om absoluta hastigheten, två algoritmer som båda är t.ex. O(N^2) kan mycket väl prestera väldigt olika på varje givet värde på N

Edit: och för att lite mellan tummen och pekfingret göra en kvalificerad gissning om hur komplexiteten ser ut så har man grovt dessa klasser:

  • uppslagning i en array, t.ex. hash-tabell: O(1)

  • sökning i ett träd med potentiellt högre "branch-factor" än 2, t.ex. LC-trie: O(log(log(N))

  • delar en mängd i två delar och fortsätter sedan bara med ena delem, t.ex. binärsökning: O(log(N))

  • gå igenom alla element i en mängd, t.ex. hitta högsta/lägsta värde i en mängd: O(N)

  • kombinationen av att dela upp något i två delar och sedan göra något på alla element i varje del, divide-and-conquer, quicksort, mergesort, heapsort: O(N*log(N))

  • Ta element X, jämför det sedan med alla efterföljande element, gå till X+1, X+2 etc till man når slutet av mängden, t.ex. bubble-sort, insert-sort, select-sort: O(N^2)

  • Välj ut element X, kombinerar det med alla tänkbara permutationer av resten av mängden: skapa alla permutationer av en mängd: O(N!)

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Hedersmedlem

För en annan approach till begreppet så kan man notera att beteckningen kommer från matematiken, där den används för att samla termer av en viss storleksordning. Om man exempelvis har uttrycket
   f(x) = (x⁴ + 7 x³ − x + 5) (−3 x⁴ +  x³ + x − 13)
så kan man, om man bara är intresserad av linjärisering kring "små" x (typiskt mycket mindre än 1), förenkla utvecklingen genom att skriva
   … = −65 + 18 x + O(x²) när x → 0
där man klumpar ihop alla termer av grad ≥ 2 i "ordo"-notationen. För väldigt små x så kommer högre potenser ändå alltid domineras av konstanten och förstagradstermen, så i stället för att "bry sig om" dessa så lägger man dessa i en "slaskterm" som man inte bryr sig om nämnvärt.

O(x²) betyder alltså att resten av uttrycket beter sig ungefär som x² här. Om det hade handlat om stora x (definitivt om x ≥ 1) så hade det varit nödvändigt att i stället ha kvar dessa högre termer. Om vi vet att x är riktigt stort så kanske det då snarare är intressant att veta vilken den största potensen är, så att man kan skriva att f(x) = O(x⁸) ovan (i praktiken skippar man ofta O()-notationen när man använder tecknet på detta generella sätt och skriver i stället bara f(x) ∝ x⁸, där "∝" betyder "proportionell mot").

För att dra ett exempel från verkliga livet så fick man lära sig på körkortsutbildningen att bromssträckan ökar med kvadraten på hastigheten v. Man vet alltså inte exakt hur förhållandet ser ut, men man kan säga att bromssträckan har utvecklingen O(v²), vilket betyder att om du kör i 100 km/h så får du (enligt modellen!) ~fyra gånger längre bromssträcka än om du kör i 50 km/h. Ett annat exempel är att volymen V av en uppblåst badboll ökar med kuben på radien r, så V = O(r³) — dubblar du radien så åttafaldigas volymen.

Det är grunden till användandet även i datavetenskapliga sammanhang: ordningen ger en "ungefärlig utveckling" av kostnaden för en viss algoritm. Den måste inte ge ett exakt svar för ett givet x (eller n som det ofta betecknas), men den säger något om hur vi kan förvänta oss att beräkningskostnaden växer med denna variabel.

Visa signatur

Nu med kortare användarnamn, men fortfarande bedövande långa inlägg.

Permalänk
Medlem

Tack för era svar!
Har last igenom dem och ja det känns som att jag har en hum om hur det fungerar ihop med tidigare kunskap.
Jag går vidare med ett exempel; målet är att genom analys av kod kunna bestämma precis hur den ligger i big-O-notation / bestämma hur tidseffektiv den är.

Har läst en del högskolematematik som analys(envariabel) och linjär algebra -->har en hum om de flesta begrepp som nämnts.

” We don’t actually know how many machine operations are generated for each Java instruction, or which of those instructions are more time-consuming than others, but we can make a simplification. We will simply count how often an array element is visited. Each visit requires about the same amount of work by other operations, such as incrementing subscripts and comparing values.”

Tolkar detta som att 'n' helt enkelt håller reda på var gång ett arrayelement accessas.

Kod från bok. Mina anteckningar är skrivna I caps (skrev notepad, visste ej kunde fetstilta här:) ), baserat på tokningen raden här ovan. Det är högst sannolikt felaktiga påståenden, jag uppskattar påpekningar/tillrättningar. Jag tror detta är lättaste sättet att förmedla min förrvirrning på.

Insertion sort:
public static void sort(int[] a)
{
for (int i = 1; i < a.length; i++)
{
int next = a[i]; <-- 1) KOMMER BESÖKA ALLA ”ARRAY-SLOTS”
int j = i;

while (j > 0 && a[j - 1] > next) <-- 2) SVÅRT OCH SÄGA HUR MÅNGA GÅNGER MAN BESÖKER ELEMENTEN.
{
a[j] = a[j - 1]; <-- 3) TVÅ BESÖK PER WHILE- LOOP.
j--;
}

//insert element
a[j] = next; <-- 4) BESÖKER ALLA ARRAYSLOTS
}
}

1) Och 4) kommer att utföras vart yttervarv, här har jag 2*n arraybesök totalt.
2) Och 3) är sammanhängande;
För (2) separat är det som sagt svårt att säga, antar jag worst case kommer det bli 1!+2!+3!+...+n! Besök totalt.
För (3) blir det två besök för vart fall i (2), så totalt med 2) och 3) blir detta
(1+2)! + (2+2)! + (3+2)! + ... . (n+2)!
Ponera att jag är rätt på det; vet ej hur gå vidare.

Tack.

Permalänk
Medlem
Visa signatur

| Fractal Design XL R2| 2x Gigabyte 680 Gtx@1254/7300mhz | Asrock Z77 OC Formula | 3570k@4.5ghz(1.36v) & Phanteks PH-TC14PE | 16gig hyperx beast series@2133mhz | Fractal Design Newton R2, 1000W 80+ | Samsung SSD Basic 840-Series 512GB | 2TB Toshiba 7200rpm SATA6 | 9x Scythe Glide Stream 2000rpm | 2x Bitfenix Recon Fan Controller | BenQ 27'' XL2720T 120Hz + Dell UltraSharp 27" U2713HM IPS 2560x1440 | Sennheiser HD595

Permalänk
Datavetare
Skrivet av Haldur:

Tack för era svar!
Har last igenom dem och ja det känns som att jag har en hum om hur det fungerar ihop med tidigare kunskap.
Jag går vidare med ett exempel; målet är att genom analys av kod kunna bestämma precis hur den ligger i big-O-notation / bestämma hur tidseffektiv den är.

Har läst en del högskolematematik som analys(envariabel) och linjär algebra -->har en hum om de flesta begrepp som nämnts.

” We don’t actually know how many machine operations are generated for each Java instruction, or which of those instructions are more time-consuming than others, but we can make a simplification. We will simply count how often an array element is visited. Each visit requires about the same amount of work by other operations, such as incrementing subscripts and comparing values.”

Tolkar detta som att 'n' helt enkelt håller reda på var gång ett arrayelement accessas.

Kod från bok. Mina anteckningar är skrivna I caps (skrev notepad, visste ej kunde fetstilta här:) ), baserat på tokningen raden här ovan. Det är högst sannolikt felaktiga påståenden, jag uppskattar påpekningar/tillrättningar. Jag tror detta är lättaste sättet att förmedla min förrvirrning på.

Insertion sort:
public static void sort(int[] a)
{
for (int i = 1; i < a.length; i++)
{
int next = a[i]; <-- 1) KOMMER BESÖKA ALLA ”ARRAY-SLOTS”
int j = i;

while (j > 0 && a[j - 1] > next) <-- 2) SVÅRT OCH SÄGA HUR MÅNGA GÅNGER MAN BESÖKER ELEMENTEN.
{
a[j] = a[j - 1]; <-- 3) TVÅ BESÖK PER WHILE- LOOP.
j--;
}

//insert element
a[j] = next; <-- 4) BESÖKER ALLA ARRAYSLOTS
}
}

1) Och 4) kommer att utföras vart yttervarv, här har jag 2*n arraybesök totalt.
2) Och 3) är sammanhängande;
För (2) separat är det som sagt svårt att säga, antar jag worst case kommer det bli 1!+2!+3!+...+n! Besök totalt.
För (3) blir det två besök för vart fall i (2), så totalt med 2) och 3) blir detta
(1+2)! + (2+2)! + (3+2)! + ... . (n+2)!
Ponera att jag är rätt på det; vet ej hur gå vidare.

Tack.

Dold text

Tänk hela tiden: vad får jag i genomsnitt.

Du kommer alltid behöva gå igenom alla element i yttre loopen: så det är O(N)
Om indata är helt slumpmässig så kommer du i genomsnitt ta N/4 varv i while-loopen och det som utförs i denna loop är konstant-tid operationer (flytta ett element), d.v.s även detta är O(N)
Det sista du gör, tilldela "last" till a[j] är en konstattid operation, O(1).

Så du utför något som är O(N) + O(1) -> O(N) ett antal gånger som är O(N), så du har en algoritm som är O(N^2).

Worst-case (sorterat åt "fel" håll) kommer bara skilja sig i att du tar "while" loopen N/2 gånger i stället, men totalt sett blir det ändå O(N^2)
Best-case är att indata reda är sorterad, då avslutas alltid "while" loopen utan att gå in i sin kropp, så du får då O(N) * (O(1) + O(1)) -> O(N)

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

Lätt att blanda ihop 'n' betydelsen.

A är antalet operationer som behövs.

B är en variabel som A beror på.

Uttryck A (B) i tex en sorteringsalgoritm. Då är det intressant att veta hur antalet operationer påverkas av tex storleken på listan som den ska sortera. Därför får B vara storleken på listan.

Om A (B) är tex A = b^3 + b^2 + 100 och C = b^3 så kommer A ~= C för stora tal på B. Alltså är b^3 all information vi behöver veta för att se hur körtiden förändras för stora b. Därför är O (b^3) ett enkelt och bra mått för en förståelse av körtiden på vår algoritm. Skrivs O (n^3) med vår vanliga notation.

Visa signatur

Topkek

Permalänk
Medlem
Skrivet av Pye:

Om A (B) är tex A = b^3 + b^2 + 100 och C = b^3 så kommer A ~= C för stora tal på B. Alltså är b^3 all information vi behöver veta för att se hur körtiden förändras för stora b.

Detta är faktum är en väsentlig del i förståelsen av kursen/ämnesgrenen (inte just det exakta beviset utan den intuitiva tanken). Att se det i exemplet ovan upplever jag som mycket enkelt.

att se det i kod däremot finner jag helt omöjligt. Men flertalet exempel i stil med detta har nämnts i tråden. De är relaterade till koden. Men hur är de relaterade till kod? (exempel önskvärt).

Tack

Permalänk
Medlem

Man "ser" inte i koden. Man tänker på vad för jobb som behöver göras. Om man ska göra nått med alla element i en lista, då blir komplexiteten O(n) om n är antalet element. Det är inget man ser, utan mer bara nått man inser. Om man ska göra en binärsökning så inser man också att det krävs log(n) steg, så tidskomplexiteten är O(log(n)). Om man ska göra nån typ av kollisionsdetektering där man kollar alla objekt mot alla andra objekt för att se om de stöter i varandra så fattar man att tidskomplexiteten blir O(n^2). Ibland krävs det lite mer avancerade resonemang än så, men det handlar i alla fall mest om att tänka igenom vad som händer, inte att se på koden.