Mateusz Mazurek – programista z pasją

Python, architektura, ciekawostki ze świata IT

Algorytmika Programowanie

kursJavy.czesc(3);

Cześć! Cieszę się, że mnie odwiedziłeś/aś. Zanim przejdziesz do artykułu chciałbym zwrocić Ci uwagę na to, że ten artykuł był pisany kilka lat temu (2013-02-03) miej więc proszę na uwadzę że rozwiązania i przemyślenia które tu znajdziesz nie muszą być aktualne. Niemniej jednak zachęcam do przeczytania.

Cześć! W tej cześci odejdziemy nieco od naszej klasy Czlowiek :) Poznamy rekurencję oraz pokaże przykład działania zmiennych statycznych a przy okazji złapiemy mały expieriance w temacie Algorytmów. Pokaże też subklasy.
Ok, słowem wstępu opowiem Wam co będziemy implementować. Jakiś czas temu wyyślono coś takiego jak struktury danych. O niektórych już pisałem na tym blogu – chociażby lista. Dla k-tego elemntu listy trzeba zrobić k kroków. Co, wbrew logice, jest kiepskim wynikiem. Bo jak mamy 1000 elementów w liście to żeby dojść do 900 elementu musimy zrobić 900 przesuwań wskaźnika.. Very bad. No, może nie very, ale nadal bad.
Lista ma zawsze jakiś następny element – jest nim kolejlny element lub null – czyli znak że jest koniec listy.
Liczby można przetrzymywać sprytniej :) Takim sposobem są Drzewa Przeszukiwań Binarnych ;) Nie bez powodu wstawiłem ten link tu. Kliknij w niego i zobacz co to jest.
BEZ TEGO NIE IDŹ DALEJ.
Popatrzyłeś? Ok, to możemy iść dalej.
Od strony implementacji jeden węzeł drzew, czyli jedno takie kółko z wartością w środku będzie wyglądało tak:

1
2
3
4
5
6
7
8
9
10
private class Node{

int val; // wartosc
Node left; // referencja do lewego dziecka
Node right; // referencja do prawego dziecka
int key; //klucz
Node parent; // referencja do rodzica
Node(int a){ this.val=a;} // konstruktor

}

Każdy węzeł będzie obiektem wewnętrznej prywatnej (lub chronionej – zależnie czy mamy w planach dziedziczyć) klasy Node.
Ważnym elementem, który czyni drzewa binarnym ciekawą strukturą danych (a przy okazji prostą) jest możliwość cwanego przeszukiwania ich wykorzystując zależności wyboru poddrzewa dla kolejnego elementu.
Ale to będę tłumaczył już przy samej implementacji.
Ok, to zaczynamy!
Stwórzmy węzeł sobie który będzie zawsze wskazywał na korzeń drzewa, czyli pierwszą wartość dodaną do drzewa oraz zmienną statyczną.. Co ona będzie robić? Na razie to mało istotne, ale to jak działa jest już całkiem ważne – zmienne statyczne nie tracą swojej wartości w obrębie jednego kontenera (funkcji, klasy itp). Czyli jeżeli stworzymy obiekt klasy i zrobimy jakieś operacje na tej zmiennej to będzie ona miała wartość zmienioną a nie początkową.. Rozumiesz? Jeśli tak to fajnie, jeśli nie, to poczekaj, zrozumiesz.

1
2
Node root;
static int key=1;

Nasza klasa będzie nazywać sie BST. Tworzymy konstruktor

1
2
3
4
5
6
public BST(int v){

root= new Node(v); // 1
root.key=1; // 2
root.parent=null;// 3
}

1. Tworzymy nowy obiekt wewnętrznej klasy (tzn. subklasy) i przypisujemy go do zmiennej root.
2. Ustawiamy key na 1, gdyż będzie to nasz pierwszy element drzewa – korzeń.
3. Skoro to korzeń to jako jedyny element w drzewie – nie ma rodzica.

Teraz chyba najtrudniesza rzecz do napisania.. Oczywiscie z tych które przygotowałem na tę lekcję. Napiszmy metodę która będzie umieszczała wartość podaną przez użytkownika w ODPOWIEDNIM miejscu drzewa. Czyli ma wyszukać takie miejsce w drzewie żeby zachować właściwość drzewa BST. Mam na myśli oczywiście to że po lewej stronie do węzła są wartości mniejsze a po prawej większe.
Algorytm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Pobierz wartość nowego węzła.
2. Stwórz węzeł w tą wartością.
3. current <= root
-- Pętla --
4. Jeśli wartosć current jest mniesza od dodawanej..
5. Sprawdź czy prawe dziecko current jest nullem (jest wolny)
6. Jeśli tak to podczepiamy stworzony węzeł jako prawe dziecko currenta i wychodzimy z pętli (przejście do 12 pkt)
7. Jeśli nie to przesuwamy current w prawo.
8. Jeśli current NIE jest mniejsze od dodawanej..
9. Sprawdź czy lewe dziecko current jest nullem (jest wolny)
10. Jeśli tak to podczepiamy stworzony węzeł pod lewe dziecko currenta i wychodzimy z pętli (przejście do 12 pkt)
11. Jeśli nie to przesuwamy current w lewo.
-- Koniec pętli --
12. Koniec.

Mimo że średnio to wygląda – jest proste. Zrobię przykład. Do takiego drzewa:
tree

I chcemy dodać liczbę 8 :)
Krok po kroku jak będzie przebiegał algorytm. Kroki zaznaczane są czerwoną strzałką.
tree2

Mam nadzieje że już rozumiesz. Teraz czas na kod :) Kroki algorytmu jako komentarze.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public boolean addNode(int v){//1

this.key++;//2
Node n = new Node(v);//2
n.key=this.key;//2
Node current=root;//3

while(true){

if(current.val<v){//4

if(current.right==null){//5

n.parent=current;//6
current.right=n;//6
break;//6

}else{

current=current.right;//7

}
}else{//8

if(current.left==null){//9

n.parent=current;//10
current.left=n;//10
break;//10

}else{

current=current.left;//11

}

}
}
return true;//12
}

Dopiszmy teraz coś co wyświetli nam wartości drzewa. Mamy do wyboru 3 algorytmy:
PostOrder
PreOrder
InOrder

Ja wybrałem PreOrder. Czym one się różnią? W sumie każde z nich jest REKURENCYJNYM sposobem przechodzenia drzewa, różnią się co do kolejności odwiedzania wierzchołków.
żeby zrozumieć rekurencję, trzeba zrozumieć rekurencję ;) musiałem to napisać. Popatrzmy na kod dwóch metod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void show(Node a){

System.out.println(a.val);

if(a.left!=null)
show(a.left);

if(a.right!=null)
show(a.right);

}

public void showAll(){

this.show(root);

}

Metoda publiczna to kontener dla metody prywatnej (opakowanie jej, nic skomplikowanego). Do metory prywatnej przekazujemy roota, czyli nasz korzeń. Metoda show() wyświetla wartość korzenia potem sprawdza czy ma lewe dziecko, jesli tak, to wywołuje siebie samą.. Może, przecież jest zdefiniowana. Funkcja który wywołuje jest zawieszona i czeka na zakończenie funkcji którą wywołała. Więc wchodzmy z a.left do show() – wyświetla ten węzeł i znów sprawdza i znów wchodzi.. I tak dalej.. Jaki efekt? Efekt algorytmu z powrotami – czyli rekurencja się cofnie w pewnym momencie i zacznie przechodzić do drugiego ifa = wyświetlac prawe poddrzewo :) Rekurencja jest mało efektywna w niektórych zastosowaniach. Ale każdą rekurencyjną implementację można zapisać w postaci iteracyjnej.

Czy widzisz może już zastosowania takiego drzewa? Popatrz na te funkcje i pomyśl jak duży zysk efektywności jest w porównaniu co do np. wcześniej wspomnianej listy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int getMax(){

Node t= root;

while(t.right != null)
t=t.right;

return t.val;

}
public int getMin(){

Node t= root;

while(t.left != null)
t=t.left;

return t.val;

}

Nawet przy średnio zrównoważonych drzewach wyszukiwanie największych/najmniejszych wartości jest bardzo szybkie. Zdecydowanie szybsze niż naiwne wyszukiwanie w tablicy czy w liście. W najgorszym możliwym przypadku (drzewo pochylone) czas jest porównywalny z listą.
Co znaczy że drzewo jest zrównoważone? Chodzi o to że wartości można tak ustawić żeby było ono jak najmniejsze. A co z tym idzie – przeszukiwanie jak najszybsze. Przeprowadźmy test..
Przy okazji zobaczycie jak w łatwy sposób mierzyć czas wykonywania się fragmentu kodu. Sprawdźmy czas wyszukiwania największego elementu w 100 elemenowym zbiorze liczb mniejszych od 1000 generowanych „losowo”. Wprowadzimy te same liczby do drzewa, do listy i do tablicy. Popatrz na kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/

package pro;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;

/**
*
* @author Matt
*/

public class Pro {

/**
* @param args the command line arguments
*/

public static void main(String[] args) {

Random r = new Random();

BST f=null; //referencja do drzewa
int i=0;// zmienna iterujaca
int tablica[] = new int[100];//tablica 100 elementow
List lista = new ArrayList (100); //lista 100 elementow
while(i<100) //wypelniamy 3 struktury danych na raz
{
int t = r.nextInt(1000);//losuje liczbe
tablica[i]= t;//wpisuje do tablicy
lista.add(t); //dodaje do listy
if(f==null) {
f=new BST(t);
}
else {
f.addNode(t);//wypelniam drzewo
}

i++;

}
//====================================================
//====================================================
//====================================================
long start=System.nanoTime(); // czas staru pomiaru
int m=f.getMax(); //szukam w drzewie maxa
long stop=System.nanoTime();//czas zakonczenia pomiaru
System.out.println("Max dla drzewa: "+m);//wyswietlam max znaleziony w drzewie
System.out.println("Czas wykonania w ns dla drzewa:"+(stop-start));//roznica w czasach to czas wykonania
//========================
//========================
//========================
int max=0;//max poczatkowy
long start1=System.nanoTime();//
for(i=0;i<100;i++){ if(tablica[i]>max)
max=tablica[i];
//iteracyjnie, naiwnie szukam maxa w tablicy
}
long stop1=System.nanoTime();
System.out.println("Max dla tablcy: "+max);
System.out.println("Czas wykonania w ns dla tablicy:"+(stop1-start1));//licze drugi czas wynokania
//========================
//========================
//========================
int max2=0;
long start2=System.nanoTime();
for(i=0;i<100;i++){ if(lista.get(i) >max2)
max2=lista.get(i);//dla listy

}
long stop2=System.nanoTime();
System.out.println("Max dla listy: "+max2);
System.out.println("Czas wykonania w ns dla listy:"+(stop2-start2));
//teraz wstawiam czasy wykonywania sie do tablicy
long a[]= new long[3];
a[0]=stop-start;
a[1]=stop1-start1;
a[2]=stop2-start2;
//szukan najmniejszej różnicy
long naj=a[0];
for(i=1;i<3;i++){
if(a[i]<naj)
naj=a[i];
}
//i wyswietlam co wygrało wyścig
if(naj==a[0])
System.out.println("Najszybciej drzewo");
if(naj==a[1])
System.out.println("Najszybciej tablica");
if(naj==a[2])
System.out.println("Najszybciej lista");

}
}

Dla 10 uruchomień wyniki przedstawiają się tak:

[ns]

1 2 3 4 5 6 7 8 9 10
Drzewo 4562 3992 3993 4563 5133 4562 14828 5132 4564 4563
Tablica 6273 5703 5703 6843 6273 6843 6844 5703 5703 5133
Lista 48474 37640 52467 38780 40491 271459 50756 38210 36499 37069

Wyniki nie są zaskakujące. Pogrubiłem strukturę która była w danej próbie najszybsza. Zapytacie.. Co się stało w próbie 7? Sprawdopodobnie napływ danych był tak pesymistyczny, że drzewo było mniej zrównoważone niż w poprzednich próbach.

Zobaczmy jak przedstawia się taka tabela gdy ilość liczb pomnożymy przez 100.

[ns]

1 2 3 4 5 6 7 8 9 10
Drzewo 13828 25093 11406 17109 12547 19960 29085 25093 22241 27374
Tablica 852017 910187 1477628 710014 820081 483608 1421739 3449699 613064 7364758
Lista 3599117 3656145 4705484 4191650 3649827 3797297 3847193 7993220 7290834 1868279

Tutaj bezsprzecznie wygrywa drzewo.

 

Może bardziej przemawiać będą wykresy:

Wykres dla 100 wartości.

 

Wykres dla 100 wartości.

 

Oraz wykres dla 10000 wartości:

Dla 10000 wartości.

 

Nie trzeba być super inżynierem żeby mieć pewnosć że nawet średno zrównoważone drzewo będzie szybsze przy szukaniu najwiekszej wartości niż lista i tablica. Drzewo równoważy się poprzez rotacje w prawo i w lewo. Ale o tym kiedy indziej.

 

Skąd taka rozbieżność między tablicą i listą? Lista traci sporo czasu na dostęp do n-tego elementu, kiedy dostęp do elementów tablicy wykonuje się w czasie stałym.

Uff, ta częśc kursu pokazała Javę od nieco algorytmicznej strony.. Ale to nie jest złe. Zobaczyłeś że klasa w klasie nie jest zła, a zmienne statyczne.. Moga być użyteczne… Właśnie. Umiałbyś przerobić klasę BST tak zeby używała pola key? :)

Dzięki za wizytę,
Mateusz Mazurek

A może wolisz nowości na mail?

Subskrybuj
Powiadom o
guest

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

1 Komentarz
Inline Feedbacks
View all comments