上一期整體探討了一下單向鏈表。在這基礎上補充兩個點,分別是單向循環鏈表和雙向循環鏈表。從字面中可以看出是將鏈表形成個環結構,區別在於這個環是只能一個方向還是兩個方向循環。
單向循環鏈表
單向循環鏈表可以理解為將單向鏈表的最后一個節點指向第一個節點,將鏈表形成一個環。那么由單向循環鏈表處理的數組數據接口比着單向鏈表來說,在添加元素和刪除元素這兩個情況下做不同的處理,保證還是一個環的狀態。
添加元素(add(int index, E element)
)
添加元素時需要特別處理 index 為 0 的情況,當在 index 為 0 的位置插入元素時,邏輯上的處理是將新元素的 next 指向原來的 first 位置元素,最后一個元素的 next 指向這個新的 first 位置元素。
在代碼處理上要特別注意,若是直接處理 first 節點指向,鏈表整體數量會增加,那么在拿最后一個元素的位置的時候就會容易出錯。這里的處理是先創建一個 newFirst 節點,保持原來完整的鏈表。
@Override
public void add(int index, E element) {
// 需要先做判斷
rangeCheckOfAdd(index);
// 當插入到 index == 0 的位置,需要把最后一個 node 的 next 指向 first(index == 0)
if (index == 0) {
// 插入第一個,並不是說 first 就是 null。也是指向有值的
// 創建 newFirst 的原因是:直接用 first 指向,會導致獲取最后一個 node 不是 size -1 的位置
Node<E> newFirst = new Node<>(element, first);
// 拿到最后一個節點
Node<E> last = size == 0 ? newFirst: node(size -1);
last.next = newFirst;
first = newFirst;
}
else {
Node<E> prev = node(index-1);
Node<E> node = new Node<>(element, prev.next);
prev.next = node;
}
size ++;
}
刪除元素(E remove(int index)
)
刪除元素時,也是要特別留意刪除 index == 0 情況。在 index == 0 情況下,如果 size == 1,則是刪除最后一個元素,那么需要 first 直接等於 null,否則這個環不會被銷毀;如果 size != 1 的情況下,需要在處理之后也要更改一下最后一個節點的指向,將其指向新的 first 指針。
@Override
public E remove(int index) {
// 需要先判斷
rangeCheck(index);
Node<E> old = first;
if (index == 0) {
// 只是刪除對應的坐標,不是刪除所有
// size==1,進行指向的時候,無法被刪除完成
if (size == 1) {
first = null;
}
else {
Node<E> last = node(size -1);
first = first.next;
last.next = first;
}
}
else {
Node<E> prev = node(index -1);
old = prev.next;
prev.next = prev.next.next;
}
size --;
return old.element;
}
雙向循環鏈表
雙向循環鏈表也是將鏈表形成一個環,環中的每個節點都有指向前一個元素的指針和指向后一個元素的指針。鏈表的數據結構中也會多一個last節點來指向最后一個元素。
區別於其他類型鏈表的地方依然是添加元素和刪除元素這兩個。在邏輯處理上會有不同,因為多了指向,在代碼處理上會多了一些便捷,但是邏輯上多了一些需要考慮的點。除此之外,雙向循環鏈表在遍歷元素的處理中可以用二分法來處理。
@SuppressWarnings("unused")
public class CircleLinkList<E> extends AbstractList<E> {
Node<E> first;
// 鏈表結構增加 last 指針
Node<E> last;
private static class Node<E> {
// 節點結構增加指向前一個節點指針
Node<E> prev;
Node<E> next;
E element;
public Node(Node<E> prev, E element, Node<E> next) {
this.prev = prev;
this.element = element;
this.next = next;
}
}
添加元素(add(int index, E element)
)
雙向鏈表中添加元素,主要考慮 index == size情況,這種情況下,可以通過 last 是否為 null 判斷鏈表中是否有元素。添加后的節點要更改它的前一個節點指向和后一個節點指向。當只有一個元素是,它的 first 和 last 是同一個。
這里有個新奇的點,判斷 index == 0 的情況,是通過 prev == last 來判斷。因為 first 節點的前一個節點就是 last 節點。
@Override
public void add(int index, E element) {
rangeCheckOfAdd(index);
if (index == size) { // 在最后面添加
Node<E> oldLast = last;
last = new Node<>(oldLast, element, first);
if (oldLast == null) { // 數組中還沒有元素
first = last;
}
else {
oldLast.next = last;
}
last.next = first;
first.prev = last;
}
else {
Node<E> next = node(index);
Node<E> prev = next.prev;
Node<E> node = new Node<>(prev, element, next);
next.prev = node;
prev.next = node;
if (prev == last) { // index == 0
first = node;
}
}
size++;
}
刪除元素(E remove(int index)
)
刪除元素的時候,如果 size == 1 是,那么就需要將 first 節點和 last 節點都指向 null。
其他情況中,如果 index == 0 則需要更改 first 節點的指向,如果 index == size-1 則需要更改 last 節點的指向。
@Override
public E remove(int index) {
Node<E> node = node(index);
if (size == 1) {
first = null;
last = null;
}
else {
Node<E> prev = node.prev;
Node<E> next = node.next;
prev.next = next;
next.prev = prev;
if (node == first ) { // index == 0
first = next;
}
if (node == last) { // index == size -1
last = prev;
}
}
size--;
return node.element;
}
遍歷元素(Node<E> node(int index)
)
正因為可以在這個環中的兩個不同方向遍歷,那么就可以先判斷 index 與中間位置,來確定從頭還是從尾遍歷。進而減少遍歷的次數。這是一個很好的處理。
private Node<E> node(int index) {
rangeCheck(index);
if (index < size/2) {
Node<E> node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
}
else {
Node<E> node = last;
for (int i = size -1; i > index; i++) {
node = node.prev;
}
return node;
}
}