欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

HashMap接口总结和常见面试问题

程序员文章站 2024-03-23 22:14:28
...

HashMap集合

一、 初识hashmap

之前学过ArrayList,LinkedList。
(1)ArrayList底层是以数组实现的,插入删除慢,查询快。
(2)LinkedList底层是以链表实现的,插入删除快,查询慢。
hashmap结合了以上二者的有点,hashmap底层是数组加单链表的形式,以(key—value)键值对结构存储数据。

二、HashMap源码分析

一)集合特点

public class HashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable
    
 static class Entry<K,V> implements Map.Entry<K,V> 
 
 private abstract class HashIterator<E> implements Iterator<E>

从继承的接口来看,有如下特点:
1、Map:是存放一对值的最大接口!即接口中的每个元素都是一对, 以key --> value的形式保存,并且Key是不重复的,元素的存储位置由key决定。也就是可以通过key去寻找key-value的位置,从而得到value的值。适合做查找工作。
/2、Cloneable*:Cloneable是标记型的接口,它们内部都没有方法和属性,实现 Cloneable来表示该对象能被克隆,能使用Object.clone()方法。如果没有实现 Cloneable的类对象调用clone()就会抛出CloneNotSupportedException。
3、Serializable:public interface Serializable类通过实现 java.io.Serializable 接口以启用其序列化功能。
4、 Iterator:可以使用迭代器遍历

二)构造函数 默认值

这里列举几个常见的初始值
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16:默认初始化的容器大小16;
static final int MAXIMUM_CAPACITY = 1 << 30; //最大的数据容量2的30次方。也就是说最多存放2的30次方个数据;
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认的加载因子 0.75f( 散列表的加载因子=填入表中的元素个数/散列表的长度,加载因子越大,说明空闲位置越小,冲突越多,散列表的性能会下降。)

hashmap提供了三个构造函数
1、 public HashMap(int initialCapacity, float loadFactor)
生成空的HashMap,传入容量和负载因子。容量和负载因子都是自定义的不能小于0的值,根据具体实际情况而定。
2、 public HashMap(int initialCapacity)
生成空的HashMap,传入初始容量,负载因子使用默认值(0.75)。
3、 public HashMap()
生成空的HashMap,初始容量和负载因子全为默认值。

三)增

和其他集合一样,增删改查同样是重点。

增的时候同样考虑1、什么时候开始初始化。2、是否需要扩容。3、计算增加到那个数组下标下,然后在此下标下使用链表的头插法插入链表中。
HashMap接口总结和常见面试问题
A:如果是第一次增加,调用构造函数进行初始化。
B:通过key计算index,在index链表下添加。{
a、key重复时新值替换旧值
b、key不重复时使用头插法插入
c、key为null时,添加在0号下标中,为防止空指针异常,需特殊处理
}
C:是否需要扩容,加载因子判断,为减少哈希冲突。扩容涉及到重新哈希,所以加载因子要设置合适

源码大家都有,附上自己的实现的源码:

 public int hash(K key) {   // 计算扰动处理后的hashcode
        if (key == null) {
            return 0;    //对null特殊处理
        }
        int h = key.hashCode(); //是一个负数
        h ^= (h >>> 20) ^ (h >>> 12);
        h = h ^ (h >>> 7) ^ (h >>> 4);  //(1)扰动处理  : 降低hashcode的 重复率,降低index的重复率
        return h;//table.length 永远是2的幂
        //h & (table.length - 1)   ==   h % table.length (2) 位运算符的计算效率高于算数运算符
        //(3)并且能够解决index为负数的问题
        //前提条件是 table.length 是2的幂
    }

    public int indexOf(int h) {  //计算index
        return h & (table.length - 1);
    }
 public void put(K key, V value) {
        if (table == Empty_Table) {  // 是否时第一次添加元素
            table = new Entry[DEFAULT_CAPCITY];
        }
        int h = hash(key);
        int index = indexOf(h);//计算key值对应的下标
        //e.key.equals(key)  比较速率较慢
        for (Entry<K, V> e = table[index]; e != null; e = e.next) {//遍历下标下的链表
            if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {//如果key值相同,则值替换
                e.value = value;
                return;  //如果key有重复  值替换掉之后方法就退出
            }
        }
        //代码走到着说明key 没有重复  头插法
        if (size >= table.length * threshold) {
            resize();  //如何扩容:2倍扩容  始终能够保证table.length是2的幂
            index = indexOf(hash(key));
        }
        //添加数据
        table[index] = new Entry<>(key, value, table[index], h);//不相同则头插
        size++;
    }
     public void resize() {
        int old_length = table.length;
        int new_length = 2 * old_length;
        Entry[] new_table = new Entry[new_length];
        //老数组中的元素放入到新数组中  由于数组长度变了,所以要重新计算每一个元素的下标
        for (Entry<K, V> e : table) {
            while (e != null) {
                Entry<K, V> next = e.next;
                int index = indexOf(hash(e.key));
                e.next = new_table[index];
                new_table[index] = e;
                e = next;
            }
        }
        table = new_table;
    }

四)删

删除时要考虑的是删除的是链表上的节点,所以也就变成了单链表节点的删除。回顾之前写的单链表删除操作,
HashMap接口总结和常见面试问题
需要找到待删节点的前驱节点,将前驱节点的next指向待删节点的后继,再把value,key域置为空。这样就需要考虑一个特殊情况,如果待删节点是头结点,没有前驱节点,所以需要单独处理,记得更新头结点。
同样附上自己实现的源码(这里用了两个指针):

 public Boolean remove(K key) {
        if (size <= 0) {
            return false;
        }
        int h = hash(key);
        int index = indexOf(h);
        Entry<K, V> pre = table[index];
        Entry<K, V> e = table[index];
        while (e != null) {
            Entry<K, V> next = e.next;
            //第一个循环进来如果判断相等就成立 删除的的就是头节点  pre == e
            if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {
                size--;
                //如果删除的节点是头节点
                if (pre == e) {  //判断删除的节点就是头节点
                    table[index] = next;
                } else {
                    pre.next = next;
                }
                e.next = null;
                e.key = null;
                e.value = null;  //只有指向空之后才能进行垃圾回收
                return true;
            }
            pre = e;
            e = next;   //pre 变成了e的前驱
        }
        return false;
    }

五)改

找到key键,新value替换旧value


    public void replace(K key, V new_value) {//通过key 找到对应的节点 然后将value替换成新的value
        if (size <= 0) {
            return;
        }
        int h = hash(key);
        int index = indexOf(h);
        for (Entry<K, V> e = table[index]; e != null; e = e.next) {//遍历下标下的链表
            if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {
                e.value = new_value;
            }
        }
    }

六)查

根据key查找value
时间复杂度O(1)
public V get(K key) { //通过key 找到对应的节点 然后将value返回
if (size <= 0) {
return null;
}
int h = hash(key);
int index = indexOf(h);
for (Entry<K, V> e = table[index]; e != null; e = e.next) {//遍历下标下的链表
if (e.hash == h && (e.key == key || (key != null && key.equals(e.key)))) {
return e.value;
}
}
return null;
}

七)迭代器实现

``

   public Iterator<Entry<K, V>> iterator() {
        return new HashIterator();
    }
    class HashIterator implements Iterator<Entry<K, V>> {
        Entry <K,V> next;        // 下一个要被遍历的节点
        Entry <K,V> current;     // 当前正在被遍历的节点
        int index; //当前所在的数组的下标
        HashIterator (){
            if(size >0){
                Entry []t=table ;
                while (index <t.length &&(next =t[index ++])==null ){
                    ;//找到第一个节点
                }
            }
        }

        @Override
        public boolean hasNext() {  // 判断是否有下一个元素
           return next !=null ;//index和size之间的关系
        }


        final    Entry<K, V> nextEntry() {   // 取出元素并且移动到下一个元素的位置
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if ((next = e.next) == null) { //如果下一个元素为空
                Entry[] t = table;
                while (index < t.length && (next = t[index++]) == null)//找到下一个不为空的节点
                    ;
            }
            current = e;//更新当前节点的位置
            return e;

        }

        @Override
        public Entry<K, V> next() {
            return nextEntry() ;
        }

        @Override
        public void remove() {
            if (current == null) {//如果当前节点为空
                throw new IllegalStateException();
            }
            K k = current.key;//保存k值
            current = null;//更新current
            MyHashMap .this.remove( k);//调用删除方法
        }
    }

遍历方式

 //1、迭代器遍历
        Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, Integer> next = iterator.next();
            System.out.println("key:" + next.getKey() + "   value:" + next.getValue());
            //System.out.println(iterator .next() .getKey() + iterator .next() .getValue() );
        }
        System.out.println("==================");

        //2、遍历key值,位置由key值决定
        Iterator<String> iterator1 = map.keySet().iterator();
        while (iterator1.hasNext()) {
            String key = iterator1.next();
            System.out.println("key:" + key);
        }
        System.out.println("===================");

        //3、遍历value
        Iterator<Integer> iterator2 = map.values().iterator();
        while (iterator2.hasNext()) {
            Integer value = iterator2.next();
            System.out.println("value" + value);
        }
        System.out.println("=================");

        //foreach遍历key ,value
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println("key: " + entry.getKey() + "   value: " + entry.getValue());
        }
    }
八、哈希冲突

当同一个index对应多个key,这就叫哈希冲突
比如15%8=7
23%8=7
31%8=7
jdk1.7版本中,hashmap中解决此方法用的是链地址法
链地址法的不足:
① 链地址法存在的不足在于,当产生哈希冲突的节点越多,那么数组索引下标查找就会变成链表的遍历操作。数组的优势在于随机取值,时间复杂度达到O(1),而链表的查找时间效率达到O(n)。
② 当产生数组扩容时,所有的元素节点都必须进行重新哈希。

我还了解到线性探测法来解决哈希冲突
线性探测法:主要根据index = val.hashcode()%element.length来解决哈希冲突问题。从数组当前位置(hashcode())开始,找到下一个空位置,进行存放。当数组填充满之后,进行数组的扩容操作,继续存放值。存储的键值对中键相等进行值的覆盖操作。进行扩容后的操作,还得进行元素的重新哈希操作。
线性探测法的不足:线性探测法当产生哈希冲突的键值对越多,遍历数组的次数就会越多。

九、常见关于hashmap面试题的整理

1.HashMap的特点
存储双值 key value
Map<K,V>
以key --> value的形式存储数据
并且Key是不重复的,key可以为null,元素的存储位置由key决定。
也就是可以通过key去寻找key-value的位置。
从而得到value的值。
适合做查找工作。

Cloneable 可以使用clone
Serializable 可以被序列化
Map.Entry 可以存储key-value具体数值。
Iterator 迭代器 只能从前往后进行遍历

2.HashMap如何计算key-value结构的存储位置
先取出key的hashCode,然后对HashCode进行扰动处理,然后将扰动处理后的HashCode和tablelength -1 进行
按位与 —》 index 这个index就是key-Value结构存储的下标。
3.哈希冲突
当多个key对应同一个数组下标。
h & (length-1) length 是 2的幂
h % length --》 index
15 % 8 = 7
23 % 8 = 7
31 % 8 = 7 哈希冲突

4.解决哈希冲突的方法,HashMap使用哪种方法
1.7 数组+链表的结构 – 链地址法
1.8 数组+链表的结构 当一个链表上节点大于8个时 改用数组+红黑树 – 链地址法
遍历链表的时间复杂度O(n)
红黑树 查找的时间复杂度(log2N)

除了链地址法外,你还了解哪些解决哈希冲突的方法(线性探测法)

5.HashMap get方法的时间复杂度
通过key计算除index 遍历链表
O(1)
答:时间复杂度近似于O(1),由于哈希冲突存在所以不一定能够达到O(1).
6.为什么hashcode要进行扰动处理
(1)扰动处理 : 降低hashcode的 重复率,降低index的重复率
h1 & (length-1) = 8
h2 & (length-1) = 8

h1’ & (length-1) =
h2’ & (length-1) =

7.为什么不直接采用经过hashCode()处理的哈希码 作为 存储数组table的下标位置?
保证index一定时小于数组长度。防止数组下标访问越界。

8.为什么hashmap的容量要保持2的幂
为了用 & (length-1) 代替 % length 。

9.为什么用按位与代替取余
因为位运算的计算效率高于 算数运算。

10. HashMap中如果添加重复key会怎样
新添加进来的key对应的newvalue代替之前老的value。

11.如何判断添加了重复的key
(1)先判断添加的key是否为空,如果为null在零号下标对应的节点处寻找key为null的节点。
(2)先判断hashcode 在判断 引用地址是否相同 最后判断equals
先比较hashcode 如果hashcode 不同,那么一定key不相同。如果hashcode 相同这两个key有可能是相同的key
然偶我们再去比较引用地址和equals

12.如果自己指定HashMap的初始容量和加载因子,那么容量的大小,和加载因子的大小对HashMap有什么影响
初始容量:如果我们指定不是2的幂,源码中有函数能够保证将最终数组容量改变到2的幂。向上取整找最近的2的幂。
初始容量越小,越容易发生哈希冲突。
加载因子 :越大 。对扩容时机有影响。
加载因子越大扩容的时机越晚,哈希冲突的几率越大,空间利用率越大。
加载因子越小,越大扩容的时机越早,哈希冲突的几率越小,空间利用率越小。

13.为什么HashMap需要扩容
不是因为没有足够存储空间。
扩容之后要重新计算每一个节点对应的index,哈希冲突概率降低。

14.HashMap的使用场景
使用场景 hashMap查询效率比较高,并且可以通过key去查询对应value
10学生
List.add(90) 100 70
10 用户信息 user(密码,sex,age,…);
list.add(user1)list.add(user2) …
HashMap<String,User>
map.put(“zhanshan”,user1);
map.get(“zhanshan”)
存储数据,可以快速根据key获取value

计数 :随机输入任意个任意字符 统计每个字符的数量。
我输入a 返回a出现的次数

 public static void  cal(String str, Map<Character ,Integer >  map) {
        System.out.println("输入要查询的字母");
        Scanner scanner =new Scanner(System .in ) ;
        Character  c=scanner.next() .charAt(0) ;
        for (int i = 0; i < str.length(); i++) {  //遍历字符串中的字母  cha
            Character cha = str.charAt(i);
            if (cha >= 'a' && cha <= 'z' || cha <= 'Z' && cha >= 'A') {
                if (map.containsKey(cha)) {
                    int count=map .get(cha );
                    map.put(cha, count+1) ;//如果map中已经有key,则使count值加一,用put方法计入value中
                } else {
                    map.put(cha, 1);//如果key是第一次出现,则把value值设为1
                }
            }
        }
        System.out.println("该字母的个数为:  "+map .get(c) );
    }

    public static void main(String[] args) {
        //字母       字母的个数
        HashMap <Character,Integer> map =new HashMap<>() ;
        String str = "dfhjgsajdkgasdgsadrewruhu";
        cal(str,map);
    }

十、hashmap的使用场景

由于hashmap可以根据key快速查找到value,所以多用于查询。
10学生
List.add(90) 100 70
10 用户信息 user(密码,sex,age,…);
list.add(user1)list.add(user2) …
HashMap<String,User>
map.put(“zhanshan”,user1);
map.get(“zhanshan”)
存储数据,可以快速根据key获取value
这里要考虑如果key值可以重复,需要将key对应的信息设置为一个类,添加时为new class()这样就不会出现新值替换旧值的情况了。

十一、hashmap和hashtable的区别

Hashtable	和HashMap底层实现和方法使用基本类似,以下是二者的区别

1、继承的父类和接口:
Hashtable :extends Dictionary<K,V>
HashMap:extends AbstractMap<K,V>
2、默认容量
Hashtable :11
HashMap:16
3、Table的初始化时机
T: 构造函数中初始化,new对象时。
M: 当第一次put元素。
4、并发操作,集合是否是线程安全的
T:由于加了synchronized关键字,所以是线程安全。但是并不是绝对的线程安全。
由于Hashtable效率低已经倍淘汰,所有多线程环境我们尽量选择
ConcurrentHashMap。
M: 线程不安全的。
5、数据遍历的方式
T: hashTable 可以使用Enumeration进行遍历
也可以使用Iterator进行遍历。
M: 只可以使用Iterator进行遍历。
6/是否支持fast-fail(快速失败机制)
T: 使用Iterator进行遍历时支持使用Enumeration进行遍历时不支持。
M: 使用Iterator进行遍历时支持。

6、是否可以添加重复的key
T,M: 不能,会用新值替代老值
7、是否接受值为null的Key 或Value?
T: Key和value都 不可以为空,否则会报出空指针异常。
M:Key和value可以为空。
根据hash值计算数组下标的算法 Hash值的计算:hashSeed ^ k.hashCode() 没有扰动处理
Index 重复率会高
8、计算index:
T: hash & 0x7FFFFFFF) 保证hash的值不为负数
(hash & 0x7FFFFFFF) % tab.length;
保证index不会大于tab.length; h ^= k.hashCode();
M: h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4); 有扰动处理 减低哈希冲突
9、计算index:
T: h & (length-1);
M: 保证index不会大于tab.length;
9、Entry数组的长度
T: 11
M: 16 始终保持他是2的幂
10、LoadFactor负荷因子
T,M: 0.75 作用也是相同 。size 》= 数组的长度 * 负荷因子 进行扩容
11、负荷超过(loadFactor * 数组长度)时
T: 内部数据的调整方式 *2 + 1 奇数 取余之后的结果相比于偶数更分散。
M: 2倍的扩容
T,M:两者都会根据key重新计算所有元素的存储位置,算法相似,时间空效率相同。