Implementacja listy dwukierunkowej ze znacznikiem

furious programming

W tym artykule opisana jest specyficzna lista dwukierunkowa. Specyficzna z tego względu, że co prawda jej budowa nie odbiega od tej znanej, to dodatkowo zapamiętuje ostatnio używany węzeł, który wykorzystywany jest podczas przeszukiwania listy. Ten specjalny wskaźnik nazywany będzie znacznikiem.

Przykład implementacji opisywanej listy opakowany jest w standardową klasę i nie zawiera obsługi błędów (poprzez wyjątki). Prezentowane kody napisane są w języku Free Pascal i przeznaczone dla kompilatora FPC, jednak bez większych modyfikacji mogą być używane w środowisku Delphi.

Pomysł listy zapamiętującej ostatnio użyty węzeł jest autorski, jednak nie jest wykluczone, że jest on już powszechnie znany i stosowany.

Spis treści:

     1 Lista dwukierunkowa
     2 Znacznik listy
     3 Implementacja w języku Free Pascal
          3.1 Typ danych węzłów
          3.2 Deklaracja klasy TDoublyLinkedList
               3.2.1 Pola klasy
                    3.2.1.1 FFirstNode
                    3.2.1.2 FLastNode
                    3.2.1.3 FLastUsedNode
                    3.2.1.4 FLastUsedNodeIndex
                    3.2.1.5 FCount
                    3.2.1.6 FOwnsElements
               3.2.2 Metody klasy
                    3.2.2.7 Create
                    3.2.2.8 Destroy
                    3.2.2.9 DisposeRemainingNodes
                    3.2.2.10 DisposeRemainingElements
                    3.2.2.11 CreateNode
                    3.2.2.12 NodeAtIndex
                    3.2.2.13 ElementAtIndex
                    3.2.2.14 AddElement
                    3.2.2.15 InsertElement
                    3.2.2.16 RemoveElement
                    3.2.2.17 RemoveAllElements
               3.2.3 Właściwości klasy
                    3.2.3.18 Count
                    3.2.3.19 OwnsElements
                    3.2.3.20 Element
     4 Podsumowanie
     5 Załączniki

Lista dwukierunkowa

Tak jak każda lista dwukierunkowa, ta opisywana składa się z węzłów, w których skład wchodzą przede wszystkim dwa wskaźniki - na poprzedni oraz następny węzeł. Natomiast domyślnymi danymi przechowywanymi w pojedynczych węzłach są obiekty, a konkretnie instancje klas(y) dziedziczących po TObject. Dzięki temu tak przygotowana bazowa lista będzie mogła przechowywać obiekty różnych klas.

Znacznik listy

Wskaźnik określany mianem znacznika jest dokładnie tego samego typu co wszystkie inne węzły listy. W przypadku, gdy lista jest pusta, znacznik zawsze wskazuje na wartość Nil, natomiast w każdej niepustej liście zawsze wskazuje na ostatnio użyty węzeł lub na pierwszy węzeł listy.

Zapamiętywanie ostatnio użytego węzła ma służyć przede wszystkim do przyspieszenia wyszukiwania węzła o zadanym indeksie. Z racji tej, że w liście dwukierunkowej iterowanie po węzłach możliwe jest w dwie strony, możliwości iterowania są cztery:

  • od pierwszego węzła w przód (w stronę końca listy),
  • od ostatnio użytego węzła w tył (w kierunku początku listy),
  • od ostatnio użytego węzła w przód (w stronę końca listy),
  • od ostatniego węzła w tył (w kierunku początku listy).

To od którego węzła i w którym kierunku odbędzie się wyszukiwanie zależy od zadanego indeksu oraz od indeksu ostatnio użytego węzła. Dzięki temu, że wyszukiwanie zadanego węzła może się odbywać łącznie od trzech różnych węzłów - maksymalna ilość koniecznych iteracji nie będzie większa, niż połowa ilości węzłów znajdujących się w liście; Ma to duże znaczenie przede wszystkim w bardzo długich listach i wielu operacjach wstawiania bądź usuwania węzłów.

Implementacja w języku Free Pascal

Typ danych węzłów

Każdy węzeł listy musi posiadać wskazanie na poprzedni oraz następny węzeł listy, a także pole do przechowywania danych, w tym przypadku obiektów bazowej klasy TObject. Deklaracja rekordu oraz wskaźnika na taką strukturę przedstawia się następująco:

type
  PListNode = ^TListNode;
  TListNode = record
    PreviousNode: PListNode;
    NextNode: PListNode;
    Element: TObject;
  end;

Typ TListNode zawiera trzy pola - wskaźnik PreviousNode przechowujący adres poprzedniego węzła, wskaźnik NextNode trzymający adres następnego węzła, a także Element, służący do przechowywania obiektu jako danych węzła.

Typ TListNode służy jedynie do deklaracji wskaźnika na taki rekord, czyli typu PListNode - wszystkie pola, metody i właściwości klasy listy będą korzystać tylko i wyłącznie ze wskaźników, nie faktycznych rekordów.

Deklaracja klasy TDoublyLinkedList

Poniżej widnieje kompletna deklaracja klasy listy dwukierunkowej. Opis jej składowych znajduje się w kolejnych podpunktach.

type
  TDoublyLinkedList = class(TObject)
  private
    FFirstNode: PListNode;
    FLastNode: PListNode;
    FLastUsedNode: PListNode;
    FLastUsedNodeIndex: Integer;
    FCount: Integer;
    FOwnsElements: Boolean;
  private
    procedure DisposeRemainingNodes();
    procedure DisposeRemainingElements();
  private
    function CreateNode(AElement: TObject): PListNode;
  private
    function NodeAtIndex(AIndex: Integer): PListNode;
    function ElementAtIndex(AIndex: Integer): TObject;
  public
    constructor Create(AOwnsElements: Boolean);
    destructor Destroy(); override;
  public
    procedure AddElement(AElement: TObject);
    procedure InsertElement(AIndex: Integer; AElement: TObject);
  public
    procedure RemoveElement(AIndex: Integer);
    procedure RemoveAllElements();
  public
    property Count: Integer read FCount;
    property OwnsElements: Boolean read FOwnsElements;
  public
    property Element[AIndex: Integer]: TObject read ElementAtIndex; default;
  end;

Pola klasy

Pierwszą grupą pól klasy są wskaźniki, na podstawie których zbudowana jest lista, a także służących do dostępu do poszczególnych jej węzłów. Drugą grupą są pola liczbowe, służące do celów informacyjnych a także do obsługi znacznika. W skład ostatniej grupy wchodzi pole z wartością logiczną, określające sposób przechowywania obiektów.

FFirstNode

Jest to wskaźnik typu PListNode, wskazujący na pierwszy węzeł listy. Jeśli lista jest pusta, wskaźnik ten posiada wartość Nil, a jeśli nie jest pusta - wskazanie na węzeł początkowy.

FLastNode

Wskaźnik typu PListNode, posiadający wskazanie na ostatni węzeł listy. W przypadku gdy lista jest pusta - posiada wartość Nil. Jeżeli lista posiada tylko jeden element, wartość tego wskaźnika jest taka sama jak wartość pola FFirstNode, dlatego że oba pola wskazują na ten sam węzeł. W pozostałych przypadkach (jeżeli lista zbudowana jest z więcej niż jednego węzła) zawiera wartość różną od Nil i różną od wartości pola FFirstNode.

FLastUsedNode

Jest to specjalny wskaźnik typu PListNode, zawierający adres ostatnio użytego węzła. Jeśli lista jest pusta, wskaźnik ten zawiera wartość Nil. Natomiast w każdej niepustej liście wskazuje na konkretny węzeł:

  • albo na pierwszy węzeł listy, jeśli lista nie była do tej pory przeszukiwana,
  • albo na ostatnio użyty węzeł (dowolny na liście, również pierwszy lub ostatni).

Używanie tego pola jest ściśle powiązane z polem FLastUsedNodeIndex.

FLastUsedNodeIndex

Pole typu Integer, przechowujące indeks ostatnio użytego węzła (czyli indeks węzła, na który wskazuje pole FLastUsedNode). Jeśli lista jest pusta, pole to przyjmuje wartość -1, natomiast w przypadku niepustej listy przechowuje indeks w postaci liczby naturalnej (indeks pierwszego węzła to 0, a ostatniego to n - 1).

FCount

Przechowuje liczbę węzłów znajdujących się w liście, w postaci liczby naturalnej typu Integer. Dla pustej listy posiada wartość 0, dla niepustej - liczbę dodatnią.

FOwnsElements

Jest to specjalne pole typu Boolean. Służy do określenia, czy podczas usuwania węzłów listy mają także zostać zwolnione instancje klas z pola Element, za pomocą metody Free. Jeśli pole to posiada wartość True - obiekty zostaną zwolnione z pamięci podczas usuwania węzłów pojedynczo, a także w destruktorze klasy, gdzie usuwane są wszystkie istniejące węzły listy.

Dzięki temu polu możliwe będzie tworzenie i usuwanie obiektów poza listą oraz traktowanie listy jako tymczasowego kontenera.

Metody klasy

Create

Konstruktor klasy ma za zadanie utworzyć instancję obiektu listy, a także zainicjować wartości wszystkich pól w taki sposób, aby od momentu utworzenia listy mogła być rozpoznana jako pusta.

constructor TDoublyLinkedList.Create(AOwnsElements: Boolean);
begin
  inherited Create();

  FFirstNode := nil;
  FLastNode := nil;

  FLastUsedNode := nil;
  FLastUsedNodeIndex := -1;

  FCount := 0;
  FOwnsElements := AOwnsElements;
end;

Wszystkim wskaźnikom przypisane zostają adresy zerowe, czyli wartości Nil. Indeks ostatnio użytego węzła to -1, a ilość węzłów na liście jest od tej pory równa 0. Pole FOwnsElements przyjmuje wartość z argumentu AOwnsElements.

Destroy

Zadaniem destruktora jest przede wszystkim zwolnienie instancji klasy listy z pamięci, ale także zwolnienie wszystkich istniejących w liście węzłów oraz ewentualnie wszystkich obiektów.

destructor TDoublyLinkedList.Destroy();
begin
  if FCount > 0 then
    DisposeRemainingNodes();

  inherited Destroy();
end;

Jeżeli lista nie jest pusta, najpierw trzeba zwolnić z pamięci wszystkie jej węzły. W tym celu sprawdzana jest wartość pola FCount i jeśli jest większa od 0 - wywoływana jest metoda DisposeRemainingNodes, zwalniająca węzły listy (metoda ta opisana jest w dalszej części artykułu).

DisposeRemainingNodes

Metoda ta służy do zwolnienia z pamięci wszystkich istniejących w liście węzłów oraz do ustawienia wszystkim polom wartości początkowych.

procedure TDoublyLinkedList.DisposeRemainingNodes();
var
  plnNext, plnDispose: PListNode;
begin
  if FOwnsElements then
    DisposeRemainingElements();

  plnDispose := FFirstNode;

  while plnDispose <> nil do
  begin
    plnNext := plnDispose^.NextNode;
    Dispose(plnDispose);

    plnDispose := plnNext;
  end;

  FFirstNode := nil;
  FLastNode := nil;

  FLastUsedNode := nil;
  FLastUsedNodeIndex := -1;

  FCount := 0;
end;

W pierwszej kolejności sprawdzana jest wartość pola FOwnsElements i jeśli posiada wartość True - wywołana zostaje metoda DisposeRemainingElements, która zwalnia z pamięci obiekty, znajdujące się w polu Element każdego istniejącego węzła.

Następnie w pętli od pierwszego węzła listy (czyli od węzła na który wskazuje pole FFirstNode) do ostatniego, zwalniane są struktury z pamęci. Do iterowania po węzłach listy używane są pola NextNode. Na koniec wszystkie wskaźniki są **Nil**owane, pole FLastUsedNodeIndex przyjmuje wartość -1, a pole FCount wartość 0.

Po wykonaniu kodu metody DisposeRemainingNodes, lista posiada dokładnie takie same wartości pól, jakie zostały ustawione w konstruktorze klasy. Dzięki temu metoda ta wykorzystywana jest w dwóch innych - w destruktorze klasy, a także w metodzie RemoveAllElements, służącej do wyczyszczenia listy.

DisposeRemainingElements

Ta metoda służy jedynie do zwolnienia z pamięci obiektów, przechowywanych w poszczególnych węzłach listy.

procedure TDoublyLinkedList.DisposeRemainingElements();
var
  plnElement: PListNode;
begin
  plnElement := FFirstNode;

  while plnElement <> nil do
  begin
    plnElement^.Element.Free();
    plnElement := plnElement^.NextNode;
  end;
end;

Jej zadaniem jest jedynie przeiterowanie po wszystkich węzłach oraz wywołanie metody Free dla każdego pola Element. Metoda ta uzywana jest tylko i wyłącznie przez metodę DisposeRemainingNodes i tylko i wyłącznie w przypadku, gdy wartość pola FOwnsElements jest równa True.

CreateNode

Jest to metoda funkcyjna, służąca do zaalokowania pamęci dla nowego węzła, który zwraca w rezultacie.

function TDoublyLinkedList.CreateNode(AElement: TObject): PListNode;
begin
  New(Result);

  Result^.PreviousNode := nil;
  Result^.NextNode := nil;
  Result^.Element := AElement;
end;

Alokuje pamięć dla nowego węzła, ustawia wskaźniki na wartość Nil, dlatego że w tym miejscu i momencie nie jest znane położenie nowo tworzonego węzła. Ostatni krok to przepisanie referencji do obiektu z parametru AElement do pola Element.

NodeAtIndex

Metoda ta służy do odnalezienia węzła, znajdującego się pod zadanym indeksem.

function TDoublyLinkedList.NodeAtIndex(AIndex: Integer): PListNode;
begin
  if FLastUsedNodeIndex - AIndex > FLastUsedNodeIndex shr 1 then
  begin
    FLastUsedNode := FFirstNode;
    FLastUsedNodeIndex := 0;
  end
  else
    if AIndex - FLastUsedNodeIndex > FCount - 1 - AIndex then
    begin
      FLastUsedNode := FLastNode;
      FLastUsedNodeIndex := FCount - 1;
    end;

  if FLastUsedNodeIndex < AIndex then
    while FLastUsedNodeIndex < AIndex do
    begin
      FLastUsedNode := FLastUsedNode^.NextNode;
      Inc(FLastUsedNodeIndex);
    end
  else
    while FLastUsedNodeIndex > AIndex do
    begin
      FLastUsedNode := FLastUsedNode^.PreviousNode;
      Dec(FLastUsedNodeIndex);
    end;

  Result := FLastUsedNode;
end;

Z racji tej, że wyszukiwanie węzła musi bazować na węźle-znaczniku - pierwsza część metody implementuje sprawdzenie, czy bliżej do szukanego węzła o indeksie z argumentu AIndex jest od początku listy, czy od jej końca.

Pierwszy warunek porównuje odległości od pierwszego węzła listy do węzła ostatnio użytego. Jeśli bliżej tej od pierwszego, ustawia znacznik na pierwszy węzeł listy, a indeks znacznika na 0. Natomiast jeżeli bliżej jest iterować od końca listy (drugi warunek) to ustawia znacznik na ostatni węzeł. W przypadku gdy oba warunki nie zostaną spełnione, wartość pól FLastUsedNode i FLastUsedNodeIndex nie zmieniają się, więc wyszukiwanie rozpocznie się od ostatnio użytego węzła;

Druga część metody to porównanie dwóch indeksów - FLastUsedNodeIndex i AIndex - aby określić kierunek iteracji; Jeśli szukany węzeł znajduje się po ostatnio użytym - iterowanie bazować będzie na polu NextNode i inkrementowaniu pola FLastUsedNodeIndex, do czasu, aż jego wartość zrówna się z indeksem z argumentu. W przeciwnym razie iterowanie odbędzie się w stronę początku listy, czyli z wykorzystaniem pola PreviousNode oraz dekrementowaniem indeksu znacznika.

Po wykonaniu warunków oraz niezbędnych iteracji, do rezultatu funkcji przypisany zostaje wskaźnik FLastUsedNode, który w tym momencie wskazuje na szukany węzeł.

Ważne w tym kodzie są dwa przypadki. Jeżeli szukanym węzłem jest ten ostatnio użyty (wartości pola FLastUsedItemIndex oraz argumentu AIndex są sobie równe), żaden z początkowych warunków sprawdzających odległości nie zostanie wykonany, a także żadna z pętli While z drugiej części metody nie wykona ani jednej iteracji. W rezultacie pole ze znacznikiem nie ulegnie zmianie.

Drugim wartym uwagi przypadkiem jest sytuacja, gdy odległość od pierwszego lub ostatniego węzła do węzła szukanego jest taka sama jak od tego ostatnio użytego. Wtedy wartość pola FLastUsedNode nie ulegnie zmianie, dzięki czemu iterowanie odbędzie się od ostatnio użytego węzła.

ElementAtIndex

Metoda ta wykorzystywana jest do pobrania obiektu z węzła, znajdującego się pod zadanym indeksem.

function TDoublyLinkedList.ElementAtIndex(AIndex: Integer): TObject;
begin
  Result := NodeAtIndex(AIndex)^.Element;
end;

Jak widać jest to tylko wrapper na metodę NodeAtIndex, dodatkowo zwracający instancję klasy z pola Element znalezionego węzła. Ważne jest tutaj użycie wymienionej metody, aby znacznik został przesunięty.

Metoda ElementAtIndex wykorzystywana jest jedynie jako akcesor dla domyślenej dla klasy właściwości Element (właściwości ta opisana jest kilka punktów dalej).

AddElement

Pierwsza publiczna metoda, służąca do dodania nowego elementu na koniec istniejącej listy.

procedure TDoublyLinkedList.AddElement(AElement: TObject);
var
  plnNew: PListNode;
begin
  plnNew := CreateNode(AElement);

  if FCount = 0 then
  begin
    FFirstNode := plnNew;
    FLastUsedNode := plnNew;
    FLastUsedNodeIndex := 0;
  end
  else
  begin
    plnNew^.PreviousNode := FLastNode;
    FLastNode^.NextNode := plnNew;
  end;

  FLastNode := plnNew;
  Inc(FCount);
end;

W pierwszej kolejności alokowana jest pamięć dla nowego węzła za pomocą prywatnej metody CreateNode, przekazując jej obiekt z parametru AElement.

Następnie sprawdzane jest, czy lista jest w pusta i jeśli tak - nowy węzeł wstawiany jest na początek listy, co wymusza ustawienie pola FFirstNode oraz pól znacznika. Natomiast jeśli lista nie jest pusta - nowy węzeł ląduje na końcu listy. Tutaj konieczne jest ustawienie pola PreviousNode na poprzedni węzeł, czyli na ostatni na liście - do tego celu używany jest wskaźnik z pola FLastNode. Z kolei węzeł z pola FLastNode nie będzie od tej chwili ostatnim, a przedostatnim, więc jego pole NextNode musi wskazywać na nowo utworzony węzeł.

Ostatnim krokiem jest ustawienie pola FlastNode na nowo utworzony węzeł oraz inkrementowanie pola FCount.

InsertElement

Druga publiczna metoda, służąca do wstawienia nowego węzła w miejsce o zadanym indeksie.

procedure TDoublyLinkedList.InsertElement(AIndex: Integer; AElement: TObject);
var
  plnNew, plnAtIndex: PListNode;
begin
  if AIndex >= FCount then
    AddElement(AElement)
  else
  begin
    plnNew := CreateNode(AElement);

    if AIndex = 0 then
    begin
      plnNew^.NextNode := FFirstNode;
      FFirstNode^.PreviousNode := plnNew;
      FFirstNode := plnNew;
    end
    else
    begin
      plnAtIndex := NodeAtIndex(AIndex);

      plnNew^.PreviousNode := plnAtIndex^.PreviousNode;
      plnNew^.NextNode := plnAtIndex;

      plnAtIndex^.PreviousNode^.NextNode := plnNew;
      plnAtIndex^.PreviousNode := plnNew;
    end;

    FLastUsedNode := FLastUsedNode^.PreviousNode;
    Inc(FCount);
  end;
end;

W pierwszej kolejności wykonuje sprawdzenie, czy indeks pod który ma być wstawiony nowy węzeł jest większy od indeksu ostatniego węzła w liście i jeśli tak - wywoływana jest metoda AddElement, do której zostaje przekazany argument AElement. Jeśli indeks wskazuje na istniejący węzeł to wykonywana jest standardowa procedura wstawiania.

Najpierw tworzony jest nowy węzeł w pamięci - alokacji obszaru pamęci dla nowego węzła dokonuje metoda CreateNode. Następnie sprawdzane jest, czy nowy węzeł ma być wstawiony na początek listy - w takim przypadku argument AIndex zawiera wartość 0. Jeśli warunek zostanie spełniony, nowemu węzłowi przypisuje się adres spod wskaźnika FFirstNode, a z kolei temu w polu PreviousNode przypisuje się adres nowego węzła. Na koniec do pola FFirstNode wpisuje się adres nowego węzła.

Jeśli nowy węzeł ma być wstawiony w dalszej części listy - metodą NodeAtIndex pobiera się węzeł, który zostanie zepchnięty w stronę końca listy o jedno oczko. W odróżnieniu od listy jednokierunkowej, nie potrzeba pobierać także drugiego wskaźnika, wskazującego na poprzedni węzeł, dlatego że ten spod indeksu posiada takie wskazanie w polu PreviousNode. Kolejną rzeczą jest ustawienie czterech wskaźników, aby od tego momentu włączyć nowy węzeł do listy.

Ostatnim krokiem jest inkrementacja pola FCount oraz cofnięcie o jedno oczko znacznika, aby pasował do indeksu z pola FLastUsedNodeIndex. Ta instrukcja wykonywana jest niezależnie od tego czy warunek zostanie spełniony czy nie. Dzieje się tak, dlatego że bez względu na to, czy nowy węzeł zostanie wstawiony na początku listy, w środku czy w miejsce ostatniego węzła, nowy węzeł zawsze wstawiany jest przed znacznikiem. Jeśli znacznik wskazuje pierwszy węzeł a nowy węzeł wstawiany jest na początek listy, nowy węzeł wstawiany jest przed znacznikiem. Natomiast jeśli znacznik wskazuje na jeden z dalszych węzłów, a nowy węzeł wstawiany jest po pierwszym węźle, ale przez znacznikiem - metoda NodeAtIndex przesunie znacznik w miejsce spod indeksu parametru AIndex. Nowy węzeł znów zostanie wstawiony przed węzeł na który wskazuje znacznik, dlatego też trzeba go cofnąć, aby zgrywał się z indeksem z pola FLastUsedNodeIndex.

Alternatywą jest pozostawienie wartości wskaźnika FLastUsedNode bez zmian, a w zamian inkrementacja samego indeksu, czyli wartości pola FLastUsedNodeIndex. W takim przypadku znacznik nie będzie wskazywał na nowo dodany węzeł, a na następny względem niego.

RemoveElement

Publiczna metoda służąca do usunięcia z listy węzła spod zadanego indeksu.

procedure TDoublyLinkedList.RemoveElement(AIndex: Integer);
var
  plnDispose: PListNode;
begin
  plnDispose := NodeAtIndex(AIndex);

  if FLastUsedNode = FLastNode then
  begin
    FLastUsedNode := FLastUsedNode^.PreviousNode;
    Dec(FLastUsedNodeIndex);
  end
  else
    FLastUsedNode := FLastUsedNode^.NextNode;

  if AIndex = 0 then
  begin
    plnDispose := FFirstNode;
    FFirstNode := FFirstNode^.NextNode;

    if FFirstNode = nil then
      FLastNode := nil
    else
      FFirstNode^.PreviousNode := nil;
  end
  else
  begin
    plnDispose^.PreviousNode^.NextNode := plnDispose^.NextNode;

    if plnDispose = FLastNode then
      FLastNode := plnDispose^.PreviousNode
    else
      plnDispose^.NextNode^.PreviousNode := plnDispose^.PreviousNode;
  end;

  if FOwnsElements then
    plnDispose^.Element.Free();

  Dispose(plnDispose);
  Dec(FCount);
end;

W pierwszej kolejności zostaje pobrany węzeł spod indeksu z argumentu AIndex (od tej pory znacznik wskazuje na usuwany węzeł). Następnie sprawdzane jest, czy znacznik przesunięty podczas pobierania węzła do usunięcia wskazuje na ostatni węzeł listy. Jeśli tak - wskaźnik znacznika zostaje cofnięty o jeden węzeł, a jego indeks z pola FLastUsedNodeIndex dekrementowany. W przeciwnym razie, gdy usuwany jest węzeł nieostatni - znacznikowi przypisuje się wskazanie na następny węzeł względem usuwanego.

Kolejnym krokiem jest spradzenie, który węzeł jest usuwany. Jeśli usuwany jest pierwszy węzeł listy - zmianie ulega wartość pola FFirstItem i przeskakuje o jedno oczko do przodu. Jeśli nowy pierwszy węzeł jest pusty (czyli metoda usuwa jedyny węzeł na liście), modyfikacji ulega także pole FLastNode, któremu przypisywany jest adres zerowy, czyli wartość Nil. W przeciwnym razie **Nil**owane jest pole PreviousNode nowego pierwszego węzła.

Jeżeli usuwany jest niepierwszy węzeł listy, zmianie ulega wskazanie spod NextNode w poprzednim węźle; Od teraz poprzedni węzeł w polu NextNode będzie posiadał adres następnego węzła po węźle usuwanym. W przypadku, gdy usuwany jest ostatni węzeł - przyjmie wartość Nil i sam stanie się ostatnim węzłem listy. Jeśli faktycznie tak jest, pole FLastNode musi otrzymać adres węzła poprzedniego od węzła usuwanego. Natomiast jeżeli usuwany jest nieostatni węzeł - pole PreviousNode w następnym węźle po usuwanym otrzymuje adres węzła poprzedzającego usuwany.

W całej tej zagmatwanej modyfikacji wskaźników zostaje zapamiętany adres usuwanego węzła w pomocniczej zmiennej plnDispose. Jeśli lista ma sama dbać o zwalnianie obiektów przechowywanych w liście (czyli gdy pole FOwnsElements zawiera wartość True) - obiekt przechowywany w polu Element usuwanego węzła zostaje najpierw zwolniony z pamięci, za pomocą wywołania metody Free.

Ostatnim krokiem jest zwolnienie pamęci zajmowanej przez usuwany węzeł (na ten blok pamięci wskazuje w tym miejscu tymczasowa zmienna plnDispose) oraz dekrementowanie pola FCount.

RemoveAllElements

Ostatnia z publicznych metod, służąca do usunięcia wszystkich węzłów listy i uczynienia jej pustą.

procedure TDoublyLinkedList.RemoveAllElements();
begin
  DisposeRemainingNodes();
end;

Jedynym jej zadaniem jest wywołanie metody DisposeRemainingNodes, która dokonuje fizycznego zwolnienia z pamięci wszystkich węzłów i ewentualnie także przechowywanych obiektów, a także ustawienia polom wartości początkowych.

Właściwości klasy

Count

Właściwość typu Integer służąca tylko do odczytu, łącząca bezpośrednio z polem FCount, zwracająca ilość przechowywanych w liście obiektów (a tym samym węzłów). Jeśli lista jest pusta - zwraca wartość 0, w przeciwnym razie liczbę dodatnią.

OwnsElements

Właściwość typu Boolean, także tylko do odczytu. Służy do sprawdzenia, czy lista ma zwalniać przechowywane obiekty podczas usuwania pojedynczych węzłów, podczas czyszczenia listy, a także podczas zwalniania z pamięci obiektu listy. Łączy bezpośrednio z polem FOwnsElements.

Element

Indeksowana właściwość tylko do odczytu, zwracająca instancję klasy ogólnego typu TObject. Wykorzystuje metodę ElementAtIndex jako metodę dostępową, do pobrania obiektu z węzła o indeksie podanym w AIndex. Właściwość ta opatrzona jest klauzulą Default, dzięki czemu jest domyślną właściwością klasy.

Domyślna właściwość oznacza, że nie ma obowiązku wstawiania operatora odniesienia oraz nazwy właściwości pomiędzy identyfikatorem obiektu a nawiasami z indeksem. Mając np. zmienną DoublyLinkedList typu TDoublyLinkedList, posiadającą referencję do instancji klasy listy, gdyby właściwość Element nie była domyślna, pobranie pierwszego obiektu w liście wyglądałoby tak:

objElement := DoublyLinkedList.Element[0];

ale że właściwość Element jest domyślna - jej nazwy nie trzeba zapisywać:

objElement := DoublyLinkedList[0];

Podsumowanie

W ten sposób przygotowana lista może być użyta do przechowywania dowolnych obiektów, nie tylko tej samej klasy. Dzięki temu, że listy dwukierunkowe nie potrzebują ciągłego bloku pamięci i każdy węzeł może znajdować się w dowolnym miejscu pamięci - może zawierać dowolną ilość węzłów (mniej więcej tyle, ile jest dostępnej dla aplikacji pamęci). A dodatkowo dzięki zapamiętywaniu ostatnio użytego węzła, jej przeszukiwanie jest co najmniej o połowe szybsze niż w przypadku tradycyjnej listy dwukierunkowej.

Prezentowana w tym artykule klasa listy jest klasą bazową. Aby móc z niej wygodnie korzystać w przypadku przechowywania obiektów tej samej klasy, należy przygotować klasę opakowującą, dziedziczącą po klasie bazowej. W tym celu wystarczy przygotować zestaw metod, które maskować będą rzutowanie ogólnego typu TObject na typ klasy, której obiekty są przechowywane.

Kod klasy został solidnie przetestowany i zdebugowany, dzięki czemu jest pewne, że zawsze będzie działał poprawnie oraz nie będzie powodował wycieków pamięci. W przypadku jakichkolwiek pytań bądź sugestii proszę o kontakt przez wiadomość prywatną w tym serwisie.

Niniejszy materiał udostępniany jest bez licencji i bez konkretnych wytycznych co do zastosowań domowych, darmowych oraz komercyjnych - bierzcie i jedzcie z tego wszyscy.

Załączniki

DblLinkedList.zip - archiwum zawierające moduł DblLinkedList.pp z implementacją listy.

0 komentarzy