[本專題會對常見的數據結構及相應算法進行分析與總結,並會在每一個系列的博文中提供幾道相關的一線互聯網企業面試/筆試題來鞏固所學及幫助咱們查漏補缺。項目地址:https://github.com/absfree/Algo。因爲我的水平有限,敘述中不免存在不清晰準確的地方,但願你們能夠指正,謝謝你們:)] node
提到鏈表,咱們你們都不陌生,在平時的編碼中咱們也或多或少地使用過這個數據結構。算法(第4版) (豆瓣)一書中對鏈表的定義以下:git
鏈表是一種遞歸的數據結構,它或者爲空(null),或者是指向一個結點(node)的引用,該節點還有一個元素和一個指向另外一條鏈表的引用。github
把以上定義用Java語言來描述大概是這樣的:面試
public class LinkedList<Item> { private Node first; private class Node { Item data; Node next; } ... }
一個LinkedList類實例便表明了一個鏈表,它的一個實例域保存了指向鏈表中第一個結點的引用。以下圖所示:算法
固然,以上咱們所介紹的鏈表是single linked list(單向鏈表),有時候咱們更喜歡double linked list(雙向鏈表),double linked list就是每一個node不只包含指向下後一個結點的引用,還包含着指向前一個結點的引用。後文咱們在介紹鏈表的具體實現是會對這兩種鏈表進行更加詳細地介紹。編程
一般來講,鏈表支持插入和刪除這兩種操做,而且刪除/插入鏈表頭部/尾部結點的時間複雜度一般都是常數級別的,鏈表的不足在於不支持高效的random access(隨機訪問)。緩存
在上文中,咱們已經簡單用用Java刻畫出了鏈表的部分結構,咱們只需爲以上的LinkedList類增長insert、delete等方法,即可以實現一個(單向)鏈表。下面咱們來介紹如何向鏈表中插入及刪除結點。數據結構
因爲咱們的LinkedList類中維護了一個指向first node的引用,因此在表頭插入結點是很容易的,具體請看如下代碼:app
public void insert(Item item) { Node oldFirst = first; first = new Node(); first.item = item; first.next = oldFirst; itemCount++; }
在表頭刪除結點的代碼也很簡單,基本是自注釋的:框架
public Item delete() { if (first != null) { Item item = first.item; first = first.next; return item; } else { throw new NullPointerException("There's no Node in the linked list."); }
雙向鏈表相比與單鏈表的優點在於它同時支持高效的正向及反向遍歷,而且能夠方便的在鏈表尾部刪除結點(單鏈表能夠方便的在尾部插入結點,但不支持高效的表尾刪除操做)。雙向鏈表的Java描述以下:
public class DoubleLinkedList<Item> { private Node first; private Node last; private int itemCount; private class Node { Node prev; Node next; Item item; } public void addFirst(Item item) { Node oldFirst = first; first = new Node(); first.item = item; first.next = oldFirst; if (oldFirst != null) { oldFirst.prev = first; } if (itemCount == 0) { last = first; } itemCount++; } public void addLast(Item item) { Node oldLast = last; last = new Node(); last.item = item; last.prev = oldLast; if (oldLast != null) { oldLast.next = last; } if (itemCount == 0) { first = last; } itemCount++; } public Item delFirst() { if (first == null) { throw new NullPointerException("No node in linked list."); } Item result = first.item; first = first.next; if (first != null) { first.prev = null; } if (itemCount == 1) { last = null; } itemCount--; return result; } public Item delLast() { if (last == null) { throw new NullPointerException("No node in linked list."); } Item result = last.item; last = last.prev; if (last != null) { last.next = null; } if (itemCount == 1) { first = null; } itemCount--; return result; } public void addBefore(Item targetItem, Item item) { //從頭開始遍歷尋找目標節點 Node target = first; if (target == null) { throw new NullPointerException("No node in linked list"); } while (target != null && target.item != targetItem) { //繼續向後尋找目標節點 target = target.next; } if (target == null) { throw new NullPointerException("Can't find target node."); } //如今target爲指向目標結點的引用 if (target.prev == null) { //此時至關於在表頭插入結點 addFirst(item); } else { Node oldPrev = target.prev; Node newNode = new Node(); newNode.item = item; target.prev = newNode; newNode.next = target; newNode.prev = oldPrev; oldPrev.next = newNode; itemCount++; } } public void addAfter(Item targetItem, Item item) { Node target = first; if (target == null) { throw new NullPointerException("No node in linked list."); } while (target != null && target.item != targetItem) { target = target.next; } if (target == null) { throw new NullPointerException("Can't find target node."); } if (target.next == null) { addLast(item); } else { Node oldNext = target.next; Node newNode = new Node(); newNode.item = item; target.next = newNode; newNode.prev = target; newNode.next= oldNext; oldNext.prev = newNode; itemCount++; } } }
上面代碼的邏輯都很直接,不過剛接觸鏈表的小夥伴有時候可能容易感到有些迷糊,這時候一個好方法即是在拿出筆紙,畫出鏈表操做相關結點的prev、next指針等的指向變化狀況,這樣鏈表相關的各種操做過程都能被很是直觀的展示出來。
有一點須要咱們注意的是,咱們上面實現鏈表使用的是pointer wrapper方式,這種方式的特色是prev/next指針包含在結點中,而數據由結點中的另外一個指針(即item)所引用。採起這種方式,在獲取結點數據時,咱們須要進行double-dereferencing,並且這種方式實現的鏈表不是一種[局部化結構],這意味着咱們拿到鏈表的一個結點數據後,沒法直接進行insert/delete操做。
另外一種實現鏈表的方式是intrusive方式,這種方式實現的鏈表也就是intrusive linked list。這種鏈表的特色是data就是node,node就是data。使用這種鏈表,咱們在獲取data時,無需double-dereferencing,而且intrusive linked list是一種局部結構。
鏈表的主要優點有兩點:一是插入及刪除操做的時間複雜度爲O(1);二是能夠動態改變大小。
因爲其鏈式存儲的特性,鏈表不具有良好的空間局部性,也就是說,鏈表是一種緩存不友好的數據結構。
下面咱們從《劍指Offer》中挑出幾道關於鏈表的經典面試題來進一步鞏固咱們對鏈表相關技術點的掌握。
這道題給咱們的框架以下,咱們要作的是在這個框架中編程來實現從頭至尾打印鏈表:
/* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/ public class Solution { public ListNode FindKthToTail(ListNode head,int k) { } }
首先咱們能夠看到這裏面表示鏈表的是ListNode類,這對應着咱們上面的單鏈表實現中的Node類。實際上,這道題的難度要比咱們上面實現的DoubleLinkedList中的addBefore/addAfter方法的難度要小。
我想到的一種直接解法以下:(若有問題但願你們能夠指出)
public class Solution { public ListNode FindKthToTail(ListNode head,int k) { //先求得鏈表的尺寸,賦值給size int size = 0; ListNode current = head; while (current != null) { size++; current = current.next; } //獲取next實例域size-k次,便可獲得倒數第k個結點(從1開始計數) for (int i = 0; i < size - k; i++) { head = head.next; } return head; } }
本題的要求是輸入一個鏈表,反轉鏈表後,輸出鏈表的全部元素。這道題的實現也比較直接,如如下代碼所示:
public ListNode ReverseList(ListNode head) { if (head == null) { return null; } ListNode current = head; //原head的next node爲null ListNode prevNode = null; ListNode newHead = null; while (current != null) { ListNode nextNode = current.next; current.next = prevNode; if (nextNode == null) { newHead = current; } prevNode = current; current = nextNode; } return newHead; }
這裏只是從劍指Offer中找了兩道關於鏈表的題來練手,之後會陸續在上面提到的項目地址跟你們分享更多的常常被用來做爲一線互聯網公司面試/筆試題的題目,這樣在鞏固本身算法基本功的同時,在面試/筆試時也可以更加駕輕就熟。