1. 程式人生 > >17.集合Set,HashSet,TreeSet及其底層實現HashMap和紅黑樹;Collection總結

17.集合Set,HashSet,TreeSet及其底層實現HashMap和紅黑樹;Collection總結

ONE.Set集合

one.Set集合的特點
無序,唯一

TWO.HashSet集合

1.底層資料結構是雜湊表(是一個元素為連結串列的陣列)

2.那麼HashSet如何來實現元素的唯一性的呢?

通過一HashSet新增字串的案例檢視HashSet中add()的原始碼,看為什麼相同的字串沒有被加入HashSet中

interface Collection {
...
}

interface Set extends Collection {
...
}

class HashSet implements Set {
private static final Object PRESENT = new
Object(); private transient HashMap<E,Object> map; //1.從這一步我們可以看出來,HashSet()其實使用HashMap()實現的 public HashSet() { map = new HashMap<>(); } public boolean add(E e) { //e=hello,world //2.add()方法的內部也是HashMap的例項物件呼叫方法,這裡e是被新增的物件,而PRESENT是一個 private static final Object PRESENT = new Object();物件。 return
map.put(e, PRESENT)==null; } } class HashMap implements Map { //3.來到HashMap實現的put方法 public V put(K key, V value) { //key=e=hello,world //4.看雜湊表是否為空,如果空,就開闢空間 if (table == EMPTY_TABLE) { inflateTable(threshold); } //5.判斷物件是否為null if (key == null) return putForNullKey(value); //6_1.呼叫hash()方法,通過檢視這個方法我們知道這個方法的返回值和物件的hashCode()方法相關
int hash = hash(key); //7.在雜湊表中查詢hash值 int i = indexFor(hash, table.length); //8.這裡for迴圈的的初始條件是把table[i]賦值給e,如果在雜湊表中找不到這個hash值得話, 就不會進入for迴圈比較,如果有的話,就會進入比較 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; //9.如果hash值一樣(說實話,這一步每太看懂,既然能夠查詢到,說明兩者的hash值必然相等), 並且地址值或者equls一樣的話,不會新增進來 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; //走這裡其實是沒有新增元素 } } modCount++; //9.把元素新增 addEntry(hash, key, value, i); return null; } transient int hashSeed = 0; //6_2.這是HashMap中的hash方法,從這個方法的實現我們可以看出,這個方法的唯一變數是 hashCode() final int hash(Object k) { //k=key=e=hello, int h = hashSeed;//這個值預設是0 if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); //這裡呼叫的是物件的hashCode()方法 // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }

HashSet其實是用HashMap()來實現的,HashMap()是Map介面的實現類
呼叫HashSet的add()方法其實就是呼叫HashMap()中的put(),put()中主要涉及到兩個方面

1).物件的hash值,通過呼叫hash()得到,這個方法是由hashCode()經過操作實現的,由hashCode()的值控制

2).建立了雜湊表,雜湊表會將每一個hash值收入
然後比較的方式是

A.先在hash表中查詢看是否有這個hash值(第一次比較,看hash表中是否有當前元素的hash值(這個值有hashcode操作的得到)),如果沒有,直接將這個hash值對應的物件新增到HashSet中,如果有還要進行第二次比較

B.如果hash表中有這個hash值,那麼獲取表中的這個hash對應的物件,如果這兩個物件的地址值(e.key == key)或者key.equal(e.key)。(第二次比較,如果兩個物件的hash值相同,還是不能認為是同一個物件,還要比較兩個物件的地址值,或者是equals(),這裡是一個||,只要有一個滿足相等,就可以認為是同一個元素,不新增

3.現在可以回答開始的問題,為什麼儲存字串的時候,字串相同的時候,只儲存了一個了,原因是String類中重寫了hashCode()和equals()而String類的hashcode值和equals結果都是由字串的內容決定的

我們來看一下String類中的hashCode()和equals()方法

1) public int hashCode()返回此字串的雜湊碼。
String 物件的雜湊碼根據以下公式計算:
s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1]
使用 int 演算法,這裡 s[i] 是字串的第 i 個字元,n 是字串的長度,^ 表示求冪。(空字串的雜湊值為 0。)

2)public boolean equals(Object anObject)將此字串與指定的物件比較
當且僅當該引數不為 null,並且是與此物件表示相同字元序列的 String 物件時,結果才為 true。

所以當物件是String型別的時候,地址值(可能相等(字串常量池知識點)),equals方法必然相等,所以不會加入

4.如果HashSet中收入的是自定義物件,那如何實現唯一呢?
1)通過上面String類的分析,我們知道,HashSet中實現唯一的兩個比較,先是比較hash值(這個值由hashcode值控制),如果這個值相同,那麼再比較地址值或者equals(),所以我們可以通過重寫hashCode()和equals(),來實現對自定義物件的唯一性判斷
2)這裡開始的想法是,其實只要重寫了equals,讓這個方法比較的是物件的內容,那麼就可以排除掉相同的物件
所以讓hashCode()返回一個常數,這樣hash就相等了,然後通過equals判斷
但是這樣會導致每一個新物件都要和老物件進行比較,太麻煩,所以我們可以仿照String類中對hashCode()重寫的方式,同樣的有物件的成員變數來決定這個物件的hashcode值,equals方法也是通過比較物件的成員變數
是否相等。這樣我們就得到了最後的結論,其實eclipse提供了自定義類重寫這兩個方法的最終版本

public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) 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;
    }

3)開發的時候,程式碼非常的簡單,自動生成即可。

THREE.TreeSet集合

1:底層資料結構是紅黑樹(是一個自平衡的二叉樹)
這裡寫圖片描述

2:TreeSet底層是如何保證元素的排序方式,和元素的唯一性呢
同樣的我們來看看看TreeSet的原始碼

interface Collection {...}

interface Set extends Collection {...}

interface NavigableMap {

}

class TreeMap implements NavigableMap {
     public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
}

class TreeSet implements Set {
    private transient NavigableMap<E,Object> m;

    public TreeSet() {
         this(new TreeMap<E,Object>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
}

真正的比較是依賴於元素的compareTo()方法,而這個方法是定義在 Comparable裡面的。
所以,你要想重寫該方法,就必須是先 Comparable介面。這個介面表示的就是自然排序。

根據原始碼,我們知道,TreeSet的底層程式碼可以通過兩種方式來實現其元素的唯一性和有序

a:自然排序(元素具備比較性)
讓元素所屬的類實現Comparable介面,重寫其中的compareTo方法

package cn.itcast_03;

public class Student implements Comparable<Student>{
    private String name;
    private int age;
    public Student() {
        super();
        // TODO Auto-generated constructor stub
    }
    public Student(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;
    }
    @Override
    public int compareTo(Student s) {
        int num = 0;
        num = this.age - s.age;
        int num2 = num == 0 ? this.name.compareTo(s.name) : num;
        return num2;

    }

}

b:比較器排序(集合具備比較性)
讓集合構造方法接收Comparator的實現類物件,在實現類中重寫compare()方法

這種方法通過呼叫集合的帶參構造來實現比較
public TreeSet(Comperator comparator)//比較器排序

Comperator 是一個介面,把介面作為引數其實就是需要這個介面的實現類的物件
通過API我們可以得知這個介面中方法的格式,另外,這種需要一個介面的實現類的物件作為引數的情況,開發中我們常常使用匿名內部類的格式

TreeSet<Student> ts = new TreeSet<Student>(new Comparator<Student>() {
            public int compare(Student s1,Student s2) {
                //注意這個三目比較,賦值的優先順序最小,==最大,三目中間
                int num = s1.getName().length() - s2.getName().length();
                int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;
                int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;
                return num3;                    
            }
        });

3.需要注意的問題

1)根據對原始碼和底層資料結構紅黑樹的理解,我們知道無論是通過自然排序還是通過比較器排序,資料結構核心是通過將根節點和子節點比較,在紅黑樹的預設方法中,小的為左兒子,大的為右兒子,實現一顆平衡二叉樹,而程式碼的體現是將成員變數進行某種比較得到一個int值,如果值大於1,就放右邊,如果值小於1就放左邊,如果值相等就不放入,通過這個int值來完成元素的收集

2)我們在考慮這個值的時候,對於大於,小於,相等的條件設定常常使用三目運算子來做判斷,熟練使用這一三目運算子

//如果按照正常的二叉排序,那麼會先輸出小的,而我們要求先輸出分高的,所以,可以將全部的s1,s2交換順序
                //就可以達到總分大的放左邊(先輸出),然後單科權重是語文——數學——英語
                int num = s2.getSum() - s1.getSum();
                int num2 = num ==0 ? s2.getChineseScore() - s1.getChineseScore() : num;
                int num3 = num2 == 0 ? s2.getMathScore() - s1.getMathScore() : num2;
                int num4 = num3 == 0 ? s2.getEnglishScore() - s1.getEnglishScore() : num3;
                return num4;

FOUR.Collection集合總結(掌握) Collection

    |--List 有序,可重複
        |--ArrayList
            底層資料結構是陣列,查詢快,增刪慢。
            執行緒不安全,效率高
        |--Vector
            底層資料結構是陣列,查詢快,增刪慢。
            執行緒安全,效率低
        |--LinkedList
            底層資料結構是連結串列,查詢慢,增刪快。
            執行緒不安全,效率高
    |--Set  無序,唯一
        |--HashSet
            底層資料結構是雜湊表。
            如何保證元素唯一性的呢?
                依賴兩個方法:hashCode()和equals()
                開發中自動生成這兩個方法即可
            |--LinkedHashSet
                底層資料結構是連結串列和雜湊表
                由連結串列保證元素有序
                由雜湊表保證元素唯一
        |--TreeSet
            底層資料結構是紅黑樹。
            如何保證元素排序的呢?
                自然排序
                比較器排序
            如何保證元素唯一性的呢?
                根據比較的返回值是否是0來決定

4:針對Collection集合我們到底使用誰呢?(掌握)
唯一嗎?
是:Set
排序嗎?
是:TreeSet
否:HashSet
如果你知道是Set,但是不知道是哪個Set,就用HashSet。

否:List
要安全嗎?
是:Vector
否:ArrayList或者LinkedList
查詢多:ArrayList
增刪多:LinkedList
如果你知道是List,但是不知道是哪個List,就用ArrayList。

如果你知道是Collection集合,但是不知道使用誰,就用ArrayList。

如果你知道用集合,就用ArrayList。

5:在集合中常見的資料結構(掌握)
ArrayXxx:底層資料結構是陣列,查詢快,增刪慢
LinkedXxx:底層資料結構是連結串列,查詢慢,增刪快
HashXxx:底層資料結構是雜湊表。依賴兩個方法:hashCode()和equals()
TreeXxx:底層資料結構是二叉樹。兩種方式排序:自然排序和比較器排序

FIVE.案例

A:獲取無重複的隨機數

B:鍵盤錄入學生按照總分從高到底輸出