①TreeMap,底层为一个搜索树,但是,不是一个普通 的搜索树,而是一棵红黑树。存储数据的形式为键值对的形式,但是,键的值不可以重复,并且所有的键都必须是可以比较大小的。
在TreeMap当中,键不可以为空,否则插入时候就会报nullPointerException。
存放的键一定是按照有序的顺序存储的,因为二叉搜索树也是有序的。
public static void main(String[] args) {
//往TreeMap当中存放元素的时候,key一定需要可以比较,否则会报错。
Map<String,Integer> map=new TreeMap<>();
//存放的顺序为键的值的比较
//谁小就谁排在前面
map.put("hi",2);
map.put("hello",3);
map.put("11",null);
//不可以存放null
map.put(null,1);
System.out.println(map);
//获取键的值的集合
Set<String> set=map.keySet();
System.out.println(set);
//Map.entrySet():获取key,value:提供遍历map的一种方法
Set<Map.Entry<String, Integer>> set1=map.entrySet();
//treeMap当中不可以存储key为NULL的元素
for (Map.Entry<String, Integer> set2:set1){
System.out.println("key:"+set2.getKey()+"value:"+set2.getValue());
}
②TreeSet
底层也是一棵搜索树,存储数据的特点为有序的,不可以重复的,所有存放的元素都是可以比较,不可重复的。因此,插入,删除,查找的时间复杂度都是log(n)
TreeSet其实底层就是TreeMap的实现,键为存入的值,当无参构造时候,会实例化一个TreeMap.
当添加键的时候,会把对应的键插入集合当中,值为一个默认的Object类型的数据。
有关HashMap的问题的延申:
一、哈希冲突:两个不同的key,通过相同的哈希函数,被分配到了同一个位置。
由于哈希表底层数组的容量往往是小于实际需要存储的key的容量的,因此哈希冲突
只能减少,不可以避免。
哈希函数的设计规则:当定义域为k时候,值域最好为[0,K-1]
因此,有一种方法,叫做除留余数法,可以设定哈希函数:
假设散列表当中允许的地址数为m,此时m=8,取一个不大于m,但是接近或者等于m的质数作为除数(p),那么,此时:Hash(key)=key%p;
此时,如果填入的key为2,再次填入为为10的key,那么这两个key就发生了哈希冲突。
二、调节负载因子
何为负载因子?
负载因子k=填入表中的元素个数/散列表的长度。
此图为负载因子与冲突率之间的一个关系。当负载因子越大的时候,冲突率越高
因此为了降低负载因子,只能增加散列表的长度,
但是,一般情况下面,哈希表是有一个负载因子的规定常数的,大约是0.75。
当超过这个大小的时候,需要进行原来数组的扩容
三、闭散列,开散列,用于处理哈希冲突的办法:
闭散列:又称为开放地址法
缺点:容易出现堆积(聚集)现象
影响平均查找长度
如何删除元素?
把待删除元素的key置为0,即:做一个标记。不能直接置为空,否则无法查找发生哈希冲突的另外的元素。
①、闭散列,使用线性探测法
说明一下上图:当存放4这个key的时候,由于散列的长度为10,那么存放的位置就是4%10=4,此时,欲再次存入44,那么需要用44%10,得到的也是4,此时发生了哈希冲突,那么存放的地址就需要一直沿着散列往下走,找到下一个为空的地址存入,上图中,即为下标5的位置。
但是,如果后面的数组位置都不为空的情况下面,就会从0下标开始,一直往后走,直到找到为空的位置存放。
缺点:会把冲突发生的元素都聚集在一起,不易进行删除操作
②、闭散列,使用二次探测法
Hi=(H0+i²)%m:
H0为第一次存放位置的下标,i为第i次发生的冲突,m为散列表的长度。Hi为第i次发生冲突时候Hi存放的位置。
③开散列(哈希桶)
开散列,就是Java的HashMap来处理的(重点掌握)
把冲突的元素都放到数组对应的位置,但是该位置存储的是链表,而不是普普通通的元素,
如图所示,4,14,44冲突,但是他们都仍然在同一个位置按照链式存储,这种操作的方式就成为开散列。
jdk1.7以及以前采用的是头插法插入,jdk1.8开始,采用尾插法。
在合适的情况下面,链表会变成红黑树,
当数组的长度超过64并且链表的长度超过8的时候,链表会变为红黑树
总结以上两种方法:采用开放地址法处理哈希冲突的时候,其平均查找长度应当大于链地址法
性能分析:通常情况下,HashMap的插入,删除,查找的时间复杂度都为O(1)
下面是一个简单的哈希表代码实现,后面还会实现一个泛型的类型的
/**
* @author 25043
*/
public class TestHashBunk {
static class Node{
public int key;
private int val;
public Node next;
public Node(int key,int val){
this.key=key;
this.val=val;
}
}
public int usedSize;
public Node[] array;
public TestHashBunk(){
array=new Node[8];
}
/**
* 插入表当中
* 键@param key
* 值@param val
*/
public void put(int key,int val){
int keyIndex=key%array.length;
//通过散列函数找到对应的节点
Node cur=array[keyIndex];
//检验是否有重复插入的值
while (cur!=null){
if(cur.key==key){
//更新值
cur.val=val;
return;
}
cur=cur.next;
}
//头插法插入元素
Node node=new Node(key, val);
node.next=array[keyIndex];
array[keyIndex]=node;
usedSize++;
//当达到负载因子的时候,要进行扩容
if(get(usedSize)>=0.75){
resize();
}
}
/**
* 对原来的哈希表进行扩容
* 扩容的思路:首先新建一个数组,大小为原来数组的2倍
* 遍历原来的数组,对其中的每一个存储的链表进行遍历,把对应的节点重新
* 使用散列函数再排一遍,因为,下标需要重新计算
*
* 举个例子:比如原来的数组有8个元素
*
* 后面如果需要扩容到16个的时候,假设原来有key为9的元素,存储到了索引为1的位置,
* 那么如果扩容之后,9%16=9,需要改变位置,因此不可以直接使用
* Arrays.copyOf(array,2*array.length)进行扩容
*/
private void resize(){
Node[] newArray=new Node[array.length*2];
for (Node node : array) {
//获取每个数组的头节点,重新排列到新的数组当中
Node cur = node;
while (cur != null) {
//记录待检查节点的下一个节点
Node curNext = cur.next;
//获取到新的位置
int newKeyIndex = cur.key % (array.length * 2);
//cur的next区域指向新的头节点,完成头插法
cur.next = newArray[newKeyIndex];
newArray[newKeyIndex] = cur;
//cur继续往下走
cur = curNext;
}
}
//array指向新的数组
array=newArray;
}
/**
* 获取负载因子
* 有效的大小@param usedSize
* 负载因子@return
*/
public float get(int usedSize){
return usedSize*1.0f/array.length;
}
public void disPlay(){
for(int i=0;i<array.length;i++){
Node cur=array[i];
System.out.println("这是第"+i+"行遍历");
while (cur!=null){
System.out.println("当前节点的key为:"+cur.key+"当前节点的值为:"+cur.val);
cur=cur.next;
}
}
}
public int getValue(int key){
int hashKey=key%array.length;
Node cur=array[hashKey];
while (cur!=null){
if(cur.key==key){
return cur.val;
}
cur=cur.next;
}
System.out.println("对应的key不存在");
return -1;
}
public static void main(String[] args) {
TestHashBunk testHashBunk=new TestHashBunk();
testHashBunk.put(1,3);
testHashBunk.put(2,6);
testHashBunk.put(9,3);
testHashBunk.disPlay();
int val= testHashBunk.getValue(9);
System.out.println(val);
}
}
泛型的,重写了HashCode方法,让相同对象的hashCode相同
class Person{
public String id;
public Person(String id){
this.id=id;
}
@Override
public String toString() {
return "Person{" +
"id='" + id + '\'' +
'}';
}
/**
* 重写hashCode,让equals的对象的哈希码的值相同
* 哈希码@return
*/
@Override
public int hashCode() {
return Objects.hash(id);
}
}
/**
* @author 25043
*/
public class HashBunk2<K,V> {
static class Node<K,V>{
public K k;
public V v;
public Node<K,V> next;
public Node(K k,V v){
this.k=k;
this.v=v;
}
}
public Node<K,V>[] array= new Node[10];
public int usedSize;
/**
* 此时,方法当中的hash相当于之前直接key的值
* 因此,如果两个对象的hashCode一样,不一定equals一样
* 反之,equals一样,hashCode一定一样
*
* hashCode一样,说明一定会分配到同一个下标的位置
*
* @param k
* @param v
*/
public void put(K k,V v){
//不可以直接除,因为类型不一样
//int keyIndex=k%array.length;
//返回hashKey
int hash=k.hashCode();
int keyIndex=hash%array.length;
Node<K,V> node=new Node(k,v);
Node<K,V> cur=array[keyIndex];
while (cur!=null){
if(cur.k.equals(k)){
cur.v=v;
return;
}
cur=cur.next;
}
node.next=array[keyIndex];
array[keyIndex]=node;
usedSize++;
if(usedSize*1.0/array.length>0.75){
resize();
}
}
private void resize() {
Node<K,V>[] newNodeArray=new Node[2*array.length];
for (Node<K, V> kvNode : array) {
Node<K, V> cur = kvNode;
while (cur != null) {
Node<K, V> curNext = cur.next;
int newKeyIndex = cur.k.hashCode() % newNodeArray.length;
cur.next = newNodeArray[newKeyIndex];
newNodeArray[newKeyIndex] = cur;
cur = curNext;
}
}
array=newNodeArray;
}
public static void main(String[] args) {
Person person1=new Person("123");
Person person2=new Person("123");
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
}
}
面试题:
HashCode如果相同,那么是否对象相同,如果对象相同,是否HashCode一定相同
首先要明白的是,HashCode一定要再哈希表当中才会发挥出它的意义的,当一个类重写了HashCode方法时候,一般情况下面,会根据把指定需要比较的属性进行比较,调用Objec.hash()方法,获取到根据对应的属性产生的哈希值,比如如下操作:
比如此Person类重写了HashCode方法,传入的参数是 id,也就意味着,如果把此Preson放入到HashMap当中,id相同的对象会分配到同一个hashCode,也就是说,hashCode相同,会被分配到HashMap当中数组的同一个索引的位置。
而equals一样,则一定代表是同一个对象,那么它的对应的HashCode的值也一定是相同的
常见哈希冲突处理:闭散列(线性探测、二次探测)、开散列(链地址法)、多次散列
已知某个哈希表的n个关键字具有相同的哈希值,如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行()次探测。'
元素1:探测1次
元素2:探测2次
元素3:探测3次
。。。
元素n:探测n次
故要将n个元素存入哈希表中,总共需要探测:1+2+3+...+n = n*(n+1)/2