數據結構(一)之線性表


基礎概念

數據結構是相互之間存在一種或多種關系的數據元素的集合。

邏輯結構和物理結構 

關於數據結構,我們可以從邏輯結構和物理結構這兩個維度去描述

邏輯結構是數據對象中數據元素之間的關系,是從邏輯意義上去描述的數據之間的組織形式。

邏輯結構有4種:

  • 集合結構(數據元素之間僅以集合的方式體現,元素之間沒有別的關系)
  • 線性結構(數據元素之間存在一對一的關系)
  • (數據元素之間為一對多或多對一的關系)
  • (數據元素之間為多對多的關系)

物理結構則是邏輯結構在計算機中內存中的存儲形式,分為兩種:

  • 順序存儲結構
  • 鏈式存儲結構

線性表(list)

性表是零個或多個數據元素的的有限序列

線性表是線性結構,元素之間存在一對一的關系,線性表可通過順序和鏈式兩種方式來實現。

順序存儲結構,用一段地址連續的存儲單元依次存儲線性表的數據元素

  

鏈式存儲結構,用一組任意的存儲單元來存儲數據元素不要求物理存儲單元的連續性,由一系列結點組成,每個結點除了要存儲數據外,還需存儲指向后繼結點或前驅結點的存儲地址。

順序存儲和鏈式存儲對比

  • 順序存儲結構
    • 優點
      • 實現比較簡單
      • 查找指定位置的元素效率很快,時間復雜度為常數階O(1) 
      • 無需額外存儲元素之間的邏輯關系(鏈式存儲由於存儲空間隨機分配,需要存儲元素之間的邏輯關系)
    • 缺點
      • 需要預先分配存儲空間,如果數據元素數量變化較大,很難確定存儲容量,並導致空間浪費
      • 若頻繁進行插入刪除操作,則可能需要頻繁移動大量數據元素
  • 鏈式存儲結構
    • 優點
      • 不需要提前分配存儲空間,元素個數不受限制
      • 對於插入刪除操作,在已找到目標位置前提下,效率很高,僅需處理元素之間的引用關系,時間復雜度為O(1)
    • 缺點
      • 實現相對復雜
      • 查找效率較低,最壞情況下需要遍歷整張表
      • 由於物理存儲位置不固定,需要額外存儲數據元素之間的邏輯關系

鏈式存儲代碼實現

單鏈表

package listdemo;
/**
 * Created by chengxiao on 2016/10/18.
 */
public class MyLinkedList {
    /**
     * 指向頭結點的引用
     */
    private Node first ;
    /**
     * 線性表大小
     */
    private int size;
    /**
     * 結點類
     */
    private static class Node{
        //數據域
        private int data;
        //指向后繼結點的引用
        private Node next;
        Node(int data){
            this.data = data;
        }
    }
    /**
     * 從頭部進行插入
     * 步驟:1.新結點的next鏈指向當前頭結點;2.將first指向新節點
     * 時間復雜度:O(1)
     * @param data
     */
    public void insertFirst(int data){
        Node newNode = new Node(data);
        newNode.next = first;
        first = newNode;
        size++;
    }
    /**
     * 從頭部進行刪除操作
     * 步驟:1.將頭結點的next鏈置空 2.將first引用指向第二個結點
     * 時間復雜度為:O(1)
     * @return
     */
    public boolean deleteFirst(){
        if(isEmpty()){
            return false;
        }
        Node secondNode = first.next;
        first.next = null;
        first = secondNode;
        size--;
        return true;
    }
    /**
     * 取出第i個結點
     * 步驟:從頭結點進行遍歷,取第i個結點
     * 時間復雜度:O(n),此操作對於利用數組實現的順序存儲結構,僅需常數階O(1)即可完成。
     * @param index
     * @return
     */
    public int get(int index) throws Exception {
        if(!checkIndex(index)){
            throw new Exception("index不合法!");
        }
        Node curr = first;
        for(int i=0;i<index;i++){
            curr = curr.next;
        }
        return curr.data;
    }
    /**
     * 遍歷線性表
     * 時間復雜度:O(n)
     */
    public void displayList(){
        Node currNode = first;
        while (currNode!=null){
            System.out.print(currNode.data+" ");
            currNode = currNode.next;
        }
        System.out.println();
    }

    /**
     * 鏈表是否為空
     * @return
     */
    public boolean isEmpty(){
        return first == null;
    }

    /**
     * index是否合法
     * @param index
     * @return
     */
    private boolean checkIndex(int index){
        return index >= 0 && index < size;
    }
    /**
     * 鏈表大小
     * @return
     */
    public int size() {
        return size;
    }
    public static  void main(String []args) throws Exception {

        MyLinkedList myLinkedList = new MyLinkedList();
        //從頭部插入
        myLinkedList.insertFirst(1);
        myLinkedList.insertFirst(2);
        myLinkedList.insertFirst(3);
        myLinkedList.insertFirst(4);
        //遍歷線性表中元素
        myLinkedList.displayList();
        //獲取第二個元素
        System.out.println(myLinkedList.get(2));
        //刪除結點
        myLinkedList.deleteFirst();
        myLinkedList.displayList();
    }
}

輸出結果

 4 3 2 1 2 3 2 1 

雙端鏈表 

  上面羅列了線性表中的幾種基本操作,考慮下,如果要提供一個在鏈表尾部進行插入的操作insertLast,那么由於單鏈表只保留了指向頭結點的應用first,需要從頭結點不斷通過其next鏈找后繼結點來遍歷,時間復雜度為O(n)。其實,我們可以在保留頭結點引用的時候,也保留一個尾結點的引用。這樣,在從尾部進行插入時就方便多了

  雙端鏈表同時保存對頭結點和對尾結點的引用

    /**
     * 指向頭結點的引用
     */
    private Node first ;
    /**
     * 指向尾結點的引用
     */
    private Node rear;

從尾部進行插入

  /**
     * 雙端鏈表,從尾部進行插入
     * 步驟:將當前尾結點的next鏈指向新節點即可
     * 時間復雜度:O(1)
     * @param data
     */
    public void insertLast(int data){
        Node newNode = new Node(data);
        if(isEmpty()){
            first = newNode;
            rear = newNode;
            size++;
            return;
        }
        rear.next = newNode;
        rear = newNode;
        size++;
    }

做其他操作的時候也需注意保持對尾結點的引用,此處不再贅述。

雙向鏈表

 再考慮下,如果我們要提供一個刪除尾結點的操作,步驟很簡單:在刪除尾結點的過程中需要將其前驅結點(即倒數第二個結點)的next鏈引用置為空,但由於我們的鏈表是單鏈表,一條道走到黑,要找倒數第二個結點得從頭開始遍歷,這種情況下,我們就可以考慮使用雙向鏈表。

  雙向鏈表的的每一個結點,包含兩個指針域,一個指向它的前驅結點,一個指向它的后繼結點。

          

  /**
     * 刪除尾結點
     * 主要步驟:1.將rear指向倒數第二個結點 2.處理相關結點的引用鏈
     * 時間復雜度:O(1)
     * @return
     */
    public void deleteLast() throws Exception {
        if(isEmpty()){
            throw new Exception("鏈表為空");
        }
        Node secondLast = rear.prev;
        rear.prev = null;
        rear = secondLast;
        if(rear == null){
            first = null;
        }else{
            rear.next = null;
        }
        size--;
    }

其他操作同理,在過程中需要同時保持對結點的前驅結點和后繼結點的引用,刪除操作時,需要注意解除廢棄結點的各種引用,便於GC。

總結

  本文對數據結構的一些基本概念,邏輯結構和物理結構,線性表等概念進行了基本的闡述。同時,介紹了線性表的順序存儲結構和鏈式存儲結構,對線性表的鏈式存儲結構(單鏈表,雙端鏈表,雙向鏈表),使用Java語言做了基本實現。數據結構的重要性毋庸置疑,它是軟件設計的基石,由於自己非科班出身,雖曾自學過一段時間,也不夠系統,最近希望能重新系統地梳理下,本篇就當自己數據結構再學習的開篇吧,共勉。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM