【數(shù)據(jù)結(jié)構(gòu)】鏈表(Linked list)

什么是鏈表?

鏈表是一種在物理上非連續(xù),非順序的數(shù)據(jù)結(jié)構(gòu),由若干節(jié)點(diǎn)(node)所組成。

單向鏈表的每一個(gè)節(jié)點(diǎn)包含兩個(gè)部分,一部分存放數(shù)據(jù)的變量,另一部分是指向下一個(gè)節(jié)點(diǎn)的指針。鏈表的第一個(gè)節(jié)點(diǎn)稱(chēng)為頭節(jié)點(diǎn),最后一個(gè)節(jié)點(diǎn)稱(chēng)為尾結(jié)點(diǎn),尾結(jié)點(diǎn)的指針指向空。與數(shù)組按照索引來(lái)隨機(jī)查找數(shù)據(jù)不同,對(duì)于鏈表的其中一個(gè)節(jié)點(diǎn)A,我們只能根據(jù)節(jié)點(diǎn)A的next指針來(lái)找到該節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)B,在根據(jù)節(jié)點(diǎn)B的next指針找到一下個(gè)節(jié)點(diǎn)C,以此類(lèi)推。

那么如何找到該節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)呢?可以使用雙向鏈表。

單向鏈表存儲(chǔ)結(jié)構(gòu)如圖:

鏈表.png

節(jié)點(diǎn)代碼如下:

/**
  * 定義存儲(chǔ)數(shù)據(jù)的節(jié)點(diǎn)
  */
private class Node {

    public E e;

    public Node next;

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

    public Node(E e) {
        this(e, null);
    }

    public Node() {
        this(null, null);
    }

    @Override
    public String toString() {
        return e.toString();
    }

}

什么是雙向鏈表?

雙向鏈表比單向鏈表稍微復(fù)雜一些,它的每一個(gè)節(jié)點(diǎn)除了擁有data和next指針還包含一個(gè)指向前置節(jié)點(diǎn)的prev指針。

雙向鏈表存儲(chǔ)結(jié)構(gòu)如圖:

雙向鏈表.png

鏈表的實(shí)現(xiàn)

1、查找節(jié)點(diǎn)

查找元素時(shí),鏈表不像數(shù)組那樣可以通過(guò)索引來(lái)進(jìn)行快速定位,只能從頭節(jié)點(diǎn)向后一個(gè)一個(gè)的查找。

2、更新節(jié)點(diǎn)

如果不考慮查找節(jié)點(diǎn)的過(guò)程,鏈表的更新過(guò)程非常節(jié)點(diǎn),直接把舊數(shù)據(jù)替換成新數(shù)據(jù)即可。

3、插入節(jié)點(diǎn)

與數(shù)組類(lèi)似,鏈表插入節(jié)點(diǎn)同樣可以分為三種情況:

  • 頭部插入

    頭部插入,分為兩個(gè)步驟:

    1、把新節(jié)點(diǎn)的next指針指向當(dāng)前頭節(jié)點(diǎn)。

    2、把新節(jié)點(diǎn)變?yōu)殒湵淼念^節(jié)點(diǎn)。

  • 尾部插入

    尾部插入是最簡(jiǎn)單的情況,直接把尾結(jié)點(diǎn)的next執(zhí)行指向新節(jié)點(diǎn)即可。

  • 中間插入

    中間插入,分為兩個(gè)步驟:

    1、新節(jié)點(diǎn)的next指針指向插入位置的節(jié)點(diǎn)。

    2、插入位置的前置節(jié)點(diǎn)的next指針指向新節(jié)點(diǎn)。

4、刪除節(jié)點(diǎn)

同樣分為三種情況:

  • 頭部刪除

    把當(dāng)前鏈表的頭結(jié)點(diǎn)更新為原頭節(jié)點(diǎn)的next指針即可。

  • 尾部刪除

    把尾結(jié)點(diǎn)的前置節(jié)點(diǎn)的next指針指向?yàn)榭占纯伞?/p>

  • 中間刪除

    把要?jiǎng)h除的前置節(jié)點(diǎn)的next指針指向要?jiǎng)h除節(jié)點(diǎn)的next指針即可。

整體代碼如下:

/**
 * 描述:鏈表。
 * <p>
 * Create By ZhangBiao
 * 2020/5/11
 */
public class LinkedList<E> {

    /**
     * 虛擬頭結(jié)點(diǎn)
     */
    private Node dummyHead;

    private int size;

    public LinkedList() {
        this.dummyHead = new Node(null, null);
        this.size = 0;
    }

    /**
     * 獲取鏈表中的元素個(gè)數(shù)。
     *
     * @return
     */
    public int getSize() {
        return size;
    }

    /**
     * 返回鏈表是否為空。
     *
     * @return
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * 在鏈表的index(0-based)位置添加新的元素e,在鏈表中不是一個(gè)常用的操作,當(dāng)做練習(xí)。
     *
     * @param index
     * @param e
     */
    public void add(int index, E e) {
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed. Illegal index.");
        }
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        prev.next = new Node(e, prev.next);
        size++;
    }

    /**
     * 在鏈表頭添加新的元素e。
     *
     * @param e
     */
    public void addFirst(E e) {
        add(0, e);
    }

    /**
     * 在鏈表末尾添加新的元素e。
     *
     * @param e
     */
    public void addLast(E e) {
        add(size, e);
    }

    /**
     * 獲得鏈表的第index(0-based)個(gè)位置的元素。
     * 在鏈表中不是一個(gè)常用的操作,當(dāng)做練習(xí)用。
     *
     * @param index
     * @return
     */
    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Get failed. Illegal index.");
        }
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.e;
    }

    /**
     * 獲得鏈表的第一個(gè)元素。
     *
     * @return
     */
    public E getFirst() {
        return get(0);
    }

    /**
     * 獲得鏈表的最后一個(gè)元素。
     *
     * @return
     */
    public E getLast() {
        return get(size - 1);
    }

    /**
     * 修改鏈表的第index(0-based)個(gè)位置的元素為e。
     * 在鏈表中不是一個(gè)常用的操作,當(dāng)做練習(xí)用。
     *
     * @param index
     * @param e
     */
    public void set(int index, E e) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Set failed. Illegal index.");
        }
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        cur.e = e;
    }

    /**
     * 查找鏈表中是否有元素e。
     *
     * @param e
     * @return
     */
    public boolean contains(E e) {
        Node cur = dummyHead.next;
        while (cur != null) {
            if (cur.e.equals(e)) {
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    /**
     * 從鏈表中刪除index(0-based)位置的元素并返回刪除的元素。
     *
     * @param index
     * @return
     */
    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Remove failed. Index is illegal.");
        }
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        Node retNode = prev.next;
        prev.next = retNode.next;
        retNode.next = null;
        size--;
        return retNode.e;
    }

    /**
     * 從鏈表中刪除第一個(gè)元素并返回刪除的元素。
     *
     * @return
     */
    public E removeFirst() {
        return remove(0);
    }

    /**
     * 從鏈表中刪除最后一個(gè)元素并返回刪除的元素。
     *
     * @return
     */
    public E removeLast() {
        return remove(size - 1);
    }

    /**
     * 從鏈表中刪除元素e
     *
     * @param e
     */
    public void removeElement(E e) {
        Node prev = dummyHead;
        while (prev.next != null) {
            if (prev.next.e.equals(e)) {
                break;
            }
            prev = prev.next;
        }
        if (prev.next != null) {
            Node delNode = prev.next;
            prev.next = delNode.next;
            delNode.next = null;
            size--;
        }
    }

    @Override
    public String toString() {
        StringBuilder result = new StringBuilder();
        /*Node cur = dummyHead.next;
        while (cur != null) {
            result.append(cur + " -> ");
            cur = cur.next;
        }*/
        for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
            result.append(cur + " -> ");
        }
        result.append("NULL");
        return result.toString();
    }


    /**
     * 定義存儲(chǔ)數(shù)據(jù)的節(jié)點(diǎn)
     */
    private class Node {

        public E e;

        public Node next;

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

        public Node(E e) {
            this(e, null);
        }

        public Node() {
            this(null, null);
        }

        @Override
        public String toString() {
            return e.toString();
        }

    }

}

在這里我使用了虛擬頭結(jié)點(diǎn),為什么使用虛擬頭結(jié)點(diǎn)?

在添加元素的過(guò)程中遇到一個(gè)問(wèn)題:現(xiàn)在要在任意位置上添加一個(gè)元素,在鏈表頭添加元素與在其他位置邏輯會(huì)有差別,那么為什么在鏈表頭添加元素比較特殊呢?這是因?yàn)槲覀優(yōu)殒湵硖砑釉氐倪^(guò)程要找到待添加元素位置的前置節(jié)點(diǎn),但是由于對(duì)于鏈表頭來(lái)說(shuō),它沒(méi)有前置節(jié)點(diǎn),所以在邏輯上就比較特殊一些,解決方式也比較簡(jiǎn)單,我們的核心問(wèn)題不就是鏈表頭它沒(méi)有前置節(jié)點(diǎn)嘛,那么我們就可以造一個(gè)鏈表頭的前置節(jié)點(diǎn),對(duì)于這個(gè)前置節(jié)點(diǎn),他不存儲(chǔ)任何的元素,這樣一來(lái),對(duì)于我們的鏈表來(lái)說(shuō),它第一個(gè)元素就是虛擬頭結(jié)點(diǎn)的next所對(duì)應(yīng)的那個(gè)元素,而不是虛擬頭結(jié)點(diǎn)。注意:==虛擬頭節(jié)點(diǎn)的那個(gè)元素是根本不存在的,對(duì)于用戶(hù)來(lái)講也是沒(méi)有意義的,這只是為了我們編寫(xiě)邏輯方便而出現(xiàn)的虛擬頭節(jié)點(diǎn)==。

添加虛擬頭節(jié)點(diǎn)后的存儲(chǔ)結(jié)構(gòu)如圖:

鏈表(虛擬頭節(jié)點(diǎn)).png

鏈表的優(yōu)劣勢(shì)

1、優(yōu)點(diǎn)

真正動(dòng)態(tài),不需要處理固定容量的問(wèn)題。

2、缺點(diǎn)

喪失隨機(jī)訪問(wèn)能力。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容