數據結構與算法——鏈表

1. 概述

前面說到了數組,利用連續的內存空間來存儲相同類型的數據,其最大的特色是支持下標隨機訪問,可是刪除和插入的效率很低。今天來看另外一種很基礎的數據結構——鏈表。鏈表不須要使用連續的內存空間,它使用指針將不連續的內存塊鏈接起來,造成一種鏈式結構。node

2. 單鏈表

首先來看看單鏈表,存儲數據的內存塊叫作節點,每一個節點保存了一個 next 指針,指向下一個節點的內存地址,結合下圖你就很容易看明白了:
在這裏插入圖片描述
其中有兩個節點指針比較的特殊,首先是鏈表頭節點的指針,它指向了鏈表的第一個節點的地址,利用它咱們能夠遍歷獲得整個鏈表。其次是尾結點的指針,它指向了 null ,表示鏈表結束。數組

不難看出,單鏈表的最大特色即是使用指針來鏈接不連續的節點,這樣咱們不用擔憂擴容的問題了,而且,鏈表的插入和刪除操做也很是的高效,只須要改變指針的指向便可。
在這裏插入圖片描述
結合上圖不難理解,單鏈表可以在 O(1) 複雜度內刪除和添加元素,這就比數組高效不少。可是,若是咱們要查找鏈表數據怎麼辦呢?鏈表的內存不是連續的,不能像數組那樣根據下標訪問,因此只能經過遍歷鏈表來查找,時間複雜度爲 O(n)。下面是單鏈表的代碼示例:數據結構

public class SingleLinkedList {
    private Node head = null;//鏈表的頭節點

    //根據值查找節點
    public Node findByValue(int value) {
        Node p = head;
        while (p != null && p.getData() != value)
            p = p.next;
        return p;
    }

    //根據下標查找節點
    public Node findByIndex(int index) {
        Node p = head;
        int flag = 0;
        while (p != null){
            if (flag == index)
                return p;
            flag ++;
            p = p.next;
        }
        return null;
    }

    //插入節點到鏈表頭部
    public void insertToHead(Node node){
        if (head == null) head = node;
        else {
            node.next = head;
            head = node;
        }
    }
    public void insertToHead(int value){
        this.insertToHead(new Node(value));
    }

    //插入節點到鏈表末尾
    public void insert(Node node){
        if (head == null){
            head = node;
            return;
        }

        Node p = head;
        while (p.next != null) p = p.next;
        p.next = node;
    }
    public void insert(int value){
        this.insert(new Node(value));
    }

    //在某個節點以後插入節點
    public void insertAfter(Node p, Node newNode){
        if (p == null) return;
        newNode.next = p.next;
        p.next = newNode;
    }
    public void insertAfter(Node p, int value){
        this.insertAfter(p, new Node(value));
    }

    //在某個節點以前插入節點
    public void insertBefore(Node p, Node newNode){
        if (p == null) return;
        if (p == head){
            insertToHead(newNode);
            return;
        }
        //尋找節點p前面的節點
        Node pBefore = head;
        while (pBefore != null && pBefore.next != p){
            pBefore = pBefore.next;
        }

        if (pBefore == null) return;
        newNode.next = pBefore.next;
        pBefore.next = newNode;
    }
    public void insertBefore(Node p, int value){
        insertBefore(p, new Node(value));
    }

    //刪除節點
    public void deleteByNode(Node p){
        if (p == null || head == null) return;
        if (p == head){
            head = head.next;
            return;
        }
        Node pBefore = head;
        while (pBefore != null && pBefore.next != p){
            pBefore = pBefore.next;
        }

        if (pBefore == null) return;
        pBefore.next = pBefore.next.next;
    }

    //根據值刪除節點
    public void deleteByValue(int value){
        Node node = this.findByValue(value);
        if (node == null) return;
        this.deleteByNode(node);
    }

    //打印鏈表的全部節點值
    public void print(){
        Node p = head;
        while (p != null){
            System.out.print(p.getData() + "  ");
            p = p.next;
        }
        System.out.println();
    }
        //定義鏈表節點
    public static class Node{
        private int data;
        private Node next;

        public Node(int data) {
            this.data = data;
            this.next = null;
        }

        public int getData() {
            return data;
        }
    }
}
3. 循環鏈表

循環鏈表和單鏈表的惟一區別即是鏈表的尾結點指針並非指向 null,而是指向了頭節點,這樣便造成了一個環形的鏈表結構:
在這裏插入圖片描述this

4. 雙向鏈表

雙向鏈表,顧名思義,就是鏈表不僅是存儲了指向下一個節點的 next 指針,還存儲了一個指向前一個節點的 prev 指針,以下圖:
在這裏插入圖片描述
爲何要使用這種具備兩個指針的鏈表呢?主要是爲了解決鏈表刪除和插入操做的效率問題。spa

在單鏈表中,要刪除一個節點,必須找到其前面的節點,這樣就要遍歷鏈表,時間開銷較高。可是在雙向鏈表中,因爲每一個節點都保存了指向前一個節點的指針,這樣咱們可以在 O(1) 的時間複雜度內刪除節點。指針

插入操做也相似,好比要在節點 p 以前插入一個節點,那麼也必須遍歷單鏈表找到 p 節點前面的那個節點。可是雙向鏈表能夠直接利用前驅指針 prev 找到前一個節點,很是的高效。code

這也是雙向鏈表在實際開發中用的更多的緣由,雖然每一個節點存儲了兩個指針,空間開銷更大,這就是一種典型的用空間換時間的思想。blog

下面是雙向鏈表的代碼示例:圖片

public class DoubleLinkedList {
    private Node head = null;//鏈表的頭節點

    //在某個節點以前插入節點,這裏方能體現出雙向鏈表的優點
    public void insertBefore(Node p, Node newNode) {
        if (p == null) return;
        if(p == head) {
            this.insertToHead(newNode);
            return;
        }
        
        newNode.prev = p.prev;
        p.prev.next = newNode;
        
        newNode.next = p;
        p.prev = newNode;
    }
    public void insertBefore(Node p, int value) {
        this.insertBefore(p, new Node(value));
    }
    
    //刪除某個節點
    public void deleteByNode(Node node) {
        if(node == null || head == null) return;
        if (node == head) {
            head = head.next;
            if(head != null) head.prev = null;
            return;
        }
        Node prev = node.prev;
        Node next = node.next;
        prev.next = next;
        if(next != null) next.prev = prev;
    }
    
    //根據值刪除節點
    public void deleteByValue(int value) {
        Node node = this.findByValue(value);
        if (node == null) return;
        this.deleteByNode(node);
    }
   
    //定義鏈表節點
    public static class Node{
        private int data;
        private Node prev;//鏈表的前驅指針
        private Node next;//鏈表的後繼指針
        
        public Node(int data) {
            this.data = data;
            this.prev = null;
            this.next = null;
        }

        public int getData() {
            return data;
        }
    }
}
相關文章
相關標籤/搜索