Java筆記 - Set集合

Set集合是無序的,元素不可重複的。
Set接口中的方法和Collection一致。
Set集合有三個重要子類HashSet,LinkedHashSet和TreeSet

1.HashSet
HashSet類實現 Set 接口,由哈希表(實際上是一個 HashMap 實例)支持。它不保證 set 的迭代順序;特別是它不保證該順序恆久不變(Set底層的存儲方式是由算法來完成的,所以說一定哪一天升級後算法就變化了,元素的存儲位置也就改變了)。此類允許使用 null 元素。 是不同步的。

什麼是哈希表?

散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。

給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數

簡單講,哈希函數是一種算法,通過這種算法算出來很多元素的地址值,把這些值存儲起來就叫做哈希表。
其實,哈希表裏面存儲的還是數組,如果一個數組想要查找元素,就要遍歷數組,一個一個去比較。而哈希這種算法對數組進行了優化。它將要存儲的元素代入到哈希函數中去,計算出一個位置,然後就把這個元素存儲到這個位置上,如果想要查找元素,就再算一遍位置,然後直接到這個位置上去獲取。

例如要存儲字符串“ab”到數組中去,一般情況下就直接把“ab”放到0角標上就行了。但是這種方法在查詢的時候需要遍歷數組逐個比較,速度慢。所以就根據元素自身的特點,定義一個函數,把“ab”代入到這個函數中去,對元素進行計算,獲取計算結果,這個結果就是“ab”在數組中的位置。這種方式的好處就是在查找“ab”在數組中的位置時,就不需要遍歷了,直接用“ab”再算一遍位置,然後去找這個位置就行了。這個算法就是哈希算法。

每個對象都有自己的哈希值,因爲每個對象都是Object類的子類,Object類中有方法int hashCode()返回該對象的哈希碼值,這個方法就是用來算對象哈希值的方法。這個方法是由Windows實現的,我們不用管,但是我們自己的對象可以覆蓋這個方法,建立對象自身的哈希值。

常用的哈希算法
1. 直接尋址法:取關鍵字或關鍵字的某個線性函數值爲散列地址。即H(key)=key或H(key) = a•key + b,其中a和b爲常數(這種散列函數叫做自身函數)
2. 數字分析法
3. 平方取中法
4. 摺疊法
5. 隨機數法
6. 除留餘數法:取關鍵字被某個不大於散列表表長m的數p除後所得的餘數爲散列地址。即 H(key) = key % p, p<=m。不僅可以對關鍵字直接取模,也可在摺疊、平方取中等運算之後取模。對p的選擇很重要,一般取素數或m,若p選的不好,容易產生同義詞。

如果哈希算法是字符Unicode碼求和再取餘
function(element){
//自定義哈希算法
97+98=195
returen 195%10;//結果是5
}
那麼“ab”的地址就是5,把“ab”放在數組角標爲5的位置上,查找的時候直接用“ab”再進行運算,算出結果是5,直接去角標5的位置上查找元素,然後判斷該元素是不是“ab”,如果不是,那麼這個數組中就沒有“ab”;如果已經把“ab”放在角標爲5的位置上,還要再存一個“ab”這個時候會先計算ab的位置,然後判斷是否相同,如果相同話就不再保存。所以這個算法提高了查詢效率,但是缺點就是不能保存重複數據。

怎麼判斷兩個元素的方式是否相同?判斷哈希值是否相同是使用hashCode方法,判斷內容是否相同是使用equals方法。

注意:如果hashCode不同,就不再判斷equals了,因爲肯定是兩個不同的元素。

如果想要存儲“ba”,首先計算哈希值,結果相同,然後判斷內容,結果不相同,這種情況叫做哈希衝突

散列衝突的解決方案
1. 開方定址法
這種方法也稱再散列法,其基本思想是:當關鍵字key的哈希地址p=H(key)出現衝突是,以p爲基礎,產生另一個哈希地址p1,如果p1仍然衝突,再以p1爲基礎,產生另一個哈希地址p2……直到找出一個不衝突的哈希地址pi,將相應元素存入其中。
2. 再哈希法
這種方法是同時構造多個不同的哈希函數,當哈希地址衝突時,在用別的哈希函數進行計算,如果衝突再換哈希函數,直到算出一個不衝突的哈希地址。
3. 鏈地址法
這種方法的基本思想是將所有哈希地址爲i的元素構成一個成爲同義詞鏈的單鏈表,並將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進行。鏈地址法適用於經常進行插入和刪除的情況。
4.建立公共溢出區
將哈希表分爲基本表和溢出表兩部分,凡是和基本表發生衝突的元素,一律填入溢出表。

存儲字符串對象

HashSet hs = new HashSet();

hs.add("ABC1");
hs.add("ABC2");
hs.add("ABC3");
hs.add("ABC4");
hs.add("ABC2");
hs.add("ABC3");
Iterator it = hs.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

輸出結果:
ABC1
ABC4
ABC2
ABC3
Set集合中存儲對象是無序的,並且相同的元素不會進行保存。
Set集合只有一種取出元素的方式,就是Iterator迭代器。

存儲自定義對象
在開發中,我們使用更多的是自定義對象而不是字符串對象

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

}


public class HashSetTest{
    public static void main(String[] args){
        HashSet hs = new HashSet();

        hs.add(new Person("lisi4",24));
        hs.add(new Person("lisi7",27));
        hs.add(new Person("lisi1",21));
        hs.add(new Person("lisi9",29));
        hs.add(new Person("lisi7",27));

        Iterator it = hs.iterator();
        while(it.hasNext()){
            Person p = (Person)it.next();//自定義對象要做強轉動作,add接受後Person提升爲Object
            System.out.println(p.getName()+"...."+p.getAge());
        }
    }
}

輸出結果:
lisi1….21
lisi7….27
lisi4….24
lisi7….27
lisi9….29

HashSet集合數據結構是哈希表,所以存儲元素的時候,使用元素的hashCode方法來確定位置,如果位置相同,再通過元素的equlas方法來判斷元素是否相同。

在該示例中,Person對象的哈希值是通過調用Object中的hashCode方法,並且判斷內容是否相同也是用的Object中的equals方法。這5個Person對象的hashCode不相同,所以HashSet集合認爲他們是5個不同的元素。所以我們需要建立自己的hashCode方法來計算元素的位置和equals方法來判斷元素是否相同,所以需要在Person類中複寫Object的hashCode和equals方法。

public class Person{
    //定義變量,構造函數,get、set方法
    public int hashCode(){
        return name.hashCode+age();//人類的特點就是姓名和年齡,所以根據姓名和年齡計算地址值
    }

    public boolean equals(Object obj){

        Person p = (Person)obj;
        return this.name.equals(p.name) && this.age==p.age;//這裏調用的equals方法是字符串name中的equals方法,比較姓名是否相同
    }
}

輸出結果:
lisi1….21
lisi9….29
lisi4….24
lisi7….27

在實際開發過程中一般要複寫hashCode、equals、toString方法,Eclipse中提供了快捷書寫hashCode和equals方法,toString方法的選項。

public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + age;
    result = prime * result + ((name == null) ? 0 : name.hashCode());//爲了防止name.hashCode+age出現重複結果,就把age隨便乘以一個數
    return result;
}

public boolean equals(Object obj) {
    if (this == obj)
        return true;
    /*
    if (!obj instanceof Person){
        throw new ClassCastException("類型錯誤");如果傳入的類型不對,直接拋出類型錯誤
    }
    */  
    if (obj == null)
        return false;
    if (getClass() != obj.getClass())
        return false;
    Person other = (Person) obj;
    if (age != other.age)
        return false;
    if (name == null) {
        if (other.name != null)
            return false;
    } else if (!name.equals(other.name))
        return false;
    return true;
}

public String toString(){//如果直接使用從Object中繼承的toString方法,返回結果就是哈希值,沒有意義,一般情況下我們需要根據對象的實際情況,返回對對象的描述,所以需要重寫toString方法。
    return name+":"+age;
}

2.TreeSet
TreeSet是按照元素的字典順序排序,這個順序我們不稱之爲有序,有序是指的存入和取出的順序。這裏的順序我們可以叫做指定的順序。
TreeSet是不同步的。

存儲字符串對象
比較簡單,略過。

存儲自定義對象

1. 自然排序(實現Comparable接口)
繼續使用Person類,創建TreeSet集合如下:

public class TreeSetDemo{

    public static void main(String[] args){
        TreeSet ts = new TreeSet();

        ts.add(new Person("wangwu",21));
        ts.add(new Person("zhaoliu",26));
        ts.add(new Person("zhangsan",23));
        ts.add(new Person("sunqi",27));
        ts.add(new Person("lisi",21));

        Iterator it = ts.iterator();
        while (it.hasNext()) {
            Person p = (Person)it.next();
            System.out.println(System.out.println(p.getName()+"......"+p.getAge()););
        }
    }
}

運行發現報錯Person cannot be cast to java.lang.Comparable
因爲TreeSet是給元素排序用的,既然排序就要進行大小的比較,但是兩個Person對象不能進行比較。

Comparable接口
此接口強行對實現它的每個類的對象進行整體排序。這種排序被稱爲類的自然排序,類的 compareTo 方法被稱爲它的自然比較方法

Person類用來描述人這類事務,如果想要把人放在TreeSet中進行排序,那麼Person應該在已經具備的基本功能外還要具備一個擴展功能,就是比較的功能。這個比較的功能已經被定義在了Comparable接口中,所以如果想要Person對象具備比較性,只要讓Person類實現Comparable接口就行了。

修改Person類

public class Person implements Comparable{
    //hashCode,equals,toString,set,get,構造函數

    public int compareTo(Object obj){
        Person p = (Person)obj;

        int temp = this.age - p.age;
        return temp==0?this.name.compareTo(p.name):temp;//先按年齡排序,年齡相同再按姓名排序

        //int temp = this.name.compareTo(p.name);先按姓名排序,姓名相同再按年齡排序
        //return temp==0?this.age-p.age:temp;


    }
}

輸出結果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27

在上面的例子中比較name時調用的compareTo方法是字符串中的compareTo方法,其實String類也實現了接口Comparable,字符串中的compareTo方法重寫了Comparable接口中的compareTo方法,所以字符串本身具備自然排序。只要對象想要進行比較,就要實現Comparable接口,並重寫compareTo方法。

這個Person類中默認的比較排序方式就是自然排序

2. 比較器排序(實現Comarator接口)
但是如果我們不想要按照類中默認的方式排序,或者這個類沒有排序的功能怎麼辦?
首先不可以修改Person類,因爲這個類有可能不是我們寫的,我們只是拿過來用。所以我們可以使用TreeSet第二種排序方法,就是讓集合自身具備比較功能。

在第一種方法中,TreeSet集合自身並不能直接對元素進行比較,是元素自身具有比較的功能,TreeSet只是按照元素比較的結果來確定元素在集合中的位置。所以如果元素自身不具備比較的功能,可以讓集合對元素進行比較。

TreeSet構造方法:
TreeSet(Comparator< ? super E> comparator) :構造一個新的空 TreeSet,它根據指定比較器進行排序。插入到該 set 的所有元素都必須能夠由指定比較器進行相互比較:對於 set 中的任意兩個元素 e1 和 e2,執行 comparator.compare(e1, e2)
Comparator接口就是比較器。創建比較器就是實現Comparator接口,然後覆蓋其中的compare方法,把比較器作爲參數傳給TreeSet構造函數,TreeSet集合就具備了比較功能。如果自然排序和比較器同時存在的時候,以比較器爲主。

public class ComparatorByAge implements Comparator{
    public int compare(Object o1,Object o2){
        Person p1 = (Person)o1;
        Person p2 = (Person)o2;

        int temp = p1.getAge() - p2.getAge();
        return temp==0?p1.getName().compareTo(p2.getName()):temp;
    }
}

public class TreeSetDemo{
    public static void main(String[] args){
        TreeSet ts = new TreeSet(new ComparatorByAge);
        //.....
    }
}

輸出結果:
lisi……21
wangwu……21
zhangsan……23
zhaoliu……26
sunqi……27

在實際開發過程中常用的是比較器。但是一般情況下只要Person要存到集合中,除了覆蓋equals,hashCode,toString方法之外也還會實現Comparable接口。Java中的很多類都實現了Comparable接口,比如String類,Integer類,所以這些類都有比較的屬性,也就是說String類,Integer類本身具備自然排序。

TreeSet排序的底層原理(二叉樹)
二叉樹
如果TreeSet中的Person對象按照年齡來排序,首先第一個元素28放在樹的最頂層,然後第二個元素如果比28小就放在左邊,如果比28大就放在右邊…以此類推。當放25的時候,因爲25比28小,所以就放在28的左邊,不需要和29再進行比較,這樣就提高了效率。
但即使如此,當元素數量很多的時候速度也會變慢。因爲前面所有確定位置的元素都是按照元素從小到大的順序排列的,是有序的,爲了加快效率,就可以使用二分查找,在每一次放元素之前都會對已有的有序元素進行折半,再確定新元素的位置。

二叉樹判斷元素大小是看返回值的,如果返回1,就說明該元素比被比較的元素要大。如果返回-1,就說明該元素要小。
依據這原理,如果想要有序,按從小到大排列,就可以固定的返回1。

public class ComparatorByName implements Comparator {

    public int compare(Object o1, Object o2) {

        Person p1 = (Person)o1;
        Person p2 = (Person)o2;
        return 1;//有序。,返回-1就是倒敘
    }
}

3.LinkedHashSet
具有可預知迭代順序的 Set 接口的哈希表和鏈接列表實現。此鏈接列表定義了迭代順序,即按照將元素插入到 set 中的順序(插入順序)進行迭代。
是鏈表結構的,存第一個元素,通過hashCode計算出地址,存第二個元素的時候,第一個元素記住第二個元素的地址…以此類推。
LinkedHashSet也是不同步的。
有了LinkedHashSet類,Set集合和List集合最大的不同點就是元素是否唯一了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章