Inom matematik och datavetenskap är Big O-notationen ett standardverktyg för att beskriva och jämföra tillväxttakten hos olika funktioner. I praktiken används den framför allt för att jämföra effektiviteten hos olika algoritmer genom att ange hur mycket minne som krävs och hur lång tid (antal grundläggande operationer) som behövs för att slutföra beräkningen när indata växer.

Big O-notationen hjälper också till att klassificera problem efter svårighetsgrad, det vill säga i vilken komplexitetsklass ett problem ligger. Historiskt användes liknande notation redan av Paul Bachmann (1837–1920) i den andra upplagan av "Analytische Zahlentheorie" (1896), och Edmund Landau (1877–1938) gjorde notationerna välkända — därför kallas de ofta för Landau-symboler.

Namnet "Big O" kommer från engelska uttrycket "order of the function" och avser funktioners tillväxt i förhållande till varandra. I praktisk algoritmanalys används Big O för att ange en övre gräns på tillväxttakten — alltså ett säkert mått på hur mycket tid eller resurser en algoritm kan kräva i ett värsta fall. På så sätt kan algoritmer grupperas efter hur dåligt de kan prestera i största skala: en algoritm med O ( 1 ) {\displaystyle O(1)}{\displaystyle O(1)} växer mycket långsammare än en med O ( n ! ) {\displaystyle O(n!)} {\displaystyle O(n!)}, vilket gör att firstnämnda i praktiken nästan alltid kommer vara snabbare för stora n oavsett hårdvara.

Mer formellt, givet två positiva funktioner f ( x ) {\displaystyle f(x)}f(x) och g ( x ) {\displaystyle g(x)}{\displaystyle g(x)}, säger vi att f {\displaystyle f}f är i det stora O av g {\displaystyle g}g (skrivet f ∈ O ( g ) {\displaystyle f\in O(g)}{\displaystyle f\in O(g)}) om det finns positiva konstanter k och x0 sådana att för alla x ≥ x0 gäller f ( x ) ≤ k g ( x ) {\displaystyle f(x)\leq k\cdot g(x)}{\displaystyle f(x)\leq k\cdot g(x)} för en viss konstant k {\displaystyle k}k. I enklare ord: f växer inte snabbare än en konstant gånger g för alla tillräckligt stora argument.

Vanliga komplexitetsklasser (tidskomplexitet)

  • O(1) — konstant tid: ex. åtkomst till ett element i en array med index. Den exekveringstid som krävs ändras inte med n.
  • O(log n) — logaritmisk tid: ex. binärsökning i en sorterad lista.
  • O(n) — linjär tid: ex. enkel linjär sökning, eller en enkel loop som går igenom alla element.
  • O(n log n) — typiskt för effektiva sorteringsalgoritmer som mergesort och heapsort.
  • O(n^2) — kvadratisk tid: ex. två nästlade loopar över samma data (enkla sorteringsalgoritmer som bubble sort i sämsta fall).
  • O(n^k) — polynomisk tid (k>1): dyrare nästlade loopar eller polynomiella beräkningar.
  • O(2^n) — exponentiell tid: många bruteforce-recursive lösningar, t.ex. generering av alla delmängder.
  • O(n!) — faktoriell tid: t.ex. analys av alla permutationen av n element (exempelvis vissa varianter av lösningar till Travelling Salesman i brute-force).

Tids- vs. minneskomplexitet

Tidskomplexitet (time complexity) beskriver hur antalet operationer växer med indata (n). Minneskomplexitet (space complexity) beskriver hur mycket extra minne som behövs utöver indata. En algoritm kan vara snabb men kräva mycket minne eller använda lite minne men vara långsam — Big O kan användas för båda typerna.

Bästa, genomsnittliga och sämsta fall

Big O anger vanligtvis en övre gräns (sämsta fall). Det finns också beteckningar för andra fall:

  • Ω (Omega) beskriver en nedre gräns (bästa fall).
  • Θ (Theta) används när en funktion har både samma övre och nedre gräns asymptotiskt (precis tillväxtklass).
  • Genomsnittlig (average-case) komplexitet försöker beskriva förväntad kostnad under en given sannolikhetsfördelning av indata.

Regler för förenkling

  • Konstanter kan ignoreras: O(2n) = O(n).
  • Endast dominerande termer räknas: O(n + n^2) = O(n^2).
  • Ofta multipliceras komplexiteter vid nästlade strukturer: en loop inom en loop ger O(n)·O(n) = O(n^2).
  • Sammanfogning (addition) används för sekventiella steg och tar den större termen: O(f) + O(g) = O(max(f,g)).

Hur man analyserar en algoritm — enkla steg

  1. Identifiera den grundläggande operationen som påverkar prestanda mest (t.ex. jämförelser eller byten).
  2. Räkna hur många gånger den operationen utförs som funktion av indatastorleken n.
  3. Förenkla uttrycket genom att ta bort konstanta faktorer och lägre ordningens termer.
  4. Ange den resulterande klassen med Big O.

Exempel: En enkel for-loop som körs n gånger gör en konstant mängd arbete per iteration → O(n). Två nästlade for-loopar, var och en över n, ger O(n^2).

Praktiska aspekter och råd

  • Big O ger asymptotisk information — för små n kan en "sämre" Big O ändå vara snabbare på grund av lägre konstantfaktorer eller bättre cacheanpassning.
  • Amortiserad komplexitet: vissa operationer kan vara O(1) i genomsnitt över många operationer även om enstaka operationer är dyrare (t.ex. dynamiska arrayer som expanderar).
  • Välj algoritm efter problemstorlek och tillgängliga resurser. För mycket stora n är skillnaden mellan O(n log n) och O(n^2) ofta avgörande.
  • Kom ihåg att Big O inte tar hänsyn till konstantkostnader, parallellism eller praktiska implementationseffekter — det är ett teoretiskt verktyg som kompletterar mätningar och profilering.

Sammanfattning

Big O-notationen är ett kraftfullt sätt att beskriva hur algoritmers tid- och minnesbehov växer med indata. Genom att förstå vanliga komplexitetsklasser, förenklingsregler och hur man analyserar kod kan man bättre jämföra och välja algoritmer för verkliga problem. Använd Big O tillsammans med mätningar och hänsyn till praktiska faktorer för att få en komplett bild av en algoritms prestanda.