Set_HashSet_LinkedHashSet_TreeSet

Set

概念

接口 Set,一个不包含重复元素的 collection。更确切地讲,set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null 元素

特性

  • 注意事项,Set集合并不一定都是无序的,有些集合是有序的。比如说:
    1:HashSet无序,并且可以存储null元素
    2:TreeSet有序,并且不可以存储null元素

API

Set中的 API 都是从Collection中继承而来。因此不再赘述。
等有空补一个Colletion的笔记~

实现类

  • AbstractSet
  • ConcurrentSkipListSet
  • CopyOnWriteArraySet
  • EnumSet
  • HashSet
  • JobStateReasons
  • LinkedHashSet
  • TreeSet
    今天我主要学习了HashSet,LinkedHashSet,TreeSet。今天上午的学习,主要是这三个实现类的概念底层数据结构,以及构造方法

HashSet

概念

此类实现 Set 接口,由哈希表(实际上是一个 HashMap 实例)支持。

  1. 它不保证 set 的迭代顺序
  2. 特别是它不保证该顺序恒久不变
  3. 此类允许使用 null 元素。
  4. 注意,此实现不是同步的。

数据结构_哈希表HashMap

在JDK中,HashSet的数据结构是HashMap(数组+链表),粗略定义,链表中的属性是如下代码:

class Entry{
	K key;
	V value;
	int hash;
	Entry next;
}

那么数组和链表是怎么构成哈希表的?
下面的图粗略的表示…每一个数组元素中存储的是一个Entry链表。链表中有 key, value组成的键值对,以及指向下一个结点的next指针。
在这里插入图片描述以上是我对哈希表的一个很粗略的理解。接下来,记录哈希表是如何实现增加,删除和查找功能的。
千万不要修改HashSet的元素值。
不然HashSet可能存储重复的值

HashMap_API

//添加键值对
V put(K key, V value){
int hash = hash(key);
int index = hash % table.length;
//此键值对应该添加到 table[index] 中, key不能重复。
//遍历链表table[index],查看key是否已经存在。
如果已经存在,更新key对应的value,并把原来的value返回。
如果不存在,在链表table[index]中添加新节点。
}

//删除键值对,并把删除的值value返回
V delete(K key){
int hash = hash(key);
int index = hash % table.length;
遍历链表,查找与键 == key相等的键值对
如果找到了:删除该节点,并把value进行返回
如果没找到:返回null
}

//查找,
V get(K key){
int hash = hash(key);
int index = hash % table.length;
遍历链表,查看key是否已经存在
是:返回value
否:返回false
}

HashMap性能

  • 假设哈希函数能够保证元素平均分布于数组中,那么,增加,删除,和查找的性能如何?

O(L) L:链表table[i]的长度。因为在链表中,增加删除和查找都需要遍历链表,链表的时间复杂度是O(n),因此,在HashMap中,增加删除和查找的性能取决于链表的长度

  • 如果让HashMap在O(1)的时间复杂度内完成增加,删除和查找操作,应该怎么办?

那必须保证数组中链表的平均长度不超过某个常数值M

  • 如何保证链表长度不超过常数值M(即加载因子)

必要的时候,对数组进行扩容,扩容就需要对键值对重新散射。

  • 如何遍历哈希表?

遍历数组第一个元素(即第一个链表),第二个元素(即第二个链表)……

  • 重新散射后,可以理解为链表结点重新排列到数组不同元素链表中,迭代顺序发生变化。
  • 回到HashSet概念,HashSet无法保证迭代顺序,也就是不知道迭代顺序;并且无法保证迭代顺序恒久不变

HashSet构造方法

HashSet(){}
构造一个新的空 set,其底层 HashMap 实例的默认初始容量是 16,加载因子是 0.75。
HashSet(Collection<? extends E> c){}
构造一个包含指定 collection 中的元素的新 set。
HashSet(int initialCapacity){}
构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和默认的加载因子(0.75)。
HashSet(int initialCapacity, float loadFactor){}

构造一个新的空 set,其底层 HashMap 实例具有指定的初始容量和指定的加载因子。

JDK中 HashMap数组的大小是2^n, 如果我传入的capacity不是2^n,那么JVM就赋一个大于等于 capacity的最小的2^n

HashSet如何保证元素的唯一性?

看到这个问题,我的想法是:在添加元素的时候,如果这个元素在哈希表中已经存在equals的元素,那么就不添加。这样就保证了他的唯一性。
实际上,也是这样的。我们看下JDK1.7中的add方法源码,因为JDK1.7相对简单一些~

/*HashSet是如何保证元素的唯一性的呢?
  HashMap的 key 值是唯一的。
*/

class HashSet<E> implements Set {
	private transient HashMap<E,Object> map; // 里面的value都是PRESENT
	
	/*占位符 dumb value*/
	private static final Object PRESENT = new Object();
	
	public boolean add(E e) { 
        return map.put(e, PRESENT)==null;
    }
}

// JDK1.7 
class HashMap<K, V> implements Map<K, V> {
	
	public V put(K key, V value) { 
		//看哈希表是否为空,如果空,就开辟空间
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        
        //判断对象是否为null
        if (key == null)
            return putForNullKey(value);
        
        int hash = hash(key); // 和key对象的hashCode()方法相关 
       
        //在哈希表中查找hash值
        int i = indexFor(hash, table.length);
		
		// 遍历链表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
			// 首先判断链表中e的哈希值和key哈希值是否一样,如果不一样,那说明key在链表中不存在,利用&&的短路效果继续判断下一个元素。如过哈希值相等,他们的key不一定相等,继续判断。
			//如果他们哈希值一样,再判断key和链表中元素e的属性key是否指向同一个对象,如果是,说明key在链表中存在。那么进入if语句,更新e.value = value,并返回oldValue。如果不是指向同一个对象,那么利用equals继续判断。
			//这个if语句,是为了追求效率
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
			    // key 与 e.key 是相等的
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
                //走这里其实是没有添加元素
            }
        }
		// 添加键值对
        modCount++;
        addEntry(hash, key, value, i); //把元素添加
        return null;
    }
    
    transient int hashSeed = 0;
    
    final int hash(Object k) { //k="hello", hash(Object k) 只有k的hashCode()方法有关。
        int h = hashSeed;
        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);
    }
}

我认为,HashMap 的put方法中,保证元素唯一性的关键语句是:if (e.hash == hash && ((k = e.key) == key || key.equals(k))),哈希值不同的key一定不同,哈希值相同的key,要再讨论key的指向,甚至是key的成员是否相等。
所以,coder在向HashSet中添加元素时,必须重写元素所在类的hashCode(), 以及equals()方法如果不重写,即使两个对象成员相同,依然可以添加到set中; 或者如果只重写equals方法,不重写hashCode()方法,不同对象实例,即使成员相同,JDK默认的Object中的hashCode()方法,返回的是对象内存地址的哈希值,哈希值一定不同,所以put方法会认为这两个成员相同的对象,是!equals()。
所以,HashSet如何保证元素的唯一性: 依赖于存储的实例的两个方法, int HashCode() & boolean equals(Object o)

LinkedHashSet

概念

  • 具有可预知迭代顺序的 Set 接口的哈希表和链接列表实现。这个可预知顺序是:key的添加顺序。
  • 此实现与 HashSet 的不同之外在于,后者维护着一个运行于所有条目(Entry)的双重链接列表。
  • 此链接列表定义了迭代顺序,即按照将元素插入到 set 中的顺序(插入顺序)进行迭代。
  • 注意,此实现不是同步的。

底层数据结构

底层是:HashMap + 双向链表
HashMap:保证key的唯一性
双向链表:保证迭代顺序是按照插入顺序进行迭代。

Set<Character> set = new LinkedHashSet<>();
set.add('X');
set.add('C');
set.add('D');
set.add('E');

在这里插入图片描述通过上述代码的运行结果,可以看出LinkedHashSet的迭代顺序就是key添加顺序。双向链表在LinkedHashSet中的原理是什么呢?

class Entry {
	K key;
	V value;
	int hash;
	//双向链表next,prev结点
	Entry next;
	Entry prev;
	//当前结点的哈希表中下一结点。
	Entry nextElement;
}

在这个Entry双向链表中,添加key以及迭代set逻辑是这样的,有一个哨兵头节点指向x,x.prev == head, x.next == c, c.prev == x, c.next ==d, d.prev == c, d.next == e, e.prev == d, e.next == tail.
x.nextElement = d;
在这里插入图片描述其余的注意事项,与HashSet相同。

TreeSet

概念

  • 基于 TreeMap 的 NavigableSet 实现。这两个map会在近期学习。
  • 使用元素的**自然顺序(字典顺序)**对元素进行排序,或者根据创建 set 时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • 此实现为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
  • 注意,此实现不是同步的
  • **TreeMap:**基于红黑树(Red-Black tree)的 NavigableMap 实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。不能存储null元素,除非在Comparator中定义null的比较规则.

构造方法

TreeSet()
构造一个新的空 set,该 set 根据其元素的自然顺序进行排序。(如果元素所在类不继承Comparable,也就是无法根据字典顺序进行排序,程序就会抛出ClassCastException
TreeSet(Collection<? extends E> c)
构造一个包含指定 collection 元素的新 TreeSet,它按照其元素的自然顺序进行排序。
TreeSet(Comparator<? super E> comparator)
构造一个新的空 TreeSet,它根据指定比较器进行排序。

如何保证元素的唯一性

TreeSet不依赖于存储元素的equals, 和hashCode()。 而是通过CompareTo() 或者比较器的。
参考JDK8中,TreeSet 的add源码:

public TreeSet() {
        this(new TreeMap<E,Object>());
    }
    
  public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    
  public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }
    
  public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    
    
  public V put(K key, V value) {
        Entry<K,V> t = root;  //t指向根节点
        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指向当前结点
                parent = t;
                //通过比较器的compare方法,比较我要插入的key和当前结点的key值
                cmp = cpr.compare(key, t.key);
                //往左往右走
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                    //如果找到相等元素,则更新树中结点的value
                else
                    return t.setValue(value);
            } while (t != null);
            //如果t == null, 那就是我当前插入的地方
        }
		//没有传入比较器
        else {
		//不能插入null元素,因为null不能进行比较。
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
			//将key强制类型转换成comparable,要求元素必须实现Comparable接口,否则抛出异常。
                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;
    }
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试
应支付0元
点击重新获取
扫码支付

支付成功即可阅读