算法與數據結構之美-鏈表

開篇思考

鏈表有一個經典的應用場景,就是LRU緩存淘汰算法,常見的緩存淘汰算法有:
先進先出FIFO、最少使用LFU、最近最少使用策略LRU
思考一下:如何用鏈表實現LRU緩存淘汰策略?

五花八門的鏈表結構

數組需要一塊連續的內存空間來存儲,對於內存的要求比較高,假如需要100MB大小的內存空間,但是內存中沒有連續的100MB空間,即便剩餘的總空間大於100MB,也會申請失敗。

鏈表恰恰相反,並不需要一塊連續的內存空間,可以通過“指針”將一組零散的內存塊串聯起來使用。

本文主要介紹:單鏈表、雙向鏈表、循環鏈表

單鏈表

單鏈表
第一個結點是頭結點,用來記錄鏈表的基地址,有了它就可以遍歷整條鏈表。最後一個結點是尾結點,它指向的下一個結點是一個NULL。與數組一樣,鏈表也支持數據的查找、插入和刪除。

與數組不同的是,鏈表在進行插入和刪除的時候不需要爲了保持內存的連續性而搬移結點,只需要考慮相鄰結點的指針變化,時間複雜度爲O(1)。但是,鏈表就不能隨機訪問第K個元素,需要根據指針依次遍歷,直到找到相應的結點。所以,鏈表隨機訪問的時間複雜度是O(n)。

循環鏈表

循環鏈表
循環鏈表是一種特殊的單鏈表,唯一的區別就是循環鏈表的尾結點,指向鏈表的頭結點。在處理環形問題時,就適合採用循環鏈表,例如約瑟夫環。

雙向鏈表

雙向鏈表
雙向鏈表需要額外的兩個空間來存儲前驅節點和後繼結點的指針。相比於單鏈表,要佔用較多的內存空間,但是支持雙向遍歷,爲雙向鏈表的操作帶來了靈活性。

實際軟件開發中,鏈表刪除一個數據主要是兩種情況:

  • 刪除結點中“值等於某個給定值”的結點
  • 刪除給定指針指向的結點

第一種情況,單雙鏈表刪除的時間複雜度都是O(1),但是遍歷查找比較費時間,對應的時間複雜度是O(n)。

第二種情況,已經遍歷到了需要刪除的結點,但是刪除結點,需要知悉其前驅結點p,單鏈表不支持前驅結點,還需要從頭開始遍歷查找其前驅結點,雙鏈表就不需要再次遍歷,只需要在O(1)的時間複雜度內解決。

同理刪除操作也是相同的,對於有序鏈表,雙向鏈表按值查詢的效率較高。可以根據上次查詢的結點P,來判斷是向前還是向後查找。

實際開發中,雙向鏈表比較佔用內存,但是比單鏈表應用更加廣泛,例如LinkedHashMap其中就用到了雙向鏈表。

當內存空間充足時,爲了追求代碼的執行效率,可以考慮採用空間換時間的方式。緩存技術就是利用了空間換時間設計思想,事先將數據加載到內存中,雖然耗費內存,但是大大提升了數據的訪問速度。

還有一種雙向鏈表+循環鏈表的組合:雙向循環鏈表
雙向循環鏈表

鏈表VS數組

性能分析
在實際開發中,不能僅僅根據複雜度分析來決定使用哪個數據結構:
數組簡單易用,需要分配連續的內存空間,藉助CPU的緩存機制,預讀數組中的數據,所以訪問效率高,鏈表不是連續存儲的,對於CPU的緩存不友好,不能預讀;

鏈表不需要分配空間,可以支持動態擴容;

針對於不同的項目,需要權衡採用數組還是鏈表。

解答開篇

採用單鏈表實現LRU算法:
維護一個單鏈表,越靠近鏈表尾部的就是越早之前訪問的,當訪問新的數據時,從鏈表頭開始遍歷鏈表:

  • 如果鏈表之前存在該數據,遍歷得到該結點並刪除,再插入到鏈表的頭部;

  • 如果鏈表沒有該數據,就分爲兩種情況:

    • 此時緩存未滿,將此結點插入鏈表頭部;
    • 若緩存已滿,就刪除鏈表的尾結點,再將數據插入到鏈表頭部;

代碼

public class LRUBaseLinkedList<T>{
	private final static Integer DEFAULT_CAPACITY = 10; //默認鏈表容量
    private SNode<T> headNode; //頭結點
    private Integer length; //鏈表長度
    private Integer capacity; //鏈表容量
	
	public LRUBaseLinkedList(Integer capacity){
		this.headNode = new SNode<>();
		this.capacity = capacity;
		this.length = 0;
	}
	public LRUBaseLinkedList(){
		this.headNode = new SNode<>();
		this.capacity = DEFAULT_CAPACITY;
		this.length = 0;
	}
	public void add(T data){
		SNode preNode = findPreNode(data);
		//鏈表中存在,就刪除原數據,插入鏈表頭部
		if(preNode!=null){
			deleteNode(preNode);
			insertNodeAtBegin(data);
		}else{
			if(length>=this.capacity){
					deleteNodeAtEnd();
			}
			insertNodeAtBegin(data);
		}
	}
	//查找元素的前一個結點
	private SNode findPreNode(T data){
		SNode node = headNode;
		while(node.getNext() != null){
			if(data.equals(node.getNext().getElement())){
				return node;
			}
			node = node.getNext();
		}
		return null;
	}
	//刪除元素
	private SNode deleteNode(SNode preNode){
		SNode temp = preNode.getNext();
		preNode.setNext(temp.getNext());
		temp = null;
		length--;
	}
	//刪除末尾的元素
	private SNode deleteNodeAtEnd(){
		SNode node = headNode;
		//空鏈表直接返回
		if(node.getNext()==null)
			return;
		//倒數第二個結點	
		while(node.getNext().getNext()!=null){
			node = node.getNext();
		}
		SNode temp = node.getNext();
		node.setNext(null);
		tmp = null;
		length--;		
	}
	private void printAll(){
		SNode node = headNode.getNext();
		while(node!=null){
			System.out.println(node.getElement()+“,”);
			node = node.getNext();
		}
		System.out.println();
	}
	public class SNode<T>{
		private T element;
		private SNode next;
		public SNode(T element) {
            this.element = element;
        }
     public SNode(T element, SNode next) {
            this.element = element;
            this.next = next;
        }
      public SNode() {
            this.next = null;
        }
      public T getElement() {
            return element;
        }
      public void setElement(T element) {
            this.element = element;
        }
      public SNode getNext() {
            return next;
        }
      public void setNext(SNode next) {
            this.next = next;
        }
    }
 publlic static void main(String[ ] args){
		LRUBaseLinkedList list = new LRUBaseLinkedList();
		Scanner sc = new Scanner(System.in);
		while(true){
			list.add(sc.nextInt());
			list.printAll();
		}
	}
}

參考

[1] 極客時間-數據結構與算法之美-鏈表(上)

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