1. Dec 22

    De som har prøvd tabeller/matriser i språk som f.eks Java, vet at det ikke er like enkelt og smertefritt å anvende de som i f.eks PHP. I PHP har vi uendelig mange muligheter med matriser og vi kan stort sett gjøre hva vi vil med de, uten å tenke så veldig mye på konsekvensene. Slik er det ikke med høynivåspråk som Java, C++ osv.

    Her har vi begrenset muligheter for hva vi kan gjøre og det er enda viktigere at det vi gjør burde være optimalisert og gå så kjapt som mulig. Det er da vi burde se på optimalisering av algoritmene vi gjør i programkodene.

    La oss ta et eksempel om en matrise med tall, som ofte er tilfellet. Vi skal søke igjennom matrisen, og finne på hvilken indeks vi har tallet X. For nybegynnere eller late, vil det naturlige valget være en lineær fremgangsmåte. Det vil si vi starter på indeks 0 og går til lengden på tabellen og går igjennom hver eneste indeks og ser om denne passer til argumentet vi har gitt. Denne fremgangsmåten kan være effektiv dersom vi treffer på resultatet på en av de første indeksene og stopper løkken der ved å returnere resultatet. Men hva om vi ikke har et treff før helt i slutten av tabellen? Da ville man ha brukt mye unødvendig tid på å lete igjennom N antall indekser før man traff på den siste. Det må altså være en bedre måte og gjøre dette på.

    Ta deg en 5-minutters tenkepause og prøv å tenk ut en bedre fremgangsmåte. Vi har flere måter å bruke en løkke-struktur på. Dersom vi har to nøstede løkker har vi en kvadratisk fremgangsmåte (n²), 3 nøstede løkker så er det n³ osv. Men vi har også noen bedre metoder som er raskere; det er logaritmiske fremgangsmåter. Og binærsøk, som vi skal se på nå, er en såkalt logaritmisk fremgangsmåte.

    I steden for å søke igjennom alle indekser, kunne det ikke tenke seg at det er enklere å starte med en sortert tabell og gå til medianen for å så sjekke om medianen er høyere eller lavere enn tallet vi er ute etter? På den måten har vi halvert tabellen vi leter igjennom og har mindre data å lete igjennom. Om man gjør dette igjen, ved å sette medianen av den nye tabellen, og sjekke om den er høyere eller lavere enn tallet vi er ute etter. Sånn fortsetter vi til vi har kommet frem til tallet. Siden det dataen halveres for hver iterasjon er det en logaritimisk fremgangsmåte, og det er lett å se for seg at dette er en mye mer effektiv fremgangsmåte.

    La oss se hvordan det blir utført i praksis. Vi starter med en sortert tabell som har verdiene, 2, 3, 6, 9, 12, 21, 24, 31, 33.

    Siden den er sortert kan vi se på medianen (som er den midterste verdien) 12. Dersom tallet vi leter etter, 24, er over 12 vet vi at den eksisterer et sted mellom 21 og 33 (siden den ikke er 12), eller fra indeksen 5 til 8. Allerede har vi gått fra en datamengde på 9 tall til bare 4 tall. Dersom vi gjør dette igjen vil medianen bli indeksen 6 (avrundet) og den er hverken over eller under 24, men lik. Derfor har vi indeksen vår, altså 6. Som vi ser er dette en mye mer effektiv måte å lete igjennom en tabell på. Her brukte vi 2 gjennomganger fremfor 6 som ville ha vært nødvendig i en lineær fremgangsmåte.

    Nå kan vi se på litt kode som gjør dette for oss. Du kan selv gjøre dette i ditt favorittspråk, men jeg foretrekker per dags dato Java, og vil gjøre det i dette språket.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    public class BinarySearch {
        public int binarySearch (int[] heystack, int needle) {
            int low = 0;
            int high = heystack.length-1;
            int median;
     
            while (low < = high) {
                median = (high+low)/2;
                if(needle > heystack[median]) {
                     // Vi skal ha en større verdi, så vi halverer i lengden fra bunnen
                     low = median+1;
                } else if (needle < heystack[median]) {
                     // Vi skal ha en mindre verdi, så vi halverer i lengden fra toppen
                     high = median-1;
                } else {
                     // Vi sitter på verdien på medianen som er lik needle, så vi har verdien vår.
                     return median;
                }
            }
            // Vi fant ingenting, derfor returnerer vi -1
            return -1;
        }
    }

    Dette er en måte å gjøre det på. Vi kan også gå frem på en rekursiv måte dersom vi vil unngå en løkke.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    public class BinarySearch {
        public int binarySearch (int[] heystack, int needle, int low, int high) {
            // Vi har gått for langt, og vi har ingen verdi..
            if(high < low) return -1;
     
            int median = (high+low)/2;
     
            if(needle > heystack[median]) {
                 // Vi skal ha en større verdi, så vi halverer i lengden fra bunnen
                 return binarySearch(heystack, needle, median+1, high);
            } else if (needle < heystack[median]) {
                 // Vi skal ha en mindre verdi, så vi halverer i lengden fra toppen
                 return binarySearch(heystack, needle, low, median-1);
            }
     
            // Vi sitter på verdien på medianen som er lik needle, så vi har verdien vår.
            return median;
        }
    }

    Mange syns kanskje den rekursive metoden er bedre enn den iterasjonsmetoden, men for min del er itereringen like grei.

    Håper dette setter litt lys på at det finnes bedre metoder å løse en ting på en den du kanskje tenker først.

  2. Dec 06

    I feiringen av fullført datateknikk eksamen, skal jeg nå sette kunnskapene mine på en siste prøve. Jeg skal nå skriv en artikkel om bitwise-operasjoner som en gjerne bruker med et høynivåspråk som f.eks Java eller C++. Før, på en tradisjonell prosessor, ble bitwise-operasjoner gjerne brukt av en CPU i steden for addisjoner og substraksjoner. Dette er som regel ikke tilfellet nå i en CPUs mer moderne form, nå som addisjon og substraksjon er like rask som bitwise-operasjoner. 

    Først litt om hva en bit og en byte er. En bit er et siffer i en binær sammenheng. Et binært tallsystem (også kjent som 2-tallsystemet) er det som blir brukt internt av en datamaskin og all data og innhold i en maskin brytes ned til binære mønster for maskinen. Her er det kun 0 og 1 (høy spenning eller lav spenning) som duger.

    Jeg nevnte at bit var et siffer i et binært mønster. En byte (som dere kanskje kjenner igjen som en størrelsesenhet på maskinen) er en samling av slike bits. 8 bits for å være helt nøyaktig. I en byte er derfor maksimalverdien 255 i ti-tallsystemet (1111 1111 i to-tallsystemet).

    Et ord derimot er en samling av bits uten en fast størrelse. Ord er et bitmønster i en lokasjon/linje i et minne. Et ord kan derfor være på f.eks 16 bit, 32 bit, 64 bit.

    Bitwise-operasjoner

    Vi har 6 forskjellige bitwise-operasjoner.

    1. AND – &
    2. OR – |
    3. XOR – ^
    4. NOT – ~
    5. RIGHT SHIFT – >>
    6. LEFT SHIFT – <<

    (Det du ser til høyre, etter -, er tegnene som blir brukt i høynivåspråk for disse operasjonene.)

    AND

    AND blir brukt til å se hvilke bits som korresponderer i et bitmønster. Her tar jeg utgangspunkt i et mønster på lengden 8 bits, altså en byte. 

    Her tar jeg utgangspunkt i tallene 133 og 203. 133 er 1000 0101 og 203 er 1100 1011.

       // 1000 0101 & 1100 1011
       1000 0101
       1100 1011
       ------------
       1000 0001

    Som du ser resulterer det i et bitmønster som har 1 hvor det er 1 på begge bytene og 0 hvor det er ulikt eller likt på 0. 

    Dette kan du f.eks bruke til å finne om en gitt bit er satt eller ikke:

    // Denne returnerer 0 (boolsk verdi: false) da bitverdien for 2 ikke er satt.
    System.out.println(4 &amp; 0x02);

    OR

    OR blir brukt til å se hvilke to bits sammenlagt har 1. Dvs dersom en av bitene på to bitmønster har 1 vil resultatet si 1. 

    Jeg tar igjen utgangspunkt i samme tall som på AND.

       // 1000 0101 | 1100 1011
       1000 0101
       1100 1011
       ------------
       1100 1111

    Som du ser blir det 1 hvor en av bitmønstrene har 1 i seg på den plassen.

    XOR

    Denne operasjonen er en “enten eller” operasjon. Den blir 1 hvis og bare hvis de to bitene er ulike. 

    Tar fortsatt utgangspunkt i samme tall.

       // 1000 0101 ^ 1100 1011
       1000 0101
       1100 1011
       ------------
       0100 1110

    NOT

    NOT-operasjonen er en måte å finne det motsatte på. Eller du kan gjerne si at du vil finne negasjonen av mønsteret.

    Tar fortsatt utgangspunkt i samme tall som før. 

       ~1000 0101
       ------------
       0111 1010

    LEFT SHIFT

    LEFT SHIFT-operasjonen flytter alle bit-verdiene ett hakk til venstre dersom noe annet ikke er gitt. Kan også flytte flere hakk til venstre dersom nødvendig.

    Tar utgangspunkt i det første tallet gitt fra AND-operasjonen. 

       1000 0101 <<
       ------------
       0000 1010
     
       1000 0101 << 2
       ------------
       0001 0100

    RIGHT SHIFT

    Samme som forrige operasjon, bare motsatt vei. Altså til høyre.

       1000 0101 >>
       ------------
       0100 0010
     
       1000 0101 >> 2
       ------------
       0010 0001

    Noen ord til slutt

    Håper dette skriveriet kan hjelpe noen til å forstå hvordan man skal bruke bitwise-operasjoner. I alle fall hvordan de fungerer. Disse operasjonene har mange forskjellige bruksområder. For eksempel kan “shifting” brukes til å finne den mest signifikante biten i et mønster og således finne ut om verdien er negativ eller positiv. NOT kan f.eks brukes til å finne absoluttverdien av et negativt tall osv.

    \\ emneord: , , ,