線性表 及Java實現 順序表、鏈表、棧、隊列


數據結構與算法是程序設計的兩大基礎,大型的IT企業面試時也會出數據結構和算法的題目,

它可以說明你是否有良好的邏輯思維,如果你具備良好的邏輯思維,即使技術存在某些缺陷,面試公司也會認為你很有培養價值,至少在一段時間之后,技術可以很快得到提高。同時,它也是軟考的重點,我們需要對這部分的內容進行一下總結。

       我們先看一下數據結構和算法的整體內容。

                                        

 

1、線性表

       概念:

             數據元素的排列方式是線性的。

      分類:

             分類規則是根據上圖中元素的存儲結構來划分的。

                                                                                   

      (1)順序表

          基本思想:元素的存儲空間是連續的。在內存中是以順序存儲,內存划分的區域是連續的。存儲結構如下圖:

                           

          

      (2)鏈表

          基本思想:元素的存儲空間是離散的,單獨的(物理),它們可以通過在邏輯上指針的聯系使得它成為了整體的鏈表。存儲結構如下圖:

                                

 

     

            1.單鏈表

                   

 

            2.循環鏈表

                        ·

            3.雙鏈表(雙向循環表)

                   

                 (圖有點小問題 :最后一個節點的 指針域 也指向頭結點)


           

 

           三者的區別(從上面三個圖我們可以總結出來):

       1、它們都有數據域(data(p))和指針域(next(p)),但是從圖中可以看出雙鏈表有兩個指針域,一個指向它的前節點,一個指向它的后節點。

       2、單鏈表最后一個節點的指針域為空,沒有后繼節點;循環鏈表和雙鏈表最后一個節點的指針域指向頭節點,下一個結點為頭節點,構成循環;

          3、單鏈表和循環鏈表只可向一個方向遍歷;雙鏈表和循環鏈表,首節點和尾節點被連接在一起,可視為“無頭無尾”;雙鏈表可以向兩個方向移動,靈活度更大。

 

    線性表操作:

         理解了順序表和鏈表的基本思想之后,線性表的操作是簡單,並且網上有很多講解插入和刪除結點的博客,在這里我就不過多的介紹了。

 

        順序表和鏈表的對比:

                          

 

          棧和隊列是特殊的線性表,既然特殊就有不同點。

 

2、棧

      基本思想:后進先出(先進后出)棧中元素被處理時,按后進先出的順序進行,棧又叫后進先出表(LIFO)

      舉例:

      日常生活中有很多棧的例子。例如,放在書桌上的一摞書,只能從書頂上拿走一本書,書也只能放在頂上。如下圖所示:

                                                 

 

3、隊列

     基本思想:先進先出即先被接收的元素將先被處理,又叫先進先出表(FIFO)。如下圖所示:

     舉例:

     隊列的例子,生活中更多。比如:買車票排隊,排頭最先買到車票,新來的排的隊尾;進車站時,安檢行李,先進去的最先出來,后進去的后出來。

                                                  

   

分類:

1.順序隊列

          如下圖所示:

                          

      順序隊列的操作,要判斷隊滿和隊空的標志,從圖中我們可以總結得到:

      1.隊空:head = tail

      2.隊滿:tail = m

2.循環隊列

        如下圖所示:

                                  

      循環隊列的操作,要判斷隊空和隊滿的情況,從圖中我們可以總結得到:

         1.隊空:head = tail

       2.隊滿:tail + 1 = head(在隊列中會留一個空着的空間,所以要加1)

 

總結

           線性表真的很簡單。

 

----------------------------------------------------------------------------------------------------

 

 

數據結構中的線性表,對應着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>    
  121.       * @param index<br>  
  122.       * @return<br>   
  123.       */
  124.      public boolean delete(int index){
  125.           validateIndex(index);
  126.           Object[] tem = new Object[capacity];// 用一個臨時數組作為備份
  127.           //開始備份數組
  128.          for (int i = 0; i < current; i++) { 
  129.                 if (i < index) {
  130.                     tem[i] = this.data[i];
  131.                 }else if(i==index){
  132.                     tem[i]=this.data[i+1];
  133.                 }else if(i>index){
  134.                     tem[i]=this.data[i+1];
  135.                 }
  136.             }
  137.           this.data=tem;
  138.           return true;
  139.      }
  140. }  

 

   自己實現線性表之鏈表

         思路: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.      * @return 
  42.      */  
  43.     private Node<E> getNode(int index) {  
  44.         // 先判斷索引正確性  
  45.         if (index > size || index < 0) {  
  46.             throw new RuntimeException("索引值有錯:" + index);  
  47.         }  
  48.         Node<E> tem = new Node<E>();  
  49.         tem = header;  
  50.         int count = 0;  
  51.         while (count != index) {  
  52.             tem = tem.next;  
  53.             count++;  
  54.         }  
  55.         return tem;  
  56.     }  
  57.   
  58.     /** 
  59.      * 根據索引,取得該索引下的數據 
  60.      *  
  61.      * @param index 
  62.      * @return 
  63.      */  
  64.     public E get(int index) {  
  65.         // 先判斷索引正確性  
  66.         if (index >= size || index < 0) {  
  67.             throw new RuntimeException("索引值有錯:" + index);  
  68.         }  
  69.         Node<E> tem = new Node<E>();  
  70.         tem = header;  
  71.         int count = 0;  
  72.         while (count != index) {  
  73.             tem = tem.next;  
  74.             count++;  
  75.         }  
  76.         E e = tem.e;  
  77.         return e;  
  78.     }  
  79.   
  80.     public int size() {  
  81.         return size;  
  82.     }  
  83.   
  84.     /** 
  85.      * 設置第N個結點的值 
  86.      *  
  87.      * @param x 
  88.      * @param e 
  89.      * @return 
  90.      */  
  91.     public boolean set(int index, E e) {  
  92.         // 先判斷索引正確性  
  93.         if (index > size || index < 0) {  
  94.             throw new RuntimeException("索引值有錯:" + index);  
  95.         }  
  96.         Node<E> newNode = new Node<E>(e);  
  97.         // 得到第x個結點  
  98.         Node<E> cNode = getNode(index);  
  99.         cNode.e = e;  
  100.         return true;  
  101.     }  
  102.   
  103.     /** 
  104.      * 用來存放數據的結點型內部類 
  105.      */  
  106.     class Node<e> {  
  107.         private E e;// 結點中存放的數據  
  108.         Node<E> next;// 用來指向該結點的下一個結點 
  109.         
  110. Node() { }  
  111.         Node(E e) {  
  112.             this.e = e;  
  113.         }  
  114.         // 在此結點后加一個結點  
  115.         void addNext(Node<E> node) {  
  116.             next = node;  
  117.         }  
  118.     }  
  119. }  

 

自己實現線性表之棧

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

         允許插入和刪除的一端稱為棧頂(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.要得到某個下標的數據,不能通過下標直接得到,需要遍歷整個鏈表。

 

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

         思路:通過兩個表,各進行大數據量操作(3W)條數據的操作,記錄操作前系統時間,操作后系統時間,得出操作時間

  實現代碼如下

 

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類添加30000個數據所需要的時間  
  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類添加30000個數據所需要的世界  
  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了。撒花!!!


免責聲明!

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



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