線性表分析及Java實現

逸卿發表於2014-05-11

 資料結構中的線性表,對應著Collection中的List介面。

      在本節中,我們將做以下三件事

            第一。我們先來看看線性表的特徵

            第二,自己用JAVA實現List

            第三,對比的線性表、鏈式表效能,以及自己的List效能與JDKList效能對比

 

      線性表特徵: 

            第一,一個特定的線性表,應該是用來存放特定的某一個型別的元素的(元素的“同一性”)

            第二, 除第一個元素外,其他每一個元素有且僅有一個直接前驅;除最後一個元素外,其他每一個元素有且僅有一個             直接後繼(元素的“序偶性”)

            第二, 元素線上性表中的“下標”唯一地確定該元素在表中的相對位置(元素的“索引性”)

       又,一.線性表只是資料的一種邏輯結構,其具體儲存結構可以為順序儲存結構和鏈式儲存結構來完成,對應可以得到順序表和連結串列,

            二.對線性表的入表和出表順序做一定的限定,可以得到特殊的線性表,棧(FILO)和佇列(FIFO)

 

 

    自己實現線性表之順序表

             思路:

                1. 順序表因為採用順序儲存形式,所以內部使用陣列來儲存資料

                2.因為儲存的具體物件型別不一定,所以採用泛型操作

                3.陣列操作優點:1.通過指標快速定位到下表,查詢快速

                               缺點:1.陣列宣告時即需要確定陣列大小。當操作中超過容量時,則需要重新宣告陣列,並且複製當前所有資料

                                        2.當需要在中間進行插入或者刪除時,則需要移動大量元素(size-index個)

  具體實現程式碼如下

Java程式碼  收藏程式碼
  1. /** 
  2.  * 自己用陣列實現的線性表 
  3.  */  
  4. public class ArrayList<E> {  
  5.     Object[] data = null;// 用來儲存此佇列中內容的陣列  
  6.     int current;// 儲存當前為第幾個元素的指標  
  7.     int capacity;// 表示陣列大小的指標  
  8.        
  9.     /** 
  10.      * 如果初始化時,未宣告大小,則預設為10 
  11.      */  
  12.     public ArrayList() {  
  13.         this(10);  
  14.     }  
  15.   
  16.     /** 
  17.      * 初始化線性表,並且宣告儲存內容的陣列大小 
  18.      * @param initalSize 
  19.      */  
  20.     public ArrayList(int initalSize) {  
  21.         if (initalSize < 0) {  
  22.             throw new RuntimeException("陣列大小錯誤:" + initalSize);  
  23.         } else {  
  24.             this.data = new Object[initalSize];  
  25.             this.current = 0;  
  26.             capacity = initalSize;  
  27.         }  
  28.     }  
  29.   
  30.     /** 
  31.      * 新增元素的方法 新增前,先確認是否已經滿了 
  32.      * @param e 
  33.      * @return 
  34.      */  
  35.     public boolean add(E e) {  
  36.         ensureCapacity(current);// 確認容量  
  37.         this.data[current] = e;  
  38.         current++;  
  39.         return true;  
  40.     }  
  41.   
  42.     /** 
  43.      * 確認系統當前容量是否滿足需要,如果滿足,則不執行操作 如果不滿足,增加容量 
  44.      * @param cur 當前個數 
  45.      */  
  46.     private void ensureCapacity(int cur) {  
  47.         if (cur == capacity) {  
  48.             // 如果達到容量極限,增加10的容量,複製當前陣列  
  49.             this.capacity = this.capacity + 10;  
  50.             Object[] newdata = new Object[capacity];  
  51.             for (int i = 0; i < cur; i++) {  
  52.                 newdata[i] = this.data[i];  
  53.             }  
  54.             this.data = newdata;  
  55.         }  
  56.     }  
  57.   
  58.     /** 
  59.      * 得到指定下標的資料 
  60.      * @param index 
  61.      * @return 
  62.      */  
  63.     public E get(int index) {  
  64.         validateIndex(index);  
  65.         return (E) this.data[index];  
  66.     }  
  67.        
  68.    /** 
  69.     * 返回當前佇列大小 
  70.     * @return 
  71.     */  
  72.     public int size() {  
  73.         return this.current;  
  74.     }  
  75.   
  76.     /** 
  77.      * 更改指定下標元素的資料為e 
  78.      * @param index  
  79.      * @param e 
  80.      * @return 
  81.      */  
  82.     public boolean set(int index, E e) {  
  83.         validateIndex(index);  
  84.         this.data[index] = e;  
  85.         return true;  
  86.     }  
  87.      
  88.     /** 
  89.      *  驗證當前下標是否合法,如果不合法,丟擲執行時異常 
  90.      * @param index 下標 
  91.      */  
  92.     private void validateIndex(int index) {  
  93.         if (index < 0 || index > current) {  
  94.             throw new RuntimeException("陣列index錯誤:" + index);  
  95.         }  
  96.     }  
  97.   
  98.     /** 
  99.      * 在指定下標位置處插入資料e 
  100.      * @param index 下標 
  101.      * @param e 需要插入的資料 
  102.      * @return  
  103.      */  
  104.     public boolean insert(int index, E e) {  
  105.         validateIndex(index);  
  106.         Object[] tem = new Object[capacity];// 用一個臨時陣列作為備份  
  107.         //開始備份陣列  
  108.         for (int i = 0; i < current; i++) {  
  109.             if (i < index) {  
  110.                 tem[i] = this.data[i];  
  111.             }else if(i==index){  
  112.                 tem[i]=e;  
  113.             }else if(i>index){  
  114.                 tem[i]=this.data[i-1];  
  115.             }  
  116.         }  
  117.         this.data=tem;  
  118.         return true;  
  119.     }  
  120. <br><br>/**<br>  * 刪除指定下標出的資料<br>    * @param index<br>  * @return<br>   */<br> public boolean delete(int index){<br>       validateIndex(index);<br>       Object[] tem = new Object[capacity];// 用一個臨時陣列作為備份<br>      //開始備份陣列<br>        for (int i = 0; i < current; i++) {<br>          if (i < index) {<br>             tem[i] = this.data[i];<br>          }else if(i==index){<br>             tem[i]=this.data[i+1];<br>          }else if(i>index){<br>               tem[i]=this.data[i+1];<br>          }<br>       }<br>       this.data=tem;<br>      return true;<br>    }<br><br>}  

 

   自己實現線性表之連結串列

         思路:1.連結串列採用鏈式儲存結構,在內部只需要將一個一個結點連結起來。(每個結點中有關於此結點下一個結點的引用)

         連結串列操作優點:1.,因為每個結點記錄下個結點的引用,則在進行插入和刪除操作時,只需要改變對應下標下結點的引用即可

                     缺點:1.要得到某個下標的資料,不能通過下標直接得到,需要遍歷整個連結串列。

 

 

  實現程式碼如下

Java程式碼  收藏程式碼
  1. /** 
  2.  * 自己用鏈式儲存實現的線性表 
  3.  */  
  4. public class LinkedList<E> {  
  5.   
  6.     private Node<E> header = null;// 頭結點  
  7.     int size = 0;// 表示陣列大小的指標  
  8.   
  9.     public LinkedList() {  
  10.         this.header = new Node<E>();  
  11.     }  
  12.   
  13.     public boolean add(E e) {  
  14.         if (size == 0) {  
  15.             header.e = e;  
  16.         } else {  
  17.             // 根據需要新增的內容,封裝為結點  
  18.             Node<E> newNode = new Node<E>(e);  
  19.             // 得到當前最後一個結點  
  20.             Node<E> last = getNode(size-1);  
  21.             // 在最後一個結點後加上新結點  
  22.             last.addNext(newNode);  
  23.         }  
  24.         size++;// 當前大小自增加1  
  25.         return true;  
  26.     }  
  27.   
  28.     public boolean insert(int index, E e) {  
  29.         Node<E> newNode = new Node<E>(e);  
  30.         // 得到第N個結點  
  31.         Node<E> cNode = getNode(index);  
  32.         newNode.next = cNode.next;  
  33.         cNode.next = newNode;  
  34.         size++;  
  35.         return true;  
  36.   
  37.     }  
  38.   
  39.     /** 
  40.      * 遍歷當前連結串列,取得當前索引對應的元素 
  41.      *  
  42.      * @return 
  43.      */  
  44.     private Node<E> getNode(int index) {  
  45.         // 先判斷索引正確性  
  46.         if (index > size || index < 0) {  
  47.             throw new RuntimeException("索引值有錯:" + index);  
  48.         }  
  49.         Node<E> tem = new Node<E>();  
  50.         tem = header;  
  51.         int count = 0;  
  52.         while (count != index) {  
  53.             tem = tem.next;  
  54.             count++;  
  55.         }  
  56.         return tem;  
  57.     }  
  58.   
  59.     /** 
  60.      * 根據索引,取得該索引下的資料 
  61.      *  
  62.      * @param index 
  63.      * @return 
  64.      */  
  65.     public E get(int index) {  
  66.         // 先判斷索引正確性  
  67.         if (index >= size || index < 0) {  
  68.             throw new RuntimeException("索引值有錯:" + index);  
  69.         }  
  70.         Node<E> tem = new Node<E>();  
  71.         tem = header;  
  72.         int count = 0;  
  73.         while (count != index) {  
  74.             tem = tem.next;  
  75.             count++;  
  76.         }  
  77.         E e = tem.e;  
  78.         return e;  
  79.     }  
  80.   
  81.     public int size() {  
  82.         return size;  
  83.     }  
  84.   
  85.     /** 
  86.      * 設定第N個結點的值 
  87.      *  
  88.      * @param x 
  89.      * @param e 
  90.      * @return 
  91.      */  
  92.     public boolean set(int index, E e) {  
  93.         // 先判斷索引正確性  
  94.         if (index > size || index < 0) {  
  95.             throw new RuntimeException("索引值有錯:" + index);  
  96.         }  
  97.         Node<E> newNode = new Node<E>(e);  
  98.         // 得到第x個結點  
  99.         Node<E> cNode = getNode(index);  
  100.         cNode.e = e;  
  101.         return true;  
  102.     }  
  103.   
  104.     /** 
  105.      * 用來存放資料的結點型內部類 
  106.      */  
  107.     class Node<e> {  
  108.         private E e;// 結點中存放的資料  
  109.   
  110.         Node() {  
  111.         }  
  112.   
  113.         Node(E e) {  
  114.             this.e = e;  
  115.         }  
  116.   
  117.         Node<E> next;// 用來指向該結點的下一個結點  
  118.   
  119.         // 在此結點後加一個結點  
  120.         void addNext(Node<E> node) {  
  121.             next = node;  
  122.         }  
  123.     }  
  124.   
  125. }  

 

自己實現線性表之棧

         棧是限定僅允許在表的同一端(通常為“表尾”)進行插入或刪除操作的線性表。

         允許插入和刪除的一端稱為棧頂(top),另一端稱為棧底(base)
         特點:後進先出 (LIFO)或,先進後出(FILO)

 

         因為棧是限定線的線性表,所以,我們可以呼叫前面兩種線性表,只需要對出棧和入棧操作進行設定即可

    具體實現程式碼

Java程式碼  收藏程式碼
  1. /** 
  2.  * 自己用陣列實現的棧 
  3.  */  
  4. public class ArrayStack<E> {  
  5.       private ArrayList<E> list=new ArrayList<E>();//用來儲存資料線性表<br>    private  int size;//表示當前棧元素個數  
  6.       /** 
  7.        * 入棧操作 
  8.        * @param e 
  9.        */  
  10.       public void push(E e){  
  11.           list.add(e);  
  12.           size++;  
  13.       }  
  14.        
  15.       /** 
  16.        * 出棧操作 
  17.        * @return 
  18.        */  
  19.       public E pop(){  
  20.          E e= list.get(size-1);  
  21.          size--;  
  22.          return e;  
  23.       }  
  24.   
  25. }  

 

 至於用連結串列實現棧,則只需要把儲存資料的順序表改成連結串列即可,此處就不給出程式碼了

 

自己實現線性表之佇列

        與棧類似

        佇列是隻允許在表的一端進行插入,而在另一端刪除元素的線性表。

        在佇列中,允許插入的一端叫隊尾(rear),允許刪除的一端稱為隊頭(front)。
        特點:先進先出 (FIFO)、後進後出 (LILO)

 

       同理,我們也可以呼叫前面兩種線性表,只需要對佇列的入隊和出隊方式進行處理即可

 

Java程式碼  收藏程式碼
  1. package cn.javamzd.collection.List;  
  2.   
  3. /** 
  4.  * 用陣列實現的佇列 
  5.  */  
  6. public class ArrayQueue<E> {  
  7.     private ArrayList<E> list = new ArrayList<E>();// 用來儲存資料的佇列  
  8.     private int size;// 表示當前棧元素個數  
  9.   
  10.     /** 
  11.      * 入隊 
  12.      * @param e 
  13.      */  
  14.     public void EnQueue(E e) {  
  15.         list.add(e);  
  16.         size++;  
  17.     }  
  18.   
  19.     /** 
  20.      * 出隊 
  21.      * @return 
  22.      */  
  23.     public E DeQueue() {  
  24.         if (size > 0) {  
  25.             E e = list.get(0);  
  26.             list.delete(0);  
  27.             return e;  
  28.         }else{  
  29.             throw new RuntimeException("已經到達佇列頂部");  
  30.         }  
  31.     }  
  32. }  
 

 

       
對比線性表和鏈式表
         前面已經說過順序表和鏈式表各自的特點,這裡在重申一遍

         陣列操作優點:1.通過指標快速定位到下標,查詢快速

                     缺點:1.陣列宣告時即需要確定陣列大小。當操作中超過容量時,則需要重新宣告陣列,並且複製當前所有資料

                              2.當需要在中間進行插入或者刪除時,則需要移動大量元素(size-index個)    

 

 

         連結串列操作優點:1.,因為每個結點記錄下個結點的引用,則在進行插入和刪除操作時,只需要改變對應下標下結點的引用即可

                     缺點:1.要得到某個下標的資料,不能通過下標直接得到,需要遍歷整個連結串列。

 

         現在,我們通過進行增刪改查操作來感受一次其效率的差異

         思路:通過兩個表,各進行大資料量操作(2W)條資料的操作,記錄操作前系統時間,操作後系統時間,得出操作時間

  實現程式碼如下

 

Java程式碼  收藏程式碼
  1. package cn.javamzd.collection.List;  
  2.   
  3. public class Test {  
  4.   
  5.     /** 
  6.      * @param args 
  7.      */  
  8.     public static void main(String[] args) {  
  9.         //測試自己實現的ArrayList類和Linkedlist類新增20000個資料所需要的時間  
  10.         ArrayList<String> al = new ArrayList<String>();  
  11.         LinkedList<String> ll = new LinkedList<String>();  
  12.         Long aBeginTime=System.currentTimeMillis();//記錄BeginTime  
  13.         for(int i=0;i<30000;i++){  
  14.             al.add("now"+i);  
  15.         }  
  16.         Long aEndTime=System.currentTimeMillis();//記錄EndTime  
  17.         System.out.println("arrylist  add time--->"+(aEndTime-aBeginTime));  
  18.         Long lBeginTime=System.currentTimeMillis();//記錄BeginTime  
  19.         for(int i=0;i<30000;i++){  
  20.             ll.add("now"+i);  
  21.         }  
  22.         Long lEndTime=System.currentTimeMillis();//記錄EndTime  
  23.         System.out.println("linkedList add time---->"+(lEndTime-lBeginTime));  
  24.           
  25.         //測試JDK提供的ArrayList類和LinkedList類新增20000個資料所需要的世界  
  26.         java.util.ArrayList<String> sal=new java.util.ArrayList<String>();  
  27.         java.util.LinkedList<String> sll=new java.util.LinkedList<String>();  
  28.         Long saBeginTime=System.currentTimeMillis();//記錄BeginTime  
  29.         for(int i=0;i<30000;i++){  
  30.             sal.add("now"+i);  
  31.         }  
  32.         Long saEndTime=System.currentTimeMillis();//記錄EndTime  
  33.         System.out.println("JDK arrylist  add time--->"+(saEndTime-saBeginTime));  
  34.         Long slBeginTime=System.currentTimeMillis();//記錄BeginTime  
  35.         for(int i=0;i<30000;i++){  
  36.             sll.add("now"+i);  
  37.         }  
  38.         Long slEndTime=System.currentTimeMillis();//記錄EndTime  
  39.         System.out.println("JDK linkedList add time---->"+(slEndTime-slBeginTime));  
  40.     }  
  41.   
  42. }  

  得到測試結果如下: 

arrylist add time--->446
linkedList add time---->9767
JDK arrylist add time--->13
JDK linkedList add time---->12

        由以上資料,我們可知:

           1.JDK中的ArrayList何LinkedList在新增資料時的效能,其實幾乎是沒有差異的

           2.我們自己寫的List的效能和JDK提供的List的效能還是存在巨大差異的

           3.我們使用連結串列新增操作,花費的時間是巨大的,比ArrayList都大幾十倍

 

      第三條顯然是跟我們最初的設計不相符的,按照我們最初的設想,連結串列的新增應該比順序表更省時

      檢視我們寫的原始碼,可以發現:

      我們每次新增一個資料時,都需要遍歷整個表,得到表尾,再在表尾新增,這是很不科學的

 

      現改進如下:設立一個Node<E>類的成員變數end來指示表尾,這樣每次新增時,就不需要再重新遍歷得到表尾

      改進後add()方法如下

 

Java程式碼  收藏程式碼
  1. public boolean add(E e) {  
  2.     if (size == 0) {  
  3.         header.e = e;  
  4.     } else {  
  5.         // 根據需要新增的內容,封裝為結點  
  6.         Node<E> newNode = new Node<E>(e);  
  7.         //在表尾新增元素  
  8.         last.addNext(newNode);  
  9.                                         //將表尾指向當前最後一個元素  
  10.         last = newNode;  
  11.     }  
  12.     size++;// 當前大小自增加1  
  13.     return true;  
  14. }  

 

       ArrayList新增的效率和JDK中對比起來也太低

       分析原因為:

       每次擴大容量時,擴大量太小,需要進行的複製操作太多

       現在改進如下:

       每次擴大,則擴大容量為當前的三倍,此改進僅需要更改ensureCapacity()方法中的一行程式碼,此處就不列出了。

 

改進後,再次執行新增元素測試程式碼,結果如下:

 

arrylist add time--->16
linkedList add time---->8
JDK arrylist add time--->7
JDK linkedList add time---->7

 雖然還有改進的空間,但是顯然,我們的效果已經大幅度改進了,而且也比較接近JDK了

 

 

接下來測試插入操作的效率

  我們只需要將測試程式碼中的新增方法(add())改成插入方法(insert(int index,E e)),為了使插入次數儘可能多,我們把index都設定為0

測試結果如下:

 

arrylist inset time--->17
linkedList inset time---->13
JDK arrylist inset time--->503
JDK linkedList inset time---->11

多次測試,發現我們寫的ArrayList在插入方法的效率都已經超過JDK了,而且也接近LinkedLst了。撒花!!!

 

接下來測試刪除、得到下標等等操作就不一一列出來了(只需要改變每次呼叫的方法即可)

 

 

 

 

恩,本來想今晚把所有的集合框架實現都寫一下的

但是不知不覺這都又2點了

明早還得去藍傑上課

果斷先睡吧

敬請大家期待我明日大作------------靜態/動態查詢表的實現,動態查詢表查詢/加入演算法的JAVA實現,Hash表的實現

 

good night

相關文章