IIUJ Metody programowania Strona: 1

Wykład 5.

Struktury wiązane

Struktura wiązana (ang. linked structure) stanowi uniwersalny mechanizm,

który moŜe mieć zastosowanie w wielu algorytmach. W szczególności moŜe zastąpić

tablicę jako podstawę implementacji innych struktur danych, takich jak listy, stosy i

kolejki.

W ogólności moŜemy uŜyć listy wiązanej w bardzo wielu sytuacjach, w których

uŜylibyśmy tablicy, o ile nie jest wymagany częsty i szybki dostęp do elementów przy

pomocy indeksów.

W liście wiązanej dostęp bezpośredni jest jedynie do elementu pierwszego, do

pozostałych elementów mamy dostęp sekwencyjny, tzn. aby pobrać informację o

danym elemencie naleŜy go odszukać na liście podąŜając łańcuchem kolejnych

połączeń.

Implementacja listy wiązanej

Lista wiązana uwalnia nas od wady tablic statycznych, kopiowania fragmentów

tablicy, w przypadku operacji wstawiania lub usuwania elementu ze środka listy.

Lista wiązana jest reprezentowana jako ciąg obiektów typu Link, kaŜdy obiekt

zawiera:

• element listy - pole o nazwie info typu np. int, long lub inny

• referencję do następnej komórki w liście – pole o nazwie next

• adres w ostatnim obiekcie ma wartość do nikąd: null

• adres do komórki zawierającej pierwszy element listy przechowywany w zmiennej

First typu referencyjnego do Link:

10

30

20

50

First

next

next

next

next

null

Przykładowa lista prosta wiązana.

IIUJ Metody programowania Strona: 2

Warunek:

First != null oznacza Ŝe lista jest niepusta (zawiera co najmniej jedną

komórkę).

Definicja klasy Link.

class Link{

public int info; // dane, w tym przypadku - typu int

public Link next; // referencja do następnego elementu

…

}

PoniŜej przedstawione zostaną podstawowe algorytmy działające na liście wiązanej.

Algorytm przeglądu listy wiązanej:

p = First ;

// ustawiamy się na początku listy

while (p != null) {

p.visit() ; // jakaś operacja na elemencie listy

p = p.next; // przejście do następnego

}

// teraz p jest równe null (wartość next z ostatniej komórki)

Algorytm wyszukiwania w liście - operacja Locate:

Operacja Locate(searchKey) zwraca referencję do pierwszego wystąpienia

szukanego elementu w liście wiązanej, przy czym, jeśli element nie występuje na

liście – zwraca null.

Link Locate (int searchKey) { // szukany element o wartości searchKey

p = First ;

// ustawiamy się na początku listy

while ( p != null && p.info != searchKey)

p = p.next;

return p; // p == null lub p.info == searchKey

}

IIUJ Metody programowania Strona: 3

Algorytm wstawiania do listy wiązanej

Dana jest referencja - p, wskazująca na pewną komórkę w liście wiązanej, oraz

wartość - dana.

Przy zadanej referencji p, aby efektywnie (bez przeglądania listy) wstawić element

do listy naleŜy:

• utworzyć komórkę zawierającą wartość dana w polu info

• wstawić tę komórkę do listy tak, aby element zawierający wartość dana był

następnym elementem w liście po elemencie listy wskazywanym przez p.

Tworzenie komórki.

• W języku Java: wywołanie operatora new przydzielającej pamięć operacyjną

dla nowo kreowanego obiektu (elementu listy):

q = new Link(dana);

• A teraz wstawienie komórki do listy:

q.next = p. next; // [1]

p. next = q ; // [2]

10

30

20

50

First

null

next

next

next

next

[2]

p

[1]

40

q

next

Ilustracja wstawiania elementu

W przypadku wstawiania nowego elementu na początek listy naleŜy wykonać

następujące operacje:

{ q = new Link(dana);

q.next = First;

First = q ; }

IIUJ Metody programowania Strona: 4

Algorytm usuwania elementu listy

Dana jest referencja - p do pewnej komórki w liście.

Podobnie jak poprzednio, gdy zadana jest referencja p, efektywna realizacja

usuwania elementu z listy wiązanej polega na usunięciu elementu następnego

(zakładamy, Ŝe istnieje) po elemencie wskazanym przez p.

Operacja ta wykonywana jest w dwóch krokach:

1. q = p.next; // w q jest referencją do elementu następnego po p

// to ten element usuwamy

2. p.next = q. next; // jako następną po p przyjąć następną po q

// (komórka juŜ jest odłączona od listy)

40

10

30

20

50

First

next

next

next

next

null

next

p

q

W Javie pamięć dla usuwanych elementów nie musi być zwalniana przez

uŜytkownika, poniewaŜ zrobi to interpreter kodu maszyny wirtualnej w czasie

procesu odzyskiwania pamięci (ang. garbage collector).

Zatem usuwanie elementu następnego po elemencie wskazanym przez p

moŜna zapisać prościej:

p. next = p.next.next;

W przypadku usuwania pierwszego elementu listy naleŜy wykonać następującą

instrukcję:

First = First.next;

IIUJ Metody programowania Strona: 5

Problem: usunąć element listy o podanej wartości

Rozwiązanie: odszukać komórkę zawierającą podaną wartość i odłączyć ją od listy

(załóŜmy, Ŝe istnieje dokładnie jedna taka komórka).

Trudność:

1. Aby odłączyć wskazaną komórkę trzeba mieć adres komórki

POPRZEDZAJĄCEJ.

2. Trzeba uwzględnić szczególny przypadek: usuwanie pierwszej komórki w

liście.

void Delete ( int v ) {

p = First;

// aktualna pozycja w trakcie przeglądu

prev = null;

// adres komórki poprzedzającej

while (p != null && p.info != v) {

prev = p ;

p = p.next; // prev.next == p

}

// p.info == v lub p ==null

if (p !=null)

// znaleziono - usuń

if (prev ==null) // usuwamy pierwszy z listy

First = p.next;

else

prev. next = p. next;

} //koniec Delete

Dodatkowe techniki usprawniające dla list wiązanych

1. Lista dwustronna

Lista dwustronna jest bardzo podobna do zwykłej listy wiązanej, ale

przechowuje dodatkową informację: referencję last do ostatniego elementu.

Posiadanie tej referencji pozwala bezpośrednio wstawić element na koniec

listy bez przechodzenia całej listy. Przy pomocy list dwustronnych będzie

IIUJ Metody programowania Strona: 6

moŜna implementować kolejkę zwykłą.

2. Lista z głową (z nagłówkiem)

Głowa (head) - to dodatkowa komórka na początku listy, jej pole info jest

nieokreślone, a pole next wskazuje na realny pierwszy element listy.

Referencja First wskazuje na głowę.

Warunek pustości listy:

First.next == null

a1

a2

an

First

. . .

null

next

next

next

next

Lista wiązana z głową

Lista wiązana z głową jest efektywną implementacją struktury ADT Lista,

postaci: a1, a2, ..., an. w której jako pozycję elementu a i , przyjmujemy referencję

do elementu a i-1 ( bezpośrednio poprzedzającego a i). Pozycją elementu a 1 jest referencja do głowy listy.

Zaleta:

Uproszczone algorytmy wstawiania i usuwania – zawsze istnieje niepusty

poprzednik. Np. operacja Locate(searchKey) - zwraca referencję do obiektu

bezpośrednio poprzedzającego obiekt o wartości searchKey.

Uwaga:

W algorytmach badających, czy nie osiągnięto koniec listy, np., instrukcję

while(p !=End() ) ... wygodniej zastąpić instrukcją while(p.next != null) ....

Link Locate (int v) { // First jest referencją do nagłówka

p = First;

// aktualna pozycja w trakcie przeglądu

while (p.next != null && p.next.info != v)

p = p.next ;

return p ;

} // koniec Locate

IIUJ Metody programowania Strona: 7

40

30

20

50

First

next

next

next

next

null

next

p

Lista po wykonaniu Locate(50)

Przyjmując tak wprowadzone pojęcie pozycji elementu w ADT Lista w implementacji listy

wiązanej z głową, operacje usuwania elementu z zadanej pozycji - Delete(p) oraz wstawiania elementu na zadaną pozycję Insert(x, p) mają następującą postać:

void Delete ( Link p ) { // zakładamy, Ŝe referencja p jest pozycją na liście

if (p.next != null) // nie usuwa elementu za ostatnim

p.next = p.next.next;

} // koniec Delete

40

30

20

50

First

next

next

next

next

null

next

p

Po wykonaniu operacji Delete(p)

void Insert(int x, Link p) { // wstawienie na po elemencie p listy

Link q = new Link(x); // tworzy nowy element

// tworzymy nowe połączenia

q.next = p.next; // q .next -> następny po p element [1]

p.next = q; // p.next -> q [2]

}

IIUJ Metody programowania Strona: 8

30

20

50

First

null

next

next

next

next

[2]

p

[1]

40

q

next

Po wykonaniu operacji Insert(40, p)

Przykład.

Klasa linkHeadList ilustruje działanie listy wiązanej z nagłówkiem

class Link

{

public int info; // dana typu np. int

public Link next; // referencja do następnego połączenia

// oba pola publiczne

// -------------------------------------------------------------

public Link(int id) // konstruktor

{

info = id; // inicjalizacja danych

// (pole next ma automatycznie wartosc null)

}

// -------------------------------------------------------------

public void displayLink() // wypisuje wartosc pól obiektu Link

{

System.out.print(info + "->" );

}

} // koniec klasy Link

//==============================================================

class LinkHeadList

{

public Link first; // referencja do pierwszego elementu listy

// -------------------------------------------------------------

public LinkHeadList() // konstruktor listy z nagłówkiem

{

first = new Link(-1); // tworzenie nagłówka listy

first.next = null; // lista nie ma jeszcze elementów

}

// -------------------------------------------------------------

public boolean isEmpty() // zwraca true, jeŜeli lista jest pusta

{

return (first.next==null);

}

// -------------------------------------------------------------

public void insert(int x, Link p)// wstawienie na po elemencie p listy

{

Link q = new Link(x); // tworzy nowy element

IIUJ Metody programowania Strona: 9

// tworzymy nowe połączenie

q.next = p.next; // q --> następny po p element

p.next = q; // p.next --> q

}

// -------------------------------------------------------------

public void delete(Link p) // usuwanie elementu po p

{ // zakładamy, Ŝe p jest referencja

// do listy

if (p.next != null)

p.next = p.nex.next; // usuwamy go z listy: p.next--

}

// -------------------------------------------------------------

public Link locate(int search) // wyszukuje na liscie zadany element

{

Link curr = first; // zaczynamy na początku listy

while(curr.next != null && curr.next.info != search)

// dopóki nie koniec listy...

{ // i nie znaleziono

curr = curr.next; // ...przechodzimy do następnego elementu

}

return curr; // jesli znaleziono zwraca referencje do elementu

// poprzedziającego

// jesli nie znaleziono zwraca referencje do ostatniego

// elementu

}

// -------------------------------------------------------------

public void displayList()

{

System.out.print("Lista: ");

Link curr = first; // zaczynamy na poczatku listy

while(curr != null) // dopóki nie koniec listy...

{

curr.displayLink(); // ...wypisujemy dane i...

curr = curr.next; // ...przechodzimy do następnego elementu.

}

System.out.println("null");

}

// -------------------------------------------------------------

} // koniec klasy LinkHeadList

3. Lista podwójnie wiązana (dwukierunkowa)

Lista dwukierunkowa daje moŜliwość poruszania się równie łatwo naprzód jak i

wstecz. Lista dwukierunkowa nie musi mieć referencji do ostatniego elementu ale

dostęp do końca listy jest przydatny, więc w naszym przykładzie lista dwukierunkowa

jest jednocześnie listą dwustronną.

IIUJ Metody programowania Strona: 10

Zatem zakładamy, Ŝe lista zawiera dwie referencje: First i Last, które

wskazują odpowiednio początek i koniec listy.

10

30

20

50

40

First

null

next

next

next

next

next

null

prev

prev

prev

prev

prev

Last

Lista podwójnie wiązana

KaŜdy element listy, oprócz pola info, zawiera dwie referencje:

do komórki następnej - next oraz

do poprzedniej - prev.

Tak więc klasa Link, uŜywana przez listę dwukierunkową ma postać:

class Link{

public long info;

public Link next;

public Link prev;

….

}

Na liście dwukierunkowej wykonywane są następujące operacje:

1. makeNull() – tworzy listę pustą

2. displayForward() – wypisuje elementy listy od początku do końca

3. displayBackward() – wypisuje elementy od końca do początku

4. insertFirst(x) – wstawia element na początek listy

5. insertLast(x) – wstawia element na koniec listy

6. insertAfter(x, key) – wstawia element po określonym kluczu

7. deleteFirst() – usuwa pierwszy element na liście

8. deleteLast() – usuwa ostatni element na liście

9. deleteKey(key) –usuwa element o podanym kluczu

10. locate (key) – wyszukuje pozycje zadanego elementu.

IIUJ Metody programowania Strona: 11

Zalety:

Podczas usuwania komórki z listy dwukierunkowej wystarcza referencja

tylko do komórki usuwanej. Ma to szczególnie duŜe znaczenie, gdy lista jest

fragmentem większej struktury, w której do komórek listy moŜna się dostać

inaczej niŜ przeglądając listę od początku.

Wady:

Podczas wstawiania elementu do listy wykonywane są cztery operacje

na referencjach. Musimy zmieniać dwie referencje do następnego elementu i

dwie do następnego. Oprócz tego, kaŜdy element listy dwukierunkowej

zajmuje więcej pamięci niŜ w liście prostej (o rozmiar dodatkowej referencji).

PoniŜej przedstawiono algorytm usuwania elementu o zadanej wartości z listy

podwójnie wiązanej:

[1]

10

30

20

50

40

First

null

next

next

next

next

next

null

prev

prev

prev

prev

prev

Last

p

[2]

void deleteKey( int v ) {

p = locate(v); // pozycja szukanego elementu w trakcie przeglądu

if ( p != null ) {

p.prev.next = p.next; // [1]

p.next.prev = p.prev; // [2]

}

} //koniec Delete

Operacja insertAfter( x, klucz) wstawia nowy element x jako następnik

elementu o podanym kluczu.

IIUJ Metody programowania Strona: 12

W tej metodzie najpierw odnajdujemy element o podanym kluczu - niech p jest

referencją do znalezionego elementu. Następnie tworzymy i wstawiamy nowy

element po elemencie wskazanym przez p (musimy wykonać cztery operacje na

referencjach).

Operację wstawiania insertAfter(x, klucz) ilustruje poniŜszy rysunek.

Last

10

30

20

50

40

First

[4]

null

next

next

next

next

next

null

prev

prev

prev

prev

prev

[2]

p

60

[1]

next

[3]

newLink

prev

void InsertAfter(int x, int key) {

p = Locate(key);

// pozycja szukanego elementu w trakcie przeglądu

newLink = new Link(x); // tworzy nowy element

if( p == last ) { // wstawiania na końcu, za ostatnim

newLink.next = null;

last = newLink;

} else {

newLink.next = p.next; [1]

p.next.prev = newLink; [2]

}

newLink.prev = p; [3]

p.next = newLink; [4]

}

Operacja insertFirst(int v), realizuje wstawianie na początek listy i jeŜeli lista

nie jest pusta zmienia pole prev w dotychczasowym pierwszym elemencie, tak aby

wskazywało na nowy element, a pole next nowego elementu przypisuje referencję do

dotychczas pierwszego elementu.

Następnie referencję do nowego elementu kopiuje do pola first.

IIUJ Metody programowania Strona: 13

JeŜeli lista jest pusta, zamiast pola first.prev modyfikujemy pole last.

Operację wstawiania insertFirst( int v) ilustruje poniŜszy rysunek:

10

30

20

50

40

First

null

next

next

next

next

next

[1]

prev

prev

prev

prev

prev

Last

15

[2]

[3]

next

newLink

null

prev

Oto przykład kodu:

void InsertFirst( int v) {

newLink = new Link(v); // tworzy nowy element

if ( isEmpty () ) Last = newLink;

else

First.prev = newLink; [1]

newLink.next = First; [2]

First = newLink; [3]

}

Operacja wstawiania na koniec jest symetryczna i pozostawiam ją oraz pozostałe

operacje na liście dwukierunkowej, jako ćwiczenie.

3. Lista prosta zamknięta (cykliczna)

W ostatniej komórce referencja next jest adresem pierwszej komórki listy (lub

nagłówka, jeśli wersja z nagłówkiem).

IIUJ Metody programowania Strona: 14

10

30

20

50

First

next

next

next

next

Przydatne w sytuacjach, gdy lista przeglądana jest w całości, ale niekoniecznie

od początku. Zamiast testu p != null naleŜy zastosować test p != s, gdzie s jest referencją komórki, od której zaczęliśmy przegląd listy

Przykład:

Algorytm usuwania elementu o podanej wartości z listy wiązanej cyklicznej z

nagłówkiem (pola: info, next):

40

30

20

50

First

next

next

next

next

next

p

Ilustracja operacji delete(50)

void delete (int v ) {

p = First;

while (p.next != First && p.next.info != v )

p = p.next;

if ( p.next != First ) // znaleźliśmy komórkę zawierająca v

p.next = p.next.next; // usunięcie v z listy wiązanej

} // koniec delete

IIUJ Metody programowania Strona: 15

Porównanie efektywności list wiązanych i tablicowych

Wstawianie i usuwanie elementów z początku listy wiązanej wymagają zmiany

jednej lub dwóch referencji, więc działają w czasie stałym O(1).

Wyszukiwanie, usuwanie lub wstawianie po elemencie o określonym kluczu,

wymaga sprawdzenia średnio połowy elementów w liście. Potrzebne jest zatem

O(n) porównań, analogicznie jak w przypadku tablicy.

Lista wiązana jest zwykle bardziej efektywna, gdyŜ nie wymaga przesuwania

elementów podczas wstawiania czy usuwania. Zwiększenie efektywności moŜe

być zauwaŜalne szczególnie w przypadku, gdy kopiowanie jest duŜo bardziej

kosztowne czasowo niŜ porównanie, np. gdy elementami listy są struktury.

Dodatkową zaletą list wiązanych w stosunku do tablic jest elastyczniejsze

wykorzystanie pamięci. Lista wiązana zawiera dokładnie tyle pamięci, ile potrzeba

(pewien narzut pamięci powoduje co prawda konieczność pamiętania w kaŜdym

elemencie referencji do następnego elementu, ale jest on rekompensowany przez

inne korzyści) i moŜe być łatwo rozszerzalna.

Rozmiar tablicy jest ustalany w momencie tworzenia, powoduje to zwykle

nieefektywne wykorzystanie pamięci, gdy tablicy jest zbyt duŜa lub przepełnienie,

gdy jest zbyt mała.

Implementacja stosu przy uŜyciu listy wiązanej.

Lista wiązana bez nagłówka jest bardzo efektywną realizacją stosu. Zmienna

top wskazuje wierzchołek stosu, który jest na początku listy, zatem - operacje

wstawiania push(x) i usuwania pop() polegają na wstawianiu lub usuwaniu

pierwszego elementu listy wiązanej.

10

30

20

50

top

null

next

next

next

next

wierzchoek

stosu

IIUJ Metody programowania Strona: 16

PoniŜszej zdefiniowano klasę LinkStack:

class Link{

public long info; // dane

public Link next; // następny element listy

// -------------------------------------------------------------

public Link(long dd) // konstruktor

{ info = dd; }

// -------------------------------------------------------------

public void displayLink() // wypisanie klucza elementu

{ System.out.print(info + "->"); }

} // koniec klasy Link

//=============================================================

class LinkStack

{

private Link top; // referencja do pierwszego elementu

// -------------------------------------------------------------

public LinkStack() // konstruktor

{ top = null; } // na razie brak elementów

// -------------------------------------------------------------

public boolean isEmpty()// zwraca true, jeŜeli lista jest pusta

{ return (top == null); }

// -------------------------------------------------------------

public void push(long v) // wstawienie na początek listy

{ // tworzymy nowy element

Link newLink = new Link(v);

newLink.next = top; // newLink -> poprzednio pierwszy

top = newLink; // top -> newLink

}

// -------------------------------------------------------------

public long pop() // usunięcie pierwszego elementu

{ // (zakładamy, Ŝe lista nie jest pusta)

Link temp = top; // zapisujemy referencję do usuwanego

// elementu

top = top.next; // usunięcie: top-> poprzednio drugi

return temp.info; // zwracamy usunięty element

}

// -------------------------------------------------------------

public void displayStack()

{

System.out.print("Top->");

Link tmp = top; // rozpoczynamy na początku listy

while(tmp != null) // dopóki nie koniec...

{

tmp.displayLink(); // ...wypisujemy dane...

tmp = tmp.next; // ...i przechodzimy do następnego

}

System.out.println("null");

}

}

//----koniec klasy LinkStack -----------------------------------

IIUJ Metody programowania Strona: 17

Implementacja kolejki prostej przy uŜyciu listy wiązanej dwustronnej

Do implementacji kolejki wygodniej jest uŜyć listy wiązanej dwustronnej z

nagłówkiem. Dzięki liście z nagłówkiem otrzymamy uproszczone algorytmy

wstawiania i usuwania (nie trzeba rozwaŜać przypadków szczególnych, np.

wstawiania elementu do kolejki pustej, czy usuwania ostatniego elementu z kolejki).

Zmienna front wskazuje głowę listy a zmienna rear – ostatni element w kolejce.

30

20

50

front

null

next

next

next

next

rear

Kolejka prosta jako lista dwustronna

Warunek pustości kolejki:

front == rear; front.next == null

;

Po utworzeniu kolejki pustej, metoda enQueue(long x) - wstawia nowy element na

koniec kolejki, przy czym będzie modyfikowana jedynie referencja - rear:

void enQueue(long x) // wstawienie na koniec listy

{

Link newLink = new Link(x); // tworzymy nowy element

rear.next = newLink; // poprzednio ostatni -> newLink

rear = newLink; // rear -> newLink

}

IIUJ Metody programowania Strona: 18

30

20

50

front

next

next

next

next

40

rear

null

next

Ilustracja wstawienia elementu 40 do kolejki

Podobnie podczas operacji usuwania z początku kolejki, faktycznie usuwany

będzie nagłówek listy a nie pierwszy element. Po usunięciu - pierwszy element kolejki

będzie nowym nagłówkiem w liście.

Oznacza to, Ŝe metoda usuwania elementu z kolejki modyfikuje jedynie referencję -

front.

long deQueue() { // usunięcie pierwszego elementu

long tmp = front.next.info; // zapamiętujemy usuwany element

front = front.next; // front -> poprzednio pierwszy element

return tmp;

}

head

30

20

50

front

null

next

next

next

next

rear

Ilustracja usuwania elementu z kolejki

Napisanie pełnej implementacji prostej kolejki wiązanej pozostawiam jako ćwiczenie.