『數據結構與算法』—— 鏈表

底層存儲結構

數組 對比,數組需要一塊 連續的內存空間 來存儲,對內存要求很高。如果我們申請 50MB 的內存,即便內存的剩餘內存大於 50MB,但是如果內存不是連續的,也是很有可能申請失敗。

鏈表 與之相反,它並不需要一塊連續的內存,通過 指針 將一組 零散的內存塊 串聯起來使用。

下面的圖片可以看出兩者之間的區別:

鏈表分類

單鏈表

單鏈表每個節點有兩部分組成,數據後繼指針 next

第一個節點稱爲 頭結點, 最後一個節點稱爲 尾節點 ,尾節點 next 指向 null。鏈表的插入增加和刪除,不需要移動,只需要改變前後節點 next 的指針即可實現。

但是,如果需要訪問第 N 個元素,則需要時間複雜度爲 O(n)

循環鏈表

循環鏈表是從鏈尾直接連接到鏈頭。

雙向鏈表

相比於單鏈表,雙向鏈表具有兩個節點:前繼節點和後繼節點。

LRU 緩存淘汰算法

維護一個有序單鏈表,越靠近鏈尾的節點是越早訪問的,當有一個新的數據訪問時,我們從鏈表頭部開始順序遍歷鏈表。

  1. 如果該數據已經存在於鏈表中,那麼遍歷拿個這個數據的節點刪除,並將新的插入到頭部。
  2. 如果此數據沒有再緩存鏈表中:
    1. 如果緩存未滿,則直接將該節點插入鏈表的頭部
    2. 如果此時鏈表已滿,則鏈表尾節點刪除,將新的數據節點插入到鏈表的頭部。

鏈表小總結

說一句尷尬的話,本人大學的時候也是掛了 算法與數據結構 這門課,說實話,對算法也是有一點陰影,總感覺自己會學不好。就拿寫鏈表而言,經常被引用的移動產生誤解,在過程中經常不知道指針移到到哪了,導致並不會實現自己預期想要的結果。其實練習題做下來還是蠻有感觸的,還是要多練習,從中可以找到一些技巧,下面總結一下:

理解指針或引用的含義

對於我這個學習 Java 的而言,其實就是要理解引用的含義。

將某個變量賦值給引用,實際上是將這個變量的地址賦值給該引用,或者反過來說,引用中存儲了這個變量的內存地址,指向了這個變量,通過這個指針就能找到這個變量。

例子

Node node1 = new Node(null, 1);
node1 = new Node(null, 2);
Node node2 = new Node(null, 3);
node1 = node2;
node1.value = 3;

只要是 node = 引用後面直接跟上等於,那就證明只是單純的將這個引用指向另一個內存地址,並不會影響任何變量的值。而如果 node. 引用通過 . 的方式,那麼他的值就有變化的可能。

警惕引用丟失和內存泄漏

比如鏈表插入節點操作。

Node targetNode = new Node(null, 1); // 待被插入的節點
Node preNode; // 已經被找到插入前一個節點

錯誤示範:

preNode.next = targetNode;
targeNode.next = preNode.next.next;

這裏現將前節點指向目標節點,然後目標節點再指向前節點的下下個節點,意思是沒有錯誤,但是會導致後節點以及之後都會被丟失。

正確的做法應該是:

targetNode.next = preNode.next.next;
preNode.next = targeNode;

雖然僅僅是調換了順序,就會產生不同的結果。同樣刪除節點時,也要記得手動釋放內存。

利用哨兵簡化實現難度

針對鏈表的插入、刪除操作,需要對插入第一個結點和刪除最後一個結點的情況進行特殊處理。

重點留意邊界條件處理

  1. 如果鏈表爲空,代碼是否能正常工作
  2. 如果鏈表只包含一個節點,代碼是否能正常工作
  3. 如果鏈表只包含兩個節點,代碼是否能正常工作
  4. 代碼邏輯在處理頭結點和尾節點的時候,代碼是否能正常工作

舉例畫圖,輔助思考

我平常在做練習題時,身邊必備一張紙和一紙筆,對特殊的邏輯進行畫圖舉例演示,很大程度上可以幫助自己理清邏輯。

多寫多練

多多練習吧!

下面寫幾個練習題

代碼實現單鏈列表

class SinglyLinkedList {

    private Node head = null;

    void insertToHead(Node node) {
        if (head == null) {
            head = node;
        } else {
            node.next = head;
            head = node;
        }
        System.out.println("insert head " + node.value + " data:\t" + toString());
    }

    void insertToHead(int value) {
        insertToHead(createNode(value));
    }

    void insertAfter(Node node, Node newNode) {
        if (node == null) return;
        newNode.next = node.next;
        node.next = newNode;
        System.out.println("insertAfter data:\t" + toString());
    }

    void insertAfter(Node node, int value) {
        insertAfter(node, createNode(value));
    }

    void insertBefore(Node before, Node newNode) {
        if (before == null) return;
        if (head == before) {
            insertToHead(newNode);
            return;
        }
        Node temp = head;
        while (temp != null && temp.next != before) {
            // 直到找打 before 的上個節點
            temp = temp.next;
        }
        newNode.next = before;
        temp.next = newNode;
        System.out.println("insertBefore data:\t" + toString());
    }

    void insertBefore(Node before, int value) {
        insertBefore(before, createNode(value));
    }

    Node findByValue(int value) {
        Node temp = head;
        while (temp != null && temp.value != value) {
            temp = temp.next;
        }
        return temp;
    }

    Node findByIndex(int index) {
        Node temp = head;
        int pos = 0;
        while (temp != null && pos != index) {
            temp = temp.next;
            pos++;
        }
        return temp;
    }

    void insertLast(Node node) {
        if (head == null) {
            head = node;
            System.out.println("insert last " + node.value + " data:\t" + toString());
            return;
        }
        Node temp = head;
        while (temp.next != null) {
            temp = temp.next;
        }
        temp.next = node;
        System.out.println("insert last " + node.value + " data:\t" + toString());
    }

    void insertLast(int value) {
        insertLast(createNode(value));
    }

    private Node createNode(int value) {
        return new Node(null, value);
    }

    void deleteByValue(int value) {
        if (head == null) {
            return;
        }
        Node before = null;
        Node current = head;
        while (current != null && current.value != value) {
            before = current;
            current = current.next;
        }
        if (current == null) {
            return;
        }
        if (before == null) {
            head = head.next;
        } else {
            before.next = current.next;
        }
        System.out.println("deleteByValue:\t" + toString());
    }

    void deleteByNode(Node node) {
        if (head == null || node == null) {
            return;
        }
        Node temp = head;
        while (temp != null && temp.next != node) {
            temp = temp.next;
        }
        temp.next = node.next;
        System.out.println("deleteByNode:\t" + toString());
    }


    @Override
    public String toString() {
        Node temp = head;
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        while (temp != null) {
            sb.append(temp.value).append(",");
            temp = temp.next;
        }
        sb.append("]");
        return sb.toString();
    }

    static class Node {
        int value;
        Node next;

        Node(Node next, int value) {
            this.next = next;
            this.value = value;
        }

        int getData() {
            return value;
        }
    }

    public static void main(String[] args) {
        SinglyLinkedList linkedList = new SinglyLinkedList();
        linkedList.insertLast(1);
        linkedList.insertLast(2);
        linkedList.insertLast(3);
        linkedList.insertLast(4);
        linkedList.insertToHead(5);
        linkedList.insertAfter(linkedList.findByValue(3), 6);
        linkedList.insertBefore(linkedList.findByValue(1), 7);

        linkedList.deleteByValue(2);
        linkedList.deleteByNode(linkedList.findByValue(1));

    }

}

輸出結果:

insert last 1 data: [1,]
insert last 2 data: [1,2,]
insert last 3 data: [1,2,3,]
insert last 4 data: [1,2,3,4,]
insert head 5 data: [5,1,2,3,4,]
insertAfter data:   [5,1,2,3,6,4,]
insertBefore data:  [5,7,1,2,3,6,4,]
deleteByValue:  [5,7,1,3,6,4,]
deleteByNode:   [5,7,3,6,4,]

判斷是否爲迴文數

核心思想:使用兩個指針,一個慢指針,每次移動一個節點,一個快指針,每次移動兩個節點。

private boolean isPalindrome() {
        if (head == null || head.next == null) {
            return false;
        }
        Node pre = null;
        Node slow = head;
        Node fast = head;
        while (fast != null && fast.next != null) {
            // 快指針每次跳兩個
            fast = fast.next.next;
            // 臨時指針指向下個節點
            Node temp = slow.next;
            // 將 slow 迴轉
            slow.next = pre;
            pre = slow;
            slow = temp;
        }
        if (fast != null) {
            // 這種代表着節點數爲偶數,slow 需要往後移動一位
            slow = slow.next;
        }
        while (slow != null && pre != null) {
            if (slow.value != pre.value) {
                return false;
            }
            slow = slow.next;
            pre = pre.next;
        }
        return true;
    }

輸出結果

insert last 1 data: [1,]
insert last 2 data: [1,2,]
insert last 2 data: [1,2,2,]
insert last 1 data: [1,2,2,1,]
是否爲迴文函數:    true

節點回轉

核心思想:每次遍歷時,迴轉節點的邏輯操作順序,且看代碼

    private static Node reversed(Node node) {
        if (node.next == null) {
            return node;
        }
        Node pre = null;
        while (node != null) {
            // 現將下個節點用 temp 保存下來
            Node temp = node.next;
            node.next = pre;
            pre = node;
            node = temp;
        }
        return pre;
    }

輸出結果:

01234

43210

判斷是否是循環節點

核心思想:循環節點的特點就在於鏈表的尾部指向頭部,只需要是用快慢節點進行遍歷,如果不是循環節點,循環一次就結束了。

    /**
     * 判斷是否是循環鏈表
     * 快慢節點,如果兩者相等必然是循環鏈表,時間複雜度爲 O(n)
     */
    private static boolean isCircle(Node node) {
        int count = 0;
        Node slow = node;
        Node fast = node;
        while (slow != null && fast != null && fast.next != null) {
            count++;
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) {
                System.out.println("循環了 " + count + "次");
                return true;
            }
        }
        System.out.println("循環了 " + count + "次");
        return false;
    }

輸出結果

循環了 5次
is circle:  true

拼接兩個有序節點

核心思想:首先創建一個空的頭節點,然後兩個有序鏈表依次遍歷,誰小就把這個空節點指向誰(默認從小到大排序)。其中需要注意的是,如果一個節點提前結束了,那麼就需要將新節點直接指向剩下的那個節點。

    private static Node mergeNode(Node aNode, Node bNode) {
        Node head = new Node(null, -1);
        Node tail = head;
        if (aNode == null) {
            head = bNode;
        }
        if (bNode == null) {
            head = aNode;
        }

        while (aNode != null && bNode != null) {
            if (aNode.value > bNode.value) {
                tail.next = bNode;
                bNode = bNode.next;
            } else {
                tail.next = aNode;
                aNode = aNode.next;
            }
            tail = tail.next;
        }
        if (aNode == null) {
            tail.next = bNode;
        }
        if (bNode == null) {
            tail.next = aNode;
        }
        if (head == null) {
            return null;
        }
        return head.next;
    }

輸出結果:

A 節點:   135

B 節點:   24689

12345689

刪除倒數第 N 個節點

核心思想:快慢指針,快的比慢 的多 N 個,遍歷到最後,慢節點指向倒數第 N 個節點,然後直接進行刪除。

    /**
     * 刪除倒數 N 個節點
     *
     */
    private static void deleteLast(Node node, int n) {
        int count = 0;
        Node head = node;
        while (head != null) {
            count ++;
            head = head.next;
        }
        if (n > count) {
            return;
        }
        if (n == count) {
            if (n == 1) {
                node.next = null;
            } else {
                node.value = node.next.value;
                node.next = node.next.next;
            }
            return;
        }
        Node slow = node;
        Node fast = node;
        int tempCount = 0;
        while (slow != null && fast.next != null) {
            if (tempCount < n) {
                tempCount ++;
            } else {
                slow = slow.next;
            }
            fast = fast.next;
        }
        slow.next = slow.next.next;
    }

輸出結果:

01234567

0124567

找出中間節點

核心思想:直接使用快慢指針進行遍歷,慢指針每次移動一個,快指針每次移動兩個。

    private static int findMiddleNode(Node node) {
        Node fast = node;
        Node slow = node;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow.value;
    }

輸出結果:

01234567

4

參考自極客時間

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章