結構與算法(8)-----線性表



    我們都知道數組作為數據存儲結構有一定的缺陷在無序數組中,查找性能差,在有序數組中,插入效率低,並且無論是有序還是無序數組都存在刪除效率低的問題,並且數組在創建后,其大小是固定了,設置的過大會造成內存的浪費,過小又不能滿足數據量的存儲,所以其擴展性也不行。

1、線性表概念

      java數據結構一書對線性表的定義: 

 線性表是由n(n>=0)相同類型的數據元素 a0,a1,…,an-1 組成的有限序列,在數學中記作(a0,a1,…,an-1)
其中ai的數據類型可以是基本數據類型(int,float等)、字符或類。 n 代表線性表的元素個數,也稱其為長度(Length)。
n=0,則為空表
n>0,則 ai(0 < i < n-1)有且僅有一個前驅(Predecessor)元素ai-1 和 一個后繼(Successor)元素ai+1,

a0第一個元素)沒有前驅元素,ai(最后一個元素)沒有后繼元素。

       線性表根據元素在空間上的排列又分為順序存儲鏈式存儲

1.1、線性表順序存儲結構:

      順序存儲結構底層是利用數組來實現的,而數組可以存儲具有相同數據類型的元素集合,如int,float或者自定義類型等,當我們創建一個數組時,計算機操作系統會為該數組分配一塊連續的內存塊這也就意味着數組中的每個存儲單元的地址都是連續的,因此只要知道了數組的起始內存地址就可以通過簡單的乘法和加法計算出數組中第n-1個存儲單元的內存地址,就如下圖所示:

 

    通過上圖可以發現為了訪問一個數組元素,該元素的內存地址需要計算其距離數組基地址(local(a0))偏移量,即用一個乘法計算偏移量然后加上基地址,就可以獲得數組中某個元素的內存地址。其中 c 代表的是元素數據類型的存儲空間大小,而序號則為數組的下標索引。整個過程需要一次乘法和一次加法運算,因為這兩個操作的執行時間是常數時間,所以我們可以認為數組訪問操作能再常數時間內完成,即時間復雜度為O(1),這種存取任何一個元素的時間復雜度為O(1)的數據結構稱之為隨機存取結構。而順序表的存儲原理正如上圖所示,因此順序表的定義如下(引用):

     線性表順序存儲結構稱之為順序表(Sequential List),它使用一維數組依次存放從a0到an-1的數據元素 (a0,a1,…,an-1),將ai(0< i <> n-1)存放在數組的第i個元素,使得ai與其前驅ai-1及后繼ai+1的存儲位置相鄰,因此數據元素在內存的物理存儲次序反映了線性表數據元素之間的邏輯次序。

1.2、線性表順序存儲的實現與分析:

順序表接口

// 順序表頂級接口
public interface ISeqList<T> {

    boolean isEmpty();  //判斷鏈表是否為空

    int length();  //鏈表長度

    T get(int index);  // 獲取元素

    T set(int index, T data);  //設置某個元素的值

    boolean add(int index, T data);  //根據index添加元素

    boolean add(T data);  //添加元素

    T remove(int index);  //根據index移除元素

    boolean remove(T data);  //根據data移除元素

    boolean removeAll(T data);  //根據data移除元素

    void clear();  //清空鏈表

    boolean contains(T data);  //是否包含data元素

    int indexOf(T data);  //根據值查詢下標

    int lastIndexOf(T data);  //根據data值查詢最后一個出現在順序表中的下標

    String toString();  //輸出格式
    
}
View Code

順序表實現

public class SeqList<T> implements ISeqList<T> {

    private Object[] table; // 數組聲明,用於存儲元素
    private int length; // 順序表的大小

    public SeqList(int capacity) {
        // 申請數組存儲空間,元素初始化為null
        this.table = new Object[Math.abs(capacity)];
        this.length = 0;
    }

    /**
     * 默認大小為64
     */
    public SeqList() {
        this(64);
    }

    /**
     * 傳入一個數組初始化順序表
     * 
     * @param array
     */
    public SeqList(T[] array) {

        if (array == null) {
            throw new NullPointerException("array can\'t be empty!");
        }
        // 創建對應容量的數組
        this.table = new Object[array.length];

        for (int i = 0; i < array.length; i++) {
            this.table[i] = array[i];
        }

        this.length = array.length;

    }

    /**
     * 判斷順序表是否為空
     * 
     * @return
     */
    @Override
    public boolean isEmpty() {
        return this.length == 0;
    }

    /**
     * 計算順序表的大小
     * 
     * @return
     */
    @Override
    public int length() {
        return this.length;
    }

    /**
     * 獲取元素
     * 
     * @param index
     * @return
     */
    @Override
    public T get(int index) {
        if (index >= 0 && index < this.length)
            return (T) this.table[index];
        return null;
    }

    /**
     * 設置某個結點的的值
     * 
     * @param index
     * @param data
     * @return
     */
    @Override
    public T set(int index, T data) {
        if (index >= 0 && index < this.length && data != null) {
            T old = (T) this.table[index];
            this.table[index] = data;
            return old;
        }
        return null;
    }

    /**
     * 根據index插入元素
     * 
     * @param index
     *            插入位置的下標,0作為起始值
     * @param data
     *            插入的數據
     * @return
     */
    @Override
    public boolean add(int index, T data) {
        if (data == null) {
            return false;
        }
        // 插入下標的容錯判斷,插入在最前面
        if (index < 0) {
            index = 0;
        }
        // 插入下標的容錯判斷,插入在最后面
        if (index > this.length) {
            index = this.length;
        }
        // 判斷內部數組是否已滿
        if (this.length == table.length) {

            // 把原數組賦值給臨時數組
            Object[] temp = this.table;

            // 對原來的數組進行成倍拓容,並把原數組的元素復制到新數組
            this.table = new Object[temp.length * 2];

            // 先把原數組下標從0到index-1(即插入位置的前一個位置)復制到新數組
            for (int i = 0; i < index; i++) {
                this.table[i] = temp[i];
            }
        }

        // 從原數組的最后一個元素開始直到index位置,都往后一個位置
        // 最終騰出來的位置就是新插入元素的位置了
        for (int j = this.length - 1; j >= index; j--) {
            this.table[j + 1] = this.table[j];
        }
        // 插入新值
        this.table[index] = data;
        // 長度加一
        this.length++;
        // 插入成功
        return true;
    }

    /**
     * 在尾部插入元素
     * 
     * @param data
     * @return
     */
    @Override
    public boolean add(T data) {

        return add(this.length, data);
    }

    /**
     * 根據index刪除元素
     * 
     * @param index
     *            需要刪除元素的下標
     * @return
     */
    @Override
    public T remove(int index) {

        if (this.length != 0 && index >= 0 && index < this.length) {

            // 記錄刪除元素的值並返回
            T old = (T) this.table[index];

            // 從被刪除的元素位置開,其后的元素都依次往前移動
            for (int j = index; j < this.length - 1; j++) {
                this.table[j] = this.table[j + 1];
            }
            // 設置數組元素對象為空
            this.table[this.length - 1] = null;
            // 順序表長度減1
            this.length--;
            return old;
        }
        return null;
    }

    /**
     * 根據data刪除某個數據
     * 
     * @param data
     * @return
     */
    @Override
    public boolean remove(T data) {
        if (this.length != 0 && data != null) {
            return this.remove(this.indexOf(data)) != null;
        }
        return false;
    }

    @Override
    public boolean removeAll(T data) {
        boolean done = false;
        if (this.length != 0 && data != null) {
            int i = 0;
            while (i < this.length)
                // 找出數據相同的選項
                if (data.equals(this.table[i])) {
                    this.remove(i);// 根據下標刪除
                    done = true;
                } else {
                    i++;// 繼續查找
                }
        }
        return done;
    }

    /**
     * 清空順序表
     */
    @Override
    public void clear() {
        this.length = 0;
    }

    /**
     * 判斷兩個順序表是否相等
     * 
     * @param obj
     * @return
     */
    public boolean equals(Object obj) {
        // 如果內存地址相當,那么兩個順序肯定相等
        if (this == obj) {
            return true;
        }
        // 判斷是否屬於同種類型對象
        if (obj instanceof SeqList) {
            // 強制轉換成順序表
            SeqList<T> list = (SeqList<T>) obj;
            for (int i = 0; i < this.length(); i++)
                // 比較每個值是否相當
                if (!(this.get(i).equals(list.get(i))))
                    return false;
            return true;
        }
        return false;
    }

    /**
     * 根據數據查詢下標
     * 
     * @param data
     * @return
     */
    @Override
    public int indexOf(T data) {
        if (data != null) {
            for (int i = 0; i < this.length; i++) {
                // 相當則返回下標
                if (this.table[i].equals(data)) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * 根據data查詢最后一個出現在順序表中的下標
     * 
     * @param data
     * @return
     */
    @Override
    public int lastIndexOf(T data) {
        if (data != null) {
            for (int i = this.length - 1; i >= 0; i--) {
                if (data.equals(this.table[i])) {
                    return i;
                }
            }
        }
        return -1;
    }

    /**
     * 查詢是否包含某個數據
     * 
     * @param data
     * @return
     */
    @Override
    public boolean contains(T data) {
        return this.indexOf(data) >= 0;
    }

    @Override
    public String toString() {
        String str = "(";
        if (this.length != 0) {
            for (int i = 0; i < this.length - 1; i++)
                str += this.table[i].toString() + ", ";
            str += this.table[this.length - 1].toString();
        }
        return str + ") ";
    }
}
View Code

實現代碼中聲明了一個Object數組,初始化數組大小默認為64,存儲的元素類型為泛型T,length則為順序表的長度,部分方法實現比較簡單,這里不過多分析,我們主要分析get(int index)、set(int index, T data)、add(int index, T data)、remove(int index)、removeAll(T data)、indexof(T data)等方法的實現

  • get(int index) 實現分析 

       從順序表中取值是一種相當簡單高效的操作,這是由於順序表內部采用了數組作為存儲數據容器。因此只需根據傳遞的索引值,直接獲取數組中對應下標的值即可,代碼如下:

public T get(int index){
   if (index>=0 && index<this.length){
       return (T) this.table[index];         
   }
   return null;
}
  • set(int index, T data) 實現分析

       在順序表中替換值也是非常高效和簡單的,只要根據傳遞的索引值index找到需要替換的元素,然后把對應元素值替換成傳遞的data值即可,代碼如下:

public T set(int index, T data){
   if (index>=0 && index<this.length&& data!=null){
         T old = (T)this.table[index];
         this.table[index] = data;
         return old;
     }
     return null;
 }
  • add(int index, T data)實現分析 

       在順序表中執行插入操作時,如果其內部數組的容量尚未達到最大值時,可以歸結為兩種情況,一種是在頭部插入或者中間插入,這種情況下需要移動數組中的數據元素,效率較低,另一種是在尾部插入,無需移動數組中的元素,效率高。但是當順序表內部數組的容量已達到最大值無法插入時,則需要申請另一個更大容量的數組並復制全部數組元素到新的數組,這樣的時間和空間開銷較大,於是效率也就不盡人意。因此在插入頻繁的場景下,順序表的插入操作並不是理想的選擇。下面是順序表在數組容量充足下頭部或中間插入操作示意圖(尾部插入比較簡單就不演示了):

      

       順序表在數組容量不充足的情況下頭部或中間插入操作示意圖: 

       

      理解了以上幾種順序表的插入操作后,我們在看一下代碼實現:

      

/**
   * 根據index插入元素
   * @param index 插入位置的下標,0作為起始值
   * @param data 插入的數據
   * @return
   */
  public boolean add(int index, T data){                                        
     if (data==null){
         return false;
     }   
     if (index<0){  //插入下標的容錯判斷,插入在最前面                            
        index=0;
     }
     
     if (index>this.length) {//插入下標的容錯判斷,插入在最后面
         index = this.length;
     }
     
     if (this.length==table.length) {//判斷內部數組是否已滿            
      
         Object[] temp = this.table;  //把原數組賦值給臨時數組

         //對原來的數組進行成倍拓容,並把原數組的元素復制到新數組
         this.table = new Object[temp.length*2];   

         //先把原數組下標從0到index-1(即插入位置的前一個位置)復制到新數組
         for (int i=0; i<index; i++) {
             this.table[i] = temp[i];
         }
     }

     //從原數組的最后一個元素開始直到index位置,都往后一個位置
     // 最終騰出來的位置就是新插入元素的位置了
     for (int j=this.length-1; j>=index; j--) {
         this.table[j + 1] = this.table[j];
     }
     
     this.table[index] = data;  //插入新值
     
     this.length++;  //長度加一
   
     return true;    //插入成功
  }
  • remove(int index) 實現分析 

       順序表的刪除和插入操作情況是類似的,如果是在中間或者頭部刪除順序表中的元素那么在刪除位置之后的元素都必須依次往前移動,效率較低如果是在順序表的尾部直接刪除的話則無需移動元素,此情況下刪除效率高。如下圖所示在順序表中刪除元素ai時,ai之后的元素都依次往前移動:

     

     刪除操作的代碼實現如下:

 /**
  * 根據index刪除元素
  * @param index 需要刪除元素的下標
  * @return
  */
 public T remove(int index){
     if (this.length!=0 && index>=0 && index<this.length){
         //記錄刪除元素的值並返回
         T old = (T)this.table[index];

         //從被刪除的元素位置開,其后的元素都依次往前移動
         for (int j=index; j<this.length-1; j++) {
             this.table[j] = this.table[j + 1];
         }
         //設置數組元素對象為空
         this.table[this.length-1]=null;
         //順序表長度減1
         this.length--;
         return old;                         
     }
     return null;
 }  

 

  • removeAll(T data) 實現分析 

 

       在順序表中根據數據data找到需要刪除的數據元素和前面分析的根據index刪除順序表中的數據元素是一樣的道理,因此我們只要通過比較找到與data相等的數據元素並獲取其下標,然后調用前面實現的remove(int index)方法來移除即可。代碼實現如下:

@Override
public boolean removeAll(T data) {
    boolean done=false;
    if (this.length!=0 && data!=null){
        int i=0;
        while (i<this.length)
            //找出數據相同的選項
            if (data.equals(this.table[i])){
                this.remove(i);  //根據下標刪除
                done = true;
            } else{
                i++;  //繼續查找
            }
    }
    return done;
}
  • indexOf(T data) 實現分析 

      要根據data在順序表中查找第一個出現的數據元素的下標,只需要通過對比數據項是否相等,相等則返回下標,不相等則返回-1,indexOf和lastIndexOf方法實現如下:

/**
 * 根據數據查詢下標
 * @param data
 * @return
 */
@Override
public int indexOf(T data){
    if (data!=null)
        for (int i=0; i<this.length; i++) {
            //相當則返回下標
            if (this.table[i].equals(data))
                return i;
        }
    return -1;
}

/**
 * 根據data查詢最后一個出現在順序表中的下標
 * @param data
 * @return
 */
@Override
public int lastIndexOf(T data){
    if (data!=null)
        for (int i=this.length-1; i>=0; i--)
            if (data.equals(this.table[i]))
                return i;
    return -1;
}

1.3、線性表順序存儲的效率分析:

         通過上述分析,我們對順序表的實現已有了比較清晰的認識,接下來主要針對獲取、插入、修改、刪除等主要操作看一下順序表的執行效率問題。前面說過,由於順序表內部采用了數組作為存儲容器而數組又是隨機存取結構的容器,也就是說在創建數組時操作系統給數組分配的是一塊連續的內存空間,數組中每個存儲單元的地址都是連續的,所以在知道數組基地址后可以通過一個簡單的乘法和加法運算即可計算出其他存儲單元的內存地址(實際上計算機內部也就是這么做的),這兩個運算的執行時間是常數時間,因此可以認為數組的訪問操作能在常數時間內完成,即順序表的訪問操作(獲取修改元素值)的時間復雜為O(1)。 
  對於在順序表中插入刪除元素,從效率上則顯得不太理想了,由於插入或者刪除操作是基於位置的需要移動數組中的其他元素,所以順序表的插入或刪除操作,所花費的時間主要是用於移動元素,如在順序表頭部插入或刪除時,效率就顯得相當糟糕了。若在最前插入或刪除,則需要移動n(這里假設長度為n)個元素;若在最后插入或刪除,則需要移動的元素為0。這里我們假設插入或刪除值為第i(0<i<=n)個元素,其概率為pipi,則插入或刪除一個元素的平均移動次數求和為:

            p1(n1)+p2(n2)+...+pi(ni)+...+pn11+pn0=i=1n(pi(ni))

如果在各個位置插入元素的概率相同即 pi=1n+1pi=1n+1 (n+1個插入位置任意選擇一個的概率)則有:

            ∑i=1n(pi(ni))=1n+1i=1n(ni)=1n+1n(n+1)2=n2=O(n)

    也就是說,在等概率的情況下,插入刪除一個順序表的元素平均需要移動順序表元素總量的一半,其時間復雜度是O(n)。

    當然如果在插入時,內部數組容量不足時,也會造成其他開銷,如復制元素的時間開銷和新建數組的空間開銷。 

 因此總得來說順序表有以下優缺點:

    • 優點

      • 使用數組作為內部容器簡單且易用

      • 在訪問元素方面效率高

      • 數組具有內存空間局部性的特點,由於本身定義為連續的內存塊,所以任何元素與其相鄰的元素在物理地址上也是相鄰的。

    • 缺點

      • 內部數組大小是靜態的,在使用前必須指定大小,如果遇到容量不足時,需動態拓展內部數組的大小,會造成額外的時間和空間開銷

      • 在內部創建數組時提供的是一塊連續的空間塊,當規模較大時可能會無法分配數組所需要的內存空間

      • 順序表的插入和刪除是基於位置的操作,可能需要移動內部數組中的其他元素,這樣會造成較大的時間開銷,時間復雜度為O(n)

 

2、線性表的鏈式存儲結構的實現(鏈表-- Linked List)

2.1、鏈表的鏈式存儲結構設計原理概要

      通過前面對線性順序表的分析,我們知道當創建順序表時必須分配一塊連續的內存存儲空間,而當順序表內部數組的容量不足時,則必須創建一個新的數組,然后把原數組的的元素復制到新的數組中,這將浪費大量的時間。而在插入或刪除元素時,可能需要移動數組中的元素,這也將消耗一定的時間。

      鑒於線性表的順序存儲的種種優劣,線性表的鏈式存儲結構,鏈表就出場了,鏈表在初始化時僅需要分配一個元素的存儲空間並且插入和刪除新的元素也相當便捷同時鏈表在內存分配上可以是不連續的內存也不需要做任何內存復制和重新分配的操作,由此看來順序表的缺點在鏈表中都變成了優勢,實際上也是如此,當然鏈表也有缺點,主要是在訪問單個元素的時間開銷上,這個問題留着后面分析,我們先通過一張圖來初步認識一下鏈表的存儲結構,如下:

     從圖可以看出,線性鏈表的存儲結構是若干地址分散的存儲單元存放數據元素邏輯上相鄰的數據元素在物理位置上不一定相鄰,因此每個存儲單元中都會有一個地址指向域,這個地址指向域指明其后繼元素的位置。

    在鏈表中存儲數據的單元稱為結點(Node),從圖中可以看出一個結點至少包含了數據域地址域其中數據域用於存儲數據,而地址域用於存儲前驅或后繼元素的地址。前面我們說過鏈表的插入和刪除都相當便捷,這是由於鏈表中的結點的存儲空間是在插入或者刪除過程中動態申請和釋放的,不需要預先給單鏈表分配存儲空間的,從而避免了順序表因存儲空間不足需要擴充空間和復制元素的過程,提高了運行效率和存儲空間的利用率。

2.2、單鏈表的設計與實現

      單鏈表是鏈表中結構最簡單的。一個單鏈表的節點(Node)分為兩個部分,第1個部分保存數據信息,另1部分存儲地址信息。最后一個節點存儲地址的部分指向空值。

   單向鏈表只可向一個方向遍歷,一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。而插入一個節點,對於單向鏈表,我們只提供在鏈表頭插入,只需要將當前插入的節點設置為頭節點,next指向原頭節點即可。刪除一個節點,我們將該節點的上一個節點的next指向該節點的下一個節點。

   在表頭增加節點:

 刪除節點:

 

鏈表節點實體:

/**
 * 節點
 */
public class Node<T> {
    
    public T data; //數據域
    
    public Node<T> next; //地址域

    public Node(T data){
        this.data=data;
    }

    public Node(T data,Node<T> next){
        this.data=data;
        this.next=next;
    }
}
View Code

鏈表接口定義:

/**
 * 鏈表
 * @param <T>
 */
public interface ILinkedList<T> {

    boolean isEmpty();  //判斷鏈表是否為空

    int length();  //鏈表長度

    T get(int index);  //獲取元素

    T set(int index, T data); //設置某個結點的的值

    boolean add(int index, T data);  //根據index添加結點

    boolean add(T data);  //添加結點

    T remove(int index); //根據index移除結點

    boolean removeAll(T data);  //根據data移除結點

    void clear();  //清空鏈表

    boolean contains(T data);  //是否包含data結點

    String toString();  //輸出格式
}
View Code

鏈表接口實現:

/**
 * 單項鏈表的實現,不含獨立頭結點,不含尾部指針
 */
public class SingleILinkedList<T> implements ILinkedList<T> {
    
    protected  Node<T>  head;  // 帶數據頭結點

    public SingleILinkedList(Node<T> head) {
        this.head = head;
    }

    public SingleILinkedList() {

    }

    /**
     * 傳入一個數組,轉換成鏈表
     * 
     * @param array
     */
    public SingleILinkedList(T[] array) {
        this.head = null;
        if (array != null && array.length > 0) {
            this.head = new Node<T>(array[0]);
            Node<T> rear = this.head;
            int i = 1;
            while (i < array.length) {
                rear.next = new Node<T>(array[i++]);
                rear = rear.next;
            }
        }
    }

    /**
     * 通過傳入的鏈表構造新的鏈表
     * 
     * @param list
     */
    public SingleILinkedList(SingleILinkedList<T> list) {
        this.head = null;
        if (list != null && list.head != null) {
            this.head = new Node<>(list.head.data);
            Node<T> p = list.head.next;
            Node<T> rear = this.head;
            while (p != null) {
                rear.next = new Node<T>(p.data);
                rear = rear.next;
                p = p.next;
            }
        }
    }

    /**
     * 判斷鏈表是否為空
     * 
     * @return
     */
    @Override
    public boolean isEmpty() {
        return this.head == null;
    }

    @Override
    public int length() {
        int length = 0;  //標記長度的變量
        Node<T> p = head; //變量p指向頭結點
        while (p != null) {
            length++;
            p = p.next; //后繼結點賦值給p,繼續訪問
        }
        return length;
    }

    /**
     * 根據index索引獲取值
     * 
     * @param index
     *            下標值起始值為0
     * @return
     */
    @Override
    public T get(int index) {

        if (this.head != null && index >= 0) {
            int count = 0;
            Node<T> p = this.head;
            // 找到對應索引的結點
            while (p != null && count < index) {
                p = p.next;
                count++;
            }

            if (p != null) {
                return p.data;
            }
        }
        return null;
    }

    /**
     * 根據索引替換對應結點的data
     * @param index 下標從0開始
     * @param data
     * @return 返回舊值
     */
    @Override
    public T set(int index, T data) {

        if (this.head != null && index >= 0 && data != null) {
            Node<T> pre = this.head;
            int count = 0;
            while (pre != null && count < index) {
                pre = pre.next;
                count++;
            }

            if (pre != null) {
                T oldData = pre.data;
                pre.data = data;// 設置新值
                return oldData;
            }

        }
        return null;
    }

    /**
     * 根據下標添加結點 1.頭部插入 2.中間插入 3.末尾插入
     * 
     * @param index
     *            下標值從0開始
     * @param data
     * @return
     */
    @Override
    public boolean add(int index, T data) {

        if (data == null) {
            return false;
        }
        // 在頭部插入
        if (this.head == null || index <= 1) {
            this.head = new Node<T>(data, this.head);
        } else {
            // 在尾部或中間插入
            int count = 0;
            Node<T> front = this.head;
            while (front.next != null && count < index - 1) {
                front = front.next;
                count++;
            }
            // 尾部添加和中間插入屬於同種情況,畢竟當front為尾部結點時front.next=null
            front.next = new Node<T>(data, front.next);
        }
        return true;
    }

    /**
     * 默認尾部插入
     * 
     * @param data
     * @return
     */
    @Override
    public boolean add(T data) {
        return add(Integer.MAX_VALUE, data);
    }

    /**
     * 根據索引刪除結點
     * 
     * @param index
     * @return
     */
    @Override
    public T remove(int index) {

        T old = null;

        if (this.head != null && index >= 0) {

            // 直接刪除的是頭結點
            if (index == 0) {
                old = this.head.data;
                this.head = this.head.next;
            } else {

                Node<T> front = this.head;
                int count = 0;
                // 查找需要刪除結點的前一個結點
                while (front.next != null && count < index - 1) {
                    front = front.next;
                    count++;
                }

                if (front.next != null) {
                    // 獲取舊值
                    old = front.next.data;
                    // 更改指針指向
                    front.next = front.next.next;
                }
            }
        }
        return old;
    }

    /**
     * 根據data移除所有數據相同的結點
     * 
     * @param data
     * @return
     */
    @Override
    public boolean removeAll(T data) {

        boolean isRemove = false;

        if (this.head != null && data != null) {

            // 如果移除的是頭結點
            if (data.equals(this.head.data)) {
                this.head = this.head.next;
                isRemove = true;
            } else {

                Node<T> front = this.head;
                Node<T> pre = front.next;
                // 查找所有數據相同的結點並刪除
                while (pre != null) {

                    if (data.equals(pre.data)) {
                        // 更改指針指向
                        front.next = pre.next;
                        pre = front.next;
                        isRemove = true;
                    } else {
                        front = pre;
                        pre = pre.next;
                    }
                }
            }
        } else {// data=null || this.head=null
            isRemove = false;
        }
        return isRemove;
    }

    /**
     * 清空鏈表
     */
    @Override
    public void clear() {
        this.head = null;
    }

    /**
     * 判斷是否包含某個值
     * 
     * @param data
     * @return
     */
    @Override
    public boolean contains(T data) {

        if (this.head != null && data != null) {

            Node<T> pre = this.head;
            while (pre != null) {
                if (data.equals(pre.data)) {
                    return true;
                }
                pre = pre.next;
            }
        }
        return false;
    }

    /**
     * 從末尾連接兩個鏈表
     * 
     * @param list
     */
    public void concat(SingleILinkedList<T> list) {
        if (this.head == null)
            this.head = list.head;
        else {
            Node<T> pre = this.head;
            while (pre.next != null)
                pre = pre.next;
            pre.next = list.head;
        }
    }

    @Override
    public String toString() {
        String str = "(";
        Node<T> pre = this.head;
        while (pre != null) {
            str += pre.data;
            pre = pre.next;
            if (pre != null)
                str += ", ";
        }
        return str + ")";
    }

    public static void main(String[] args) {

        String[] letters = { "A", "B", "C", "D", "E", "F" };
        SingleILinkedList<String> list = new SingleILinkedList<>(letters);

        System.out.println("list.get(3)->" + list.get(3)); // D
        System.out.println("list:" + list.toString()); // (A, B, C, D, E, F)

        System.out.println("list.add(4,Y)—>" + list.add(4, "Y")); // true
        System.out.println("list.add(Z)—>" + list.add("Z")); // true
        System.out.println("list:" + list.toString()); // (A, B, C, D, Y, E, F,
                                                        // Z)

        System.out.println("list.contains(Z)->" + list.contains("Z")); // true
        System.out.println("list.set(4,P)-->" + list.set(4, "P")); // Y
        System.out.println("list:" + list.toString()); // (A, B, C, D, P, E, F,
                                                        // Z)

        System.out.println("list.removeAll(Z)->" + list.removeAll("Z")); // true
        System.out.println("list.remove(4)-->" + list.remove(4)); // P
        System.out.println("list:" + list.toString()); // (A, B, C, D, E, F)
    }

}
View Code

 我們來具體解析一下實現類:創建一個單鏈表 SingleILinkedList 並實現ILinkedList接口,覆蓋其所有方法,聲明一個單鏈表的頭結點head,代表鏈表的開始位置,如下:

public class SingleILinkedList<T> implements ILinkedList<T> {
protected Node<T> headNode; //聲明一個頭結點 public SingleILinkedList(Node<T> head) { this.headNode = head; } //其他代碼先省略 ..... }
  •  boolean  isEmpty()實現分析 

需要判斷鏈表是否為空的依據是頭結點head是否為null,當head=null時鏈表即為空鏈表,因此我們只需判斷頭結點是否為空即可,isEmpty方法實現如下:

 //判斷鏈表是否為空
@Override
public boolean isEmpty() {
    return this.head==null;
}
  • int length()實現分析 

 由於單鏈表的結點數就是其長度,因此我們只要遍歷整個鏈表並獲取結點的數量即可獲取到鏈表的長度。遍歷鏈表需要從頭結點HeadNode開始,為了不改變頭結點的存儲單元,聲明變量p指向當前頭結點和局部變量length,然后p從頭結點開始訪問,沿着next地址鏈到達后繼結點,逐個訪問,直到最后一個結點,每經過一個結點length就加一最后length的大小就是鏈表的大小。實現代碼如下:

@Override
public int length() {
   int length=0;      //標記長度的變量
   Node<T> p=head;   //變量p指向頭結點
   while (p!=null){
       length++;
       p=p.next;   //后繼結點賦值給p,繼續訪問
   }
   return length;
}
  • T get(int index)實現分析 

      在單鏈表中獲取某個元素的值是一種比較費時的操作,需要從頭結點開始遍歷直至傳入值index指向的位置,其中需要注意的是index是從0開始計算,也就是說傳遞的index=3時,查找的是鏈表中第4個位置的值。其查詢獲取值的過程如下圖所示:

 

通過上圖和如下代碼,我們就可以很容易理解鏈表中取值操作的整個過程了。

 

/** * 根據index索引獲取值 * @param index 下標值起始值為0 * @return
 */
@Override
public T get(int index) {

    if(this.head!=null&&index>=0){   
        int count=0;
        Node<T> p=this.head;
        //找到對應索引的結點
        while (p!=null&&count<index){
            p=p.next;
            count++;
        }

        if(p!=null){
            return p.data;
        }
    }
    return null;
}
  • add(int index, T data)實現分析 

單鏈表的插入操作分四種情況

情況1、空表插入一個新結點,插語句如下:

head=new  Node<T>(x,null);

情況2、在鏈表的表頭插入一個新結點(即鏈表的開始處),此時表頭head!=null,因此head后繼指針next應該指向新插入結點p,而p的后繼指針應該指向head原來的結點,代碼如下:

//創建新結點
Node<T> p =new Node<T>(x,null);
p.next=head;  //p的后繼指針指向head原來的結點
head=p;  //更新head

以上代碼可以合並為如下代碼:

//創建新結點,其后繼為head原來的結點,head的新指向為新結點
head=new Node<T>(x,head);

執行過程如下圖:

情況3、在鏈表的中間插入一個新結點p,需要先找到給定插入位置的前一個結點,假設該結點為front,然后改變front的后繼指向為新結點p,同時更新新結點p的后繼指向為front原來的后繼結點,即front.next,其執行過程如下圖所示:

代碼實現如下:

//新結點p
Node<T> p =new Node<T>(x,null);
//更新p的后繼指向
p.next=front.next;
//更新front的后繼指向
front.next=p;

以上三句代碼合並為一句簡潔代碼:

front.next=new  Node<T>(x,front.next);

情況4、在鏈表的表尾插入一個新結點(鏈表的結尾)在尾部插入時,同樣需要查找到插入結點P的前一個位置的結點front(假設為front),該結點front為尾部結點,更改尾部結點的next指針指向新結點P,新結點P的后繼指針設置為null,執行過程如下: 

其代碼語句如下:

//front的next指針指向新結點,新結點的next指針設置為null
front.next=new Node<T>(x,null);

到此我們也就可以發現單向鏈表中的中間插入尾部插入其實可以合並為一種情況。最后這里給出該方法整體代碼實現,從代碼實現上來看中間插入和尾部插入確實也視為同種情況處理了。

/** * 根據下標添加結點 * 1.頭部插入 * 2.中間插入 * 3.末尾插入 * @param index 下標值從0開始 * @param data * @return
     */
    @Override
    public boolean add(int index, T data) {

        if (data==null){ 
            return false;
        }
        //在頭部插入
        if (this.head==null||index<=1){
            this.head = new Node<T>(data, this.head);
        }else {
            //在尾部或中間插入
            int count=0;
            Node<T> front=this.head;
            //找到要插入結點位置的前一個結點
            while (front.next!=null&&count<index-1){
                front=front.next;
                count++;
            }
            //尾部添加和中間插入屬於同種情況,畢竟當front為尾部結點時front.next=null
            front.next=new Node<T>(data,front.next);
        }
        return true;
    }
  • T   remove(int index)  刪除結點實現分析 

在單向鏈表中,根據傳遞index位置刪除結點的操作分3種情況,並且刪除后返回被刪除結點的數據: 
a.刪除鏈表頭部(第一個)結點,此時需要刪除頭部head指向的結點,並更新head的結點指向,執行圖示如下:

代碼實現如下:

//頭部刪除,更新head指向
head=head.next;

b.刪除鏈表的中間結點,與添加是同樣的道理,需要先找到要刪除結點r(假設要刪除的結點為r)位置的前一個結點front(假設為front),然后把front.next指向r.next即要刪除結點的下一個結點,執行過程如下: 

代碼語句如下:

Node<T> r =front.next;
//更新結點指針指向
front.next=r.next;
r=null;

c.刪除鏈表的最后一個結點,通過遍歷操作找到最后一個結點r的前一個結點front,並把front.next設置為null,即可。執行過程如下: 

代碼如下:

front.next=null;
r=null;

我們把中間刪除和尾部刪除合並為如下代碼:

Node<T> r =front.next;
//如果不是尾部結點,更新結點front指針指向
if(r=!null){
    front.next=r.next;
    r=null;
}

該方法整體代碼實現如    /**

 * 根據索引刪除結點 * @param index * @return */ @Override public T remove(int index) { T old=null; if (this.head!=null&&index>=0){ //直接刪除的是頭結點
            if(index==0){ old=this.head.data; this.head=this.head.next; }else { Node<T> front = this.head; int count = 0; //查找需要刪除結點的前一個結點
                while (front.next != null && count < index - 1) { front = front.next; count++; } //獲取到要刪除的結點
                Node<T> r = front.next; if ( r!= null) {                      old =r.data; //獲取舊值
front.next=r.next; //更改指針指向
r=null; //釋放
} } }
return old; }

當然還有如下更簡潔的代碼寫法:

@Override
  public T remove(int index) {
      T old=null;
      if (this.head!=null&&index>=0){  
          if(index==0){  //直接刪除的是頭結點
              old=this.head.data;
              this.head=this.head.next;
          }else {
              Node<T> front = this.head;
              int count = 0;
              //查找需要刪除結點的前一個結點
              while (front.next != null && count < index - 1) {
                  front = front.next;
                  count++;
              }

              if ( front.next!= null) {             
                  old =front.next.data;    //獲取舊值
                  front.next=front.next.next;   //更改指針指向
              }
          }
      }
      return old;
  }
  • void clear() 實現分析 

清空鏈表是一件非常簡單的事,只需讓head=null即可;代碼如下:

/** * 清空鏈表 */
@Override
public void clear() {
    this.head=null;
}

到此單鏈表主要的添加、刪除、獲取值、設置替換值、獲取長度等方法已分析完畢~

通過如上我們也發現:使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。


 

2.3、帶特殊頭結點的單鏈表

前面分析的單鏈表是不帶特殊頭結點的 (Node<T>   head=new  Node<T>(data,null);

                                                                                                                   (數據,地址)

所謂的特殊頭結點就是一個沒有值的結點即(Node<T>   head=new  Node<T>(null,null);)

此時空鏈表的情況如下:

       

          正常的單鏈表(頭結點數據區有數據)              特殊的單鏈表(頭結點數據區為null,沒有數據)


 

     那么多了頭結點的單向鏈表有什么好處呢?通過對沒有帶頭結點的單鏈表的分析,我們可以知道,在鏈表插入和刪除時都需要區分操作位,比如插入操作就分頭部插入中間或尾部插入兩種情況(中間或尾部插入視為一種情況對待即可),如果現在有不帶數據的頭結點,那么對於單鏈表的插入和刪除不再區分操作的位置也就是說頭部、中間、尾部插入都可以視為一種情況處理了,這是因為此時頭部插入和頭部刪除無需改變head的指向了,頭部插入如下所示: 

接着再看看在頭部刪除的情況: 

帶頭結點遍歷從head.next開始:

因此無論是插入還是刪除,在有了不帶數據的頭結點后,在插入或者刪除時都無需區分操作位了(不再區分是頭部插入,中間插入還是尾部插入了),

到此我們來小結一下帶頭結點的單鏈表特點:

  • a.  空單鏈表只有一個結點,head.next=null。
  • b.  遍歷的起點為p=head.next。
  • c.  頭部插入和頭部刪除無需改變head的指向。

  同時為了使鏈表在尾部插入時達到更加高效,我們可在鏈表內增加一個尾部指向的結點rear,如果我們是在尾部添加結點,那么此時只要通過尾部結點rear進行直接操作即可,無需從表頭遍歷到表尾,帶尾部結點的單鏈表如下所示: 

從尾部直接插入的代碼實現如下:

/** * 尾部插入 * @param data * @return
 */
@Override
public boolean add(T data) {
    if (data==null){
        throw new NullPointerException("data can\'t be empty!");
    }
    this.rear.next = new Node<T>(data);
    //更新末尾指針的指向
    this.rear = this.rear.next;
    return true;
}

 

 

 

 

 

 

 

 

 

2.4、循環單鏈表

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  鏈表通常由一連串節點組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置的鏈接("links")

  鏈表Linked list)是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,而是在每一個節點里存到下一個節點的指針(Pointer)。

  使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,空間開銷比較大。

 

2、單向鏈表(Single-Linked List

  單鏈表是鏈表中結構最簡單的。一個單鏈表的節點(Node)分為兩個部分,第一個部分(data)保存或者顯示關於節點的信息,另一個部分存儲下一個節點的地址。最后一個節點存儲地址的部分指向空值。

  單向鏈表只可向一個方向遍歷,一般查找一個節點的時候需要從第一個節點開始每次訪問下一個節點,一直訪問到需要的位置。而插入一個節點,對於單向鏈表,我們只提供在鏈表頭插入,只需要將當前插入的節點設置為頭節點,next指向原頭節點即可。刪除一個節點,我們將該節點的上一個節點的next指向該節點的下一個節點。

  

  在表頭增加節點:

  

  刪除節點:

  

  ①、單向鏈表的具體實現

/** * 根據索引刪除結點 * @param index * @return */@Overridepublic T remove(int index) { T old=null; if (this.head!=null&&index>=0){ //直接刪除的是頭結點if(index==0){ old=this.head.data; this.head=this.head.next; }else { Node<T> front = this.head; int count = 0; //查找需要刪除結點的前一個結點while (front.next != null && count < index - 1) { front = front.next; count++; } //獲取到要刪除的結點 Node<T> r = front.next; if ( r!= null) { //獲取舊值 old =r.data; //更改指針指向 front.next=r.next; //釋放 r=null; } } } return old; }


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com