本文整理來源 《輕鬆學算法——互聯網算法面試寶典》/趙燁 編著node
鏈表
雖然在不少的高級語言中,鏈表已經儘可能的被隱藏起來,並且其應用之處還有不少的。面試
什麼是鏈表
鏈表與數據結構有些不一樣。棧和隊列都是申請一段連續的空間,而後按順序存儲數據;鏈表是一種物理上的非連續、非順序的存儲結構,數據元素之間的順序是經過每一個元素的指針關聯的。算法
鏈表由一系列節點組成,每一個節點通常至少會包含兩部分信息;一部分是元素數據自己,另外一部分是指向下一個元素的指針。這樣的存儲結構讓鏈表相比其餘線性的數據結構來講,操做會複雜一些。數組
相比數組,鏈表具備其餘優點;鏈表克服了數組須要提早設置長度的缺點,在運行時能夠根據須要隨意添加元素;計算機的存儲空間並不老是連續可用的,而鏈表能夠靈活地使用存儲空間,還能更好地對計算機的內存進行動態管理。數據結構
鏈表分爲兩種類型:單向鏈表和雙向鏈表。咱們平時說的鏈表指單向鏈表。雙向鏈表的每一個節點除存儲元素數據自己外,還額外存儲兩個指針,分別是上一個節點和下一個節點的地址。性能
鏈表的存儲結構
對於鏈表來講,咱們只須要關心鏈表之間的關係,不須要關係鏈表實際存儲位置,因此在表示一個鏈表關係時,通常使用箭頭來關聯兩個聯繫的元素節點。測試
從鏈表的存儲結構可知,鏈表的每一個節點包含兩個部分,分別是數據(叫作data)和指向下個節點地址的指針(叫作next)。在存儲了一個鏈表以後怎麼找到它?這裏須要一個頭節點,這個頭節點是一個鏈表的第1個節點,它的指針指向下一個節點的地址,以此類推,知道指針指向爲空時,邊表示沒有下一個元素了。this
鏈表的操做
鏈表的操做有:建立、插入、刪除、輸出。spa
這裏提出的插入、刪除操做,其位置並不必定是開頭或者結尾。因爲鏈表特殊結構,在鏈表中間進行數據元素的插入與刪除也是很容易實現的。操作系統
建立操做就是空間分配,把頭、尾指針以及鏈表信息初始化。
1. 插入操做
插入操做分爲三種狀況,分別是頭插入、尾插入、中間插入。
頭插入的操做,其實是增長一個新的節點,而後把新增的節點的指針指向原來頭指針指向的元素,再把頭指針指向的元素指向新增的節點。
尾插入的操做,也就是增長一個指針爲空的節點,而後把原尾指針指向節點的指針向新增的節點,
中間插入元素的操做會稍微複雜一些。首先新增一個節點,而後把新增的節點的指針指向插入位置的後一個位置的節點,把插入位置的前一個節點的指針指向新增的節點。
2. 刪除操做 刪除操做與插入操做相似,也有三種狀況,分別是頭刪除、尾刪除、中間刪除。
刪除頭元素,先把頭指針指向下一個節點,而後把原頭結點的指針置空。
刪除尾元素時,首先找到鏈表中倒數第2個元素,而後把尾指針指向的這個元素,接着把原倒數第2個元素的指針置空
刪除中間元素時會相對複雜一些,首先要把刪除的節點的以前一個節點的指針要指向刪除節點的下一個節點,接着要把刪除節點的指針置空。
public class Link<T> { private int size = 0; private Node<T> first; private Node<T> last; public Link() { } /** * 鏈表後部插入 * * @param data 插入元素 */ public void addLast(T data) { if (size == 0) { //爲空初始化先後元素 fillStart(data); } else { Node<T> node = new Node<>(); node.setData(data); last.setNext(node); //把最後插入的元素設置爲鏈表尾部的元素 last = node; } size++; } /** * 鏈表頭部插入元素 * * @param data 插入元素 */ public void addFirst(T data) { if (size == 0) { fillStart(data); } else { Node<T> node = new Node<>(); node.setData(data); //把元素的下一個位置的指針指向頭元素 node.setNext(first); //把剛插入的元素設置爲鏈表頭元素 first = node; } size++; } /** * 在鏈表的指定位置後面插入 * * @param data 插入元素 * @param index 下表,從0開始 */ public void add(T data, int index) { if (size > index) { if (size == 0) { //爲空初始化先後元素 fillStart(data); size++; } else if (index == 0) { addFirst(data); } else if (size == index + 1) { addLast(data); } else { Node<T> temp = get(index); Node<T> node = new Node<>(); node.setData(data); node.setNext(temp.getNext()); temp.setNext(node); size++; } } else { throw new IndexOutOfBoundsException("鏈表沒有那麼長"); } } /** * 刪除頭元素 */ public void removeFirst() { if (size == 0) { throw new IndexOutOfBoundsException("鏈表沒有元素"); } else if (size == 1) { //只剩下一個時須要清除first和last clear(); } else { Node<T> temp = first; first = temp.getNext(); size--; } } /** * 刪除尾元素 */ public void removeLast() { if (size == 0) { throw new IndexOutOfBoundsException("鏈表沒有元素"); } else if (size == 1) { clear(); } else { //獲取最後一個元素以前的一個元素 Node<T> temp = get(size - 2); temp.setNext(null); size--; } } /** * 刪除鏈表中間的元素 * @param index 下標 */ public void removeMiddle(int index){ if(size == 0){ throw new IndexOutOfBoundsException("鏈表沒有元素"); }else if (size == 1){ //只剩下一個時須要清除first和last clear(); }else { if (index == 0){ removeFirst(); }else if (size == index - 1){ removeLast(); }else { Node<T> temp = get(index - 1); Node<T> next = temp.getNext(); temp.setNext(next.getNext()); size--; } } } private void clear() { first = null; last = null; size = 0; } public int size(){ return size; } public Node<T> get(int index) { Node<T> temp = first; for (int i =0; i< index ; i++){ temp = temp.getNext(); } return temp; } private void fillStart(T data) { first = new Node<>(); first.setData(data); last = first; } public void printAll(){ Node<T> temp = first; System.out.println(temp.getData()); for (int i = 0 ; i < size -1; i++){ temp = temp.getNext(); System.out.println(temp.getData()); } } private class Node<V> { private V data; private Node<V> next; public V getData() { return data; } public void setData(V data) { this.data = data; } public Node<V> getNext() { return next; } public void setNext(Node<V> next) { this.next = next; } } }
測試代碼
public class LinkTest { @Test public void main(){ Link<Integer> link= new Link<>(); link.addFirst(2); link.addFirst(1); link.addFirst(4); link.addFirst(5); link.add(3,1); link.printAll(); link.removeFirst(); link.removeLast(); link.removeMiddle(1); link.printAll(); link.removeFirst(); link.removeFirst(); Assert.assertEquals(0,link.size()); } }
鏈表的實現邏輯有點複雜,在程序中存在拋異常的狀況,在中間插入和刪除也考慮到index爲頭和尾的狀況,這樣避免調用方法失誤而致使程序出錯。
鏈表的特色
鏈表因爲自己存儲結構的緣由,有如下幾個特色: 1. 物理空間不連續,空間開銷大。
鏈表的最大一個特色就是在物理空間上能夠不連續。這樣的有點能夠利用操做系統的動態內存管理,缺點是須要更多的存儲空間去存儲指針信息。
2. 運行時能夠動態添加
因爲數組須要初始化時設定長度,因此在使用數組時每每會出現長度不夠的狀況,這時只能再聲明一個更長的數組,而後把舊數據的數據複製進去,在前面棧的實現中已經看到這一點。使用鏈表,則不會出現空間不夠用的狀況。
3. 查找元素須要順序查找
經過上面的代碼能夠看出,查找元素時,須要逐個遍歷日後查找元素。其實在測試代碼中採用循環隊列的方法的效率並不高,尤爲是當鏈表很長時,所須要查找的元素的位置越靠後,效率越低。在執行刪除操做時,也會遇到相似問題。
4. 操做稍顯複雜
在增長和刪除時,不須要處理數據,還須要處理指針。從代碼上看,刪除最後一個元素時,獲取最後一個元素很方便,可是因爲操做須要實現倒數第二個元素的next指向設置爲空,因此只能從頭遍歷並獲取倒數第二個元素以後在進行刪除操做。
鏈表的適用場景
如今計算機的空間愈來愈大,物理空間的開銷已經再也不是咱們要關心的問題,運行效率纔是咱們在開發中須要考慮的問題。
咱們在前面提到,鏈表除了單向鏈表,還有雙向鏈表。通常狀況下咱們會使用雙向鏈表,由於多使用的那個指針所佔的空間對於如今的計算機資源來講並不重要。雙向鏈表相對於單向鏈表的一個優點就是 ,不須要從頭仍是從尾查找,操做都是同樣的。所以對於尾操做進行操做時就不用逐個從頭遍歷了,能夠直接從尾往前查找元素。
鏈表能夠在運行時動態添加元素,這對於不肯定長度的順序存儲來講很重要。集合(列表)採用數組實現,在空間不夠時須要換更大的數組,而後進行復制操做。這時若是採用鏈表就很是方便了。
鏈表的劣勢就是在查找中間元素時就須要遍歷。通常而言,鏈表也常常配合其餘結構一同使用,如散列表、棧、隊裏等。
通常的程序裏可能會使用一個簡單的隊列進行消息緩衝,而隊列的操做只能從頭、尾進行,因此這時使用鏈表(雙向鏈表)去實現就很是方便了。
鏈表的性能分析
通常分析性能時,將單向鏈表做爲分析對象。鏈表的插入分爲三種:頭插、尾插、和中間插。頭、尾是可以直接插入的,其時間複雜度爲O(1);而中間插須要遍歷鏈表,因此時間複雜度爲O(L),L爲插入下標。鏈表的刪除也分爲三種:頭刪、尾刪、和中間刪。頭刪是可以直接刪除的,其時間複雜度爲O(1);而中間刪須要遍歷鏈表,因此時間複雜度爲O(L),L爲刪除下標。尾刪的時間複雜度則達到了O(N),N爲鏈表長度。
對於查詢來說,時間複雜度爲O(L),L同樣是下標。
因此對於鏈表來講,咱們能夠發現,鏈表的頭插和頭刪都是O(1)的時間複雜度,這和棧很想,因此棧能夠直接使用單向鏈表實現。
面試舉例:如何反轉鏈表
通常在數據結構或者算法的面試題當中,儘可能不使用額外的空間去實現,儘管如今的計算機空間很充足,可是面試考察的仍是對總體性能的考慮。
方法其實有不少,咱們能夠依次遍歷鏈表,而後依次使用頭插入的方法來達到目的。
其中有個簡單的方法,就是把鏈表的每一個指針反轉。
/** * 反轉鏈表 */ public void reverse(){ Node<T> temp = first; last = temp; Node<T> next = first.getNext(); for (int i = 0 ; i < size - 1; i++){ //下下個 Node<T> nextNext = next.getNext(); next.setNext(temp); temp = next; next = nextNext; } last.setNext(null); first = temp; }