6. Opowiedz nam o Liście
Lista to interfejs reprezentujący uporządkowaną strukturę obiektów, który nazywa się listą. „Sztuczka” tej struktury polega na tym, że elementy zawarte w Liście można wstawiać, modyfikować lub usuwać za pomocą indeksu, czyli wewnętrznego identyfikatora Listy . Inaczej mówiąc, indeks oznacza: „ile elementów jest od początku listy”. Pierwszy element List ma indeks 0, drugi ma indeks 1 i tak dalej. Zatem piąty element jest oddalony o cztery elementy od początku listy. Jak wspomniano powyżej, kolejność dodawania pozycji do listy jest ważna. Dlatego struktura danych nazywa się listą . Podajemy metody unikalne dla tej struktury, które mają na celu pracę z elementami według indeksu:- get - zwraca element na podanej pozycji (według wartości indeksu),
- usuń - usuwa element z podanej pozycji,
- set - zastępuje element na określonej pozycji elementem określonym w metodzie.
- push - dodaje przekazany element na górę stosu;
- peek - zwraca element znajdujący się na szczycie stosu;
- pop - również zwraca element znajdujący się na górze stosu, ale go usuwa;
- pusty – sprawdza, czy stos jest pusty – prawda czy nie – fałsz ;
- search - przeszukuje stos pod kątem danego elementu. Jeśli element zostanie znaleziony, zwracany jest jego numer kolejny względem wierzchołka stosu. Jeżeli element nie zostanie znaleziony, zwracana jest wartość -1.
7. Opowiedz nam o Mapie
Jak wspomniano powyżej, mapa to kolekcja posiadająca odrębną strukturę interfejsów i ich implementacji. Jest oddzielny, ponieważ tutaj wartości nie są przechowywane pojedynczo, ale w parze „klucz-wartość”. Podstawowe metody map :- put(klawisz K, wartość V) - dodanie elementu do Mapy;
- get(Object key) - szukaj wartości według klucza;
- zawieraKey(Object key) — sprawdza Mapę pod kątem obecności tego klucza;
- zawieraValue(Wartość obiektu) - sprawdza Mapę pod kątem obecności tej wartości;
- usuń(obiekt klucz) - usunięcie wartości według jej klucza.
- HashMap - przeznaczony do przechowywania wartości w losowej kolejności, ale pozwala na szybkie wyszukiwanie elementów mapy. Umożliwia określenie klucza za pomocą słowa kluczowego null , ale nie więcej niż raz, ponieważ pary z tymi samymi kluczami są zapisane jeden na drugim. Głównym warunkiem jest unikalność kluczy: wartości można powtarzać (może być kilka wartości null).
- LinkedHashMap to odpowiednik HashMap , który przechowuje wartości w kolejności, w jakiej zostały dodane. W związku z tym, podobnie jak LinkedList , ma nagłówek - głowę podwójnie połączonej listy. Po zainicjowaniu wskazuje na siebie.
LinkedHashMap ma również accessOrder , który określa sposób dostępu do elementów, gdy używany jest iterator. Jeżeli accessOrder ma wartość false, dostęp zostanie wykonany w kolejności wstawiania elementów. Jeśli ma wartość true, elementy będą uporządkowane w kolejności, do której ostatnio uzyskiwano dostęp (element, do którego ostatnio uzyskiwano dostęp, zostanie umieszczony na końcu).
- TreeMap to mapa , która sortuje elementy według wartości kluczy. Podobny do TreeSet , ale dla par w oparciu o wartości kluczy. Aby ustawić reguły sortowania TreeMap , klucze muszą implementować interfejs Comparable . W przeciwnym razie powinien istnieć Komparator zorientowany na klucz (ten, który jest określony w konstruktorze TreeMap ), TreeSet - zaimplementowany z obiektem TreeMap w środku, w którym tak naprawdę dzieje się cała magia.
Więcej o sortowaniu w TreeMap przy użyciu czerwono-czarnych drzew przeczytasz w artykule o możliwościach TreeMap .
- Hashtable jest podobny do HashMap , ale nie pozwala na przechowywanie wartości null jako kluczy lub wartości. Jest starannie zsynchronizowany z wielowątkowego punktu widzenia, co z kolei oznacza, że jest bezpieczny z wielowątkowego punktu widzenia. Ale ta implementacja jest przestarzała i powolna, więc teraz nie zobaczysz Hashtable w mniej lub bardziej nowych projektach.
8. ArrayList vs LinkedList. Którego z nich najlepiej używać?
To pytanie jest prawdopodobnie najbardziej popularne w przypadku struktur danych i niesie ze sobą pewne pułapki. Zanim odpowiemy, dowiedzmy się więcej o tych strukturach danych. ArrayList implementuje interfejs List i działa na wewnętrznej tablicy, która jest rozwijana w razie potrzeby. Kiedy tablica wewnętrzna jest całkowicie wypełniona i trzeba wstawić nowy element, tworzona jest nowa tablica o rozmiarze (oldSize * 1,5) +1. Następnie wszystkie dane ze starej tablicy zostaną skopiowane do nowego + nowego elementu, a stary zostanie usunięty przez moduł zbierający elementy bezużyteczne . Metoda add dodaje element do ostatniej pustej komórki tablicy. Oznacza to, że jeśli mamy tam już 3 elementy, doda kolejny do czwartej komórki. Przejdźmy do wykonania podstawowych metod:- get(int indeks) - pobranie elementu tablicy według indeksu jest najszybsze w O(1) ;
- add(Object obj) - jeśli w tablicy wewnętrznej jest wystarczająco dużo miejsca na nowy element, to przy normalnym wstawieniu zostanie wykorzystany czas O(1) , ponieważ dodawanie jest kierowane do ostatniej komórki.
Jeśli będziemy musieli utworzyć nową tablicę i skopiować do niej zawartość, to nasz czas będzie wprost proporcjonalny do liczby elementów w tablicy O(n) ;
- usuń(int indeks) - usuwając element np. ze środka, otrzymamy czas O(n/2), gdyż będziemy musieli przesunąć elementy na prawo od niego o jedną komórkę do tyłu. Odpowiednio, jeśli usuwamy od początku listy, to O(n), od końca - O(1);
- add(int indeks, Object obj) - sytuacja podobna do usuwania: przy dodawaniu do środka będziemy musieli przesunąć elementy z prawej strony o jedną komórkę do przodu, więc czas będzie wynosił O(n/2). Oczywiście od początku - O(n), od końca - O(1);
- set(int indeks, Object obj) - tutaj sytuacja jest inna, ponieważ wystarczy znaleźć żądany element i nad nim nadpisać, nie przesuwając reszty, więc O(1).
- get(int indeks) - szukając elementu znajdującego się w środku listy, rozpoczyna przeszukiwanie wszystkich elementów po kolei, aż do znalezienia żądanego. Logicznie rzecz biorąc, wyszukiwanie powinno zająć O(n/2) , ale LinkedList ma również ogon, więc wyszukiwanie odbywa się jednocześnie z obu stron. Odpowiednio czas zmniejsza się do O(n/4) .
Jeżeli element znajduje się blisko początku listy lub jej końca, to czas będzie wynosić O(1) ;
- add(Object obj) - podczas dodawania nowego elementu element „ogon” będzie miał link do dodanego kolejnego elementu, a nowy otrzyma link do tego poprzedniego elementu i stanie się nowym „ogonem”. Odpowiednio czas będzie wynosić O(1) ;
- usuń(int indeks) - logika podobna do metody get(int indeks) . Aby usunąć element ze środka listy, należy go najpierw znaleźć. To znowu O(n/4) , podczas gdy samo usunięcie właściwie nic nie bierze, ponieważ zmienia jedynie wskaźnik sąsiednich obiektów (zaczynają się do siebie odnosić). Jeśli element znajduje się na początku lub na końcu, to znowu - O(1) ;
- add(int indeks, obiekt obj) i set(int indeks, obiekt obj) - metody będą miały złożoność czasową identyczną jak get(int indeks) , ponieważ większość czasu spędza się na poszukiwaniu elementu. Zatem dla środka listy - O(n/4) , na początek - O(1).
Operacja | Lista tablic | Połączona lista |
---|---|---|
Uzyskaj według indeksu get(index) | O(1) | W środku O(n/4) |
Dodaj nowy element add(obj) |
O(1) Jeśli chcesz skopiować tablicę, to - O(n) |
O(1) |
Usuń element usuń(int indeks) |
Od początku - O(n) Od środka - O(n/2) Od końca - O(1) |
W środku - O(n/4) Na końcu czy na początku - O(n) |
Dodaj element add(int indeks, obiekt obj) |
Powrót do góry - O(n) Do środka - O(n/2) Do końca - O(1) |
W środku - O(n/4) Na końcu czy na początku - O(n) |
Zamień zestaw elementów (indeks, obj) | O(1) |
W środku - O(n/4) Na końcu czy na początku - O(n) |
- najlepszy wybór, jeśli najczęstszą operacją jest wyszukiwanie elementu, nadpisywanie elementu;
- najgorszy wybór jeśli operacja polega na wstawieniu i usunięciu na początku-środku, ponieważ nastąpią operacje przesunięcia elementów po prawej stronie.
- najlepszy wybór jeśli naszą częstą operacją jest wstawianie i usuwanie na początku-środku;
- najgorszy wybór, jeśli najczęstszą operacją jest wyszukiwanie.
9. W jaki sposób elementy są przechowywane w HashMap?
Kolekcja HashMap zawiera wewnętrzną tablicę Node[] table , której komórki nazywane są także segmentami . Węzeł zawiera:- klucz - link do klucza,
- wartość - odniesienie do wartości,
- hash - wartość skrótu,
- next -link do następnego węzła .
- Klucz jest sprawdzany pod kątem wartości null . Jeśli ma wartość null , klucz zostanie zapisany w komórce table[0] , ponieważ kod skrótu dla wartości null wynosi zawsze 0.
- Jeśli klucz nie ma wartości null , wywoływana jest metoda hashcode() obiektu klucza , która zwróci jego kod skrótu. Ten kod skrótu służy do określenia komórki tablicy, w której będzie przechowywany obiekt Node.
- Następnie ten kod skrótu jest umieszczany w wewnętrznej metodzie hash() , która oblicza kod skrótu, ale w rozmiarze tablicy table[] .
- Następnie, w zależności od wartości skrótu, Node umieszczany jest w określonej komórce tablicy table [] .
- Jeśli komórka table[] użyta do zapisania bieżącego elementu Node nie jest pusta, ale zawiera już jakiś element, wówczas elementy Node są iterowane po kolejnej wartości , aż do osiągnięcia ostatniego elementu. Oznacza to, że ten, którego następne pole ma wartość null .
Podczas tego wyszukiwania klucz chronionego obiektu Node jest porównywany z kluczami przeszukiwanych obiektów:
- jeżeli zostanie znalezione dopasowanie, wyszukiwanie zostanie zakończone, a nowy Węzeł nadpisze Węzeł , w którym znaleziono dopasowanie (nadpisane zostanie jedynie pole jego wartości );
- jeśli nie zostaną znalezione żadne kluczowe dopasowania, nowy węzeł stanie się ostatnim na tej liście, a poprzedni będzie miał do niego następny link.
10. Wyjaśnij iterator
Na powyższym diagramie mapowania hierarchii kolekcji interfejs kolekcji był miejscem, w którym zaczynała się cała hierarchia, ale w praktyce nie do końca tak jest. Kolekcja dziedziczy po interfejsie za pomocą metody iterator() , która zwraca obiekt implementujący interfejs Iterator<E> . Interfejs Iteratora wygląda następująco:public interface Iterator <E>{
E next();
boolean hasNext();
void remove();
}
next() - wywołując tę metodę można uzyskać kolejny element. hasNext() - pozwala dowiedzieć się, czy jest następny element i czy osiągnięto koniec kolekcji. A gdy nadal są elementy, hasNext() zwróci true . Zwykle metoda hasNext() jest wywoływana przed metodą next() , ponieważ next() zgłosi wyjątek NoSuchElementException , gdy osiągnie koniec kolekcji . usuń() – Usuwa element pobrany przez ostatnie wywołanie metody next() . Celem Iteratora jest iteracja po elementach. Na przykład:
Set<Integer> values = new TreeSet<>();
values.add(5);
values.add(3);
values.add(6);
values.add(8);
values.add(2);
values.add(4);
values.add(1);
values.add(7);
Iterator<Integer> iter = values.iterator();
while(iter.hasNext()){
System.out.println(iter.next());
}
W rzeczywistości pętla for-each jest implementowana pod maską za pomocą iteratora. Więcej na ten temat możesz przeczytać tutaj . List udostępnia własną wersję iteratora, ale fajniejszą i bardziej wyrafinowaną - ListIterator . Interfejs ten rozszerza Iterator i posiada dodatkowe metody:
- hasPrevious zwróci wartość true, jeśli w kolekcji znajduje się poprzedni element, w przeciwnym razie false;
- poprzedni zwraca bieżący element i przechodzi do poprzedniego; jeśli go nie ma, zgłaszany jest wyjątek NoSuchElementException;
- add wstawi przekazany obiekt przed elementem, który ma zostać zwrócony przez następne wywołanie next () ;
- set przypisuje bieżącemu elementowi referencję do przekazanego obiektu;
- nextIndex zwraca indeks następnego elementu. Jeśli czegoś takiego nie ma, zwracany jest rozmiar listy;
- poprzedniIndeks zwraca indeks poprzedniego elementu. Jeśli go nie ma, zwracana jest liczba -1.
GO TO FULL VERSION