1. 程式人生 > 實用技巧 >重學資料結構(一、線性表)

重學資料結構(一、線性表)

@目錄


1、線性表的概念

線性表是最常見也是最簡單的一種資料結構。簡言之, 線性表是n個數據元素的有限序列。 其一般描述為:

A={a1,a2,……an)

一個數據元素通常包含多個數據項, 此時每個資料元素稱為記錄, 含有大量的記錄的線性表稱為檔案。

例如十二生肖,就是一個線性表:

在稍微複雜的線性表中, 一個數據元素可以由若干個資料項組成

例如例如,學生名單,含學生的學號、姓名、年齡、性別等資訊。

從上例中可以看出每個資料元素具有相同的特性:

  • 即每個資料元素最多隻能有一個直接前趨元素, 每個資料元素最多隻能有一個直接後繼元素
  • 只有第一個資料元素沒有直接前趨元素, 而最後一個數據元素沒有直接後繼元素

線性表是一個比較靈活的資料結構, 它的長度根據需要增長或縮短, 也可以對線性表的資料元素進行不同的操作(如訪問資料元素, 插入、 刪除資料元素等)。

線性表的儲存結構分為順序儲存和鏈式儲存。


2、順序表

線性表的順序儲存, 也稱為向量儲存, 又可以說是一維陣列儲存。 線性表中結點存放的物理順序與邏輯順序完全一致, 它叫向量儲存(一般指一維陣列儲存)。

順序表儲存結構如下:


線性表的第一個資料元素的位置通常稱做起始位置或基地址。

表中相鄰的元素之間具有相鄰的儲存位置。


2.1、順序表初始化

順序分配的線性表可以直接使用一維陣列描述為:

type arraylist[];  //type 的型別根據實際需要確定//

在Java中,由於所有類都是Object的子類,所以,可以宣告一個Object陣列:

    //存放元素的陣列
    private Object list[];

該程式碼只是對應用陣列的宣告, 還沒有對該陣列分配空間, 因此不能訪問陣列。 只有對陣列進行初始化並申請記憶體資源後, 才能夠對陣列中元素進行使用和訪問。

    //預設容量
    private static  int defaultSize=10;

    //表長:實際儲存元素的個數
    private int length;

    public SequenceList() {
        //初始化陣列,宣告記憶體資源
        this.list=new Object[defaultSize];
    }

2.2、新增

在這個方法裡,我們對陣列進行了動態擴容,一旦陣列空間溢位(size>=defaultSize),就建立一個新的陣列,容量為原來的兩倍,將原陣列的元素搬到新陣列。示意圖如下

插入操作是將將操作位置的所有後繼元素向後順次移動。

      /**
     * 新增元素
     * @param item 資料元素
     * @param index 位置
     */
    public  void add(Object item,int index){
        //list[0]=item;
        if (index>=size||index<0){
            System.out.println("index can not be this value");
            return;
        }
        //陣列擴容
        if (size>=defaultSize){
            defaultSize=2*defaultSize;
           //陣列容量擴充兩倍
            Object[] newArray=new Object[defaultSize];
            for (int j=0;j<list.length;j++){
                newArray[j]=list[j];
            }
            list=newArray;
        }
        //插入
        for (int k=size;k>=index;k--){
            //所有元素後移一位
            list[k+1]=list[k];
            list[index]=item;
        }
        size++;
    }

時間複雜度分析

  • 陣列擴容
    在陣列擴容的操作中,需要把舊陣列複製到新的陣列,時間複雜度是O(n)。

  • 插入操作
    插入操作的主要時間消耗是移動陣列元素,該語句最壞的情況下, 移動次數是 list.length, 最好的情況下是 0。時間複雜度是O(n)。


2.3、刪除

    /**
     * 移除資料元素
     * @param index
     */
    public  void remove(int index){
        if (index>list.length-1||index<0){
            System.out.println("index can not be this value");
            return;
        }
        //所有元素前移
        for (int k=index;k<list.length;k++){
            list[k]=list[k+1];
        }
        size--;
    }

時間複雜度分析

刪除的操作和新增類似,刪除是將元素前移,最好情況是移動0次,最壞情況是移動list.length次,時間複雜度為O(n)。


2.4、刪除

    /**
     * 取資料元素
     * @param index
     * @return
     */
    public Object get(int index){
        return list[index];
    }

時間複雜度分析

取資料元素直接根據陣列下標獲取即可,不存在元素的移動,所以時間複雜度為O(1)。


2.5、更新

    /**
     * 更新資料元素
     * @param o
     * @param index
     */
    public void set(Object o,int index){
        if (index>=size||index<0){
            System.out.println("index can not be this value");
            return;
        }
        list[index]=o;
    }

時間複雜度分析

更新和上面的獲取類似,時間複雜度為O(1)。


2.6、AraayList和Vector

Java本身也提供了順序表的實現:java.util.ArrayList和java.util.Vector。

實際上,java.util.ArrayLis的實現用了一些Native方法,可以直接操作記憶體效率會高很多。瞭解ArrayList原始碼:ArrayList原始碼閱讀筆記

java.util.Vector是一個歷史遺留類,並不建議使用。


3、連結串列

線性表的順序儲存結構的特點是邏輯關係上相鄰的兩個元素在物理位置上也相鄰, 因此隨機存取元素時比較簡單, 但是這個特點也使得在插入和刪除元素時, 造成大量的資料元素移動, 同時如果使用靜態分配儲存單元, 還要預先佔用連續的儲存空間, 可能造成空間的浪費或空間的溢位。 如果採用鏈式儲存, 就不要求邏輯上相鄰的資料元素在物理位置上也相鄰, 因此它沒有順序儲存結構所具有的缺點, 但同時也失去了可隨機存取的優點。

3.1、單向連結串列

單項鍊表是最簡單的連結串列,每個節點包含兩部分,資料域 (data)和指標域 (next),資料域存放資料元素的值,指標域存放存放相鄰的下一個結點的地址。

單向連結串列是指結點中的指標域只有一個沿著同一個方向表示的鏈式儲存結構。示意圖如下:

3.1.1、節點類

因為結點是一個獨立的物件, 所以需要一個獨立的結點類。 以下是一個結點類的定義:

    /**
     * 節點類
     */
    class Node<T>{
        private Object data;     //資料
        private Node next;      //下一個節點
        Node(Object it,Node nextVal){
            this.data=it;
            this.next=nextVal;
        }

        Node(Node nextVal){
            this.next=nextVal;
        }

        Node(){}

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }

3.1.2、單鏈表類

需要定義一個單鏈表類,包含一些基本的屬性,構造方法:

public class SinglyLinkedList<T> {
    private Node head;     //頭結點
    private Node tail;    //尾節點
    private int size;     //連結串列長度

    public SinglyLinkedList(){
       head=null;
       tail=null;
       size=0;
    }
}    

3.1.2、獲取元素

這裡實現了按照序號獲取元素和獲取元素資料域的方法:

    /**
     * 獲取元素
     * @param index
     * @return
     */
    public Node getNodeByIndex(int index){
        if (index>=size||size<0){
            System.out.println("Out of bounds");
            return null;
        }
        Node node=head;
        for (int i=0;i<size;i++,node=node.next){
            if (index==i){
                return node;
            }
        }
        return null;
    }

     /**
     * 獲取資料元素資料域
     * @param index
     * @return
     */
    public Object get(int index){
        return getNodeByIndex(index).getData();
    }

時間複雜度分析
這是一個迴圈,從頭(head)開始, 然後再逐個向後査找,直到找到第index個元素,時間複雜度為O(n)。


3.1.3、插入元素

這裡有三種插入方法:

  • 頭插入法
    頭插入法示意圖如下:
    /**
     * 頭插入法
     * @param element
     */
    public void addHead(T element){
        head=new Node(element,head);
        //如果插入的是空連結串列,尾結點即首節點
        if(tail==null){
            tail=head;
        }
        size++;
    }
  • 尾插入法
    尾插入法和頭插入法類似
    /**
     * 尾插入
     * @param element
     */
    public void addTail(T element){
        //如果是空表
        if (head==null){
            head=new Node(element,null);
            tail=head;
        }else {
            Node node=new Node(element,null);
            //舊的尾結點指向插入的節點
            tail.setNext(node);
            //尾結點後移
            tail=node;
        }
        size++;
    }

  • 中間插入法
    中間插入,改變前面節點的指向和插入元素的指向就可以了,示意圖如下:
    /**
     * 在指定位置插入資料元素
     * @param element
     * @param index
     */
    public void add(T element,int index){
        if (index>size||size<0){
            System.out.println("Out of bounds");
            return;
        }
        if (index==0){
            addHead(element);
        }else if (index==size){
            addTail(element);
        }else{
            //index位置前節點
            Node preNode=getNodeByIndex(index-1);
            //index位置節點
            Node indexNode=getNodeByIndex(index);
            //插入的節點,後繼執行之前index位置的節點
            Node insertNode=new Node<T>(element,indexNode);
            //前趨節點指向插入的節點
            preNode.setNext(insertNode);
            size++;
        }
    }

3.1.4、刪除元素

刪除元素,找到目標元素,改變前節點的指向。

    /**
     * 刪除元素
     * @param index
     */
    public void remove(int index){
        if (index>size||size<0){
            System.out.println("Out of bounds");
            return;
        }
        //刪除頭節點,只需將頭節點置為下一個節點
        if (index==0){
            head=head.next;
        } else {
            //將要被刪除的節點
            Node indexNode=getNodeByIndex(index);
            //被刪除節點的前一個節點
            Node preNode=getNodeByIndex(index-1);
            //前節點指向目標節點的後節點
            preNode.setNext(indexNode.next);
            //如果刪除的是最後一個元素,尾結點前移
            if(index==size-1){
                tail=preNode;
            }
        }
        size--;
    }

3.2、迴圈連結串列

迴圈連結串列又稱為迴圈線性連結串列, 其儲存結構基本同單向連結串列。

它是在單向連結串列的基礎上加以改進形成的, 可以解決單向連結串列中單方向查詢的缺點。 因為單向連結串列只能沿著一個方向, 不能反向查詢, 並且最後一個結點指標域的值是 null, 為解決單向連結串列的缺點, 可以利用末尾結點的空指標完成前向查詢。 將單鏈表的末尾結點的指標域的 null 變為指向第—個結點, 邏輯上形成一個環型, 該儲存結構稱之為單向迴圈連結串列。 示意圖如下:

它相對單鏈表而言, 其優點是在不增加任何空間的情況下, 能夠已知任意結點的地址,可以找到連結串列中的所有結點(環向查詢)。

空的迴圈線性連結串列根據定義可以與單向連結串列相同, 也可以不相同。 判斷迴圈連結串列的末尾結點條件也就不同於單向連結串列, 不同之處在於單向連結串列是判別最後結點的指標域是否為空, 而迴圈線性連結串列末尾結點的判定條件是其指標域的值指向頭結點。

迴圈連結串列的插入、 刪除運算基本同單向連結串列, 只是查詢時判別條件不同而已。 但是這種迴圈連結串列實現各種運算時的危險之處在於: 連結串列沒有明顯的尾端, 可能使演算法進入死迴圈。


3.3、雙鏈表

在前面的單鏈表裡,連結串列只有一個指向後一個節點的指標,而雙鏈表多出一個指向前一個節點的指標。這樣可以從任何一個節點訪問前一個節點,當然也可以訪問後一個節點,以至整個連結串列。


3.3.1、節點類

對比單鏈表,節點類裡需要新增前趨節點。

    /**
     * 節點類
     */
    class Node<T>{
        private Object data;     //資料
        private Node next;      //下一個節點
        private Node prev;      //上一個節點 
        Node(Node prevVal,Object it,Node nextVal){
            this.data=it;
            this.next=nextVal;
            this.prev=prevVal;
        }

        Node(Node prevVal,Node nextVal){
            this.prev=prevVal;
            this.next=nextVal;
        }

        Node(){}

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }

        public Node getPrev() {
            return prev;
        }

        public void setPrev(Node prev) {
            this.prev = prev;
        }
    }

3.3.2、雙鏈表類

定義一個雙鏈表類,包含構造方法和一些基本的屬性。

public class DoublyLinkedList<T> {
    private Node head;     //頭結點
    private Node tail;    //尾節點
    private int size;     //連結串列長度

    public DoublyLinkedList(){
        head=null;
        tail=null;
        size=0;
    }
}    

3.3.3、獲取元素

雙向連結串列查詢結點的實現基本同單向連結串列, 只要按照 next 的方向找到該結點就可以了。

    /**
     * 獲取資料元素
     * @param index
     * @return
     */
    public Node getNodeByIndex(int index){
        if (index>=size||size<0){
            System.out.println("Out of bounds");
            return null;
        }
        Node node=head;
        for (int i=0;i<=size;i++,node=node.next){}
        return node;
    }

    /**
     * 獲取資料元素資料域
     * @param index
     * @return
     */
     public Object get(int index){
        return getNodeByIndex(index).getData();
     }

3.3.4、插入元素

  • 頭插入法:將新的節點前趨指向null,後繼指向頭結點,頭節點的前趨指向新節點
    /**
     * 頭插入法
     * @param element
     */
    public void addHead(T element){
         Node node=new Node(null,element,null);
        // 如果表頭為空直接將新節點作為頭節點
         if (head==null){
             head=node;
         }else{
             //新節點後繼指向頭節點
             node.next=head;
             //頭結點前趨指向新節點
             head.prev=node;
             //頭結點重新賦值
             head=node;
         }
     }
  • 尾插入法:尾結點的後繼指向新節點,新節點的前趨指向尾結點
    /**
     * 尾插入
     * @param element
     */
    public void addTail(T element){
        //新節點
        Node node=new Node(null,element,null);
         // 如果表頭為空直接將新節點作為頭節點
         if (head==null){
             head=node;
         }else{
             //尾結點的後繼指向新節點
             tail.next=node;
             //新節點的前趨指向尾結點
             node.prev=tail;
             //尾結點重新賦值
             tail=node;
         }
         size++;
     }
  • 中間插入法:根據索引插入資料元素,找到插入位置的元素,改變此元素的前趨指向,和前趨元素的後繼指向
    /**
     * 從指定位置插入
     * @param element
     * @param index
     */
     public void add(T element,int index){
         if (index>size||size<0){
             System.out.println("Out of bounds");
             return;
         }
         if (index==0){
             addHead(element);
         }else if (index==size){
             addTail(element);
         }else{
             //插入位置的節點
             Node indexNode=getNodeByIndex(index);
             //插入位置前趨節點
             Node preNode=indexNode.prev;
             //新節點,設定新節點的前趨和後繼
             Node node=new Node(preNode,element,indexNode);
             //插入位置節點前趨指向新節點
             indexNode.prev=node;
             //前節點後繼指向新節點
             preNode.next=node;
         }
     }

3.3.5、刪除元素

刪除元素,只需要找到被刪除的元素,改變前趨節點的後繼,後繼節點的前趨

    /**
     * 刪除元素
     * @param index
     */
    public void remove(int index){
         if (index>size||size<0){
             System.out.println("Out of bounds");
             return;
         }
         //被刪除的節點
         Node node=getNodeByIndex(index);
         //前趨
         Node preNode=node.prev;
         //後繼
         Node nextNode=node.next;
         //尾結點
         if (nextNode==null){
             //前趨節點後繼置為null
             preNode.next=null;
             //尾結點重新賦值
             tail=preNode;
         }else if(node.prev==null){  //頭節點
             //後繼節點前趨置為null
             nextNode.prev=null;
             //頭結點重新賦值
             head=nextNode;
         }else{
             //前趨節點的後繼指向後繼節點
             preNode.next=nextNode;
             //後繼節點的前趨指向前趨節點
             nextNode.prev=preNode;
         }
     }

雙向迴圈連結串列

雙向迴圈連結串列的各種演算法與雙向連結串列的演算法大同小異, 其區別與單鏈表和單向迴圈連結串列的區別一樣, 就是判斷末尾結點的條件不同。

  • 雙向連結串列的末尾結點後繼指標域為空, 而雙向迴圈連結串列的末尾結點的後繼指標域指向第一個結點;
  • 而反向査找時, 雙向連結串列的頭結點前趨指標域為空, 而雙向迴圈連結串列的頭結點的前趨指標域指向最後一個結點。

3.3、LinkedList

在Java的集合中,LinkedList是基於雙向連結串列(jdk1.8以前是雙向迴圈連結串列)實現的。

具體原始碼分析可檢視:LinkedList原始碼閱讀筆記


4、總結




本文為學習筆記類部落格,主要資料來源如下!

參考:

【1】:鄧俊輝 編著. 《資料結構與演算法》
【2】:王世民 等編著 . 《資料結構與演算法分析》
【3】: Michael T. Goodrich 等編著.《Data-Structures-and-Algorithms-in-Java-6th-Edition》
【4】:嚴蔚敏、吳偉民 編著 . 《資料結構》
【5】:程傑 編著 . 《大話資料結構》
【6】:資料結構知否知否系列之 — 線性表的順序與鏈式儲存篇
【7】:線性表及其演算法(java實現)
【8】:資料結構與演算法Java版——單鏈表的實現
【9】:資料結構與演算法——單鏈表
【10】:《我的第一本演算法書》
【11】:看動畫輕鬆理解「連結串列」實現「LRU快取淘汰演算法」
【12】:java實現雙向連結串列
【13】:雙向連結串列的實現(Java)
【14】:雙向連結串列和雙向迴圈連結串列