菜雞的Java筆記 第二十七 連結串列基本概念
連結串列基本概念
1.連結串列的基本形式
2.單向連結串列的完整實現
認識連結串列
連結串列= 可變長的物件陣列,屬於動態物件陣列的範疇
連結串列是一種最簡單的線性資料結構,之所以會存在有資料結構問題主要是解決亮點:儲存的資料不受限制,查詢速度快
物件陣列有那些問題呢?
物件陣列可以儲存一組物件方便開發
物件陣列的長度固定,而且資料的刪除,修改,增加處理麻煩
所有的開發之中都100%不可能避免掉物件陣列的使用
正因為如此現在如果要想讓其可以編寫出便於維護的程式碼,那麼就需要實現一個動態物件陣列,那麼就可以使用連結串列完成
但是現在如果要想實現動態的物件陣列,要考慮兩個問題:
為了適應於所有的開發要求,此物件陣列要求可以儲存所有的資料型別,那麼一首選Object型別
為了可以儲存多個數據,需要採用引用的方式來進行儲存,但是資料本身是不可能儲存順序的
所以需要有一個可以負責儲存順序的類來包裝這個資料
分析 結論:
儲存資料為了方便使用 Object
資料本身不包含有先後的邏輯關係,所以將資料封裝在一個 Node 類,負責關係的維護
範例:定義出如下的一個類
class Node{// b表示定義的節點 private Object data;// 要儲存的資料 private Node next; // 儲存下一個節點 public Node(Object data){ // 有資料才可以儲存節點 this.data = data; } public void setNext(Node next){// 設定節點 this.next = next; } public Node getNext(){ //取得節點 return this.next; } } public class linkedList{ public static void main(String args[]){ } }
完成節點之後,下面就可以進行節點的基本使用了
範例:採用迴圈的方式操作節點
class Node{// b表示定義的節點 private Object data;// 要儲存的資料 private Node next; // 儲存下一個節點 public Node(Object data){ // 有資料才可以儲存節點 this.data = data; } public void setNext(Node next){// 設定節點 this.next = next; } public Node getNext(){ // 取得節點 return this.next; } public Object getData(){ return this.data; } } public class linkedList{ public static void main(String args[]){ //1.定義各自獨立的操作節點 Node root = new Node ("火車頭"); Node n1 = new Node("車廂1"); Node n1 = new Node("車廂2"); //2.設定彼此間的關係 root.setNext(n1); n1.setNext(n2); // 3.輸出 Node currentNode = root;// 從根節點開始取出資料 while(currentNode != null){ System.out.println(currentNode.getData());// 取出資料 currentNode = currentNode.getNext();//下一個節點 } } }
以上的操作如果使用迴圈並不方便。最好的做法是遞迴呼叫
範例:利用遞迴的方式實現內容的取得
class Node{// b表示定義的節點 private Object data;// 要儲存的資料 private Node next; // 儲存下一個節點 public Node(Object data){ // 有資料才可以儲存節點 this.data = data; } public void setNext(Node next){// 設定節點 this.next = next; } public Node getNext(){ // 取得節點 return this.next; } public Object getData(){ return this.data; } } public class linkedList{ public static void main(String args[]){ //1.定義各自獨立的操作節點 Node root = new Node ("火車頭"); Node n1 = new Node("車廂1"); Node n1 = new Node("車廂2"); //2.設定彼此間的關係 root.setNext(n1); n1.setNext(n2); // 3.輸出 } public static void print(Node node){ if(node == null){ return;// 結束方法呼叫 } System.out.println(node.getData()); print(node,getNext()); } } }
通過以上的結構講解,應該已經清楚了連結串列在整個實現的關鍵就是 Node 類,Node 類要儲存資料與下一個節點
連結串列開發入門
雖然以上的程式碼已經實現了鏈的形式,但是以上的程式碼之中存在有兩個問題:
使用者需要自己手工定義Node 類,但是實際上Node 類對使用者沒用
Node 類的先後關係如果交由使用者處理,那麼整個程式碼就亂了
所以現在發現,需要有一個類,這個類可以負責所有的Node 的關係匹配,而使用者只需要通過這個類儲存資料或取得資料即可
那麼就可以編寫一個Link類完成
範例:基本結構
class Node{// b表示定義的節點 private Object data;// 要儲存的資料 private Node next; // 儲存下一個節點 public Node(Object data){ // 有資料才可以儲存節點 this.data = data; } public void setNext(Node next){// 設定節點 this.next = next; } public Node getNext(){ // 取得節點 return this.naxt; } public Object getData(){ return this.data; } } class link{ // 表示一個連結串列操作類,利用此類來隱藏Node的節點匹配 public void add(Object obj){// 向連結串列裡面追加資料 } public void print(){// 輸出連結串列中的全部資料 } } public class linkedList{ public static void main(String args[]){ Link all = new Link(); all.add("商品1"); all.add("商品2"); all.add("商品3"); all.print(); } }
使用者不關心Node,使用者只關心通過Link操作完成後可以取得資料
範例:完善程式
class Node{// b表示定義的節點 private Object data;// 要儲存的資料 private Node next; // 儲存下一個節點 public Node(Object data){ // 有資料才可以儲存節點 this.data = data; } public void setNext(Node next){// 設定節點 this.next = next; } public Node getNext(){ // 取得節點 return this.next; } public Object getData(){ return this.data; } // 第一次呼叫:Link.root // 第一次呼叫:Link.root.next // 第一次呼叫:Link.root.next.next public void addNode(Node newNode){ if(this.next == null){ // 當前節點之後沒有節點 this.next = newNode; }else{// 如果現在當前節點後有節點 this.next.addNode(newNode); } } // 第一次呼叫:this = Link.root // 第一次呼叫:this = Link.root.next public void printNode(){ System.out.println(this.data);// 當前節點資料 if(this.next != null){ // 還有後續的節點 this.next.printNode(); } } } class Link{ // 表示一個連結串列操作類,利用此類來隱藏Node的節點匹配 private Node root;// 需要有一根元素 public void add(Object obj){// 向連結串列裡面追加資料 // 將操作的資料包裝為Node類物件,這樣才可以進行先後關係的排列 Node newNode = new Node(obj); //x現在沒有根節點 if(this.root == null){// this出現在Link類,表示Link類的當前物件 this.root = newNode;// 將第一個節點作為根節點 }else{// 根節點存在了 // this.root.setNext(newNode); this.root.addNode(newNode); // 由根節點負責呼叫 }//(Node 負責排序Link 負責根) } public void print(){// 輸出連結串列中的全部資料 if(this.root != null){ // 現在有資料 this.root.printNode(); // 輸出節點資料 } } } public class linkedList{ public static void main(String args[]){ Link all = new Link(); all.add("商品1"); all.add("商品2"); all.add("商品3"); all.print(); } }
此時的程式碼就實現了連結串列的基本操作,整個過程之中,使用者不關心Node的處理,只關心資料的儲存和輸出
開發可用連結串列
以上的程式碼只能夠說是基本的連結串列結構形式,但是從另外一個方面,以上的程式碼給我們提供了連結串列的實現思路
可是如何才能設計一個好的連結串列呢?
連結串列的實現必須依靠於節點類Node類來實現,但是整個的過程之中一定要清楚,使用者不需要操作Node
而且通過現在的程式碼可以發現Node類裡的操作有特定的需要
但是這個時候Node類寫在了外面,那麼就表示使用者可以直接操作Node類物件
所以程式現在的問題在於:如何可以讓Node類只為Link類服務,但是有不讓其他類所訪問
那麼自然就要想到使用內部類完成,而且內部類的好處在於:可以與外部類直接進行私有屬性的訪問
範例:合理的結構規劃
class Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } } //************************************ private Node root; // 根元素 } public class linkedList{ public static void main(String args[]){ } }
如果要開發程式,那麼一定要建立自己的操作標準,那麼一旦說到標準就應該想到使用介面來完成
interface Link{ } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } } //************************************ private Node root; // 根元素 } public class linkedList{ public static void main(String args[]){ } }
在隨後完善程式碼的過程之中,除了功能的實現之外,實際上也屬於介面功能的完善
實現資料增加操作 public void add(Object data)
1.需要在接口裡面定義好資料增加的操作方法
interface Link{ public void add(Object data);//資料增加 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } } //************************************ private Node root; // 根元素 } public class linkedList{ public static void main(String args[]){ } }
2.進行程式碼的實現,同樣實現的過程之中 LinkImpl 類只關心根節點,而具體的子節點的排序都交由 Node 類負責處理
在Link類中實現add()方法:
interface Link{ public void add(Object data);//資料增加 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } } //************************************ private Node root; // 根元素 public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } } } public class linkedList{ public static void main(String args[]){ } }
在Node類中進行資料的追加操作:
interface Link{ public void add(Object data);//資料增加 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } public void addNode(Node newNode){ if(this.next == null){ this.next = newNode; }else{ this.next.addNode(newNode); } } } //************************************ private Node root; // 根元素 public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } } } public class linkedList{ public static void main(String args[]){ } }
此時的程式碼實現過程與基本的實現是完全一樣的
取得儲存元素個數: public int size()
每個Link介面的物件都要儲存各自的內容,所以為了方便控制儲存個數,可以增加一個Link類中的屬性,並且用此屬性在資料成功追加之後實現自增操作
在Link類中定義一個 count 屬性,預設值為:0
interface Link{ public void add(Object data);//資料增加 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } public void addNode(Node newNode){ if(this.next == null){ this.next = newNode; }else{ this.next.addNode(newNode); } } } //************************************ private Node root; // 根元素 private int count = 0;// 當資料已經成功新增完畢之後實現計數的統計 public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } this.count ++; // 當節點儲存完畢之後就可以進行資料增加了 } } public class linkedList{ public static void main(String args[]){ } }
而在Link接口裡面追加 size() 的方法,同時在LinkImpl子類裡面進行方法的覆寫
interface Link{ public void add(Object data);//資料增加 public int size();// 取得儲存元素的個數 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } public void addNode(Node newNode){ if(this.next == null){ this.next = newNode; }else{ this.next.addNode(newNode); } } } //************************************ private Node root; // 根元素 private int count = 0;// 當資料已經成功新增完畢之後實現計數的統計 public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } this.count ++;// 當節點儲存完畢之後就可以進行資料增加了 } public int size(){ return this.count; } } public class linkedList{ public static void main(String args[]){ Link all = new LinkImpl(); System.out.println(all.size()); all.add("商品1"); all.add("商品2"); all.add("商品3"); System.out.println(all.size()); } }
此操作直接與最後的輸出有關
判斷是否為空集合: public boolean isEmpty()
所謂的空連結串列指的是連結串列之中沒有任何的資料存在
如果要想判斷集合是否為空,有兩種方式:長度為 0 ,另外一個就是判斷根元素是否為 null
範例:在Link 介面中追加一個新的方法: isEmpty
interface Link{ public void add(Object data);//資料增加 public int size();// 取得儲存元素的個數 public boolean isEmpty();//判斷是否為空集合 }
範例:在LinkImpl類中實現此方法
interface Link{ public void add(Object data);//資料增加 public int size();// 取得儲存元素的個數 public boolean isEmpty();//判斷是否為空集合 } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } public void addNode(Node newNode){ if(this.next == null){ this.next = newNode; }else{ this.next.addNode(newNode); } } } //************************************ private Node root; // 根元素 private int count = 0;// 當資料已經成功新增完畢之後實現計數的統計 public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } this.count ++; } public int size(){ return this.count; } public boolean isEmpty()( return this.count == 0; // 或者 return this.root == null; ) } public class linkedList{ public static void main(String args[]){ Link all = new LinkImpl(); System.out.println(all.isEmpty()); all.add("商品1"); all.add("商品2"); all.add("商品3"); System.out.println(all.isEmpty()); } }
實際上此操作與 size() 幾乎一脈相承
資料查詢: public boolean contains(Object data)
任何情況下 Link 類只負責與根元素操作有關的內容,而所有額其他元素的資料的變更,查詢,關係的匹配都應該交由 Node 類來負責處理
1.在Link接口裡面建立一個新的方法
interface Link{ public void add(Object data);//資料增加 public int size();// 取得儲存元素的個數 public boolean isEmpty();//判斷是否為空集合 public boolean contains(Object data);// 判斷是否有指定的元素 }
2.在LinkImpl 子類裡面要通過根元素開始呼叫查詢,所有的查詢交由 Node 類負責
在LinkImpl 發出具體的查詢要求之前,必須要保證有集合資料
public boolean contains(Object data){ if(this.root == null){// 沒有集合資料 return false; } return this.root.containsNode(data);
// 根元素交給 Node 類完成 }
在Node 類中實現資料的查詢
// 第一次:this.LinkImpl.root // 第二次:this.LinkImpl.root.next public boolean currentNode(Object data){ if(this.data.equals(data)){ // 該節點資料符合於查詢資料 return true; }else{// 繼續向下查詢 if(this.next != null){// 當前節點之後還有下一個節點 return this.next.containsNode(data); }else{ return false; } } }
這樣查詢的模式實質上也屬於逐行的判斷掃描
根據索引取得資料: public Object get(int index)
既然連結串列屬於動態的物件陣列,所以陣列本身一定會提供有根據索引取得資料的操作支援,那麼在連結串列中也可以定義與之類似的方法
但是在進行資料儲存的時候並沒有設定索引,那麼現在有兩個方案:
修改 Node 類的結構,為每一個節點自動匹配一個索引,資料的刪除不方便
在操作索引時動態生存索引,適合集合的修改
1.修改Lnik 類為其增加一個 foot 的屬性,之所以將foor屬性定義在 LinkImpl 類之中,主要目的是方便多個 Node 共同進行屬性的操作使用的,,同時內部類可以方便的訪問外部類中的私有成員
private int foot = 0;//操作索引的腳標
2.在Link 接口裡面首先定義出新的操作方法
public Object get(int index);//根據索引取得內容,索引從0開始
3.在LinkImpl 類裡面定義功能實現:
在Node 類中應該提供有一個 getNode() 的方法,那麼這個方法的功能是依靠判斷每一個索引值的操作形式
public Object getNode(int index){// 傳遞索引的序號 if(LinkImpl.this.foot++ == index){
// 當前的索引為要查詢的索引 return this.data;//返回當前節點物件 }else{ return this.next.getNode(index); } }
在Link 類中實現 get() 方法
public Object get(int index){ if(index >= this.count){ // 索引不存在 return null; } this.foot = 0;// 查詢之前執行一次清零操作 return this.root.getNode(index); }
這種查詢的模式與 contains() 最大的不同一個是數字索引,一個是內容
修改資料: public void set(int index,Object obj)
與get() 相比 set() 方法依然需要進行迴圈的判斷,只不過 get() 索引判斷成功之後會返回資料,而 set() 只需要用新的資料更新已有節點資料即可
1.在Link接口裡面建立一個新的方法
public void set(int index,Object obj)
2.修改LnikImopl 子類,流程與 get() 差別不大:
在Node 類裡面追加一個新的 setNode() 方法;
public void setNode(int index,Object obj){ if(LinkImpl.this.foot ++ == index){ this.obj = obj;// 重新儲存資料 }else{ this.next.setNode(index,obj); } }
在LinkImpl 子類裡面覆寫 set() 方法,在 set() 方法編寫的時候也需要針對於給定的索引進行驗證
public void set(int index,Object obj){ if(index >= this.count){ // 索引不存在 return null; } this.foot = 0;// 查詢之前執行一次清零操作 this.root.setNode(index,obj); }
set() 與 get() 方法實際上在使用時都有一個固定的條件:集合中的儲存資料順序應該為新增順序
資料刪除: public void remove(Object obj)
如果要進行資料的刪除,那麼對於整個連結串列而言就是節點的刪除操作
而節點的刪除操作過程之中需要考慮的問題是什麼?
要刪除的是根節點還是子節點問題
1.要刪除的是根節點:Link 類處理,因為根節點有關的所有節點都應該交由 Link 類管理
Link.root = Link.root.next
2.要刪除的是子節點:交由 Node 類負責處理
刪除節點的上一個節點.next = 刪除節點.next
1.在Link接口裡面建立一個新的方法
public void remove(Object data);// 刪除資料
2.修改LnikImopl 類的操作:
在Node 類中增加一個 removeNode() 的方法
//第一次:this.LinkImpl.root.next,previous = LinkImpl.root; // 第二次:this.LinkImpl.root.next.next,previous = LinkImpl.root.next public void remove(Node previous,Object data){ if(this.data.equals(data)){ previous.next = this.next;// 空出當前節點 }else{ this.next.removeNode(this,data); } }
在Link類中增加新的操作:
public void remove(Object data){ if(this.contains(data)){ // 資料如果存在則刪除 if(this.root.equals(data)){// 根元素為要刪除的元素 this.root = this.root.next; // 第二個元素作為根元素 }else{ // 不是根元素,根元素一斤判斷完了 this.root.next.removeNode(this.root,data); } this.count --; } }
整個刪除操作很好的體現了 this 的特性
contains() 和 remove() 方法必須有物件比較的支援,物件比較使用的就是 Object 類中的 equals() 方法
清空連結串列: public void clear()
當連結串列中的資料不需要在使用的時候,那麼可以進行清空,而清空最簡單的做法就是將 root設定為 null
1.在Link接口裡面建立一個新的方法
public void clear();//清空連結串列
2.直接在 LinkImpl 類中修改清空操作
public void clear(){ this.foot = null; this.count = 0; // 元素的儲存個數清0 System.gc();//回收記憶體空間 }
實際上這種程式碼還欠缺一個很好的記憶體釋放問題
返回資料: public Object[] toArray()
恆定的概念:連結串列就是動態物件陣列,但是要想操作連結串列中的資料,那麼最好的做法是將其轉換為物件陣列返回
所以這個時候就需要針對資料做遞迴處理
1.在Link 接口裡面定義返回物件陣列的方法
public Object[] toArray()
2.修改LnikImopl 子類
由於Node 類需要操作連結串列資料讀取,所以應該在LinkImpl 子類裡面應該提供有一個物件陣列的屬性
public Object retData[] = null;
在LinkImpl 子類裡面覆寫 toArray() 方法,並且要根據長度開闢陣列空間
public Object[] toArray(){ if(this.root == null){ return null; } this.retData = new Object[this.count]; this.foot = 0; this.root.toArrayNode(); return this.retData; }
在Node類裡面實現資料的儲存操作
public void toArrayNode(){ LinkImpl.this.retData[LinkImpl.this.foot ++] = this.data; if(this.next != null){ this.next.toArrayNode(); } }
不過以上的設計都沒有考慮過效能問題
interface Link{ public void add(Object data);//資料增加 public int size();// 取得儲存元素的個數 public boolean isEmpty();//判斷是否為空集合 public boolean contains(Object data);// 判斷是否有指定的元素 public Object get(int index);//根據索引取得內容,索引從0開始 public void set(int index,Object obj); public void remove(Object data);// 刪除資料 public void clear();//清空連結串列 public Object[] toArray(); } class LinkImpl implements Link{ //外部的程式只關心此類 private class Node{//使用私有內部類,防止外部使用此類 private Object data; private Node next; public Node(Object data){ this.data = data; } public void addNode(Node newNode){ if(this.next == null){ this.next = newNode; }else{ this.next.addNode(newNode); } } public Object getNode(int index){// 傳遞索引的序號 if(LinkImpl.this.foot++ == index){ // 當前的索引為要查詢的索引 return this.data;//返回當前節點物件 }else{ return this.next.getNode(index); } } public void setNode(int index,Object data){ if(LinkImpl.this.foot ++ == index){ this.data = data;// 重新儲存資料 }else{ this.next.setNode(index,data); } } //第一次:this.LinkImpl.root.next,previous = LinkImpl.root; // 第二次:this.LinkImpl.root.next.next,previous = LinkImpl.root.next public void remove(Node previous,Object data){ if(this.data.equals(data)){ previous.next = this.next;// 空出當前節點 }else{ this.next.removeNode(this,data); } } // 第一次:this.LinkImpl.root // 第二次:this.LinkImpl.root.next public boolean currentNode(Object data){ if(this.data.equals(data)){ // 該節點資料符合於查詢資料 return true; }else{// 繼續向下查詢 if(this.next != null){// 當前節點之後還有下一個節點 return this.next.containsNode(data); }else{ return false; } } } public void toArrayNode(){ LinkImpl.this.retData[LinkImpl.this.foot ++] = this.data; if(this.next != null){ this.next.toArrayNode(); } } } //************************************ private Node root; // 根元素 private int count = 0;// 當資料已經成功新增完畢之後實現計數的統計 private int foot = 0;//操作索引的腳標 public Object retData[] = null; public void add(Object data){ if(data == null){//現在沒有要增加的資料 return;//結束呼叫 } Node newNode = new Node(data);//建立新的節點 if(this.root == null){//保留有根節點 this.root = root; }else{//應該交由Node類負責處理 this.root.addNode(newNode); } this.count ++; } public void remove(Object data){ if(this.contains(data)){ // 資料如果存在則刪除 if(this.root.data.equals(data)){// 根元素為要刪除的元素 this.root = this.root.next; // 第二個元素作為根元素 }else{ // 不是根元素,根元素一斤判斷完了 this.root.next.removeNode(this.root,data); } this.count --; } } public void clear(){ this.foot = null; this.count = 0; System.gc();//回收記憶體空間 } public int size(){ return this.count; } public boolean isEmpty(){ return this.count == 0; // 或者 return this.root == null; } public boolean contains(Object data){ if(this.root == null){// 沒有集合資料 return false; } return this.root.containsNode(data);// 根元素交給 Node 類完成 } public Object[] toArray(){ if(this.root == null){ return null; } this.retData = new Object[this.count]; this.foot = 0; this.root.toArrayNode(); return this.retData; } public Object get(int index){ if(index >= this.count){ // 索引不存在 return null; } this.foot = 0;// 查詢之前執行一次清零操作 return this.root.getNode(index); } public void set(int index,Object data){ if(index >= this.count){ // 索引不存在 return null; } this.foot = 0;// 查詢之前執行一次清零操作 this.root.setNode(index,data); } } public class linkedList{ public static void main(String args[]){ Link all = new LinkImpl(); System.out.println(all.isEmpty()); all.add("A"); all.add("B"); all.add(<