實現一個屬於自己的鏈表類

一、鏈表的基本方法

鏈表是一種通過節點存儲元素,並且節點與節點之間是連接起來的數據結構。

0->1->2->3->4->5->NULL

  • 對於鏈表,如果要想訪問存在這個鏈表中的所有節點,相應的必須把鏈表的頭給存儲起來。通常鏈表的頭叫做head,即在鏈表LinkedList類中,應該又一個Node型的變量head,它指向鏈表中的第一個節點。private Node head;
  • 需要設置一個私有的size來記錄鏈表中元素的個數,這個size只能在類內操作,不允許外部進行修改。
  • 添加查看鏈表中元素個數和判斷鏈表是否爲空的方法
// 支持泛型
public class LinkedList<E> {
    // 節點設計成鏈表類中的內部類
    // 設計私有類,只有在鏈表數據結構內纔可以訪問到Node
    private class Node {
        // 設計成public 在LinkedList中可以隨意訪問操作 不需要設置get,set方法
        public E e;   // 存放元素
        public Node next;  // 指向Node的引用

        public Node(E e, Node next) {
            // 將用戶傳來的數據交給節點
            this.e = e;
            this.next = next;
        }

        // 用戶只傳來e
        public Node(E e) {
            this(e, null);
        }

        // 用戶什麼都不傳
        public Node() {
            this(null, null);
        }
        // 對每一個節點設置toString方法
        @Override
        public String toString() {
            // 每一個節點,直接打印e所對應的toString
            return e.toString();
        }
    }
    
    // 第一個節點
    private Node head;
    // 用戶不能在外部直接修改size
    private int size;  // 來記錄鏈表中有多少個元素

    // 鏈表構造函數
    public LinkedList() {
        // 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
        head = null;
        size = 0;
    }

    // 獲取鏈表中元素的個數
    public int getSize() {
        return size;
    }

    // 返回鏈表是否爲空
    public boolean isEmpty() {
        // size是否爲0
        return size == 0;
    }
    // 獲取鏈表中元素的個數
	public int getSize() {
	    return size;
	}
	
	// 返回鏈表是否爲空
	public boolean isEmpty() {
	    // size是否爲0
	    return size == 0;
	}
}

二、在鏈表頭添加元素

1. 與數組的對比

對於數組,向數組尾添加一個元素非常的方便。相反,對於鏈表來說,在鏈表的頭部添加元素是非常方便的。
這是因爲在數組中,size直接指向了數組中最後一個元素的下一個位置,它跟蹤數組的尾巴。而對於鏈表,設立了鏈表的頭head,有一個變量跟蹤鏈表的頭,沒有相應的變量去跟蹤鏈表的尾部,所以在鏈表的頭部添加元素非常方便。

2. 添加的具體過程

假設將新的元素10添加進0->1->2->3->4->5->NULL這個鏈表中,首選需要把元素10放進一個節點裏,這個節點裏存儲了元素10以及相應的next。添加的關鍵在於如何將這個節點掛接到鏈表中,同時不破壞現有鏈表的結構。

  • 將元素10這個節點(Node)的next指向現在鏈表的頭部(head)也就是node.next=head
  • 此時的鏈表已經變成了10->0->1->2->3->4->5->NULL,存儲10的節點成了新的頭部。
  • 接下來需要維護一下head,讓head=node,即讓head指向新的10的節點。這樣便完成了將10插入到鏈表的頭部。
  • 整個過程是在一個函數中執行的,對於node這個變量來說,指向的是“10”這個節點,在函數結束之後,node的塊作用域也就結束了,node變量相應的也就沒用了。

LinkedList中添加一個新的方法,作用在鏈表頭添加新的元素。

public void addFirst(E e) {
    // 首先創建一個新的節點 此時next爲空
    Node node = new Node(e);
    // 讓node的next指向head
    node.next = head;
    // 更新一下head爲新的node元素
    head = node;
    // 之後維護一下size
    size++;
}

其中也可以將邏輯的直接一行表示,讓head直接指向新的節點

public void addFirst(E e) {
    // 首先new一個新的Node,傳入e,Node直接指向當前鏈表的head,然後將Node賦值給head進行更新
    head = new Node(e, head);
    // 之後維護一下size
    size++;
}

三、在鏈表中插入元素

1. 插入元素的具體邏輯

已有鏈表0->1->2->3->4->5->NULL
在鏈表“索引”爲2的地方添加一個新的元素9,對於鏈表來說沒有索引的概念,具體來說是在當前節點1的位置之後添加一個新的元素9,之後是節點2。

  • 首先創建“9”的節點
  • 要想把“9”插入到正確的位置,就必須要找到插入“9”節點之後,這個“9”節點之前的節點是誰。把它稱做prev。prev的初始化適合head在同一個地方。
  • 在這個例子中,需要先找到“1”節點讓它的next指向“9”,再將“9”的next指向原本“1”節點之後節點“2”
  • 當前需要搜索插入“9”之前的節點是誰,因爲插入了“9”之後索引爲2,那麼插入“9”之前的節點索引爲1。這是就需要從0開始遍歷,遍歷到索引爲1的位置就可以了,相應的維護prev找到1的位置。

一旦找到對應位置1後

  1. 首先將node的next指向prev的下一個元素,node.next = prev.next;在這裏prev.next是插入位置的後一個節點
  2. 之後讓prev的next等於nodeprev.next = node;在這裏,prev.next指向新的node節點
    (這兩點的順序是不允許改變的,如果調換順序執行,prev.next已經賦值node了,再執行node.next = prev.next,實際是讓它又指向了自己。順序是非常重要的
Node node = new Node(e);
node.next = prev.next;
prev.next = node;

也可以進行相應的縮寫

prev.next = new Node(e, prev.next);

這個過程的關鍵,在於找到待添加節點的前一個節點,也是prev變量進行的事情。但值得注意的是:如果想把新的元素,添加在“索引”爲0的地方,即添加在鏈表的頭部,鏈表頭部的節點是不存在前一個節點的。對於這種情況需要特殊處理,就是前面一節提到的往鏈表頭部添加新的元素。

2. 實現添加元素的方法

// 鏈表的index添加新的元素e
// 真正使用鏈表很少有這樣的操作,當選擇使用鏈表的時候,通常選擇不使用"索引"
public void add(int index, E e) {
    // 需要先判斷index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    if (index == 0) {
        addFirst(e);
    } else {
        // 設計一個Node從head開始
        Node prev = head;
        for (int i = 0; i < index - 1; i++) {
            // 當前prev存的節點的下一個節點放進prev中
            // 遍歷一直挪動prev位置存放下一個節點
            prev = prev.next;
        }
        // 插入過程
        prev.next = new Node(e, prev.next);
        // 插入之後維護size
        size++;
	}
}

3. 相應實現末尾添加

// 在鏈表的末尾添加一個新的元素e
public void addLast(E e) {
    // 複用add(),只需要在size添加即可
    add(size, e);
}

四、爲鏈表設立虛擬頭結點

1. 鏈表頭添加元素時的特殊性

在鏈表添加元素的過程中,在向鏈表任意一個地方添加元素的時候,在鏈表頭添加元素和在鏈表其他位置添加元素的邏輯是不一樣的,因爲鏈表頭沒有前一個元素的節點,所以需要進行特殊處理。

2. 解決辦法

可以造一個鏈表頭之前的節點,這個節點不存儲任意的元素
null->0->1->2->3->4->5->NULL
將這個空節點稱之爲整個鏈表真正的head,通常叫做dummyHead,即虛擬頭節點。這樣,鏈表的第一個元素是dummyHead.next所對應的元素。dummyHead位置的元素是根本不存在的,對用戶來講也根本沒有意義,它是0節點之前的虛擬元素。

3. 具體的邏輯實現

在LinkedList()中,head存放一個具體的元素,初始化時是null。引入虛擬頭節點,則private Node dummyHead
構造函數初始化時,則需要引人一個節點

public LinkedList() {
    // 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
    dummyHead = new Node(null, null);
    size = 0;
}

對於add()添加操作也需要進行相應的改動,不再需要判斷index==0的情況

public void add(int index, E e) {
    // 需要先判斷index的合法性
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    // 此時dummyHead指向的是0元素之前一個的位置的節點
    Node prev = dummyHead;
    // 只需要遍歷到index就可以了,因爲是從dummyHead開始遍歷的
    for (int i = 0; i < index; i++) {
        // 當前prev存的節點的下一個節點放進prev中
        // 遍歷一直挪動prev位置存放下一個節點
        prev = prev.next;
    }

    prev.next = new Node(e, prev.next);
    // 插入之後維護size
    size++;
}

如此一來,在鏈表的頭部添加一個元素可以複用add()方法

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

五、遍歷、查詢、修改

1. 按照“索引”查找元素

這裏需要注意需要查找index元素,應該從整個鏈表的第一個元素開始遍歷,即dummyHead.next開始遍歷。

// 查詢操作
public E get(int index) {
    // get之前先判斷合法性
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }

    // 遍歷鏈表,是從索引爲0開始的,從當前的開始遍歷
    Node cur = dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    // 最終cur裏存儲的e就是需要查找的元素
    return cur.e;
}

// 獲取鏈表第一個元素
public E getFirst() {
    return get(0);
}

// 獲取鏈表最後第一個元素
public E getLast() {
    return get(size - 1);
}

2. 更新鏈表元素

// 鏈表的更新,修改
public void set(int index, E e) {
    // 判斷合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }
    // 進行一次遍歷,找到第index元素進行替換
    Node cur = dummyHead.next;
    for (int i = 0; i < index; i++)
        cur = cur.next;
    // 最終cur裏存儲的e等於新的e
    cur.e = e;
}

3. 查找鏈表中是否有元素e

這裏的遍歷和以前的查詢操作遍歷有所不同,之前的遍歷都有索引,知道自己要查詢或者修改的是鏈表中的第幾個節點,而這裏需要從頭對鏈表都進行一次遍歷。

// 查找鏈表中是否存在元素e
public boolean contains(E e) {
    // 設置cur從第一個節點開始
    Node cur = dummyHead.next;
    // 不知道循環多少次使用while
    // cur 節點不等他null的話,意味着當前cur節點是一個有效節點
    while (cur != null) {
        // cur.e是用戶傳來的e,返回true
        if (cur.e.equals(e))
            return true;
        // 否則就看下一個節點
        cur = cur.next;
        // 直到cur爲空,說明把整個鏈表遍歷了一遍
    }
    return false;
}

六、從鏈表中刪除元素

1. 刪除邏輯

依然使用有虛擬頭節點的鏈表,刪除的節點爲取名爲delNode
null->0->1->2->3->4->5->NULL
假設刪除“索引”爲2的位置的元素,其實就是刪除節點2元素。

  • 首先找到待刪除元素的前一個元素節點1用prev進行標註
  • 此時要刪除的節點2就是prev.next對應的節點
  • 將prev節點的next的指針指向要刪除節點2的下一個節點3,即是將“1的節點”指向“3的節點”。具體的操作爲prev.next = delNode.next;
  • 此時null->0->1->2->3->4->5->NULL相當於1直接連接到3節點看似刪除了,2節點其實並沒有刪除
  • 爲了方便Java能夠回收2節點的空間,還需要讓2節點的next和整個鏈表脫離,即delNode.next = null。運行之後纔算真正的刪除。

需要刪除元素,必須找到next產生變化

2. 實現刪除方法remove()

// 刪除鏈表中index位置元素 返回刪除的元素
public E remove(int index) {
    // 判斷合法
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("Add failed.Illegal index.");
    }

    // prev存了待刪除節點之前的節點
    Node prev = dummyHead;
    for (int i = 0; i < index; i++) {
        prev = prev.next;
    }
    // 將刪除的節點保存進retNode;
    Node retNode = prev.next;
    // 將當前的前一個元素的節點指向當前後一個節點
    prev.next = retNode.next;
    // 讓刪除元素的節點的指向爲空
    retNode.next = null;
    // 維護size
    size--;
    // 將刪除的元素返回
    return retNode.e;
}

// 刪除第一個元素,返回刪除的元素
public E removeFirst() {
    return remove(0);
}

// 刪除第一個元素,返回刪除的元素
public E removeLast() {
    return remove(size - 1);
}

七、完整的鏈表類LinkedList

LinkedList.java

// 支持泛型
public class LinkedList<E> {
    // 節點設計成鏈表類中的內部類
    // 設計私有類,只有在鏈表數據結構內纔可以訪問到Node
    private class Node {
        // 設計成public 在LinkedList中可以隨意訪問操作 不需要設置get,set方法
        public E e;   // 存放元素
        public Node next;  // 指向Node的引用

        public Node(E e, Node next) {
            // 將用戶傳來的數據交給節點
            this.e = e;
            this.next = next;
        }

        // 用戶只傳來e
        public Node(E e) {
            this(e, null);
        }

        // 用戶什麼都不傳
        public Node() {
            this(null, null);
        }

        // 對每一個節點設置toString方法
        @Override
        public String toString() {
            // 每一個節點,直接打印e所對應的toString
            return e.toString();
        }
    }

    // 修改爲dummyHead設置虛擬頭節點
    private Node dummyHead;
    // 用戶不能在外部直接修改size
    private int size;  // 來記錄鏈表中有多少個元素

    // 鏈表構造函數
    public LinkedList() {
        // 對於一個空的鏈表來說,它是存在一個節點的,這個節點就是唯一的虛擬頭節點
        dummyHead = new Node(null, null);
        size = 0;
    }

    // 獲取鏈表中元素的個數
    public int getSize() {
        return size;
    }

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

    // 使用虛擬頭節點添加元素
    public void add(int index, E e) {
        // 需要先判斷index的合法性
        if (index < 0 || index > size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // 此時dummyHead指向的是0元素之前一個的位置的節點
        Node prev = dummyHead;
        // 只需要遍歷到index就可以了,因爲是從dummyHead開始遍歷的
        for (int i = 0; i < index; i++) {
            // 當前prev存的節點的下一個節點放進prev中
            // 遍歷一直挪動prev位置存放下一個節點
            prev = prev.next;
        }

        prev.next = new Node(e, prev.next);
        // 插入之後維護size
        size++;
    }

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

    // 在鏈表的末尾添加一個新的元素e
    public void addLast(E e) {
        // 複用add(),只需要在size添加即可
        add(size, e);
    }

    // 查詢操作
    public E get(int index) {
        // get之前先判斷合法性
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // 遍歷鏈表,是從索引爲0開始的,從當前的開始遍歷
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur = cur.next;
        // 最終cur裏存儲的e就是需要查找的元素
        return cur.e;
    }

    // 獲取鏈表第一個元素
    public E getFirst() {
        return get(0);
    }

    // 獲取鏈表最後第一個元素
    public E getLast() {
        return get(size - 1);
    }

    // 鏈表的更新,修改
    public void set(int index, E e) {
        // 判斷合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }
        // 進行一次遍歷,找到第index元素進行替換
        Node cur = dummyHead.next;
        for (int i = 0; i < index; i++)
            cur = cur.next;
        // 最終cur裏存儲的e等於新的e
        cur.e = e;
    }

    // 查找鏈表中是否存在元素e
    public boolean contains(E e) {
        // 設置cur從第一個節點開始
        Node cur = dummyHead.next;
        // 不知道循環多少次使用while
        // cur 節點不等他null的話,意味着當前cur節點是一個有效節點
        while (cur != null) {
            // cur.e是用戶傳來的e,返回true
            if (cur.e.equals(e))
                return true;
            // 否則就看下一個節點
            cur = cur.next;
            // 直到cur爲空,說明把整個鏈表遍歷了一遍
        }
        return false;
    }

    // 刪除鏈表中index位置元素 返回刪除的元素
    public E remove(int index) {
        // 判斷合法
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("Add failed.Illegal index.");
        }

        // prev存了待刪除節點之前的節點
        Node prev = dummyHead;
        for (int i = 0; i < index; i++) {
            prev = prev.next;
        }
        // 將刪除的節點保存進retNode;
        Node retNode = prev.next;
        // 將當前的前一個元素的節點指向當前後一個節點
        prev.next = retNode.next;
        // 讓刪除元素的節點的指向爲空
        retNode.next = null;
        // 維護size
        size--;
        // 將刪除的元素返回
        return retNode.e;
    }

    // 刪除第一個元素,返回刪除的元素
    public E removeFirst() {
        return remove(0);
    }

    // 刪除第一個元素,返回刪除的元素
    public E removeLast() {
        return remove(size - 1);
    }

    @Override
    public String toString() {
        // 需要遍歷一邊整個鏈表中的所有的元素
        StringBuilder res = new StringBuilder();
//        Node cur = dummyHead.next;
//        while (cur != null) {
//            res.append(cur + "->");
//            cur = cur.next;
//        }
        for (Node cur = dummyHead.next; cur != null; cur = cur.next) {
            res.append(cur.e + "->");
        }
        // 最後跟一個空表示達到了鏈表的結尾
        res.append("NULL");
        return res.toString();
    }

}

八、測試類及結果

編寫測試函數對鏈表類進行測試
Main.java

public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> linkdelist = new LinkedList<>();
        // 爲鏈表添加0-4這5個元素
        for (int i = 0; i < 5; i++) {
            linkdelist.addFirst(i);
            System.out.println(linkdelist);
        }

        // 測試2的位置添加100
        linkdelist.add(2,100);
        System.out.println(linkdelist);

        // 測試是否存在元素3
        boolean res = linkdelist.contains(3);
        System.out.println(res);

        // 測試刪除元素
        linkdelist.remove(1);
        System.out.println(linkdelist);

        // 刪除最後一個元素
        linkdelist.removeLast();
        System.out.println(linkdelist);

    }
}

測試結果:
需要注意的是:當遍歷存入5個元素的時候,0在最右側,4在最左側

0->NULL
1->0->NULL
2->1->0->NULL
3->2->1->0->NULL
4->3->2->1->0->NULL
4->3->100->2->1->0->NULL
true
4->100->2->1->0->NULL
4->100->2->1->NULL

九、鏈表的時間複雜度分析

添加操作 O(n)

  • addLast(e) 向鏈表尾添加一個元素,O(n) 必須從鏈表頭遍歷到尾部
  • addFirst(e) 向鏈表頭添加一個元素,O(1)
  • add(index,e) 鏈表任意位置添加元素,O(n/2) = O(n)

刪除操作 O(n)

  • removeLast(e) 從鏈表尾部刪除一個元素 O(n)
  • removeFirst(e) 從鏈表頭部刪除一個元素 O(1)
  • remove(index) 鏈表任意位置刪除一個元素 O(n/2) = O(n)

修改操作 O(n)
鏈表不支持隨機訪問,需要修改元素時必須從頭像後遍歷查找,所以set(index,e)是O(n)

查找操作 O(n)
不論是get(index),還是contains(e),都需要從頭遍歷整個鏈表,所以是O(n)

綜上所述,對於鏈表來說,增、刪、改、查的時間複雜度都是O(n)級別的,其中對於鏈表頭的操作都是O(1)級別的。


寫在最後

如果代碼有還沒有看懂的或者我寫錯的地方,歡迎評論,我們一起學習討論,共同進步。
推薦學習地址:
liuyubobobo老師的《玩轉數據結構》:https://coding.imooc.com/class/207.html
最後,祝自己早日鹹魚翻身,拿到心儀的Offer,衝呀!

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