數據結構_4:鏈表

鏈表 LinkedList

寫在開頭

  • 許久之前文章提到過的:動態數組隊列,其底層均依託於靜態數組,通過resize()進行動態擴容操作。
  • 鏈表,則爲真正的動態數據結構,同樣也是最簡單的動態數據結構
  • 鏈表這種數據結構可以幫助我們瞭解計算機中指針(引用)遞歸等概念。

節點 Node

  • 數據存儲在節點中,需要多少節點就生產多少節點進行掛接,但失去了隨機訪問的能力,適合索引無語義的情況。

    class node {
        E e;
        Node next;
    }
    

鏈表數據結構創建 LinkedList,爲保證節點信息安全性,採用內部類方式進行構造

 /**
 * @author by Jiangyf
 * @classname LinkedList
 * @description 鏈表
 * @date 2019/9/28 13:08
 */
 public class LinkedList<E> {

     /**
     * 節點內部類
     */
     private class Node {
         public E e;
         public Node next;

         public Node(E e, Node next) {
             this.e = e;
             this.next = next;
         }

         public Node(E e) {
             this(e, null);
         }

         public Node() {
             this(null, null);
         }

         @Override
         public String toString() {
             return "Node{" +
                     "e=" + e +
                     ", next=" + next +
                     '}';
         }
     }

     private Node head;
     int size;

     public LinkedList() {
         head = null;
         size = 0;
     }

     // 獲取鏈表容量
     public int getSize() {
         return size;
     }

     // 判斷鏈表是否爲空
     public boolean isEmpty() {
         return size == 0;
     }
 }
添加操作方法
  • 從鏈表頭部添加元素

     public void addFirst(E e) {
          head = new Node(e, head);
          size ++;
      }
    
  • 從鏈表中間位置index處添加元素,注意:先連後斷

    public void add(int index, E e) throws IllegalAccessException {
               // 索引校驗
               if (index < 0 || index > size) {
                   throw new IllegalAccessException("Add failed. Illegal index.");
               }
               // 判斷是操作是否爲頭部添加
               if (index == 0) {
                   addFirst(e);
               } else {
                   // 創建前置節點
                   Node prev = head;
                   // 定位到待插入節點前一個節點
                   for (int i = 0; i < index -1 ; i++) {
                       prev = prev.next;
                   }
                   prev.next = new Node(e, prev.next);
                   size ++;
               }
           }
    
  • 在鏈表尾部位置添加元素

     public void addLast(E e) throws IllegalAccessException {
         add(size, e);
     }
    
  • 爲鏈表設立虛擬頭結點(dummyHead),解決從頭部添加和其他位置添加的邏輯不一致情況

    • 虛擬頭結點作爲鏈表內部機制進行設置,在原有head節點改進爲dummyHead.next = head,適配節點添加邏輯。
    • 修改代碼
      • 添加虛擬頭結點dummyHead,不存放任何內容

          	private Node dummyHead;
        			
        	public LinkedList() {
        	     dummyHead = new Node(null, null);
        	     size = 0;
        	 }
        
      • 修改add(index, e)方法

          // 從鏈表中間添加元素 先連後斷
           public void add(int index, E e) throws IllegalAccessException {
               // 索引校驗
               if (index < 0 || index > size) {
                   throw new IllegalAccessException("Add failed. Illegal index.");
               }
               // 創建前置節點
               Node prev = dummyHead;
               // 定位到待插入節點前一個節點,遍歷index次原因爲 dummyHead爲head節點前一個節點
               for (int i = 0; i < index ; i++) {
                   prev = prev.next;
               }
               prev.next = new Node(e, prev.next);
               size ++;
           }
        
      • 修改addFirst(e)方法

        public void addFirst(E e) throws IllegalAccessException {
              add(0, e);
        }
        
  • 獲取指定位置index的節點元素

     public E get(int index) throws IllegalAccessException {
         // 索引校驗
         if (index < 0 || index > size) {
             throw new IllegalAccessException("Get failed. Illegal index.");
         }
         // 定位到head節點
         Node cur = dummyHead.next;
         for (int i = 0; i < index; i++) 
             cur = cur.next;
         return cur.e;
     }
    
  • 獲取頭結點、尾結點

     public E getFirst() throws IllegalAccessException {
        return get(0);
     }
    
     public E getLast() throws IllegalAccessException {
         return get(size - 1);
     }
    
  • 更新指定位置元素

    public void set(int index, E e) throws IllegalAccessException {
        // 索引校驗
         if (index < 0 || index > size) {
             throw new IllegalAccessException("Set failed. Illegal index.");
         }
         Node cur = dummyHead.next;
         for (int i = 0; i < index ; i++)
             cur = cur.next;
         cur.e = e;
     }
    
  • 查找鏈表中是否存在元素

     public boolean contains(E e) {
         Node cur = dummyHead.next;
         while(cur != null) {
             if (cur.e.equals(e)) {
                 return true;
             }
             cur = cur.next;
         }
         return false;
     }
    
  • 刪除鏈表元素節點

     public E remove(int index) throws IllegalAccessException {
         // 索引校驗
         if (index < 0 || index > size) {
             throw new IllegalAccessException("Remove failed. Illegal index.");
         }
         // 定位到待刪除節點的前一節點
         Node prev = dummyHead;
         for (int i = 0; i < index - 1 ; i++)
             prev = prev.next;
         // 保存待刪除節點
         Node retNode = prev.next;
         // 跨過待刪除節點進行連接
         prev.next = retNode.next;
         // 待刪除節點next置空
         retNode.next = null;
         size --;
         return retNode.e;
     }
    
     public E removeFirst() throws IllegalAccessException {
         return remove(0);
     }
    
     public E removeLast() throws IllegalAccessException {
         return remove(size - 1);
     }
    
  • 通過上述方法,我們可以分析得出:鏈表的CURD操作的平均時間複雜度均爲O(n),鏈表的操作均要進行遍歷。


仔細想想,如果對鏈表的操作僅限於頭部呢? 細思極恐,是不是複雜度就降爲O(1)啦?又由於鏈表是動態的,不會造成空間的浪費,所以當且僅當頭部操作下,優勢是很明顯的!

  • 基於頭部操作,用鏈表實現,關於Stack接口,可以查看 數據結構_2:棧

     public class LinkedListStack<E> implements Stack<E> {
    
           private LinkedList<E> list;
    
           public LinkedListStack(LinkedList<E> list) {
               this.list = new LinkedList<>();
           }
    
           @Override
           public int getSize() {
               return list.getSize();
           }
    
           @Override
           public boolean isEmpty() {
               return list.isEmpty();
           }
    
           @Override
           public void push(E e) throws IllegalAccessException {
               list.addFirst(e);
    
           }
    
           @Override
           public E pop() throws IllegalAccessException {
               return list.removeFirst();
           }
    
           @Override
           public E peek() throws IllegalAccessException {
               return list.getFirst();
           }
       }
    
  • 既然有了實現,那就拿鏈表棧數組棧比較下吧,創建測試函數

    private static double testStack(Stack<Integer> stack, int opCount) throws IllegalAccessException {
            long startTime = System.nanoTime();
            Random random = new Random();
            for (int i = 0; i < opCount; i ++)
                stack.push(random.nextInt(Integer.MAX_VALUE));
            for (int i = 0; i < opCount; i ++)
                stack.pop();
            long endTime = System.nanoTime();
            return (endTime - startTime) / 1000000000.0;
    }
    
  • 分別創建鏈表棧數組棧進行一百萬次的入棧和出站操作,比較兩者用時,貌似好像鏈表棧好一點。
    在這裏插入圖片描述

  • 繼續加大數據量到一千萬次的入棧和出棧操作,此時鏈表棧的表現就不佳了。在這裏插入圖片描述
    原因大致是:數組棧pop和push操作基於數組尾部進行處理;而鏈表棧pop和push操作基於鏈表頭部操作,且投保操作含有創建新節點的操作(new Node),因此比較耗時。

  • 結構已經創建,那麼隊列也是必不可少的,前文中的數組隊列的構建是從頭部和尾部均進行了操作,由於出列操作的複雜度爲O(n),入列操作的複雜度爲O(1),進行了隊列結構的優化,於是產生了數組實現的循環隊列,且性能要遠高於普通的數組隊列。於是我們對鏈表這一結構進行分析:

    • 由於存在head頭指針,頭部操作的複雜度爲O(1)【dummyHead的設定】

    • 那麼基於這個原理,添加上tail尾指針,記錄鏈表尾部(索引),是否可以將尾部的操作複雜度降低呢? head指針的定位是依賴於虛擬頭指針的結構設定,而tail指針無此設定,若要進行尾部元素刪除操作,還需要定位到待刪除元素的前一元素,仍需要進行遍歷。

    • 基於上述,鏈表節點Node的next設定,更有利於我們從鏈表首部進行出隊操作鏈表尾部進行入隊操作

    • 採用head + tail改造我們的LinkedListQueue

      /**
      * @author by Jiangyf
      * @classname LinkedListQueue
      * @description 鏈表隊列
      * @date 2019/9/28 16:35
      */
      public class LinkedListQueue<E> implements Queue<E> {
      
          /**
          * 節點內部類
          */
          private class Node {
              public E e;
              public Node next;
      
              public Node(E e, Node next) {
                  this.e = e;
                  this.next = next;
              }
      
              public Node(E e) {
                  this(e, null);
              }
      
              public Node() {
                  this(null, null);
              }
      
              @Override
              public String toString() {
                  return "Node{" +
                          "e=" + e +
                          ", next=" + next +
                          '}';
              }
          }
      
          private Node head, tail;
          private int size;
      
          public LinkedListQueue() {
              this.head = null;
              this.tail = null;
              this.size = 0;
          }
      
          @Override
          public int getSize() {
              return size;
          }
      
          @Override
          public boolean isEmpty() {
              return size == 0;
          }
      
          @Override
          public void enqueue(E e) {
              // 入隊 從鏈表尾部進行
              if (tail == null) {
                  // 表示鏈表爲空
                  tail = new Node(e);
                  head = tail;
              } else {
                  // 不爲空,指向新創建的元素,尾指針後移
                  tail.next = new Node(e);
                  tail = tail.next;
              }
              size ++;
          }
      
          @Override
          public E dequeue() {
              // 出隊 從鏈表頭部進行
              if (isEmpty()) {
                  throw new IllegalArgumentException("Queue is empty");
              }
              // 獲取待出隊元素
              Node retNode = head;
              // 頭指針後移
              head = head.next;
              // 待刪除元素與鏈表斷開
              retNode.next = null;
              if (head == null) {
                  // 鏈表中僅有一個元素的情況,頭指針移動後變爲空鏈表
                  tail = null;
              }
              size --;
              return retNode.e;
          }
      
          @Override
          public E getFront() {
              if (isEmpty()) {
                  throw new IllegalArgumentException("Queue is empty");
              }
              return head.e;
          }
      }
      
    • 同樣,與之前的數組隊列循環隊列鏈表隊列進行下性能測試(10萬數量級)
      在這裏插入圖片描述
      可見,循環隊列和鏈表隊列的性能遠高於數組隊列,其原因就是頭尾指針動態控制數據結構,而數組隊列出列時要反覆的進行數據複製,因此消耗時間較長。


最後,上述代碼已經上傳個人倉庫,需要的小夥伴可以下載查看

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