線性表結構:單向鏈表

單向鏈表簡介

在底層結構上,單向鏈表通過指針將一組零散的內存塊串聯在一起。其中,我們把內存塊稱爲鏈表的“結點”。爲了將所有的結點串起來,每個鏈表的結點除了存儲數據之外,還需要記錄鏈上的下一個結點的地址。如下圖所示,我們把這個記錄下個結點地址的指針叫作後繼指針 next。

img

從畫的單鏈表圖中,你應該可以發現,其中有兩個結點是比較特殊的,它們分別是第一個結點和最後一個結點。我們習慣性地把第一個結點叫作頭結點,把最後一個結點叫作尾結點。其中,頭結點用來記錄鏈表的基地址。有了它,我們就可以遍歷得到整條鏈表。而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址 NULL,表示這是鏈表上最後一個結點。

單向鏈表的增刪改查操作

1. 插入操作

  • 頭部插入:時間複雜度O(1)
  • 尾部插入:時間複雜度O(1)
  • 指定位置後面插入:時間複雜度O(1)
  • 指定位置前面插入:時間複雜度O(n)

2. 刪除操作

刪除操作的時間複雜度和插入操作的時間複雜度類似。

  • 刪除頭部節點:時間複雜度O(1)
  • 刪除尾部節點:時間複雜度O(n),因爲首先需要遍歷鏈表,找到尾部節點的前一個節點(後面分析雙向鏈表時發現就不會有這個問題)
  • 刪除指定節點:時間複雜度O(n)

3. 更新操作

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

4. 查詢操作

  • 時間複雜度:O(n)

由於鏈表的底層數據是不連續的,所以不能通過隨機訪問進行數據尋址。只能通過遍歷進行查找數據。

單向鏈表的Java代碼實現

package com.csx.algorithm.link;

public class SinglyLinkedList<E> {


    public static void main(String[] args) {
        SinglyLinkedList<Integer> list = new SinglyLinkedList<>();
        //尾部插入,遍歷鏈表輸出
        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 SinglyLinkedList() {
    }

    public SinglyLinkedList(E data) {
        Node node = new Node<>(data, 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) {
        //允許節點值爲空
        //if(data==null){
        //    return;
        //}
        Node node = new Node(data, head);
        head = node;
        if (tail == null) {
            tail = node;
        }
    }

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

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

    /**
     * @param data
     * @param node node節點必須在鏈表中
     */
    public void addBefore(E data, Node node) {
        if (node == null) {
            return;
        }
        Node p = head;
        if (p == null) {
            throw new RuntimeException("node not in LinkedList...");
        }
        if (p == node) {
            Node newNode = new Node(data, node);
            head = newNode;
            return;
        }
        while (p.next != null) {
            if (p.next == node) {
                break;
            }
            p = p.next;
        }
        if (p.next == null) {
            throw new RuntimeException("node not in LinkedList...");
        }
        Node newNode = new Node(data, node);
        p.next = newNode;
    }

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

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

    public void removeNode(Node node) {
        if (node == null) {
            return;
        }
        Node p = head;
        if (p == null) {
            return;
        }
        while (p.next != null && p.next != node) {
            p = p.next;
        }
        if (p.next != null) {
            p.next = node.next;
        }
    }

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

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

}

單向鏈表的JDK實現

如果你使用高級編程語言,一般都會有現成的單向鏈表實現。比如你使用的是Java,其中的LinkedList就可以實現單向鏈表功能(雖然LinkedList底層是雙向鏈表,但是雙向鏈表可以實現單向鏈表的所有功能)。

有時候你可能只是想實現一個鏈表的結構,並不想暴露太多的操作API給用戶。這時候使用LinkedList可能不太能滿足你的需求,因爲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);
    }

}

鏈表的使用場景

因爲雙向鏈表、循環鏈表都能實現單鏈表的功能,所以這邊舉例的使用場景不僅僅是針對單鏈表的,使用其他鏈表也可以實現。

鏈表一個經典的鏈表應用場景就是 LRU 緩存淘汰算法。

緩存是一種提高數據讀取性能的技術,在硬件設計、軟件開發中都有着非常廣泛的應用,比如常見的 CPU 緩存、數據庫緩存、瀏覽器緩存等等。

緩存的大小有限,當緩存被用滿時,哪些數據應該被清理出去,哪些數據應該被保留?這就需要緩存淘汰策略來決定。常見的策略有三種:先進先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。

使用單鏈表實現LRU算法的大致思路是:

維護一個有序單鏈表(鏈表長度有限),越靠近鏈表尾部的結點是越早之前訪問的。當有一個新的數據被訪問時,我們從鏈表頭開始順序遍歷鏈表。

  1. 如果此數據之前已經被緩存在鏈表中了,我們遍歷得到這個數據對應的結點,並將其從原來的位置刪除,然後再插入到鏈表的頭部。

  2. 如果此數據沒有在緩存鏈表中,又可以分爲兩種情況:

  • 如果此時緩存未滿,則將此結點直接插入到鏈表的頭部;
  • 如果此時緩存已滿,則鏈表尾結點刪除,將新的數據結點插入鏈表的頭部。

實現上面算法的時間複雜度是O(n)。

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