雙向鏈表和雙向循環鏈表

雙向鏈表簡介

單向鏈表只有一個方向,結點只有一個後繼指針 next 指向後面的結點。而雙向鏈表,顧名思義,它支持兩個方向,每一個結點不止有一個後繼指針 next 指向後面的結點,還有一個前驅指針 prev 指向前面的結點。java

img

從上圖中能夠看出來,雙向鏈表須要額外的兩個空間來存儲後繼結點和前驅結點的地址。因此,若是存儲一樣多的數據,雙向鏈表要比單鏈表佔用更多的內存空間。雖然兩個指針比較浪費存儲空間,但能夠支持雙向遍歷,這樣也帶來了雙向鏈表操做的靈活性。那相比單鏈表,雙向鏈表適合解決哪一種問題呢?node

從結構上來看,雙向鏈表能夠支持 O(1) 時間複雜度的狀況下找到前驅結點,正是這樣的特色,也使雙向鏈表在某些狀況下的插入、刪除等操做都要比單鏈表簡單、高效數組

雙向鏈表的增刪改查操做

1. 插入操做緩存

  • 頭部插入:時間複雜度O(1)
  • 尾部插入:時間複雜度O(1)
  • 指定位置後面插入:時間複雜度O(1)
  • 指定位置前面插入:時間複雜度O(1) ---注意和單向鏈表的區別

2. 刪除操做安全

刪除操做的時間複雜度和插入操做的時間複雜度相似。數據結構

  • 刪除頭部節點:時間複雜度O(1)
  • 刪除尾部節點:時間複雜度O(1)
  • 刪除值等於某個數的節點:時間複雜度O(n)
  • 刪除某個具體節點:O(1)

關於刪除操做,這邊作下說明。測試

在實際的軟件開發中,從鏈表中刪除一個數據無外乎這兩種狀況:this

  1. 刪除結點中「值等於某個給定值」的結點;線程

  2. 刪除給定指針指向的結點。指針

對於第一種狀況,不論是單鏈表仍是雙向鏈表,爲了查找到值等於給定值的結點,都須要從頭結點開始一個一個依次遍歷對比,直到找到值等於給定值的結點,而後再經過我前面講的指針操做將其刪除。

儘管單純的刪除操做時間複雜度是 O(1),但遍歷查找的時間是主要的耗時點,對應的時間複雜度爲 O(n)。根據時間複雜度分析中的加法法則,刪除值等於給定值的結點對應的鏈表操做的總時間複雜度爲 O(n)。

對於第二種狀況,咱們已經找到了要刪除的結點,可是刪除某個結點 q 須要知道其前驅結點,而單鏈表並不支持直接獲取前驅結點,因此,爲了找到前驅結點,咱們仍是要從頭結點開始遍歷鏈表,直到 p->next=q,說明 p 是 q 的前驅結點。

可是對於雙向鏈表來講,這種狀況就比較有優點了。由於雙向鏈表中的結點已經保存了前驅結點的指針,不須要像單鏈表那樣遍歷。因此,針對第二種狀況,單鏈表刪除操做須要 O(n) 的時間複雜度,而雙向鏈表只須要在 O(1) 的時間複雜度內就搞定了!

除了插入、刪除操做有優點以外,對於一個有序鏈表,雙向鏈表的按值查詢的效率也要比單鏈表高一些。由於,咱們能夠記錄上次查找的位置 p,每次查詢時,根據要查找的值與 p 的大小關係,決定是往前仍是日後查找,因此平均只須要查找一半的數據。

如今,你有沒有以爲雙向鏈表要比單鏈表更加高效呢?這就是爲何在實際的軟件開發中,雙向鏈表儘管比較費內存,但仍是比單鏈表的應用更加普遍的緣由。若是你熟悉 Java 語言,你確定用過 LinkedHashMap 這個容器。若是你深刻研究 LinkedHashMap 的實現原理,就會發現其中就用到了雙向鏈表這種數據結構。

3. 更新操做

  • 更新指定節點:時間複雜度O(1)
  • 將鏈表中值等於某個具體值的節點更新:時間複雜度O(n)

4. 查詢操做

  • 時間複雜度:O(n)

雙向鏈表的Java代碼實現

public class TwoWayLinkedList<E> {


    public static void main(String[] args) {
        TwoWayLinkedList<Integer> list = new TwoWayLinkedList<>();
        //尾部插入,遍歷鏈表輸出
        System.out.println("尾部插入[1-10]");
        for (int i = 1; i <= 10; i++) {
            list.addLast(Integer.valueOf(i));
        }
        list.printList();
        //頭部插入,遍歷鏈表輸出
        System.out.println("頭部插入[1-10]");
        for (int i = 1; i <= 10; i++) {
            list.addFirst(Integer.valueOf(i));
        }
        list.printList();
        //在指定節點後面插入
        System.out.println("在頭節點後面插入[100]");
        list.addAfter(100, list.head);
        list.printList();
        System.out.println("在頭節點前面插入[100]");
        list.addBefore(100, list.head);
        list.printList();
        System.out.println("在尾節點前面插入[100]");
        list.addBefore(100, list.tail);
        list.printList();
        System.out.println("在尾節點後面插入[100]");
        list.addAfter(100, list.tail);
        list.printList();

        System.out.println("------------刪除方法測試-----------");
        System.out.println("刪除頭節點");
        list.removeFirst();
        list.printList();
        System.out.println("刪除尾節點");
        list.removeLast();
        list.printList();
        System.out.println("刪除指定節點");
        list.removeNode(list.head.next);
        list.printList();
    }


    private Node head;
    private Node tail;

    public TwoWayLinkedList() {
    }

    public TwoWayLinkedList(E data) {
        Node node = new Node<>(data, null,null);
        head = node;
        tail = node;
    }

    public void printList() {
        Node p = head;
        while (p != null && p.next != null) {
            System.out.print(p.data + "-->");
            p = p.next;
        }
        if (p != null) {
            System.out.println(p.data);
        }
    }

    public void addFirst(E data) {
        Node newNode = new Node(data,null ,head);
        if(head!=null){
            head.pre = newNode;
        }
        head = newNode;
        if (tail == null) {
            tail = newNode;
        }
    }

    public void addLast(E data) {
        Node newNode = new Node(data, tail,null);
        if (tail == null) {
            head = newNode;
            tail = newNode;
        } else {
            tail.next = newNode;
            tail = newNode;
        }
    }

    /**
     * @param data
     * @param node node節點必須在鏈表中
     */
    public void addAfter(E data, Node node) {
        if (node == null) {
            return;
        }
        Node newNode = new Node(data, node,node.next);
        if(node.next!=null){
            node.next.pre = newNode;
        }
        node.next = newNode;
        if (tail == node) {
            tail = newNode;
        }
    }

    public void addBefore(E data, Node node) {
        if (node == null) {
            return;
        }
        if(node==head){
            addFirst(data);
        }else {
            Node newNode = new Node(data,node.pre,node);
            node.pre.next = newNode;
            node.pre = newNode;
        }
    }

    public void removeFirst() {
        if (head == null) {
            return;
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            if(head.next!=null){
                head.next.pre = null;
            }
            head = head.next;
        }
    }

    public void removeLast() {
        if (tail == null) {
            return;
        }
        if (head == tail) {
            head = null;
            tail = null;
        } else {
            if(tail.pre!=null){
                tail.pre.next = null;
                Node p = tail.pre;
                tail.pre = null;
                tail = p;
            }
        }
    }

    public void removeNode(Node node) {
        if (node == null) {
            return;
        }
        if(node==head){
            removeFirst();
        }
        if(node==tail){
            removeLast();
        }
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    private static class Node<E> {
        E data;
        Node pre;
        Node next;

        public Node(E data, Node pre, Node next) {
            this.data = data;
            this.pre = pre;
            this.next = next;
        }
    }

}

雙向鏈表的JDK實現

JDK中的LinkedList就是一個雙向鏈表。咱們能夠直接拿來使用,或者作簡單的封裝。

package com.csx.algorithm.link;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.Set;
import java.util.function.Predicate;

public class SinglyLinkedList2<E> {

    private LinkedList<E> list;

    public SinglyLinkedList2() {
        this.list = new LinkedList<>();
    }

    public SinglyLinkedList2(E data){
        Set<E> singleton = Collections.singleton(data);
        this.list = new LinkedList<>(singleton);
    }

    public SinglyLinkedList2(Collection<? extends E> c){
        this.list = new LinkedList<>(c);
    }

    // ----------------------------------新增方法---------------------------------------

    public void addFirst(E data){
        list.addFirst(data);
    }

    public void addLast(E data){
        list.addLast(data);
    }
    // 在鏈表末尾添加
    public boolean add(E date){
        return list.add(date);
    }

    public boolean addAll(Collection<? extends E> collection){
        return list.addAll(collection);
    }

    public boolean addBefore(E data,E succ){
        int i = list.indexOf(succ);
        if(i<0){
            return false;
        }
        list.add(i,data);
        return true;
    }

    public boolean addAfter(E data,E succ){
        int i = list.indexOf(succ);
        if(i<0){
            return false;
        }
        if((i+1)==list.size()){
            list.addLast(data);
            return true;
        }else {
            list.add(i+1,data);
            return true;
        }
    }
    // ---------------------------------- 刪除方法---------------------------------------
    // 刪除方法,默認刪除鏈表頭部元素
    public E remove(){
        return list.remove();
    }
    // 刪除方法,刪除鏈表第一個元素
    public E removeFirst(){
        return list.removeFirst();
    }
    // 刪除方法,刪除鏈表最後一個元素
    public E removeLast(){
        return list.removeLast();
    }
    // 刪除鏈表中第一次出現的元素,成功刪除返回true
    // 對象相等的標準是調用equals方法相等
    public boolean remove(E data){
        return list.remove(data);
    }
    // 邏輯和remove(E data)方法相同
    public boolean removeFirstOccur(E data){
        return list.removeFirstOccurrence(data);
    }
    // 由於LinkedList內部是雙向鏈表,因此時間複雜度和removeFirstOccur相同
    public boolean removeLastOccur(E data){
        return list.removeLastOccurrence(data);
    }
    // 批量刪除方法
    public boolean removeAll(Collection<?> collection){
        return list.removeAll(collection);
    }
    // 按照條件刪除
    public boolean re(Predicate<? super E> filter){
        return list.removeIf(filter);
    }
    // ----------------------------- 查詢方法----------------------------
    // 查詢鏈表頭部元素
    public E getFirst(){
        return list.getFirst();
    }
    // 查詢鏈表尾部元素
    public E getLast(){
        return list.getLast();
    }
    // 查詢鏈表是否包含某個元素
    // 支持null判斷
    // 相等的標準是data.equals(item)
    public boolean contains(E data){
        return list.contains(data);
    }
    public boolean containsAll(Collection<?> var){
        return list.containsAll(var);
    }

}

仍是作下提醒,LinkedList並非線程安全的。若是須要保證線程安全,須要你本身作同步控制。

雙向循環鏈表

其實就是將頭節點的前趨指針指向尾節點,將尾節點的後驅指針指向頭節點。

img

數組和鏈表的比較

img

不過,數組和鏈表的對比,並不能侷限於時間複雜度。並且,在實際的軟件開發中,不能僅僅利用複雜度分析就決定使用哪一個數據結構來存儲數據。

數組簡單易用,在實現上使用的是連續的內存空間,能夠藉助 CPU 的緩存機制,預讀數組中的數據,因此訪問效率更高。而鏈表在內存中並非連續存儲,因此對 CPU 緩存不友好,沒辦法有效預讀。

數組的缺點是大小固定,一經聲明就要佔用整塊連續內存空間。若是聲明的數組過大,系統可能沒有足夠的連續內存空間分配給它,致使「內存不足(out of memory)」。若是聲明的數組太小,則可能出現不夠用的狀況。這時只能再申請一個更大的內存空間,把原數組拷貝進去,很是費時。鏈表自己沒有大小的限制,自然地支持動態擴容,我以爲這也是它與數組最大的區別。

除此以外,若是你的代碼對內存的使用很是苛刻,那數組就更適合你。由於鏈表中的每一個結點都須要消耗額外的存儲空間去存儲一份指向下一個結點的指針,因此內存消耗會翻倍。並且,對鏈表進行頻繁的插入、刪除操做,還會致使頻繁的內存申請和釋放,容易形成內存碎片,若是是 Java 語言,就有可能會致使頻繁的 GC(Garbage Collection,垃圾回收)。因此,在咱們實際的開發中,針對不一樣類型的項目,要根據具體狀況,權衡到底是選擇數組仍是鏈表。

相關文章
相關標籤/搜索