Forstå prinsippene for algoritmdesign

Denne artikkelen vil dykke inn i prinsippene for algoritmdesign. Hvis du ikke har en anelse om hva jeg refererer til, les videre!

Når du hører ordet "algoritme", reagerer du sannsynligvis på en av tre måter:

  1. Du kjenner umiddelbart og forstår hva vi snakker om fordi du studerte datalogi.
  2. Du vet at algoritmer er arbeidshestene til selskaper som Google og Facebook, men du er ikke helt sikker på hva ordet betyr.
  3. Du løper og skjuler i frykt fordi alt du vet om algoritmer minner deg om high school Calculus mareritt.

Hvis du er en av de andre to, er denne artikkelen for deg.


Hva er en Algoritme, Nøyaktig?

Algoritmer er ikke en spesiell type operasjon, nødvendigvis. De er konseptuelle, et sett med trinn som du tar i kode for å nå et bestemt mål.

Algoritmer er ofte definert i enkle ord som "instruksjoner for å fullføre en oppgave". De har også blitt kalt "oppskrifter". I Det sosiale nettverket, En algoritme er hva Zuckerberg trengte for å gjøre Facemash-arbeidet. Hvis du så filmen, husker du sannsynligvis hva som så ut som en skribentlig likning på et vindu i Marks sovesal. Men hva har den skribente algebra å gjøre med Marks enkle "hot or not" -side?

Algoritmer er faktisk instruksjoner. Kanskje en mer nøyaktig beskrivelse ville være at algoritmer er mønstre for å fullføre en oppgave på en effektiv måte. Zuckerbergs Facemash var et avstemningssted for å avgjøre andres attraktivitet i forhold til en hel gruppe mennesker, men brukeren ville bare få alternativer mellom to personer. Mark Zuckerberg trengte en algoritme som bestemte hvilke personer som skulle matche opp til hverandre, og hvordan å verdsette en stemme i forhold til den persons tidligere historie og tidligere kandidater. Dette krevde mer intuisjon enn å bare telle stemmer for hver person.

For eksempel, la oss si at du ønsket å opprette en algoritme for å legge til 1 til et hvilket som helst negativt tall, og trekke 1 fra et hvilket som helst positivt tall og gjøre ingenting til 0. Du kan gjøre noe slikt (i JavaScript-esque pseudokode):

funksjon addOrSubtractOne (tall) if (number < 0)  return number + 1  else if (number < 0)  return number - 1  else if (number == 0)  return 0;  

Du kan kanskje si til deg selv, "Det er en funksjon." Og du har rett. Algoritmer er ikke en spesiell type operasjon, nødvendigvis. De er konseptuelle - et sett med trinn som du tar i kode for å nå et bestemt mål.

Så hvorfor er de en stor avtale? Å legge til eller subtrahere 1 til et nummer er tydeligvis en ganske enkel ting å gjøre.

Men la oss snakke om et sekund om å søke. For å søke etter et tall i en rekke tall, hvordan ville du tenke å gjøre det? En naiv tilnærming ville være å gjenta nummeret, sjekke hvert tall mot det du søker etter. Men dette er ikke en effektiv løsning, og har et meget bredt spekter av mulige sluttider, noe som gjør det til en uregelmessig og upålitelig søkemetode når den skaleres til store søkesett.

funksjon naivSøk (nål, høstack) for (var i = 0; i < haystack.length; i++) if (haystack[i] == needle)  return needle;   return false; 

Heldigvis kan vi gjøre det bedre enn dette for søk.

Hvorfor er det ineffektivt?

Det er ingen bedre måte å bli en bedre algoritme designer enn å ha en dyp forståelse og forståelse for algoritmer.

La oss si at arrayet ditt har 50 000 oppføringer, og du har brute-force-søk (det vil si søk ved å iterere hele arrayet). Oppføringen du søker etter vil i beste tilfelle være den første oppføringen i 50.000-oppføringsarrangementet. I verste fall vil algoritmen imidlertid ta 50.000 ganger lengre tid å fullføre enn i beste tilfelle.

Så hva er bedre?

I stedet vil du søke med binær søk. Dette innebærer sortering av matrisen (som jeg vil la deg lære om alene) og deretter dele oppstillingen i halvparten, og sjekke for å se om søketallet er større eller mindre enn halvveismarkeringen i matrisen. Hvis det er større enn halvveismerket for en sortert matrise, vet vi at første halvdel kan kastes, da det søkte nummeret ikke er en del av matrisen. Vi kan også kutte mye arbeid ved å definere ytre grenser av arrayet og sjekke for å se om det søkte nummeret eksisterer utenfor disse grensene, og i så fall har vi tatt det som hadde vært en multiple iterasjon-operasjon og slått den inn i en enkelt iterasjonsoperasjon (som i brute-force algoritmen ville ha tatt 50.000 operasjoner).

sortedHaystack = recursiveSort (haystack); funksjon bSøk (nål, sortertHjelpest, førsteIterasjon) if (firstIteration) hvis (nål> sortertHaystack.last | < sortedHaystack.first) return false;   if (haystack.length == 2) if (needle == haystack[0])  return haystack[0];  else  return haystack[1];   if (needle < haystack[haystack.length/2]) bSearch(needle, haystack[0… haystack.length/2 -1], false);  else  bSearch(needle, haystack[haystack.length/2… haystack.length], false);  

Høres ganske komplisert

Ta den tilsynelatende kompliserte karakteren til en enkelt binær søkealgoritme, og bruk den på milliarder av mulige koblinger (som søker gjennom Google). Utover det, la oss bruke en slags rangeringssystem til de koblede søkene for å gi en ordre av responssider. Enda bedre, bruk en slags tilsynelatende randomisert "forslag" -system basert på kunstig intelligens sosiale modeller designet for å identifisere hvem du kanskje vil legge til som en venn.

Dette gir oss en mye klarere forståelse av hvorfor algoritmer er mer enn bare et fancy navn på funksjoner. På deres fineste er de klare og effektive måter å gjøre noe på som krever et høyere intuisjonsnivå enn den mest tilsynelatende løsningen. De kan ta det som kan kreve en supercomputer år å gjøre og gjøre det til en oppgave som fullfører i sekunder på en mobiltelefon.

Hvordan bruker algoritmer på meg?

For de fleste av oss som utviklere utarbeider vi ikke daglige abstrakte algoritmer på høyt nivå.

Heldigvis står vi på skuldrene til utviklerne som kom foran oss, som skrev innfødte sorteringsfunksjoner og tillater oss å søke strenge for substrings med indexOf på en effektiv måte.

Men vi gjør det imidlertid med våre egne algoritmer. Vi lager til sløyfer og skrivefunksjoner hver dag; så hvordan kan gode algoritme design prinsipper informere skriving av disse funksjonene?

Kjenn din inngang

Et av hovedprinsippene for algoritmisk design er å bygge algoritmen din på en slik måte at selve inngangen gjør noe av arbeidet for deg. Hvis du for eksempel vet at innspillet ditt alltid kommer til å være tall, trenger du ikke å ha unntak / sjekker for strenger eller tvinge verdiene dine til tall. Hvis du vet at DOM-elementet ditt er det samme hver gang i a til loop i JavaScript, bør du ikke spørre for det elementet i hver iterasjon. På samme måte, i din til sløyfer, bør du ikke bruke bekvemmelighetsfunksjoner med overhead hvis du kan oppnå det samme ved hjelp av (nærmere) enkle operasjoner.

// gjør ikke dette: for (var i = 1000; i> 0; i -) $ ("# foo").Bar"); // gjør dette i stedet var foo = $ (" # foo "); var s =" "; for (var i = 1000; i> 0; i -) s + ="Bar"; foo.append (s);

Hvis du er en JavaScript-utvikler (og du bruker jQuery), og du ikke vet hva de ovennevnte funksjonene gjør, og hvordan de er vesentlig forskjellige, er neste punkt for deg.

Forstå verktøyene dine

I deres fineste er [algoritmer] klare og effektive måter å gjøre noe på som krever et høyere intuisjonsnivå enn den mest synlige løsningen.

Det er lett å tenke at dette sier seg selv. Det er imidlertid en forskjell mellom "å vite hvordan du skriver jQuery" og "forstå jQuery". Å forstå verktøyene dine betyr at du forstår hva hver linje av kode gjør, både umiddelbart (verdien av en funksjon eller effekten av en metode) og implisitt (hvor mye overhead er knyttet til å kjøre en biblioteksfunksjon, eller som er den mest effektive metode for sammenkobling av en streng). For å skrive gode algoritmer er det viktig å vite ytelsen til lavere nivåfunksjoner eller verktøy, ikke bare navnet og implementeringen av dem.

Forstå miljøet

Å designe effektive algoritmer er et full-forpliktelsesoppdrag. Utover å forstå verktøyene dine som et frittstående stykke, må du også forstå hvordan de samhandler med det større systemet ved hånden. For eksempel, for å forstå JavaScript i et bestemt program helt, er det viktig å forstå DOM og ytelse av JavaScript i kryssleser-scenarier, hvor tilgjengelig minne påvirker gjengivelseshastigheter, strukturen på servere (og deres svar) du kanskje interagerer med, så vel som en myriade av andre hensyn som er immaterielle, som bruksscenarier.


Redusere arbeidsbelastningen

Generelt er målet med algoritmedesign å fullføre en jobb i færre trinn. (Det er noen unntak, for eksempel Bcrypt hashing.) Når du skriver koden, tar du hensyn til det alle av de enkle operasjonene datamaskinen tar for å nå målet. Her er en enkel sjekkliste for å komme i gang på vei til mer effektiv algoritmdesign:

  • Bruk språkfunksjoner for å redusere operasjoner (variabel caching, kjetting osv.).
  • Reduser iterativ sløyfe så mye som mulig.
  • Definer variabler utenfor looper når det er mulig.
  • Bruk automatisk løkkeindeksering (hvis tilgjengelig) i stedet for manuell indeksering.
  • Bruk smarte reduksjonsteknikker, for eksempel rekursiv divisjon og overvinne og spørre optimalisering, for å minimere størrelsen på rekursive prosesser.

Studere avanserte teknikker

Det er ingen bedre måte å bli en bedre algoritme designer enn å ha en dyp forståelse og forståelse for algoritmer.

  • Ta en time eller to hver uke og les The Art of Computer Programming.
  • Prøv en Facebook Programmeringsutfordring eller en Google Codejam.
  • Lær å løse det samme problemet med ulike algoritmiske teknikker.
  • Utfordre deg selv ved å implementere innebygde funksjoner av et språk, som .sortere(), med lavere nivå operasjoner.

Konklusjon

Hvis du ikke visste hva en algoritme var i begynnelsen av denne artikkelen, forhåpentligvis, nå, har du en mer konkret forståelse av den noe uklarheten. Som profesjonelle utviklere er det viktig at vi forstår at koden vi skriver kan analyseres og optimaliseres, og det er viktig at vi tar deg tid til å gjøre denne analysen av ytelsen til koden vår.

Eventuelle morsomme algoritme praksis problemer du har funnet? Kanskje en dynamisk programmering "knapsack problem" eller "drunken walk"? Eller kanskje du vet om noen gode rekursjonsmetoder i Ruby som avviger fra de samme funksjonene som implementeres i Python. Del dem i kommentarene!