在底層結構上,單向鏈表經過指針將一組零散的內存塊串聯在一塊兒。其中,咱們把內存塊稱爲鏈表的「結點」。爲了將全部的結點串起來,每一個鏈表的結點除了存儲數據以外,還須要記錄鏈上的下一個結點的地址。以下圖所示,咱們把這個記錄下個結點地址的指針叫做後繼指針 next。java
從畫的單鏈表圖中,你應該能夠發現,其中有兩個結點是比較特殊的,它們分別是第一個結點和最後一個結點。咱們習慣性地把第一個結點叫做頭結點,把最後一個結點叫做尾結點。其中,頭結點用來記錄鏈表的基地址。有了它,咱們就能夠遍歷獲得整條鏈表。而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址 NULL,表示這是鏈表上最後一個結點。node
1. 插入操做算法
2. 刪除操做數據庫
刪除操做的時間複雜度和插入操做的時間複雜度相似。編程
3. 更新操做瀏覽器
4. 查詢操做緩存
因爲鏈表的底層數據是不連續的,因此不能經過隨機訪問進行數據尋址。只能經過遍歷進行查找數據。編程語言
package com.csx.algorithm.link; public class SinglyLinkedList<E> { public static void main(String[] args) { SinglyLinkedList<Integer> list = new SinglyLinkedList<>(); //尾部插入,遍歷鏈表輸出 System.out.println("尾部插入[1-10]"); for (int i = 1; i <= 10; i++) { list.addLast(Integer.valueOf(i)); } list.printList(); //頭部插入,遍歷鏈表輸出 System.out.println("頭部插入[1-10]"); for (int i = 1; i <= 10; i++) { list.addFirst(Integer.valueOf(i)); } list.printList(); //在指定節點後面插入 System.out.println("在頭節點後面插入[100]"); list.addAfter(100, list.head); list.printList(); System.out.println("在頭節點前面插入[100]"); list.addBefore(100, list.head); list.printList(); System.out.println("在尾節點前面插入[100]"); list.addBefore(100, list.tail); list.printList(); System.out.println("在尾節點後面插入[100]"); list.addAfter(100, list.tail); list.printList(); System.out.println("------------刪除方法測試-----------"); System.out.println("刪除頭節點"); list.removeFirst(); list.printList(); System.out.println("刪除尾節點"); list.removeLast(); list.printList(); System.out.println("刪除指定節點"); list.removeNode(list.head.next); list.printList(); } private Node head; private Node tail; public SinglyLinkedList() { } public SinglyLinkedList(E data) { Node node = new Node<>(data, null); head = node; tail = node; } public void printList() { Node p = head; while (p != null && p.next != null) { System.out.print(p.data + "-->"); p = p.next; } if (p != null) { System.out.println(p.data); } } public void addFirst(E data) { //容許節點值爲空 //if(data==null){ // return; //} Node node = new Node(data, head); head = node; if (tail == null) { tail = node; } } public void addLast(E data) { Node node = new Node(data, null); if (tail == null) { head = node; tail = node; } else { tail.next = node; tail = node; } } /** * @param data * @param node node節點必須在鏈表中 */ public void addAfter(E data, Node node) { if (node == null) { return; } Node newNode = new Node(data, node.next); node.next = newNode; if(tail==node){ tail = newNode; } } /** * @param data * @param node node節點必須在鏈表中 */ public void addBefore(E data, Node node) { if (node == null) { return; } Node p = head; if (p == null) { throw new RuntimeException("node not in LinkedList..."); } if (p == node) { Node newNode = new Node(data, node); head = newNode; return; } while (p.next != null) { if (p.next == node) { break; } p = p.next; } if (p.next == null) { throw new RuntimeException("node not in LinkedList..."); } Node newNode = new Node(data, node); p.next = newNode; } public void removeFirst() { if (head == null) { return; } if (head == tail) { head = null; tail = null; } else { head = head.next; } } public void removeLast() { if (tail == null) { return; } if (head == tail) { head = null; tail = null; } else { Node p = head; while (p.next != tail) { p = p.next; } p.next = null; tail = p; } } public void removeNode(Node node) { if (node == null) { return; } Node p = head; if (p == null) { return; } while (p.next != null && p.next != node) { p = p.next; } if (p.next != null) { p.next = node.next; } } private static class Node<E> { E data; Node next; public Node(E data, Node next) { this.data = data; this.next = next; } } }
若是你使用高級編程語言,通常都會有現成的單向鏈表實現。好比你使用的是Java,其中的LinkedList就能夠實現單向鏈表功能(雖然LinkedList底層是雙向鏈表,可是雙向鏈表能夠實現單向鏈表的全部功能)。性能
有時候你可能只是想實現一個鏈表的結構,並不想暴露太多的操做API給用戶。這時候使用LinkedList可能不太能知足你的需求,由於LinkedList除了鏈表相關的操做,還暴露了其餘的一些接口,這樣可能會給用戶太多的操做權限。測試
其實這個問題也不是太大,咱們是要作下適當的封裝就好了。
package com.csx.algorithm.link; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.Set; import java.util.function.Predicate; public class SinglyLinkedList2<E> { private LinkedList<E> list; public SinglyLinkedList2() { this.list = new LinkedList<>(); } public SinglyLinkedList2(E data){ Set<E> singleton = Collections.singleton(data); this.list = new LinkedList<>(singleton); } public SinglyLinkedList2(Collection<? extends E> c){ this.list = new LinkedList<>(c); } // ----------------------------------新增方法--------------------------------------- public void addFirst(E data){ list.addFirst(data); } public void addLast(E data){ list.addLast(data); } // 在鏈表末尾添加 public boolean add(E date){ return list.add(date); } public boolean addAll(Collection<? extends E> collection){ return list.addAll(collection); } public boolean addBefore(E data,E succ){ int i = list.indexOf(succ); if(i<0){ return false; } list.add(i,data); return true; } public boolean addAfter(E data,E succ){ int i = list.indexOf(succ); if(i<0){ return false; } if((i+1)==list.size()){ list.addLast(data); return true; }else { list.add(i+1,data); return true; } } // ---------------------------------- 刪除方法--------------------------------------- // 刪除方法,默認刪除鏈表頭部元素 public E remove(){ return list.remove(); } // 刪除方法,刪除鏈表第一個元素 public E removeFirst(){ return list.removeFirst(); } // 刪除方法,刪除鏈表最後一個元素 public E removeLast(){ return list.removeLast(); } // 刪除鏈表中第一次出現的元素,成功刪除返回true // 對象相等的標準是調用equals方法相等 public boolean remove(E data){ return list.remove(data); } // 邏輯和remove(E data)方法相同 public boolean removeFirstOccur(E data){ return list.removeFirstOccurrence(data); } // 由於LinkedList內部是雙向鏈表,因此時間複雜度和removeFirstOccur相同 public boolean removeLastOccur(E data){ return list.removeLastOccurrence(data); } // 批量刪除方法 public boolean removeAll(Collection<?> collection){ return list.removeAll(collection); } // 按照條件刪除 public boolean re(Predicate<? super E> filter){ return list.removeIf(filter); } // ----------------------------- 查詢方法---------------------------- // 查詢鏈表頭部元素 public E getFirst(){ return list.getFirst(); } // 查詢鏈表尾部元素 public E getLast(){ return list.getLast(); } // 查詢鏈表是否包含某個元素 // 支持null判斷 // 相等的標準是data.equals(item) public boolean contains(E data){ return list.contains(data); } public boolean containsAll(Collection<?> var){ return list.containsAll(var); } }
由於雙向鏈表、循環鏈表都能實現單鏈表的功能,因此這邊舉例的使用場景不單單是針對單鏈表的,使用其餘鏈表也能夠實現。
鏈表一個經典的鏈表應用場景就是 LRU 緩存淘汰算法。
緩存是一種提升數據讀取性能的技術,在硬件設計、軟件開發中都有着很是普遍的應用,好比常見的 CPU 緩存、數據庫緩存、瀏覽器緩存等等。
緩存的大小有限,當緩存被用滿時,哪些數據應該被清理出去,哪些數據應該被保留?這就須要緩存淘汰策略來決定。常見的策略有三種:先進先出策略 FIFO(First In,First Out)、最少使用策略 LFU(Least Frequently Used)、最近最少使用策略 LRU(Least Recently Used)。
使用單鏈表實現LRU算法的大體思路是:
維護一個有序單鏈表(鏈表長度有限),越靠近鏈表尾部的結點是越早以前訪問的。當有一個新的數據被訪問時,咱們從鏈表頭開始順序遍歷鏈表。
若是此數據以前已經被緩存在鏈表中了,咱們遍歷獲得這個數據對應的結點,並將其從原來的位置刪除,而後再插入到鏈表的頭部。
若是此數據沒有在緩存鏈表中,又能夠分爲兩種狀況:
實現上面算法的時間複雜度是O(n)。