1. 程式人生 > 實用技巧 >Java基礎-容器篇(概念性)

Java基礎-容器篇(概念性)

一、Tree、Hash和Linked

  Tree,即樹,多數情況尤指二叉樹,在C/C++中,樹的實現依託於連結串列。二叉排序樹是一種比較有用的折衷方案。陣列的搜尋比較方便,可以直接用下標,但刪除或者插入某些元素就比較麻煩。連結串列與之相反,刪除和插入元素很快,但查詢很慢。二叉排序樹就既有連結串列的好處,也有陣列的好處。檔案系統和資料庫系統一般都採用樹(特別是B樹)的資料結構資料,主要為排序和檢索的效率。

平衡二叉樹都有哪些應用場景

二叉樹支援動態的插入和查詢,保證操作在O(height)時間,這就是完成了雜湊表不便完成的工作,動態性。但是二叉樹有可能出現worst-case,如果輸入序列已經排序,則時間複雜度為O(N)

平衡二叉樹/紅黑樹就是為了將查詢的時間複雜度保證在O(logN)範圍內。
所以如果輸入結合確定,所需要的就是查詢,則可以考慮使用雜湊表,如果輸入集合不確定,則考慮使用平衡二叉樹/紅黑樹,保證達到最大效率

平衡二叉樹主要優點集中在快速查詢。
如果你知道SGI/STL的set/map底層都是用紅黑樹(平衡二叉樹的一種)實現的,相信你會對這些樹大有興趣。

缺點:

順序儲存可能會浪費空間(在非完全二叉樹的時候),但是讀取某個指定的節點的時候效率比較高O(0)

鏈式儲存相對二叉樹比較大的時候浪費空間較少,但是讀取某個指定節點的時候效率偏低O(nlogn)

  Hash,雜湊,即雜湊,即將輸入的資料通過hash函式得到一個key值,輸入的資料儲存到陣列中下標為key值的陣列單元中去。

陣列是將元素在記憶體中連續存放。

連結串列中的元素在記憶體中不是順序儲存的,而是通過存在元素中的指標聯絡到一起。

陣列必須事先定義固定的長度,不能適應資料動態地增減的情況。當資料增加時,可能超出原先定義的元素個數;當資料減少時,造成記憶體浪費。

連結串列動態地進行儲存分配,可以適應資料動態地增減的情況。

(靜態)陣列從棧中分配空間, 對於程式設計師方便快速,但是自由度小。

連結串列從堆中分配空間, 自由度大但是申請管理比較麻煩。​

根據陣列和連結串列的特性,陣列和連結串列的優劣勢分兩類情況討論。

a.當進行資料查詢時,陣列可以直接通過下標迅速訪問陣列中的元素。而連結串列則需要從第一個元素開始一直找到需要的元素位置,顯然,陣列的查詢效率會比連結串列的高。

b.當進行增加或刪除元素時,在陣列中增加一個元素,需要移動大量元素,在記憶體中空出一個元素的空間,然後將要增加的元素放在其中。同樣,如果想刪除一個元素,需要移動大量元素去填掉被移動的元素。而連結串列只需改動元素中的指標即可實現增加或刪除元素。

綜上,選擇Hash可以具備陣列的快速查詢的優點又能融合連結串列方便快捷的增加刪除元素的優勢。但是,不相同的資料通過hash函式得到相同的key值。這時候,就產生了hash衝突。解決hash衝突的方式有兩種。一種是掛鏈式,也叫拉鍊法。掛鏈式的思想在產生衝突的hash地址指向一個連結串列,將具有相同的key值的資料存放到連結串列中。另一種是建立一個公共溢位區。將所有產生衝突的資料都存放到公共溢位區,也可以使問題解決。

  Linked,連結串列,C中連結串列的單元節點為一個具有指標和資料的結構體,Java中為一個物件LinkNet。連結串列為一個個單元節點相連而成。長於增刪,短於查詢。

二、List、Set、Queue和Map

  

  通常,程式總是根據執行時才知道的某些條件取建立新物件。在此之前,不會知道所需物件的數量,乃至其確切的型別。因為我們不知道要在何時何地建立何種數量的物件,所以我們就不能依靠建立命名的引用來持有每一個物件,因為引用的數量也是不確定的。

  陣列是一種一組物件或者最基本資料型別最有效的方式,然而資料的尺寸在宣告時就已經固定了,當我們並不知道需要多少個物件時,或者需要更復雜的方式來儲存物件時,陣列尺寸固定就很不合適了。

  Java實用類庫提供了一套相當完整的容器來解決這類問題,其基本的型別是List、Set、Queue和Map。這類物件型別叫做集合類,但Java類庫中已經使用了Collection來指代該類庫中的一個特殊子集,所以將他們歸類為表示範圍更廣的“容器”。

  其中淡綠色的表示介面,紅色的表示我們經常使用的類。

1、基本概念
  根據Java容器類類庫儲存物件的用途來看,可以將其分為兩類

Collection,一個獨立元素的序列。List必須按照插入順序儲存元素,Set不能含有重複元素,Queue按照排隊規則來確定物件產生的順序。
Map,一組成對的“鍵值對”物件,允許用鍵來查詢值。ArrayList從某種意義上是將數字與物件關聯在一起。Map可以使用一個物件來查詢某個物件,也稱為對映表,關聯陣列,字典。

2、List

  List承諾可以將元素維護在特定的序列中。List介面在Collection的基礎上加入了大量的方法,使得可以在List中間可以插入和移除元素。

ArrayList,長於隨機訪問元素,但在List中插入和移除元素時較慢
LinkedList,長於插入和移除操作,有優化的順序訪問,但在隨機訪問方面比較慢。

關於ArrayList為什麼在中間插入元素比較慢,程式碼如下

public void add(int index, E element) {
        rangeCheckForAdd(index);//驗證(可以不考慮)

        ensureCapacityInternal(size + 1);  // Increments modCount!!(超過當前陣列長度進行擴容)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);(核心程式碼)
        elementData[index] = element;
        size++;
    }

System.arraycopy(elementData, index, elementData, index + 1)第一個引數是源陣列,源陣列起始位置,目標陣列,目標陣列起始位置,複製陣列元素數目。那麼這個意思就是從index索性處每個元素向後移動一位,最後把索引為index空出來,並將element賦值給它。這樣一來我們並不知道要插入哪個位置,所以會進行匹配那麼它的時間賦值度就為n。

LinkedList採用的是鏈式儲存。鏈式儲存就會定一個節點Node。包括三部分前驅節點、後繼節點以及data值。所以儲存儲存的時候他的實體地址不一定是連續的。

關於LinkedList的插入操作,程式碼如下

public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

可以看出先獲取插入索引元素的前驅節點,然後把這個元素作為後繼節點,然後在建立新的節點,而新的節點前驅節點和獲取前驅節點相同,而後繼節點則等於要移動的這個元素。所以這裡是不需要迴圈的,從而在插入和刪除的時候效率比較高

3、Stack

  “棧”是指後進先出(LIFO)的容器。LinkedList具有能夠實現棧的所有功能的方法,因此可以直接將其作為棧使用。

4、Set

  Set不儲存重複的元素,這裡的元素指的是某個物件的例項。Set最常被使用的是測試歸屬性,即查詢某個物件是否在Set中。並且Set是具有和Collection完全一樣的介面,沒有額外的功能,只是表現的行為不同。HashSet這一實現對快速查詢進行了優化。出於速度原因的考慮,HashSet使用了雜湊,但是其維護的順序與TreeSet和LinkedHashSet都不同,這取決於他們的元素儲存方式的實現不同,TreeSet將元素儲存在紅黑樹資料結構中,而LinkedHashSet雖然也和HashSet一樣使用雜湊,但其是使用連結串列來維護元素的插入順序。所以如果想要對結果排序,使用TreeSet更合適。

5、Map

  Map能將物件對映到其他物件,同時鍵具有不可重複性。Map在實際開發中使用非常廣,特別是HashMap,想象一下我們要儲存一個物件中某些元素的值,如果我們在建立一個物件顯得有點麻煩,這個時候我們就可以用上map了,HashMap採用是雜湊函式所以查詢的效率是比較高的,如果我們需要一個有序的我們就可以考慮使用TreeMap。這裡主要介紹一下HashMap的方法,大家注意HashMap的鍵可以是null,而且鍵值不可以重複,如果重複了以後就會對第一個進行鍵值進行覆蓋。

常用方法:put進行新增值鍵對,containsKey驗證主要是否存在、containsValue驗證值是否存在、keySet獲取所有的鍵集合、values獲取所有值集合、entrySet獲取鍵值對。

6、Queue 

  Queue是佇列,佇列是典型的先進先出(FIFO)的容器,就是從容器的一端放入元素,從另一端取出,並且元素放入容器的順序和取出的順序是相同的。LinkedList提供了對Queue的實現,LinkedList向上轉型為Queue。其中Queue有offer、peek、element、pool、remove等方法

offer是將元素插入隊尾,返回false表示新增失敗。peek和element都將在不移除的情況下返回對頭,但是peek在對頭為null的時候返回null,而element會丟擲NoSuchElementException異常。poll和remove方法將移除並返回對頭,但是poll在佇列為null,而remove會丟擲NoSuchElementException異常,以下是例子

public static void main(String[] args){
        Queue<Integer> queue=new LinkedList<Integer>();
        Random rand=new Random();
        for (int i=0;i<10;i++){
            queue.offer(rand.nextInt(i+10));
        }
        printQ(queue);
        Queue<Character> qc=new LinkedList<Character>();
        for (char c:"HelloWorld".toCharArray()){
            qc.offer(c);
        }
        System.out.println(qc.peek());
        printQ(qc);
        List<String> mystrings=new LinkedList<String>();
        mystrings.add("1");
        mystrings.get(0);
        Set<String> a=new HashSet<String>();
        Set<String> set=new HashSet<String>();
        set.add("1");
    }
    public static void printQ(Queue queue){
        while (queue.peek

三、Collections和Arrays

  為了方便對Array物件、Collection物件進行操作,Java中提供了Arrays類和Collections類對其進行操作。

Arrsys:是陣列的工具類,提供了對陣列操作的工具方法。

Collections:是集合物件的工具類,提供了操作集合的工具方法。

其中Arrays和Collections中所有的方法都為靜態的,不需要建立物件,直接使用類名呼叫即可。

1.Collections

Collection和Collections

  • Collection:java.util.Collection 是描述所有序列容器的共性的根介面,是一個集合介面(集合類的一個頂級介面)。它提供了對集合物件進行基本操作的通用介面方法。Collection介面在Java 類庫中有很多具體的實現。Collection介面的意義是為各種具體的集合提供了最大化的統一操作方式,其直接繼承介面有List與Set。這與C++不同,標準C++類庫中沒有其容器的任何公共基類,容器之間的共性依靠迭代器達成。但在Java中,迭代器和Collection被繫結在了一起,實現Collection就意味著要提供iterator()方法。
  • Collections:Collections則是集合類的一個工具類/幫助類,其中提供了一系列靜態方法,用於對集合中元素進行排序、搜尋以及執行緒安全等各種操作。此類不能例項化,就像一個工具類,服務於Java的Collection框架。

關於Collections執行緒安全問題,先挖個坑,單開一貼。

Collection和Iterable

  在jdk1.5中,Collection增加了一個父介面Iterable ,該介面的出現封裝了iterator方法,並提供了一個增強型的for迴圈。

2。Arrays

Arrays提供了asList()和toArray()兩個方法實現陣列與集合的轉化。

陣列變集合

//陣列中的元素都是物件
class ArraysDemo
{
    public static void main(String[] args)
    {
        String[] arr={"abc","cc","kkk"};
    
        list<String> list =Arrays.asList(arr);
        
        System.out.println(list)
    }
}
 
執行結果:
[abc,cc,kkk]
//陣列中的元素都是基本資料型別
class ArraysDemo
{
    public static void main(String[] args)
    {
        int[] num={2,3,4};
    
        list<int[]> li =Arrays.asList(num);
        
        System.out.println(li)
    }
}
 
執行結果:是一個數組的雜湊值
[[I@de6ced]
 

如果陣列中的元素都是物件,那麼變成集合時,陣列中的元素就直接轉成集合中的元素

如果陣列中的元素都是基本資料型別,那麼會將該陣列作為集合中的元素存在

這樣可以使用集合的思想和方法來運算元組中的元素,但是,將陣列變成集合,不可以使用集合的增刪方法,因為陣列的長度是固定的,如果你增刪,那麼會發生UnsupportedOperationException

集合變陣列

當指定型別的陣列長度小於了集合的size,那麼該方法內部會建立一個新的陣列,長度為集合的size

當指定型別的陣列長度大於了集合的size,就不會新建立陣列,而是使用傳遞進來的陣列

所以建立一個剛剛好的陣列最優,這是為了限定使用者的操作,不需要其進行增刪操作

public static void main(Stirng[] agrs)
{
    ArrayList<String> list =new ArrayList<String>();
 
    list.add("a1");
    list.add("a2");
    list.add("a3");
    
 
    Stirng[] arr=list.toArray(new String[list.size()]);
    System.out.println(Arrays.toString(arr));
}

四、雜湊與雜湊碼

我們知道Map以鍵值對的形式來儲存資料。有一點值得說明的是,如果要使用我們自己的類作為鍵,我們必須同時重寫hashCode() 和 equals()兩個方法。HashMap使用equals方法來判斷當前的鍵是否與表中的鍵相同。equals()方法需要滿足以下5個條件

  • 自反性 x.equals(x) 一定返回true
  • 對稱性 x.equals(y)返回true,則y.equals(x) 也返回true
  • 傳遞性 x.equals(y)返回true,y.equals(z)返回true,則x.equals(y)返回true
  • 一致性 如果物件中的資訊沒有改變,x.equals(y)要麼一直返回true,要麼一直返回false
  • 對任何不是null的x,想x.equals(null)一定返回false

1.hashCode()

雜湊的價值在於速度:雜湊使得查詢得以快速執行。由於速度的瓶頸是對“鍵”進行查詢,而儲存一組元素最快的資料結構是陣列,所以用它來代表鍵的資訊,注意:陣列並不儲存“鍵”的本身。而通過“鍵”物件生成一個數字,將其作為陣列的下標索引。這個數字就是雜湊碼,由定義在Object的hashCode()生成(或成為雜湊函式)。同時,為了解決陣列容量被固定的問題,不同的“鍵”可以產生相同的下標。那對於陣列來說?怎麼在同一個下標索引儲存多個值呢??原來陣列並不直接儲存“值”,而是儲存“值”的 List。然後對 List中的“值”使用equals()方法進行線性的查詢。這部分的查詢自然會比較慢,但是如果有好的雜湊函式,每個下標索引只儲存少量的值,只對很少的元素進行比較,就會快的多。

參考資料:《Java程式設計思想(第四版)》