鏈表和數組一樣也是線性表的一種。和數組不同,它不需要再內存中開辟連續的空間。
鏈表通過指針將一組零散的內存塊連接在一起。我們把內存塊稱為鏈表的“結點”(是節點還是結點,結點連接起來打個結所以叫“結點”?開個玩笑),也就是說這些結點可以在內存的任意地方,只要有其他的結點的指針指向這個位置就可以。
鏈表又分為單向鏈表,雙向鏈表,循環鏈表
單向鏈表
循環鏈表:最后一個節點指向第一個結點
雙向鏈表:比單向鏈表多了一個前驅指針,指向前面一個結點
從上面的結構和內存中的儲存結構來看,就可以發現鏈表相比數組來說,隨機查詢效率是O(n),只有從頭結點開始查找;但是它的插入修改效率比數組高,找到位置之后只需要修改下指針指向,而不需要進行后續元素的遷移。理論上來說是無限容量,不像數組滿了還需要擴容,擴容還要重新申請內存,然后遷移數據,鏈表只需要在內存找個一小塊空地,放好數據,讓前面那個指向這里就是了。
我們也可以看到雙向鏈表比單向鏈表更靈活,因為通過一個結點可以找到前后兩個結點。但是多了個指針所占空間肯定比單向鏈表大。
Java中LinkedList就是一個雙向鏈表。
簡單實現一個單向鏈表
package com.nijunyang.algorithm.link; /** * Description: * Created by nijunyang on 2020/3/31 22:09 */ public class MyLinkedList<E> { private Node<E> head; private int size = 0; /** * 頭部插入O(1) * @param data */ public void insertHead(E data){ Node newNode = new Node(data); newNode.next = head; head = newNode; size++; } public void insert(E data,int position){ if(position == 0) { insertHead(data); }else{ Node cur = head; for(int i = 1; i < position ; i++){ cur = cur.next; //一直往后遍歷 } Node newNode = new Node(data); // newNode.next = cur.next; //新加的點指向后面 保證不斷鏈 cur.next = newNode; //把當前的點指向新加的點 size++; } } public void deleteHead(){ head = head.next; size--; } public void delete(int position){ if(position == 0) { deleteHead(); }else{ Node cur = head; for(int i = 1; i < position ; i ++){ cur = cur.next; //找到刪除位置的前一個結點 } cur.next = cur.next.next; //cur.next 表示的是刪除的點,后一個next就是我們要指向的 size--; } } public int size( ){ return size; } public String toString( ){ if (size == 0) { return "[]"; } StringBuilder sb = new StringBuilder(); sb.append('['); Node<E> node = head; sb.append(node.value); int counter = 0; for (;;) { if (++counter == size) { break; } sb.append(","); node = node.next; sb.append(node.value); } sb.append(']'); return sb.toString(); } public static void main(String[] args) { MyLinkedList myList = new MyLinkedList(); myList.insertHead(5); System.out.println(myList); myList.insertHead(7); System.out.println(myList); myList.insertHead(10); System.out.println(myList); myList.delete(0); System.out.println(myList); myList.deleteHead(); System.out.println(myList); myList.insert(11, 1); System.out.println(myList); } private static class Node<E>{ E value; //值 Node<E> next; //下一個的指針 public Node() { } public Node(E value) { this.value = value; } } }
約瑟夫環問題:
說到鏈表就要提一個下約瑟夫環問題:N個人圍成一圈,第一個人從1開始報數,報M的被殺掉,下一個人接着從1開始報,循環反復,直到剩下最后一個。看到這個就想到用循環鏈表來實現,無限remove知道鏈表只剩下一個為止。
之前去力扣上面做的時候就用循環鏈表實現了下,驗證是可以過的,但是代碼提交之后顯示超時,過不了。仔細分析之后之后發現循環鏈表實現,時間線復雜度是O(n*m),如果數據大了,必定是個問題。然后就換了,數組(ArrayList)來實現,每次移除之后大小減一,通過取模size來實現循環報數的效果。會發現數組實現的,時間復雜度僅僅是O(n),兩種方式代碼如下:
package com.nijunyang.algorithm.link; import java.util.ArrayList; /** * Description: * Created by nijunyang on 2020/3/30 21:49 */ public class Test { public static void main(String[] args){ long start = System.currentTimeMillis(); int result = yuesefuhuan_link(70866, 116922); long end = System.currentTimeMillis(); System.out.println(result); System.out.println("鏈表耗時:" + (end - start)); System.out.println("-------------------------"); start = System.currentTimeMillis(); result = yuesefuhuan_arr(70866, 116922); end = System.currentTimeMillis(); System.out.println(result); System.out.println("數組耗時:" + (end - start)); } /** * 數組約瑟夫環 */ public static int yuesefuhuan_arr(int n, int m) { int size = n; ArrayList<Integer> list = new ArrayList<>(size); for (int i = 0; i < size; i++) { list.add(i); } int index = 0; while (size > 1) { //取模可以回到起點 index = (index + m - 1) % size; list.remove(index); size--; } return list.get(0); } /** * 循環鏈表約瑟夫環力扣超時 * @param n * @param m * @return */ public static int yuesefuhuan_link(int n, int m) { if (n == 1) { return n - 1; } Node<Integer> headNode = new Node<>(0); Node<Integer> currentNode = headNode; //尾結點 Node<Integer> tailNode = headNode; for (int i = 1; i < n; i++) { Node<Integer> next = new Node<>(i); currentNode.next = next; currentNode = next; tailNode = currentNode; } //成環 tailNode.next = headNode; //保證第一次進去的時候指向頭結點 Node<Integer> remove = tailNode; Node<Integer> preNode = tailNode; int counter = n; while (true) { for (int i = 0; i < m; i++) { //一直移除頭結點則,前置結點不動 if (m != 1) { preNode = remove; } remove = remove.next; } preNode.next = remove.next; if (--counter == 1) { return preNode.value; } } } static class Node<E>{ E value; Node next; public Node() { } public Node(E value) { this.value = value; } } }
運行之后看下結果對比,我的機器CPU還算可以I7-8700,內存16G,結果都是一樣的說明我們的兩種算法都是正確的,但是耗時的差別就很大很大了
鏈表耗時30多秒,數組耗時86毫秒,,差不多400倍的差距。
之前也說到數據在內存中是連續的,可以借助CPU的緩存機制預讀數據,而鏈表每次還需要根據指針去尋找。其次就是兩種方式時間復雜度是不一樣的,我們上面的用的數據70866, 116922。O(n*m)和O(n)差距有多大。所以說不同算法對程序的性能影響還是很大的,這應該就是體現了“算法之美”了吧。提到這個問題,最先想到的可能就是循環鏈表來解決,最后卻發現,循環鏈表並不是一個很好的解決方式。這就像我們平時寫代碼,需求下來的時候想着怎樣怎樣去實現,但是最后上線版本中,肯定改了又改的版本,有些方案可能整體換血都可能。再者就是從這個問題中就看出了算法的重要性。當然在力扣上面還有一種反推法實現的,比數組實現的代碼更少,性能更高,感興趣的自己搜,這里就不列出來對比了。