看完你觉得你真的了解Map吗

什么是Map

Map是一个接口类,该类没有继承自Collection,该类中存储的是<k,v>结构的键值对,并且k一定是唯一的,不能重复。
Map.Entry<K, V> 是Map内部实现的用来存放<key, value>键值对映射关系的内部类

注意:

  1. Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
  2. Map中存放键值对的Key是唯一的,value是可以重复的。
  3. 在Map中插入键值对时,key不能为空,否则就会抛NullPointerException异常,但是value可以为空。
  4. Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
  5. Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
  6. Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。

Map的两种实例化方式

HashMap

底层数据结构

在jdk1.7之前,HashMap底层是由数组+链表的方式实现的,在jdk1.8之后,改成了数组+链表+红黑树

哈希表

什么是哈希?

哈希是用来进行高效查找的一种数据结构,该种数据结构是通过某种方式(哈希函数)将元素与其在表格中的存储位置建立一一对应的关系,在查找时就不需要进行遍历,因此可以得到高效的查找。
哈希中可能会存在哈希冲突(碰撞),即不同元素通过相同哈希函数计算出相同的哈希地址。例如:占座

常见哈希函数

  • 直接定址法
  • 除留余数法
  • 折叠法
  • 平方取中法
  • 随机数法
  • 数学分析法
    注意:
    不论一个哈希函数设计有多精妙,都不能完全解决哈希冲突——哈希函数设计越精妙,产生冲突的概率越低。
    解决哈希冲突的方式:
  • 闭散列:从发生哈希冲突的位置开始,找“下一个”空位置
    1)线性探测:从发生哈希冲突的位置开始,逐个挨着依次往后查找,所以必须给每个位置设置状态,EMPTY、EXIST、DELETE
    在这里插入图片描述
    优点:找下一个地址的方式简单
    缺点:容易产生数据堆积——原因:找下一个空位置时挨着往后依次查找
    2)二次探测
    假设第一次计算出的哈希地址H0,第i次探测时,哈希地址为H(i)
    H(i)=H0+i2 H(i)=H0-i2
    优点:解决了线性探测容易产生数据堆积的问题
    缺点:如果空位置比较少,可能需要探测很多次
  • 开散列:链地址法拉链法
    哈希表中没有直接存元素,每个位置将来挂接都是一个链表,即:将发生冲突的元素通过链表的方式组织起来

实现原理

1.HashMap实现了Map接口
2.HashMap的默认初始容量是16

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

3.HashMap的最大容量是230

static final int MAXIMUM_CAPACITY = 1 >> 30

4.HashMap的默认负载因子是0.75

扫描二维码关注公众号,回复: 11357191 查看本文章
static final float DEFAULT_LOAD_FACTOR = 0.75f;

5.何时链表和红黑树相互转化
若链表中节点个数超过8时,会将链表转化为红黑树;若红黑树中节点小于6时,红黑树退还为链表;如果哈希桶中某条链表的个数超过8,并且桶的个数超过64时才会将链表转换为红黑树,否则直接扩容。
6. HashMap桶中放置的节点—该节点是一个单链表的结构
7.哈希函数

hashFunc(x) = x % capacity;

8.扩容机制

// 每次都是将cap扩展到大于cap最近的2的n次幂
static final int tableSizeFor(int cap) {
 int n = cap - 1;
 n |= n >>> 1;
 n |= n >>> 2;
 n |= n >>> 4;
 n |= n >>> 8;
 n |= n >>> 16;
 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }

9.LinkedHashMap在这里插入图片描述

  1. LinkedHashMap继承自HashMap,并实现了Map接口
  2. LinkedHashMap底层使用了哈希桶和双向链表两种结构
  3. LinkedHashMap需要重写HashMap中的:afterNodeInsertion/afterNodeAcess/afterNodeRemove等方法
  4. LinkedHashMap使用迭代器访问时,可以保证一个有序的结果,注意此处的有序不是自然有序,而是元素的插入次序。另外,当向哈希表中重复插入某个键的时候,不会影响到原来的有序性。也就是说,假设你插入的键的顺序为1、2、3、4,后来再次插入2,迭代时的顺序还是1、2、3、4,而不会因为后来插入的2变成1、3、4、2
  5. LinkedHashMap可以作为LRU来使用,但是要重写removeEldestEntry方法。

模拟实现

//哈希桶---> 数组 + 链表实现的---> 可以用来帮助用户快速定位要将元素插入到那个链表来组织链表
//数组中存储的元素实际为元素的引用
public class HashBucket {
    public static class Node{
        int key;
        int value;
        Node next;
        Node(int key, int value){
            this.key = key;
            this.value = value;
            next = null;
        }
    }
    //哈希桶中的成员数据
    Node[] table;
    int capacity;//表格的容量--》桶的个数
    int size;//有效元素的个数
    double loadFact = 0.75;
    public HashBucket(int initCap){
        //保证哈希桶的容量至少为10
        capacity = initCap;
        if(initCap < 10){
            capacity = 10;
        }
        table = new Node[capacity];
        size = 0;
    }
    public int put(int key, int value){
        //1.通过哈希函数,计算key所在的桶号
        int bucketNo = hashFunc(key);
        //2.在bucketNo桶中检测key是否存在
        //检测方式:遍历链表
        Node cur = table[bucketNo];
        while(null != cur){
            if(cur.key == key){
                int oldValue = cur.value;
                cur.value = value;
                return oldValue;
            }
            cur = cur.next;
        }
        //3.key不存在,将key-value插入到哈希桶中
        cur = new Node(key, value);
        cur.next = table[bucketNo];
        table[bucketNo] = cur;
        size++;
        return value;
    }
    //将哈希桶中为key的键值对删除
    public boolean remove(int key){
        //1.通过哈希函数计算key的桶号
        int bucketNo = hashFunc(key);
        //2.在bucketNo桶中找到key对应的节点,找到后删除
        Node cur = table[bucketNo];
        Node prev = null;
        while(null != cur){
            if(cur.key == key){
                //找到与key所对应的节点,将其删除
                if(null == prev){
                    //删除的节点刚好是第一个节点
                    table[bucketNo] = cur.next;
                }
                else{
                    //删除其他节点
                    prev.next = cur.next;
                }
                --size;
                return true;
            }
            else{
                prev = cur;
                cur = cur.next;
            }
        }
        return false;
    }

    public boolean containsKey(int key){
        //1.计算key所在的桶号
        int bucketNo = hashFunc(key);
        //2.在bucketNo桶中找key
        Node cur = table[bucketNo];
        while(null != cur){
            if(cur.key == key){
                return true;
            }
            cur = cur.next;
        }
        return false;
    }

    public boolean containsValue(int value){
        for(int bucketNo = 0; bucketNo < capacity; bucketNo++){
            Node cur = table[bucketNo];
            while(null != cur){
                if(cur.value == value){
                    return true;
                }
            }
        }
        return false;
    }

    public int size(){
        return size;
    }

    public boolean empty(){
        return 0 == size;
    }

    private void resize(){
        //装载因子超过0.75时进行扩容,按照2倍的方式进行扩容
        if(size*10 / capacity > loadFact*10){
            int newCap = capacity * 2;
            Node[] newTable = new Node[newCap];

            //将table中所有的节点搬到newTable中
            for(int i = 0; i < capacity; i++){
                Node cur = table[i];
                //将table中i号桶中所对应链表中所有的节点插入到newTable中
                while(null != cur){
                    table[i] = cur.next;
                    //将cur节点插入到newTable中
                    //1.计算cur在newTable中的桶号
                    int bucketNo = cur.key % newCap;
                    //2.将cur插入到newTable中
                    cur.next = newTable[bucketNo];
                    newTable[bucketNo] = cur;

                    //取table中i号捅的下一个节点
                    cur = table[i];
                }
            }
            table = newTable;
            capacity = newCap;
        }
    }

    private int hashFunc(int key){
        return key % capacity;
    }
    
}

TreeMap

TreeMap是一个Map实现,默认情况下会根据其键的自然顺序对其所有条目进行排序。

底层数据结构

红黑树:一棵二叉搜索树 + 增加节点的颜色限制以及性质约束,即最长路径节点个数一定不会超过最短路径中节点个数的2倍称为平衡树。

红黑树

红黑树是一棵平衡二叉树。

  1. 每个节点都只能是红色或黑色
  2. 根节点是黑色
  3. 每个叶节点(NIL节点、NULL)是黑色的
  4. 如果一个节点是红的,则它的两个子节点都是黑的,也就是说一条路径上不能出现相邻的两个红色节点
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
    在这里插入图片描述

实现原理

二叉搜索树:进行中序遍历,可以得到一个有序的序列。
树中最左侧节点一定是最小的节点,最右侧节点一定是最大的。
基本操作
a.查找
在二叉搜索树中找某个节点——最差情况下比较的就是树的高度
b.插入
空树——>直接插入,然后返回
非空:
①在二叉搜索树中找待插入节点的位置,在找的过程中,必须要保存双亲
②插入新节点
c.删除
①找待删除节点在树中的位置,若未找到,直接返回;反之,则继续;
②删除该节点cur
cur有以下几种情况:
1)cur是一个叶子节点
2)cur只有右孩子
3)cur只有左孩子
4)cur左右孩子均存在,该节点不能直接删除,在其子树中找一个替代节点然后再进行删除

模拟实现

public class BSTree {
    public static class BSTNode{
        BSTNode left = null;
        BSTNode right = null;
        int val;
        public BSTNode(int val){
            this.val = val;
        }
    }
    private BSTNode root = null;
    boolean contains(int val){
        BSTNode cur = root;
        while(cur != null){
            if(val == cur.val){
                return true;
            }
            else if(val < cur.val){
                cur = cur.left;
            }
            else{
                cur = cur.right;
            }
        }
        return false;
    }

    //将val插入到二叉搜索树中,插入成功返回true,否则返回false
    public boolean put(int val){
        if(null == root){
            root = new BSTNode(val);
            return true;
        }
        BSTNode cur = root;
        BSTNode parent = null;
        while(cur != null){
            parent = cur;
            if(val < cur.val){
                cur = cur.left;
            }
            else if(val > cur.val){
                cur = cur.right;
            }
            else{
                return false;
            }
        }
        //找到带插入结点的位置 ---》插入新节点
        //将新节点插到parent的左侧或右侧
        cur = new BSTNode(val);
        if(val < parent.val){
            parent.left = cur;
        }
        else{
            parent.right = cur;
        }
        return true;
    }

    boolean remove(int val){
        if(null == root){
            return false;
        }
        //非空--找待删除节点的位置
        BSTNode cur = root;
        BSTNode parent = null;
        while(cur != null){
            if(val == cur.val){
                break;
            }
            else if(val < cur.val){
                parent = cur;
                cur = cur.left;
            }
            else{
                parent = cur;
                cur = cur.right;
            }
        }
        //没有找到
        if(cur == null){
            return false;
        }
        //已经找到待删除节点在树中的位置
        //必须要对cur的孩子节分点分情况
        //1.没有孩子
        //2.只有左孩子
        //3.只有右孩子
        //4.左右孩子均存在
        if(null == cur.left){
            //cur只有右孩子
            if(null == parent){
                //cur双亲不存在
                root = cur.right;
            }
            else{
                //双亲存在
                if(cur == parent.left){
                    parent.left = cur.right;
                }
                else{
                    parent.right = cur.right;
                }
            }
        }
        else if(null == cur.right){
            //cur只有左孩子
            if(null == parent){
                //双亲不存在
                root = cur.left;
            }
            else{
                //双亲存在
                if(parent.right == cur){
                    parent.right = cur.left;
                }
                else{
                    parent.left = cur.left;
                }
            }
        }
        else{
            //cur节点的左右孩子均存在--不能直接删除
            //在cur子树中找一个替代节点删除
            //方式一:在其右子树中找最小的节点:即最左侧节点
            //方式二:在其左子树中找最大的节点:及最右侧节点
            BSTNode del = cur.right;
            parent = cur;
            while(null != del.left){
                parent = del;
                del = del.left;
            }
            //替代节点找到
            cur.val = del.val;
            //删除替代节点
            if(del == parent.left){
                parent.left = del.right;
            }
            else{
                parent.right = del.right;
            }
        }
        return true;
    }

    public void inOrder(){
        inOrder(root);
    }

    private void inOrder(BSTNode root){
        if(null != root){
            inOrder(root.left);
            System.out.println(root);
            inOrder(root.right);
        }
    }
    //最左侧节点--最小的节点
    public int mostLeft(){
        if(null == root){
            //抛异常--空指针异常
        }
        BSTNode cur = root;
        while(cur.left != null){
            cur = cur.left;
        }
        return cur.val;
    }

    //最右侧节点
    public int mostRight(){
        if(null == root){
            //抛异常--空指针异常
        }
        BSTNode cur = root;
        while(cur.right != null){
            cur = cur.right;
        }
        return cur.val;
    }
}

HashMap与TreeMap的比较

相同点

都存储的是k-v键值对
都实现了Map接口

不同点

TreeMap HashMap
红黑树 哈希桶
O(logN) O(1)
关于key有序 不一定
需要key有序 不关心key有序,需要更高的查询效率

猜你喜欢

转载自blog.csdn.net/qq_43452252/article/details/105897058
今日推荐