連結串列(LinkedList)
連結串列可能是繼陣列之後第二種使用最廣泛的通用儲存結構。
-
單鏈表
-
雙端連結串列
-
有序連結串列
-
雙向列表
-
有迭代器的列表
連結串列與陣列一樣,都作為資料的基本儲存結構,但是在儲存原理上二者是不同的。在陣列中,資料是儲存在一段連續的記憶體空間中,我們可以通過下標來訪問陣列中的元素;而在連結串列中,元素是儲存在不同的記憶體空間中,前一個元素的位置維護了後一個元素在記憶體中的地址,在Java中,就是前一個元素維護了後一個元素的引用。在本教程我們,我們將連結串列中的每個元素稱之為一個節點(Node
)。對比陣列, 連結串列的資料結構可以用下圖表示
這張圖顯示了一個連結串列的資料結構,連結串列中的每個Node都維護2個資訊:一個是這個Node自身儲存的資料Data
Next
表示。對於最後一個Node,因為沒有下一個元素了,所以其並沒有引用其他元素,在圖中用紫色框來表示。
這張圖主要顯示的是連結串列中Node的內部結構和Node之間的關係。一般情況下,我們在連結串列中還要維護第一個Node的引用,原因是在連結串列中訪問資料必須通過前一個元素才能訪問下一個元素,如果不知道第一個Node的話,後面的Node都不可以訪問。事實上,對連結串列中元素的訪問,都是從第一個Node中開始的,第一個Node是整個連結串列的入口;而在陣列中,我們可以通過下標進行訪問元素。
Node.Java:
- publicclassNode{
- //Node中維護的資料
- privateObjectdata;
- //下一個元素的引用
- privateNodenext;
- //settersandgetters
- }
一、單鏈表Java實現
本節介紹單鏈表的Java實現,我們用SingleLinkList
來表示。
分析:
1、SingleLinkList中要維護的資訊:維護第一個節點(firstNode
)的引用,作為整個連結串列的入口;
2、插入操作分析:基於連結串列的特性,插入到連結串列的第一個位置是非常快的,因為只要改變fisrtNode的引用即可。因此對於單鏈表,我們會提供addFirst
方法。
3、查詢操作分析:從連結串列的fisrtNode開始進行查詢,如果確定Node中維護的data就是我們要查詢的資料,即返回,如果不是,根據next獲取下一個節點,重複這些步驟,直到找到最後一個元素,如果最後一個都沒找到,返回null。
4、刪除操作分析
首先查詢到要刪除的元素節點,同時將這個節點的上一個節點和下一個節點也要記錄下來,只要將上一個節點的next引用直接指向下一個節點即可,這就相當於 刪除了這個節點。如果要刪除的是第一個節點,直接將LinkList的firstNode指向第二個節點即可。如果刪除的是最後一個節點,只要將上一個節 點的next引用置為null即可。上述分析,可以刪除任意節點,具有通用性但是效率較低。通常情況下,我們還會提供一個removeFirst
方法,因為這個方法效率較高,同樣只要改變fisrtNode的引用即可。
此外,根據情況而定,可以選擇是否要維護連結串列中元素的數量size
,不過這不是實現一個連結串列必須的核心特性。
SingleLinkList.java
- publicclassSingleLinkList<T>{
- //連結串列中第一個節點
- protectedNodefirstNode=null;
- //連結串列中維護的節點總量
- protectedintsize;
- /**
- *新增到連結串列的最前面
- *@paramelement
- */
- publicvoidaddFirst(Telement){
- Nodenode=newNode();
- node.setData(element);
- NodecurrentFirst=firstNode;
- node.setNext(currentFirst);
- firstNode=node;
- size++;
- }
- /**
- *如果連結串列中包含要刪除的元素,刪除第一個匹配上的要刪除的元素,並且返回true;
- *如果沒有找到要刪除的元素,返回false
- *@paramelement
- */
- publicbooleanremove(Telement){
- if(size==0){
- returnfalse;
- }
- if(size==1){
- firstNode=null;
- size--;
- }
- Nodepre=firstNode;
- Nodecurrent=firstNode.getNext();
- while(current!=null){
- /*如果當前節點中維護的值就是要刪除的值,
- 直接將上一個節點pre的next應用指向當前節點current的下一個節點介面*/
- if((current.getData()==null&&element==null)
- ||(current.getData().equals(element))){
- pre.setNext(current.getNext());
- size--;
- returntrue;
- }
- //如果當前元素不是要刪除的元素,繼續迴圈
- pre=current;
- current=current.getNext();
- }
- returnfalse;
- }
- /**
- *如果包含返回true,如果不包含,返回false
- *@paramelement
- *@return
- */
- publicbooleancontains(Objectelement){
- if(size==0){
- returnfalse;
- }
- Nodecurrent=firstNode;
- while(current!=null){
- if((current.getData()==null&&element==null)
- ||(current.getData().equals(element))){
- returntrue;
- }
- //如果當前元素不是要刪除的元素,繼續迴圈
- current=current.getNext();
- }
- returnfalse;
- }
- publicbooleanisEmpty(){
- returnsize==0;
- }
- publicintsize(){
- returnsize;
- }
- /**
- *打印出所有的元素
- */
- publicvoiddisplay(){
- if(!isEmpty()){
- Nodecurrent=firstNode;
- while(current!=null){
- System.out.print(current.getData()+"\t");
- current=current.getNext();
- }
- }
- }
- /**
- *刪除第一個元素
- */
- publicTremoveFisrt(){
- Noderesult=null;
- if(size!=0){
- result=firstNode.getNext();
- firstNode=result;
- return(T)result.getData();
- }
- returnnull;
- }
- publicTgetFirst(){
- return(T)firstNode.getData();
- }
- }
測試新增addFirst
- @Test
- publicvoidtestAddFisrt(){
- SingleLinkList<Integer>linkList=newSingleLinkList<Integer>();
- for(inti=0;i<10;i++){
- linkList.addFirst(i);
- }
- linkList.display();
- }
控制檯輸出:
9 8 7 6 5 4 3 2 1 0
因為總是新增到最前面,因此時降序的。
需要注意的是:在本案例中,不能同時呼叫addFirst,addLast。因為我們在addFirst方法中並沒有維護lastNode的資訊,因此同時使用這兩種方法可能會出錯,有待繼續完善。
測試刪除任意元素:
- @Test
- publicvoidtestRemove(){
- SingleLinkList<Integer>linkList=newSingleLinkList<Integer>();
- for(inti=0;i<10;i++){
- linkList.addFirst(i);
- }
- if(!linkList.isEmpty()){
- linkList.remove(5);
- }
- linkList.display();
- }
控制要輸出:
0 1 2 3 4 6 7 8 9
可以看到5的確沒有了
測試刪除第一個元素:
- @Test
- publicvoidtestRemoveFisrt(){
- SingleLinkList<Integer>linkList=newSingleLinkList<Integer>();
- for(inti=0;i<10;i++){
- linkList.addFirst(i);
- }
- linkList.removeFisrt();
- linkList.display();
- }
控制檯輸出:
1 2 3 4 5 6 7 8 9
測試包含:
- @Test
- publicvoidtestContains(){
- SingleLinkList<Integer>linkList=newSingleLinkList<Integer>();
- for(inti=0;i<10;i++){
- linkList.addFirst(i);
- }
- System.out.println(linkList.contains(5));
- System.out.println(linkList.contains(10));
- }
控制檯輸出:
true
false
結果顯示,包含5,不包含10.
二、雙端連結串列Java實現
本節介紹雙端連結串列的Java實現,我們用DoubleLinkJava
來表示。
雙端連結串列與傳統的連結串列非常類似,但是它有一個新增的特性:即對連結串列中最後一個節點的引用lastNode
。我們可以像在單鏈表中在表頭插入一個元素一樣,在連結串列的尾端插入元素。如果不維護對最後一個節點的引用,我們必須要迭代整個連結串列才能得到最後一個節點,然後再插入,效率很低。因此我們在雙鏈表中新增一個addLast
方法,用於新增節點到末尾。
addLast方法分析:直接將連結串列中維護的lastNode的next引用指向新的節點,再將lastNode的引用指向新的節點即可。
因為單鏈表中,大部分的程式碼在雙端連結串列中都可以重用,所以此處我們編寫的DoubleLinkList只要繼承SingleLinkList,新增必要的屬性和方法支援從尾部操作即可。
DoubleLinkList.java
- packagecom.tianshouzhi.algrithm.list;
- publicclassDoubleLinkList<T>extendsSingleLinkList<T>{
- //連結串列中的最後一個節點
- protectedNodelastNode=null;
- /**
- *新增到連結串列的最後
- *@paramelement
- */
- publicvoidaddLast(Telement){
- Nodenode=newNode();
- node.setData(element);
- if(size==0){//說明沒有任何元素,說明第一個元素
- firstNode=node;
- }else{//如果有元素,將最後一個節點的next指向新的節點即可
- /*這裡有一個要注意的地方:
- 當size=1的時候,firstNode和lastNode指向同一個引用
- 因此lastNode.setNext時,fisrtNode的next引用也會改變;
- 當size!=1的時候,lastNode的next的改變與firstNode無關*/
- lastNode.setNext(node);
- }
- //將lastNode引用指向新node
- lastNode=node;
- size++;
- }
- /**
- *當連結串列中沒有元素時,清空lastNode引用
- */
- @Override
- publicbooleanremove(Telement){
- booleanresult=super.remove(element);
- if(size==0){
- lastNode=null;
- }
- returnresult;
- }
- /**
- *因為在SingleLinkList中並沒有維護lastNode的資訊,我們要自己維護
- */
- @Override
- publicNodeaddFirst(Telement){
- Nodenode=super.addFirst(element);
- if(size==1){//如果連結串列為size為1,將lastNode指向當前節點
- lastNode=node;
- }
- returnnode;
- }
- }
測試addLast
- @Test
- publicvoidtestAddFisrt(){
- DoubleLinkList<Integer>linkList=newDoubleLinkList<Integer>();
- for(inti=0;i<5;i++){
- linkList.addFirst(i);
- }
- for(inti=0;i<5;i++){
- linkList.addLast(i);
- }
- linkList.display();
- }
控制檯輸出:
4 3 2 1 0 0 1 2 3 4
從輸出中我們可以到,前五個元素因為是addFirst新增的,所以是降序的,而後面五個元素是addLast新增的,所以是升序的。
三、有序連結串列Java實現
本節講解有序連結串列,使用SortedLinkList表示。
所謂有序連結串列,就是連結串列中Node節點之間的引用關係是根據Node中維護的資料data的某個欄位為key值進行排序的。
為了在一個有序連結串列中插入,演算法必須首先搜尋連結串列,直到找到合適的位置:它恰好在第一個比它大的資料項前面。
當演算法找到了要插入的資料項的位置,用通常的方式插入資料項:把新的節點Node指向下一個節點,然後把前一個節點Node的next欄位改為指向新的節點。然而,需要考慮一些特殊情況,連線點有可能插入在表頭或者表尾。
在本例中,我們建立一個類Person表示插入的資料,我們希望連結串列中資料是按照Person的enName屬性升序排列的。
三、Java中的雙端連結串列實現LinkedList
java中已經提供了雙端連結串列的實現,java.util.LinkedList,以下是這個類部分方法摘要,相信不需要介紹,根據方法名字,你就可以知道其含義:
- publicclassLinkedList<E>
- extendsAbstractSequentialList<E>
- implementsList<E>,Deque<E>,Cloneable,java.io.Serializable
- {
- publicEgetFirst();
- publicEgetLast();
- publicEremoveFirst();
- publicEremoveLast();
- publicvoidaddFirst(Ee);
- publicvoidaddLast(Ee);
- publicbooleancontains(Objecto);
- publicintsize();
- publicbooleanadd(Ee);//等價於addLast
- publicbooleanremove(Objecto);
- publicvoidclear();
- ....
- }