在上一篇博客中,博主和大家一起實現了線性表的鏈式存儲,由於鏈表中每個節點都只有一個指針域指向着後繼節點,因此這種鏈表也叫單鏈表。
這種鏈表的缺點顯而易見,因爲只能夠單向遍歷,即使我們想要訪問最後一個元素也不得不從第一個元素開始遍歷到最後一個元素。
爲了解決這個問題,雙鏈表應運而生。和單鏈表十分相似,唯一的區別在於每個節點當中包含兩個指針域,分別指向着這個節點的前驅節點和後繼節點。
因此,對於每一個數據元素ai來說,除了存儲其本身的數據信息之外,還需要存儲一個指示其直接後繼元素的內存位置和一個其直接前驅元素的內存位置。我們把存儲數據元素信息的域稱爲數據域,把存儲直接後繼元素位置的域稱爲後繼指針域,把存儲直接前驅元素位置的域稱爲前驅指針域。這三部分信息組成數據元素ai的存儲映像,稱爲節點(Node)。n個節點連接成一個鏈表(a1,a2, … ,an),因爲這個鏈表的每個節點中包含兩個指針域(指向它的直接後繼元素和前驅元素),所以這種鏈表被稱爲雙鏈表。
與單鏈表相比,如果我們需要訪問較爲靠後的元素數據,只需要從尾節點向前遍歷即可。
- 下面我們就來一起用Java實現一個雙鏈表MyDoublyLinkedList
//雙鏈表中的首標記節點,此節點不存放元素,前驅結點指向null,後繼節點指向鏈表中的第一個元素節點
private Node firstFlagNode;
//雙鏈表中的尾標記節點,此節點不存放元素,後繼結點指向null,前驅節點指向鏈表中的最後一個元素節點
private Node lastFlagNode;
//雙鏈表中元素的數量
private int size;
public MyDoublyLinkedList() {
firstFlagNode = new Node (null,null,null);
//首標記節點的後繼指向尾標記節點,尾標記節點的前驅指向首標記節點
lastFlagNode = new Node (null,firstFlagNode,null);
firstFlagNode.nextNode = lastFlagNode;
size = 0;
}
//內部類Node,用來表示雙鏈表中每一個數據元素
class Node{
//數據域
E data;
//前驅指針域,指向該節點的前一個節點
Node prevNode;
//後繼指針域,指向該節點的後一個節點
Node nextNode;
public Node(E data, Node prevNode, Node nextNode) {
this.data = data;
this.prevNode = prevNode;
this.nextNode = nextNode;
}
}
- 接下來我們實現獲取元素個數和查看是否爲空的方法,很簡單單行代碼即可實現
@Override
public int size() {
// TODO Auto-generated method stub
return this.size;
}
@Override
public boolean isEmpty() {
// TODO Auto-generated method stub
return size()==0;
}
- 然後是兩種不同的插入方法
@Override
public boolean add(E e) {
// TODO Auto-generated method stub
add(size(),e);
return true;
}
@Override
public void add(int index, E element) {
// TODO Auto-generated method stub
//當index=0時,相當於在第一個位置插入元素,當index=size時相當於在鏈表末尾插入一個元素
if(index<0 || index>size())
throw new IndexOutOfBoundsException();
Node newNode = new Node(element,null,null);
Node n = getPrevNodeByIndex(index);
//在它之後插入新的節點
n.nextNode.prevNode = newNode;
newNode.prevNode = n;
newNode.nextNode = n.nextNode;
n.nextNode = newNode;
this.size++;
return;
}
//私有方法獲取當前index位置的前一個節點
private Node getPrevNodeByIndex(int index) {
//雙鏈表能夠從前後兩個方向遍歷,根據index的大小選擇方向遍歷
Node n = null;
if(index<size()/2) {
n = firstFlagNode;
for(int i=0; i<index; i++) {
n = n.nextNode;
}
}else {
n = lastFlagNode;
for(int i = size(); i>=index; i--) {
n = n.prevNode;
}
}
return n;
}
- 緊接着是兩個不同的刪除方法
@Override
public boolean remove(Object o) {
// TODO Auto-generated method stub
if(o == null) {
for(Node n = firstFlagNode.nextNode; n!=null&&n!=lastFlagNode; n = n.nextNode) {
if(n.data == null) {
//刪除本節點
n.prevNode.nextNode = n.nextNode;
n.nextNode.prevNode = n.prevNode;
//清空舊節點的數據域與指針域,方便GC工作
n.data = null;
n.nextNode = null;
n.prevNode = null;
n = null;
this.size--;
return true;
}
}
return false;
}else {
for(Node n = firstFlagNode.nextNode; n!=null&&n!=lastFlagNode; n = n.nextNode) {
if(o.equals(n.data)) {
//刪除本節點
n.prevNode.nextNode = n.nextNode;
n.nextNode.prevNode = n.prevNode;
//清空舊節點的數據域與指針域,方便GC工作
n.data = null;
n.nextNode = null;
n.prevNode = null;
n = null;
this.size--;
return true;
}
}
return false;
}
}
@Override
public E remove(int index) {
// TODO Auto-generated method stub
//檢查index的邊界
checkIndexValidation(index);
//獲取index位置的節點
Node targetNode = getPrevNodeByIndex(index).nextNode;
E oldElement = targetNode.data;
//舊節點的前驅節點指向舊節點的後繼節點,舊節點的後繼節點指向舊節點的前驅節點
targetNode.prevNode.nextNode = targetNode.nextNode;
targetNode.nextNode.prevNode = targetNode.prevNode;
//清空舊節點的數據域與指針域,方便GC工作
targetNode.data = null;
targetNode.nextNode = null;
targetNode.prevNode = null;
targetNode = null;
this.size--;
return oldElement;
}
private void checkIndexValidation(int index) {
if(index<0 || index>=size())
throw new ArrayIndexOutOfBoundsException();
}
- 之後是獲取,修改,查看是否包含,以及清空整個列表這幾個簡單的方法了:
@Override
public void clear() {
// TODO Auto-generated method stub
for(Node n = firstFlagNode.nextNode; n!=null&&n!=lastFlagNode; ) {
Node next = n.nextNode;
n.data = null;
n.nextNode = null;
n.prevNode = null;
n = next;
}
firstFlagNode.nextNode = lastFlagNode;
lastFlagNode.prevNode = firstFlagNode;
this.size = 0;
}
@Override
public E get(int index) {
// TODO Auto-generated method stub
checkIndexValidation(index);
return getPrevNodeByIndex(index).nextNode.data;
}
@Override
public E set(int index, E element) {
// TODO Auto-generated method stub
checkIndexValidation(index);
Node targetNode = getPrevNodeByIndex(index).nextNode;
E data = targetNode.data;
targetNode.data = element;
return data;
}
@Override
public boolean contains(Object o) {
// TODO Auto-generated method stub
if(o == null) {
for(Node n = firstFlagNode.nextNode; n!=null&&n!=lastFlagNode; n = n.nextNode) {
if(n.data == null) {
return true;
}
}
return false;
}else {
for(Node n = firstFlagNode.nextNode; n!=null&&n!=lastFlagNode; n = n.nextNode) {
if(o.equals(n.data)) {
return true;
}
}
return false;
}
}
雙鏈表相比單鏈表來說耗費了更多的空間去存儲前驅節點的位置,以此來獲得某些操作更好的時間性能。這種以少量空間換取更優的性能做法,在很多地方也都可以見到。
本節博客的源碼全部放在這裏,歡迎大家下載,下載後記得修改源碼中的package名字哦。