常見數據結構與算法專題篇之單向鏈表


因為不是CS科班,一個科研狗偏偏要來碼代碼,以前沒有系統學習過數據結構和算法的知識,后期實踐中越來越覺得基礎的重要,所以近期開始來刷一波兒基礎的數據結構和算法的知識。以下內容是我整理總結的,如果有錯誤,歡迎指出

鏈表

鏈表是一種物理儲存結構上非連續、非順序的儲存結構。數據元素的邏輯順序是依靠鏈表中指針鏈接次序實現的。鏈表有一系列節點組成,這些節點在內存中不必相連。這些節點有數據部分Data和鏈表指針部分Next組成,其中Next指向下一個節點。這樣節點的狀態改變可以通過改變指針的指向來實現,效率很高。

這里主要以單鏈表來說明鏈表實現基本思路

1. 一個簡單的鏈表示例

首先定義Node類作為數據的載體。
Node 類中需要完成的操作就是添加Node 和 輸出數據的方法。這里我們先假設數據就是String 數據。我們往Node類添加這兩個方法。

class Node {
    private String data;
    private Node next;

    public Node(String data) {
        this.data = data;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    public String getData() {
        return data;
    }

    public Node getNext() {
        return next;
    }

    /**
     * 添加下一個節點
     * @param newNode
     */
    public void addNode(Node newNode){
        if(this.next==null){
            this.next=newNode;
        }else {
            this.next.addNode(newNode);
        }
    }

    /**
     * 輸出節點數據,這里以簡單輸出數據
     */
    public void printNode(){
        System.out.println(this.data);
        if(this!=null){
            this.next.printNode();
        }
    }
}

我們還需要專門定義一個Link類的對象專門操作Node。Link中我們需要保存一個根節點,根節點作為鏈表的頭。

class Link{
    private Node root;
    public void add(String data){
        Node newNode=new Node(data);//數據必須要封裝在節點中,封裝之后才可以操作數據。
        if(this.root==null){
            this.root=newNode;
        }else {
            this.root.addNode(newNode);
        }
    }
    public void print(){
        if(this.root!=null){
            this.root.printNode();
        }
    }
}

以上我們定義了一個Link 類,主要是要把數據封裝在Node 中,通過Link 類操作Node ,避免直接操作Node 數據。

這里我們給出這個簡單鏈表類的完整程序(包含main函數中示例數據)。

class Node {
    private String data;
    private Node next;

    public Node(String data) {
        this.data = data;
    }

    public void setNext(Node next) {
        this.next = next;
    }

    public String getData() {
        return data;
    }

    public Node getNext() {
        return next;
    }

    /**
     * 添加下一個節點
     * @param newNode
     */
    public void addNode(Node newNode){
        if(this.next==null){
            this.next=newNode;
        }else {
            this.next.addNode(newNode);
        }
    }

    /**
     * 輸出節點數據,這里以簡單輸出數據
     */
    public void printNode(){
        System.out.println(this.data);
        if(this.next!=null){
            this.next.printNode();
        }
    }
}
class Link{
    private Node root;
    public void add(String data){
        Node newNode=new Node(data);//數據必須要封裝在節點中,封裝之后才可以操作數據。
        if(this.root==null){
            this.root=newNode;
        }else {
            this.root.addNode(newNode);
        }
    }
    public void print(){
        if(this.root!=null){
            this.root.printNode();
        }
    }
}


public class simpleLink {
    public static void main(String[] args){
        Link all=new Link();
        all.add("data1");
        all.add("data2");
        all.add("data3");
        all.print();
    }
}

這里為了展示鏈表的基本操作形式只給出了最簡單的一個鏈表的示例。代碼并不具備實用性,數據是直接進行輸出的,以上的代碼沒有真正對Node類進行很好的封裝,用戶可以直接操作Node類。

2. 進一步完善鏈表并添加相應方法

我們通過定義內部內類的方式把Node類數據進行封裝(這里我們還是以儲存String 數據為例)。ps:這里我本來想說這是某種設計模式,但是并沒有學過設計模式啥的,等后來把設計模式看了再來完善一下吧。^^大神求放過。

這里我們貼出代碼

public class Link {//用戶主要關注Link類
   private Node root;//保存根節點
   private class Node{//定義內部類Node,定義內部內的不僅僅對Node類進行了很好的封裝,而且Node 類可以直接訪問外部內的數據
       private String data;
       private Node next;

       public Node(String data) {
           this.data = data;
       }

       public void setNext(Node next) {
           this.next = next;
       }
       public Node getNext(){
           return this.next;
       }

   }
}

2.1 Link類添加增加和取出數據的操作

通過Link 類中新增add()方法實現數據的添加

public void add(String data){
       Node newNode=new Node(data);
       if(this.root==null){//首先判斷root節點是否為空,如果root為空就把新建立的節點設為root節點
           this.root=newNode;
       }else{
           this.root.addNode(newNode);//如果root不為空,將添加交給Node類的addNode方法去處理
       }

Link類中確定根節點,然后調用Node的addNode() 方法來實現數據添加

public void addNode(Node newNode){
           if(this.next==null){
               this.next=newNode;//如果root的下一個節點為空,新節點就加到root后
           }else {
               this.next.addNode(newNode);//如果root的下一個不為空,就判斷下一個的下一個,遞歸
           }
       }

2.2 取得保存對象的個數

Link 類中定義一個count 的變量,對對象數量進行記錄。每次添加數據或者刪除數據后相應地改變count的值。這樣要知道保存多少對象的花就可以直接取出count的值。

定義size()方法

    private int count;//用來保存對象數量
    public int size(){
        return this.size();//返回對象的數量
    }

要注意是,在add()方法中記得添加this.count++,這樣再每次添加數據后count值就相應改變。

2.3 判斷是否為空鏈表

如果我們通過判斷count是否為0就可以判斷鏈表是否為空了。

public boolean isEmpty(){
        return this.count==0;//如果個數為0 則返回ture,反之false。
    }

2.4 清空鏈表

整個鏈表中最關鍵的就是Link 類的root ,如果我們把root 直接指向null, 那么整個鏈表就是null了。

public void clean(){
        this.root=null;
        this.count=0;//注意清空鏈表后count歸零
    }

2.5 取得鏈表中制定索引位置的數據

首先在Link中增加foot屬性,用來標記鏈表索引。然后在Link中增加 get(int index) 方法,用來得到制定索引的數據。

private int foot;
public String get(int index){
        if(index>this.count){
            return null;
        }
        this.foot=0;
        return this.root.getNode(index);//交給Node類中getNode()方法處理。
    }

在Node 類中定義getNode()方法,進行查詢并修改foot屬性的內容,內部類可以很方便訪問外部內屬性。

public String getNode(int index){
            if(index==Link.this.foot++){
                return this.data;
            }else{
                return this.next.getNode(index);
            }
        }

2.6 判斷制定數據是否存在

首先在Link類中定義一個contains()方法,這個方法首先要判斷數據是否為null,如果不為null 繼續把判斷交給Node類處理。

定義contains()方法

public boolean contains(String data){
        if(data==null){
            return false;
        }
        return this.root.containsNode(data);
    }

在Node類中定義containsNode方法

public boolean containsNode(String data){
            if(data.equals(this.data)){//這里用equals()方法,后期便于擴展數據類型
                return true;
            }
            if(this.next==null){
                return false;
            }else{
                return this.next.containsNode(data);
            }
        }

2.7 刪除數據

Link 類中定義remove()方法用于刪除節點數據。首先判斷要刪除的數據是否是根節點,如果是根節點的話就在Link中直接將根節點指向下一個節點。如果刪除的不是根節點,則將操作交給Node類去處理。

    public void remove(String data){
        if(data.equals(this.root.data)){
            this.root=this.root.next;//改變根節點指向下一個
        }else{
            this.root.next.removeNode(this.root,data);
        }
        this.count--;
    }

在Node類中定義removeNode()方法,如果某個節點與要刪除的數據匹配,那么改節點的前一個節點的next應該指向該匹配節點的下一個節點。如示意圖:

定義removeNode()方法

public void removeNode(Node prev,String data){
            if(data.equals(this.data)){
                prev.next=this.next;
            }else {
                this.next.removeNode(this,data);
            }
        }

2.8 返回所有數據

返回所有的數據并保存為對象數組的形式,對象數組必須對用戶可見,所以在Link中定義。在Link 中定義toArray()方法,將所有數據以對象數組方式返回。

定義對象數組和 toArray()方法。

private String[] retData;
public String[] toArray(){
        if(this.count==0){
            return null;
        }
        this.foot=0;//設置數組的索引
        this.retData=new String[this.count];
        this.root.toArrayNode();
        return this.retData;
    }

定義toArrayNode()方法,主要是把鏈表數據取出保存在對象數組中。

public void toArrayNode(){
            Link.this.retData[Link.this.foot++]=this.data;
            if(this.next!=null){
                this.next.toArrayNode();
            }
        }

完整的程序清單

public class Link{
    private class Node {
        private String data;
        private Node next;

        public Node(String data) {
            this.data = data;
        }

        public void addNode(Node node) {
            if (this.next == null) {
                this.next=node;
            } else {
                this.next.addNode(node);
            }
        }

        public void printNode() {
            System.out.println(this.data);
            if (this.next != null) {
                this.next.printNode();
            }
        }
        public String getNode(int index){
            if(index==Link.this.foot++){
                return this.data;
            }else{
                return this.next.getNode(index);
            }
        }
        public boolean containsNode(String data){
            if(data==this.data){
                return true;
            }else {
                if(this.next!=null){
                    return this.next.containsNode(data);
                }else{
                    return false;
                }

            }
        }
        public void removeNode(Node previous,String data){
            if(this.data.equals(data)){
                previous.next=this.next;
            }else {
                this.next.removeNode(this,data);
            }
        }
        public void toArrayNode(){
            Link.this.retData[Link.this.foot++]=this.data;
            if(this.next!=null){
                this.next.toArrayNode();
            }

        }
    }
    private Node root;
    private int count=0;
    private int foot;
    private String[] retData;
    public void add(String data){//向鏈表中增加數據
        Node newNode=new Node(data);
        if(data==null) return;

        if(this.root==null){
            this.root=newNode;
        }else{
            this.root.addNode(newNode);
        }
        this.count++;
    }
    public void print(){
        if(this.root!=null){
            this.root.printNode();
        }
    }
    public int size(){//鏈表的大小
        return this.count;
    }
    public boolean isEmpty(){//判斷是否為空鏈表
        return this.count==0;
    }
    public void clean(){//清空鏈表
        this.root=null;
        this.count=0;
    }
    public String get(int index){
        if(index>this.count){
            return null;
        }else{
            return this.root.getNode(index);
        }

    }
    public boolean contains(String data){
        if(data==null){
            return false;
        }
        return this.root.containsNode(data);
    }
    public void remove(String data){
        if(this.contains(data)){
            if(this.root.data==data){
                this.root=this.root.next;
            }else {
                this.root.next.removeNode(this.root,data);
            }
        }
    }
    public String[] toArray(){
        if(this.count==0){
            return null;
        }
        this.foot=0;
        this.retData=new String[this.count];
        this.root.toArrayNode();
        return this.retData;
    }
}

基本鏈表的方法都寫得差不多了,以上鏈表為了展示鏈表結構是以String數據為例。實際使用的大多是用戶自定義的對象。

3. 進一步完善簡單鏈表

以上鏈表中是以保存的String 類型的數據為例的,但是實際生活中更多的是保存用戶自己定義的簡單JavaBean類對象。我們不想每次定義了Bean類時每次都重新根據這個類來寫一個鏈表,這樣不符合代碼去重復的初衷。在這里我們通過java 中泛型的特性來改寫以上的鏈表,這樣當需要保存用戶不同的類對象數據時我們只需要指定相應地泛型類型。

在以上的鏈表中我們查詢數據和刪除數據依靠的是String 的equals() 方法,這個方法的實際作用是對象比較,用戶指定泛型對象時必須要覆寫對象的compare()方法,指定比較規則。

定義泛型的Link類并改寫鏈表,由于基本思路大致相同,所以在這里我們直接貼出完整代碼。

public class Link<T>{
    private class Node {
        private T data;
        private Node next;

        public Node(T data) {
            this.data = data;
        }

        public void addNode(Node node) {
            if (this.next == null) {
                this.next=node;
            } else {
                this.next.addNode(node);
            }
        }

        public void printNode() {
            System.out.println(this.data);
            if (this.next != null) {
                this.next.printNode();
            }
        }
        public T getNode(int index){
            if(index==Link.this.foot++){
                return this.data;
            }else{
                return this.next.getNode(index);
            }
        }
        public boolean containsNode(T data){
            if(data==this.data){
                return true;
            }else {
                if(this.next!=null){
                    return this.next.containsNode(data);
                }else{
                    return false;
                }

            }
        }
        public void removeNode(Node previous,T data){
            if(this.data.equals(data)){
                previous.next=this.next;
            }else {
                this.next.removeNode(this,data);
            }
        }
        public void toArrayNode(){
            Link.this.retData.add(this.data);
            //Link.this.retData[Link.this.foot++]=this.data;
            if(this.next!=null){
                this.next.toArrayNode();
            }

        }
    }
    private Node root;
    private int count=0;
    private int foot;
    private ArrayList<T> retData;//定義一個ArrayList<T>來保存鏈表全部數據
    public void add(T data){//向鏈表中增加數據
        Node newNode=new Node(data);
        if(data==null) return;

        if(this.root==null){
            this.root=newNode;
        }else{
            this.root.addNode(newNode);
        }
        this.count++;
    }
    public void print(){
        if(this.root!=null){
            this.root.printNode();
        }
    }
    public int size(){//鏈表的大小
        return this.count;
    }
    public boolean isEmpty(){//判斷是否為空鏈表
        return this.count==0;
    }
    public void clean(){//清空鏈表
        this.root=null;
        this.count=0;
    }
    public T get(int index){
        if(index>this.count){
            return null;
        }else{
            return this.root.getNode(index);
        }

    }
    public boolean contains(T data){
        if(data==null){
            return false;
        }
        return this.root.containsNode(data);
    }
    public void remove(T data){
        if(this.contains(data)){
            if(this.root.data==data){
                this.root=this.root.next;
            }else {
                this.root.next.removeNode(this.root,data);
            }
        }
    }
    public ArrayList<T> toArray(){
        if(this.count==0){
            return null;
        }
        this.foot=0;
        this.retData=new ArrayList<T>(this.count);
        this.root.toArrayNode();
        return this.retData;
    }
}

以上就是我們改寫的鏈表代碼。值得注意的是前面String數據我們通過把鏈表數據賦給對象數組retData[]來取出數據。在定義了泛型后,沒辦法進行 T[] retData=new T[this.count]這樣類似的初始化一個對象數組。后來在網上看到有人通過T[] retData=(T[])new object[this.count]來進行初始化,但是這種方法可能隱含著ClassCastException的報錯。

鏈表還有很多其他的實現方式,常見的有循環單鏈表、雙向鏈表、循環雙鏈表。循環單鏈表實際上就是上面我們鏈表的最后一個節點指向第一個節點,整體構成一個鏈環。雙向鏈表主要是鏈表中包含兩個指針,一個指向前驅元,一個指向后繼元(LinkList就是雙向鏈表),循環雙向鏈表就是最后一個節點指向第一個節點。

文中的一部分內容是參考了網易云課堂李興華老師的java課程。這個課程很適合想要入門Java的初學者,內容很全面。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容