1. 程式人生 > >面向對象來理解鏈表

面向對象來理解鏈表

因此 public 環長 app codes des || 指向 boolean

目錄

一、鏈表知識
1.1 鏈表定義
1.2 鏈表結構
1.3 說明
二、面向對象分析鏈表
2.1 節點封裝類Node.java
2.2 鏈表封裝類ChainTable.java
2.3 關於環的補充
2.4 鏈表測試類TestChainTable.java

一、鏈表知識

1.1 鏈表定義

百度百科:鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。

1.2 鏈表結構

鏈表的元素是節點,由一個或多個結點組成。節點包括兩個元素:數據域(存儲數據的)和指針域(指向下一個結點地址)。

簡單理解的話,類似鏈條,一節一節相連,每個鏈節都有自己的零件(數據)並且鏈接下一個鏈接(指針)。

自行車的車鏈是一個環狀的鏈子,或者說是首位相連的,這是鏈表中環結構下面會有說明。

技術分享圖片 圖1-1、鏈條圖

我們可以從圖1-1 鏈表中抽象出鏈表如圖1-2 所示,從首節點headNode 依此指向下一個node,直至尾節點tailNode

技術分享圖片

圖1-2、抽象鏈表圖

進而具體到單向鏈表

  • 初始化指針指向首節點headNode
  • 每一個節點node 都由數據域和指針域組成,數據域存放當前節點的數據,指針域存放當前指針的下一個指向節點
  • 尾節點tailNode 的指針為空
技術分享圖片 ?圖1-3、單向鏈表圖

1.3 說明

常見鏈表有單向鏈表和雙向鏈表,單向鏈表是只有一個方向,從鏈表的首節點headNode 一直指向尾節點tailNode。雙向鏈表是鏈表即可從headNode 指向tailNode 又可反向從tailNode 指向heaNode。

此處以單向鏈表為例,面向對象分析鏈表結構,將節點對象化,鏈表對象化,此處使用Java 語言演示。

本文涉及到鏈表的初始化,添加節點,刪除節點,查詢節點,鏈表的逆置,模擬鏈表的環結構,判斷鏈表是否存在環結構,尋找環的入口。關於鏈表的相交問題暫不涉及。

二、面向對象分析鏈表

2.1 節點封裝類Node.java

有兩個屬性,當前節點存儲的數據Object data,指向下一節點的指針Node next。

/**
 * @Description :節點類
 * @Author: niaonao
 * @Date: 2018/8/11 13:07
 */
public class Node {

    //當前節點的數據
    Object data;
    //當前節點的下一個節點
    Node next;

    public Object getData() { return data; }
    public void setData(Object data) { this.data = data; }
    public Node getNext() { return next; }
    public void setNext(Node next) { this.next = next; }

    /**
     * 無參構造函數
     */
    public Node() { next = null; }

    /**
     * 帶參構造函數
     * @param data
     */
    public Node(Object data) {
        this.data = data;
        next = null;
    }
}

2.2 鏈表封裝類ChainTable.java

封裝單鏈表的常用方法包括插入首節點,插入尾節點,指定位置插入節點,刪除指定位置節點等。

方法不一一分開介紹了,其中的方法都有註釋說明。

基本方法:

  • void insert(Object val) 插入鏈表元素的方法(插入鏈表首位, 插入鏈表尾部, 插入鏈表指定位置)
  • int getLength() 獲取鏈表長度的方法
  • Node getPosition(int position) 獲取鏈表指定位置元素(正序獲取, 逆序獲取)
  • boolean judgeLoop() 判斷鏈表是否有環的方法
  • int getLoopLength() 獲取環的長度
  • Node entryLoop() 查找環入口的方法
  • Node reverse() 逆置鏈表的方法
  • void clear() 清除鏈表
/**
 * 鏈表
 * 由一個或多個節點組成
 * 每一個節點存放下一個元素, 通過首節點即可獲取鏈表全部節點元素, 因此可以將首節點作為鏈表對象
 * 節點:
 * 每個節點包含兩個元素: 數據對象data 和下一個節點對象next
 * 通過data 存儲當前節點的數據
 * 通過next 存儲下一個節點
 * 基本方法:
 * void insert(Object val)     插入鏈表元素的方法(插入鏈表首位, 插入鏈表尾部, 插入鏈表指定位置)
 * int getLength()     獲取鏈表長度的方法
 * Node getPosition(int position)      獲取鏈表指定位置元素(正序獲取, 逆序獲取)
 * boolean judgeLoop()     判斷鏈表是否有環的方法
 * int getLoopLength()     獲取環的長度
 * Node entryLoop()     查找環入口的方法
 * Node reverse()      逆置鏈表的方法
 * void clear()     清除鏈表
 *
 * @Description :鏈表類
 * @Author: niaonao
 * @Date: 2018/8/11 13:07
 */
public class ChainTable {

    //聲明鏈表
    private Node chain;

    /**
     * 構造函數
     */
    public ChainTable() {
        chain = new Node();
    }

    /**
     * 獲取鏈表的長度
     *
     * @return
     */
    public int getLength() {
        //遍歷計算鏈表長度,當前節點的next 下一個節點不為null, 節點數自增一即鏈表長度自增一
        int count = 0;
        Node cursor = chain;
        while (cursor.next != null) {
            cursor = cursor.next;
            count++;
        }
        return count;
    }

    /**
     * 在鏈表首部添加節點
     */
    public void insertHead(Object val) {
        //新建鏈表節點,下一節點指向鏈表的首位,然後更新鏈表首位為此節點。
        Node head = new Node(val);
        head.next = chain.next;
        chain.next = head;
    }

    /**
     * 在鏈表尾部添加節點
     *
     * @param val
     */
    public void insertTail(Object val) {
        //新建鏈表節點node,當cursor.next為null即cursor已經在鏈表尾部
        Node tail = new Node(val);
        Node cursor = chain;
        while (cursor.next != null) {
            cursor = cursor.next;
        }
        //獲取到最後一個節點,使其next 指向tail 節點
        cursor.next = tail;

    }

    /**
     * 鏈表下表從1 開始,在指定位置插入節點
     *
     * @param val
     * @param position
     */
    public void insert(Object val, int position) {
        // 新建節點, 遍歷獲取鏈表的第port 個節點
        Node Node = new Node(val);
        Node cursor = chain;
        // 找到插入的位置前的的那一個節點
        if (position >= 0 && position <= this.getLength()) {
            for (int i = 1; i < position; i++) {
                cursor = cursor.next;
            }
        }
        // 插入
        Node.next = cursor.next;
        cursor.next = Node;
    }

    /**
     * 刪除指定位置的節點
     * @param position
     * @return
     */
    public void delete(int position) {

        if (chain == null)
            return ;

        if (position > 0 && position < this.getLength()) {
            for (int i = 1; i < this.getLength()+1; i++)
                //當前位置的上一個節點的Next 指向當前節點的Next 節點, 此時當前節點不存在於該鏈表中
                if (i == position) {
                    this.getPosition(i-1).next = this.getPosition(i+1);
                }
        }
    }

    /**
     * 鏈表逆置方法
     * 將指向逆置, 更新原鏈表的下一個節點Next 為上一個節點,
     * 原鏈表的第一個節點逆置後作為新鏈表的最後一個節點, 其Next 指向null 即可
     *
     * @return
     */
    public Node reverse() {
        Node pre = null;
        Node cursor = chain;
        // 當cursor.next==null時即cursor到尾部時再循環一次把cursor變成頭指針。
        while (cursor != null) {
            Node cursorNext = cursor.next;
            if (cursorNext == null) {
                // 逆序後鏈表
                chain = new Node();
                chain.next = cursor;
            }
            if (pre != null && pre.getNext() == null && pre.getData() == null)
                pre = null;
            cursor.next = pre;
            pre = cursor;
            cursor = cursorNext;
        }
        return chain;
    }

    /**
     * 鏈表下標從1開始,獲取第position 個節點
     *
     * @param position
     * @return
     */
    public Node getPosition(int position) {
        Node cursor = chain;
        //節點插入的位置超出鏈表範圍
        if (position < 0 || position > this.getLength()) {
            return null;
        }
        for (int i = 0; i < position; i++) {
            cursor = cursor.next;
        }
        return cursor;
    }

    /**
     * 獲取倒數第position 個節點
     *
     * @param position
     * @return
     */
    public Node getBackPosition(int position) {
        Node cursor = chain;
        //節點插入的位置超出鏈表範圍
        if (position < 0 || position > this.getLength())
            return null;

        //找到倒數第position 個位置
        for (int i = 0; i < this.getLength() - position + 1; i++)
            cursor = cursor.next;

        return cursor;

    }

    /**
     * 鏈表存在環
     * 追逐方法解決該問題
     * 環模型:
     * 存在環即鏈表中存在某個節點的Next 指向它前面的某個節點, 此時遍歷鏈表是一個死循環
     *
     * @return
     */
    public boolean judgeLoop() {
        if (chain == null)
            return false;

        //快節點和慢節點從首位節點一起出發
        Node fast = chain;
        Node slow = chain;
        //快節點每次走兩步, 慢節點每次走一步, 如果是環, 則快節點會追上慢節點
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow)
                return true;
        }
        return false;
    }

    /**
     * 計算環的長度
     * @return
     */
    public int getLoopLength() {
        if (chain == null)
            return 0;
        Node fast = chain;
        Node slow = chain;
        // 標識是否相遇
        boolean  tag = false;
        // 初始化環長
        int length = 0;
        // fast走兩步, slow走一步, 快指針與慢指針第二次相遇時慢指針走的長度就是環的大小
        while(fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            // 初始化tag 為false, 第一次相遇標記tag 為true
            if(fast == slow && tag == false) {
                tag =true;
            }
            // 第一次相遇後開始計算環的長度
            if(tag == true ) {
                length++;
            }
            // tag為true 是已經相遇過了,當fast == slow 此時是第二次相遇
            if(fast == slow && tag == true) {
                break;
            }
        }
        return length;
    }

    /**
     * 環的入口
     *
     * 環後續單獨拉出來寫一個博客, 此處應該畫個圖更好理解
     * 暫時先給個不錯的鏈接: https://www.cnblogs.com/fankongkong/p/7007869.html
     * @return
     */
    public Node entryLoop(){
        // 指向首節點
        Node fast = chain;
        Node slow = chain;
        // 找環中相匯點, 快節點走兩步慢節點走一步
        while(slow.next != null && fast.next.next != null){
            fast = fast.next.next;
            slow = slow.next;
            // 相遇, 此時慢節點走了x 個節點, 快節點走了2x 個節點, 設環有m 個節點, fast 多走了n 圈
            // 有x + m*n = 2*x 即m*n = x, slow 走到環入口後就一直在環裏, 所以相遇點距離環入口的長度和慢節點從首節點走到環入口的距離相等
            if(fast == slow) {
                break;
            }
        }
        // fast 從相遇點一步一步走, slow 從首節點開始一步一步走, 走相同的距離相遇的點就是環的入口
        slow = chain;
        while(fast != slow){
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }

    /**
     * 清除鏈表元素
     */
    public void clear() {
        chain = new Node();
    }
}

2.3 關於環的補充

關於環,在下面2-3 中測試類中模擬環,讓尾節點指向首節點,實現一個簡單的環結構。存在環結構的鏈表遍歷會進入死循環。

測試類中在模擬環結構之前,鏈表結構如下:

技術分享圖片

圖2-1、鏈表逆置的數據圖

模擬環結構之後,鏈表數據結構如下:

技術分享圖片 圖2-2、鏈表模擬環結構的數據圖

2-4 鏈表測試類TestChainTable.java

測試鏈表封裝類的常用方法,以供參考。

/**
 * @Description :鏈表測試類
 * @Author: niaonao
 * @Date: 2018/8/11 16:11
 */
public class TestChainTable {

    //聲明鏈表
    private static ChainTable chainLink;

    public static void main(String a[]) {
        testChainMethod();
    }

    /**
     * 測試鏈表方法
     */
    private static void testChainMethod() {
        System.out.println("\n1.1 初始化鏈表數據");
        init();

        if (chainLink == null || chainLink.getLength() < 1) {
            return;
        }
        showChain();

        System.out.println("\n2.1 鏈表第三個節點之後插入數據為Three 的節點");
        chainLink.insert("Three", 3);

        showChain();

        System.out.println("\n3.1 獲取鏈表中第5 個節點的數據: " + chainLink.getPosition(5).getData());
        showChain();


        System.out.println("\n4.1 獲取鏈表中倒數第5 個節點的數據: " + chainLink.getBackPosition(5).getData());
        showChain();

        System.out.println("\n5.1 鏈表刪除第2 個節點 ");
        chainLink.delete(2);
        showChain();

        System.out.println("\n6.1 鏈表逆序");
        chainLink.reverse();
        showChain();

        System.out.println("\n7.1 鏈表是否相交: " + chainLink.judgeLoop());
        showChain();

        System.out.println("\n7.2 模擬環模型, 使當前鏈表尾節點的Next 指向首節點");
        Node firstNode = chainLink.getPosition(1);
        Node lastNode = chainLink.getBackPosition(1);
        lastNode.setNext(firstNode);
        //出現環時, 鏈表遍歷是個死循環
        //showChain();

        if (chainLink.judgeLoop()){
            System.out.print("\n7.3 鏈表是否有環: " + Boolean.TRUE + "\n\t出現環時, 鏈表遍歷是個死循環");

            System.out.print("\n\t環的長度: " + chainLink.getLoopLength());
            System.out.print("\n\t環的入口: " + chainLink.entryLoop()
                            + "\n\t\t環入口節點的數據data: " + chainLink.entryLoop().getData()
                            + "\n\t\t下一個節點對象node: " + chainLink.entryLoop().getNext());
        }
        else {
            System.out.println("\n7.3 鏈表是否有環: " + Boolean.TRUE + "\n\t" + chainLink);
        }

    }

    /**
     * 初始化數據
     */
    private static void init() {

        chainLink = new ChainTable();

        System.out.println("\t在鏈表首位添加A,鏈表尾部依此添加B、C、D、E");
        chainLink.insertHead("A");
        chainLink.insertTail("B");
        chainLink.insertTail("C");
        chainLink.insertTail("D");
        chainLink.insertTail("E");

    }

    /**
     * 鏈表數據顯示
     */
    private static void showChain() {
        System.out.println("\t鏈表長度: " + chainLink.getLength());
        System.out.print("\t鏈表數據: ");
        for (int i = 0; i < chainLink.getLength(); i++) {
            Node node = chainLink.getPosition(i + 1);
            System.out.print("\t " + node.getData());
        }
    }
}

測試結果:

1.1 初始化鏈表數據
在鏈表首位添加A,鏈表尾部依此添加B、C、D、E
鏈表長度: 5
鏈表數據: A B C D E
2.1 鏈表第三個節點之後插入數據為Three 的節點
鏈表長度: 6
鏈表數據: A B Three C D E
3.1 獲取鏈表中第5 個節點的數據: D
鏈表長度: 6
鏈表數據: A B Three C D E
4.1 獲取鏈表中倒數第5 個節點的數據: B
鏈表長度: 6
鏈表數據: A B Three C D E
5.1 鏈表刪除第2 個節點
鏈表長度: 5
鏈表數據: A Three C D E
6.1 鏈表逆序
鏈表長度: 5
鏈表數據: E D C Three A
7.1 鏈表是否相交: false
鏈表長度: 5
鏈表數據: E D C Three A
7.2 模擬環模型, 使當前鏈表尾節點的Next 指向首節點

7.3 鏈表是否有環: true
出現環時, 鏈表遍歷是個死循環
環的長度: 1
環的入口: Node@2503dbd3
環入口節點的數據data: E
下一個節點對象node: Node@4b67cf4d

面向對象來理解鏈表