1. 程式人生 > >表、棧和隊列(1)

表、棧和隊列(1)

移除 clas for循環 set 基本 棧和隊列 返回 異常 -m

目錄

  • 1、抽象數據類型
  • 2、表ADT
    • 2.1、表的簡單數組實現
    • 2.2、簡單鏈表
  • 3、Java Collections API中的表
    • 3.1、Collection接口
    • 3.2、Iterator接口
    • 3.3、List接口、ArrayList類和LinkedList類
    • 3.4、例子:remove方法對LinkedList類的應用
    • 3.5、關於ListInterator

本系列討論最簡單的和最基本的三種數據類型:表、棧和隊列,實際上,每一個有意義的程序都將顯式的用到一種或多種這樣的數據結構,而棧在程序中總要被間接的使用到。
本系列的重點:

  • 介紹抽象數據類型的概念。
  • 闡述如何有效的執行表的操作。
  • 介紹棧ADT及其在實現遞歸方面的應用。
  • 介紹隊列ADT及其在操作系統和算法設計中的應用。
  • 在這一系列中會實現ArrayList 和LinkedList 的代碼。

1、抽象數據類型

抽象數據類型(abstract data type,ADT)是帶有一組操作的一些對象的集合。諸如表、集合、圖以及他們各自操作一起形成的這些對象都可以被看成抽象數據類型,就像整數、實數、布爾數都是數據類型一樣,他們各自都有與之相關的操作,而抽象數據類型也是如此。 對於集合ADT,可以有添加(add)、刪除(remove)以及包含(cantain)這樣一些操作,但是對於每一種ADT並不存在什麽法則來告訴我們必須要有哪些操作,這一版都取決去程序的設計者。

2、表ADT

我們將處理形如A0,A1,A2,...,AN-1的一般的表。我們說這個表的大小是N,如果N=0,那我們稱這個特殊的表為空表(empty list)。
對於除空表之外的任何表我們說Ai-1的後繼是Ai,Ai的前驅是Ai-1,表中的第一個元素是A0,最後一個元素是AN-1,元素Ai在表中的位置是i+1。與這些定義相關的是要在表ADT上操作的集合,printList和makeEmpty是常用的操作,find返回某一項首次出現的位置,insert和remove一般是從表的某個位置插入和刪除某個元素,findKth則返回(作為參數而被指定的)某個位置上的元素。如果34,23,11,45,34是一個表則find(11)會返回2,insert(x,2)則可以把表變成34,23,x,11,45,34,remove(11)則又會將表變成34,23,x,45,34。
當然一個方法怎麽樣才是恰當的完全由程序員自己來確定。比如find(1)會返回什麽?或者我們可以添加一下操作比如next,previous他們取一個位置作為參數返回其後繼元和前驅元。

2.1、表的簡單數組實現

對於表的這些所有的操作都可以用數組來實現。雖然數組的長度是固定的,但是在必要的時候我們可以對數組進行擴容。


        int[] arr = new int[10];
            ...        
        //創建一個新數組,長度是原數組長度乘以2的1次方也就是兩倍
        int[] newArr = new int[arr.length<<1];
        //把老數組的復制到新數組
        newArr = Arrays.copyOf(arr, arr.length);

數組的實現是printList以線性時間被執行,而findKth操作花費常數時間,這是我們能預期的,但是插入和刪除卻又昂貴的開銷。最壞的情況在位置0處(表的最前端)插入首先要將整個數組後移一個位置以空出空間來,而刪除一個第一個元素則需要將表中的所有的元素前移一個位置,存在許多情況,表是通過高端進行插入操作建立的,其後只會發生對數組的訪問(只有findKth操作),在這種情況之下數組是表的一種恰當實現,然而,如果發生對表的一下插入和刪除操作,特別是對表的低端操作,那麽數組就不是一個很好的選擇,這個時候我們就需要一個新的數據結構鏈表(linked list)

2.2、簡單鏈表

為了避免插入和刪除的線性開銷,我們需要保證表可以不連續存儲,否則表的每個部門的移動都可能造成整體的移動。


技術分享圖片


鏈表由一系列節點組成,這些節點不必再內存中相連,每一個節點都含有表元素和到後繼元的節點的鏈(link),我們稱之為next鏈,最後一個next鏈應用null。為了執行printList或者find(x),必須要從表的第一個節點開始然後用一些後繼的next鏈遍歷改表即可。findKth操作不如數組實現時效率高,findKth花費O(i)的時間並以這種明顯的方式遍歷鏈表完成的,remove方法可以通過修改一個next引用來實現,insert方法需要使用new操作從系統中取得一個新節點,此後執行兩次引用的調整。


技術分享圖片


這樣我們可以看到添加或者刪除數據就不需要移動每一個數據了,但是如果要刪除指定元素Ai,我們就需要把Ai-1的next引用指向Ai+1,我們都知道上面的鏈表我們可以通過Ai拿到Ai+1,但是我們不能通過Ai找到Ai-1,因為我們之前通過一個元素找他的後繼元,但是找不到他的前驅元,所以我們就有了雙向鏈表


技術分享圖片


雙向鏈表每一個數據元不但有當前數據和指向後繼元素的next鏈,而且還有一個指向前驅元的previou鏈。這樣我們可以通過一個數據元既可以找到他的後繼又可以找到他的前驅。

3、Java Collections API中的表

3.1、Collection接口

??Collection API位於java.util包中,集合的概念在Collection接口中得到抽象,它存儲一組類型相同的對象

public interface Collection<E> extends Iterable<E> {
        int size();//返回集合中的項數
        boolean isEmpty();//當且僅當集合的大小為0是返回true
        void clear();//
        boolean contains(Object o);//當集合包含o時返回true,但是它不規定怎麽樣才算包含, 具體包含的定義可以由他的實現類自己定義
        boolean add(Object o);//添加數據成功返回true
        boolean remove(Object o);//刪除數據成功返回true
        Iterator<E> iterator();
}

上述方法都是該接口最重要的部分,Collection接口擴展了Iterable接口,實現Iterable接口的那些類可以使用增強for循環,該循環施於這些類之上以觀察他們所有的項(2018年8月22日23:59:23)
因為Collection接口繼承了Iterable接口,所以下面的代碼可以打印任意集合中的所有的項

    public static <T> void printColl(Collection<T> coll){
        for(T item:coll){
            System.out.println(item);
        }
    }

3.2、Iterator接口

實現Interator接口必須提供一個稱為iterator方法,這個方法返回一個Interator類型的對象

    public interface Iterator<E>{
        boolean hasNext();//有沒有下一個
        E next();//每次調用獲取下一個元素,第一次調用給出第1項,第二次調用給出第2項
        void remove();//移除next()方法返回的對象,以後我們就不能調用這個方法,知道下一次調用next才能使用這個方法
    }

所以刪除用增強for遍歷的方法其實是

    public static<T> void printColl(Collection<T> coll){
        Iterator<T> itr = coll.iterator();
        while(itr.hasNext()){
            T item = itr.next();
            System.out.println(item);
        }

    }

Collection接口也包含一個remove方法,Iteraor的remove方法的主要優點在於,Collection的remove方法必須先找出要被刪除的項。如果知道要刪除項的位置,那麽刪除它的開銷就 可能小的多,這個後面有機會寫一個例子。當直接使用Interator(而不是用增強for間接使用)時候,如果正在被叠代的集合在結構上進行改變比如add,remove,clear方法時,那麽叠代器就會不合法並且在後使用叠代器時候被拋出Concurrent-ModfificationException異常,這就意味著我們只有在立即需要使用的叠代器時候才會獲取叠代器。然而叠代器調用自己的remove方法,那麽這個叠代器依舊是合法的。這是我們有時候更願意使用叠代器的remove方法的原因

3.3、List接口、ArrayList類和LinkedList類

List接口繼承了Collection接口,所以他包含Collection接口所有的方法,另外它還外加了一些自己的方法。

    public interface List<E> extends Collection<E> {
            E get(int index);//獲取指定位置上的元素
            E set(int index,E e);//修改指定位置上的元素
            void add(int index,E e);//在指定位置上添加一條新的元素
            void remove(int index);//刪除指定位置上的元素

    }

List ADT有兩種實現方式,ArrayList類提供了List ADT的一種可增長數組的實現方式,使用ArrayList的有點在於對get和set調用花費常數時間,其缺點在於刪除現有項或者新增項花費比較多的時間,除非在末端添加或者刪除。LikedList類提供了List ADT的雙向鏈表實現,使用LikedList的有點在於新增項和刪除項花費時間較少(在一直變動項位置的前提下),這意味著在表的前端或者末端添加和刪除時候用常數時間的操作,由此LinkedList提供了addFirst和removeFirst、addLast和removeLast以及getFirst和getLast等以有效添加刪除訪問表兩端的數據項,使用LikedList的缺點是它不容易做索引,因此對get的調用花費時間較多。下面我們看一個例子

    public static  void makeList1(List<Integer> list,int N){
        list.clear();
        for (int i = 0; i < N; i++) {
            list.add(i);
        }
    }

不管傳遞的參數是ArrayList 還是LinkedList,makeList 的運行時間都是O(N),因為add方法都是在表的末端添加數據,從而花費的都是常數時間(這裏我們忽略ArrayList偶爾的擴容所花費的時間),現在我們通過在表的前端添加數據來構造一個List

    public static  void makeList2(List<Integer> list,int N){
        list.clear();
        for (int i = 0; i < N; i++) {
            list.add(0,i);
        }
    }

那麽對於LinkedList而言,他的運行時間依舊是O(N),但是對於ArrayList來說他的運行時間O(N2),因為在ArrayList中在前端添加一條數據要把後面所有的數據都要往後移動,光這個步驟花費的時間是O(N)。
下面我們來計算一個List中的數之和

    public static int sum(List<Integer> list){
        int total = 0;
        for (int i = 0; i < list.size(); i++) {
            total += list.get(i);
        }
        return total;
    }

這裏ArrayList的運行時間是O(N),但對於LinkedList來說,其運行時間是O(N2),因為在LinkedList中,每一次調用get花費時間是O(N)。可是如果我們用的是增強for循環的話,那麽它對任意List的運行時間都是O(N),因為叠代器將有效的從一項到下一項推進。
對搜索而言,ArrayList和LinkedList都是低效的。對於Collection的contains和remove方法調用均花費的是線性時間。
在ArrayList中有一個容量的概念,它表示基礎數組的大小,在需要的時候,ArraList將自動增加其容量以保證它至少有表的大小。如果該大小的早期估計存在,那麽ensureCapacity可以設置一個容量足夠大的容量避免以後的擴容,再有trimToSize可以在所有的ArrayList添加操作完成之後使用避免浪費空間。

3.4、例子:remove方法對LinkedList類的應用

作為一個例子,我們先提出一個需求,將一個表中所有的偶數刪除(如果表包含6,5,4,8,1,9,則在調用放過後表中只有5,1,9)。
這個需求對於ArrayList來說幾乎是一個失敗的策略,因為從一個ArrayList中幾乎刪除任意項都是非常昂貴的操作,但是在LinkedList中我們知道從已知位置刪除可以通過重新安排鏈從而有效的完成。
想法一:

    public static void removeEnentVer1(List<Integer> list){
        int i = 0;
        while (i < list.size()){
            if(list.get(i) % 2 == 0 ){
                list.remove(i);
            }else{
                i++;
            }
        }
    }

這個方法暴露了兩個問題,首先LinkedList對get調用效率不高,其次對remove的調用同樣不高,因為到達位置i的代價是昂貴的。

想法二:

    public static void removeEnentVer2(List<Integer> list){
        for (Integer itm : list) {
            if(itm % 2 == 0){
                list.remove(itm);
            }
        }
    }

這個方法我們不用get,而是用增強for,間接使用叠代器一步步遍歷該表,這是高效率的,但是我們使用Collection的remove方法來刪除,首先這不是一個高效的操作,因為remove方法必須先搜索到該項,它花費是線性時間,再次我們運行這個程序時發現會拋出異常,因為我們在一項被刪除時,由增強for循環使用的基礎叠代器是非法的。

想法三:

    public static void removeEventVer3(List<Integer> list){
        Iterator<Integer> iterator = list.iterator();
        while (iterator.hasNext()){
            if(iterator.next()%2==0){
                iterator.remove();
            }
        }
    }

這個想法是比較成功的,在叠代器找到一個偶數值項時候,我們可以使用叠代器的remove方法來刪除剛剛看到的值,對於一個LinkedList而言,對該叠代器的remove方法調用只花費常數時間,因為叠代器就位於要被刪除的節點。因此,對於LikedList整個程序花費的時間是線性的而不是二次的,而對於ArrayList即使叠代器位於要刪除項的位置,remove方法仍然是昂貴的,因為刪除一項數組後面的所有的數據都要向前移動一位,所以對於ArrayList而言整個程序所花費的時間還是二次的。

如果我們傳遞一個LikedList

3.5、關於ListInterator

ListIterator擴展了List的Iterator的功能,ListInterator和Iterator的區別:

  • ListIterator有add()方法,可以向List中添加對象,而Iterator不能。
  • ListIterator和Iterator都有hasNext()和next()方法,可以實現順序向後遍歷。但是ListIterator有hasPrevious()和previous()方法,可以實現逆向(順序向前)遍歷。Iterator就不可以。
  • ListIterator可以定位當前的索引位置,nextIndex()和previousIndex()可以實現。Iterator 沒有此功能。
  • 都可實現刪除對象,但是ListIterator可以實現對象的修改,set()方法可以實現。Iterator僅能遍歷,不能修改。因為ListIterator的這些功能,可以實現對LinkedList等List數據結構的操作。
    public interface ListIterator<E> extends Iterator<E> {
            boolean hasPrevious();//有沒有前一項
            E previous();//拿到前一項
            void add(E e);//有next沒有previous添加在第一位,既有next又有previous添加到它們之間,有previous沒有next添加在最後
            void set(E e);//修改
    }

(2018年8月23日23:28:00)

表、棧和隊列(1)