Java集合之Map

Posted 小王子jvm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java集合之Map相关的知识,希望对你有一定的参考价值。

Map接口

Map接口概述

Map与Collection并列存在。用于保存具有映射关系的数据:key-value

Map 中的 key 和 value 都可以是任何引用类型的数据,Map 中的 key 用Set来存放,不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法

常用String类作为Map的“键”

key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到 唯一的、确定的 value

Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中,HashMap是 Map 接口使用频率最高的实现类

共有的方法

//添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据

//元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
    
//元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

简单的操作:

public static void main(String[] args) 
    HashMap map = new HashMap();
    map.put("a",1);
    map.put("b",4);
    map.put("g",4);
    System.out.println("所有的KEY如下:");
    Set set = map.keySet(); //获取Key的Set集合
    //遍历方式1
    Iterator iterator = set.iterator();
    while (iterator.hasNext())
        System.out.println(iterator.next());
    
    //遍历方式二
    for (Object o : set) 
        System.out.println(o);
    
    System.out.println("======================");
    //获取所有的Value
    Collection values = map.values();
    for (Object value : values) 
        System.out.println(value);
    
    //获取指定的Value
    Object a = map.get("a");
    System.out.println(a);

    System.out.println("------------------");
    Set set1 = map.entrySet();
    for (Object o : set1) 
        System.out.println(o);  //a=1,b=4,g=4
    

HashMap实现类

基本知识

HashMap是 Map 接口使用频率最高的实现类。允许使用null键和null值,与HashSet一样,不保证映射的顺序。

所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()

所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()

一个key-value构成一个entry,所有的entry构成的集合是Set:无序的、不可重复的

HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。

HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true

  • JDK 7及以前版本:HashMap是数组+链表结构(即为链地址法)
  • JDK 8版本发布以后:HashMap是数组+链表+红黑树实现。

JDK8源码分析

传统 HashMap 的缺点(JDK7的HashMap的缺点)

  • JDK 1.8 以前 HashMap 的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。
  • 当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有 n 个元素,遍历的时间复杂度就是 O(n), 完全失去了它的优势。

说明一下:在 java 编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap 也不例外。HashMap 实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

前置知识之hash函数

先来理解什么是hash函数,这个东西啊,就是一种计算的方式,就是把所有的值通过表达式计算得到我们的表达式想要的值。

举个例子

我们假设有一个数组a[10],我们存放数据的时候可以先把要存放的数据计算得到一个0—9的下标,然后存放到对应的位置:(简单直接的代码)

public class firve 
    public static void main(String[] args) 
        int a[] = new int[10];
        // 假设我们存放的数据为23,45.这个时候先获取hash值作为存放在这个数组的下标。
        int num1 = getHash(23);
        a[num1] = 23;

        int num2 = getHash(45);
        a[num2] = 45;

        //如果我们要取出这个数据呢?同样的,调用这个getHash方法获取下标即可
        int n = getHash(23);
        int myNum = a[n];
        System.out.println(myNum);
    

    public static int getHash(int a)
        return a % 10;
    

这个代码很简单,计算的表达式也很简单,时间复杂度直接降为O(1),如果不用这种方式呢,我们就需要遍历整个数组进行比对。

但是这样还是有问题的,比如假设11和21对10取余都是1,但是只有一个位置怎么办呢,这个就是常说的Hash冲突了。

Hash冲突

显然,一个数组的容量始终是有限的,如果设置的太小,冲突就会非常的频繁,但是如果太大呢,又非常的浪费空间。想到我们有一种东西可以无限容量,有多少数据就放出多少空间的结构,那就是链表。

我们把所有的数据存放在节点之中,对于没有冲突的,把这个节点直接放在数组中,然后把冲突了的放在已有的节点的后面,就像这个样子:

public class Test 
    public static void main(String[] args) 
        Node node1[] = new Node[10];	//假设长度10
        
        int hash1 = getHash(11);	//没有冲突的情况
        node1[hash1] = new Node(11,null);
        
        int hash2 = getHash(13);
        node1[hash2] = new Node(13,null);

        //假设冲突了,这个时候原有的数据不应该被覆盖
        int hash3 = getHash(23);
        Node node3 = new Node(23,null);
        node1[hash2].next = node3;	//原来的数据的指针指向这个新的数据

        //取数据
        int n = getHash(23);
        while (node1[n].num != 23)
            node1[n] = node1[n].next;
        
        System.out.println(node1[n].num);
    

    public static int getHash(int a)
        return a % 10;
    

//Node的结构:
public class Node 
    int num;
    Node next;

    public Node(int num, Node next) 
        this.num = num;
        this.next = next;
    

画个图理解:

从上图中可以看出,采用数组加链表的结构方式,就可以非常好的解决数组长度不够问题,但是这样又会有一个新的问题,如果冲突的元素非常多呢?那冲突的那条链可就太长了,查询就跟普通链表没差别,怎么办呢,这个时候就有会有新的结构来补充现有链表的不足——树(本质依然是链表)

前置知识之二叉树结构

假设我们链表多一个指针,一共两指针,一个指向左边,一个指向右边。就像这样:

public class Tree 
    int num;
    Tree R;
    Tree L;

    public Tree(int num, Tree r, Tree l) 
        this.num = num;
        R = r;
        L = l;
    

假设每次添加元素都进行比较,如果大于这个元素就放在这个节点的右边,小于放在左边。于是之前的代码写成这个样子:

public class TestTree 
    public static void main(String[] args) 
        Tree tree[] = new Tree[10];
        int n1 = getHash(11);
        tree[n1] = new Tree(11,null,null);
        int n2 = getHash(13);
        tree[n2] = new Tree(13,null,null);

        //这个元素比冲突的元素大,于是放在右边
        int n3 = getHash(23);
        Tree tree1 = new Tree(23,null,null);
        tree[n2].R = tree1;

        //这个元素比那个小,放在左边
        int n4 = getHash(3);
        Tree tree2 = new Tree(3,null,null);
        tree[n2].L = tree2;

        //取数据
        int num = getNum(tree[n2], 23);
        System.out.println(num);
    
    public static int getHash(int a)
        return a % 10;
    
    
    //递归遍历
    public static int getNum(Tree tree,int num)
        if (tree.num > num)
            return TestTree.getNum(tree.L,num);
        else if (tree.num < num)
            return TestTree.getNum(tree.R,num);
        else
            return tree.num;
        
    

画个图理解一下:

这样一来很好的解决了大量数据冲突造成一条长长的链的麻烦。但是这样难道就没有问题了吗:

假设出现这种情况呢,就是那么的碰巧这个数据都比初始节点小,又出现了一条长长的链,这该怎么解决,这个时候又出现了一种改进的树——平衡二叉树。

前置知识之平衡二叉树(AVL)

平衡二叉树就是为了解决二叉查找树退化成一颗链表而诞生了,平衡树具有如下特点

  • 具有二叉查找树的全部特性(左边节点比根节点小,右边比这个大)
  • 每个节点的左子树和右子树的高度差至多等于1

例如:图一就是一颗平衡树了,而图二则不是(节点右边标的是这个节点的高度)

对于图二,因为节点9的左孩子高度为2,而右孩子高度为0。他们之间的差值超过1了。

平衡树基于这种特点就可以保证不会出现大量节点偏向于一边的情况了。

听起来这种树还不错,可以对于图1,如果我们要插入一个节点3,按照查找二叉树的特性,我们只能把3作为节点4的左子树插进去,可是插进去之后,又会破坏了AVL树的特性,那我们那该怎么弄?

左-左型 右旋

我们把这种倾向于左边的情况称之为 左-左型。这个时候,我们就可以对节点9进行右旋操作,使它恢复平衡。

即:顺时针旋转两个节点,使得父节点被自己的左孩子取代,而自己成为右孩子,同时原来的左孩子的右孩子成为这个还没有变化前的父节点的左孩子(这句话特别拗口,多读几遍,对着图多理解几遍,以为接下来的旋转都是这样类似!)

再比如:

节点4和9高度相差大于1。由于是左孩子的高度较高,此时是左-左型,进行右旋。

这里要注意,节点4的右孩子成为了节点6的左孩子了

用一个动图表示:

右-右型 左旋

左旋和右旋一样,就是用来解决当大部分节点都偏向右边的时候,通过左旋来还原。例如:

我们把这种倾向于右边的情况称之为 右-右型

右-左型

出现了这种情况怎么办呢?对于这种 右-左型 的情况,单单一次左旋或右旋是不行的,下面我们先说说如何处理这种情况。

处理的方法是先对节点10进行右旋把它变成右-右型。

然后在进行左旋。

所以对于这种 右-左型的,我们需要进行一次右旋再左旋

同理,也存在 左-右型的。对于左-右型的情况和刚才的 右-左型相反,我们需要对它进行一次左旋,再右旋。

所以对于刚才那种情况处理过后就是这个样子:

总结一下

在插入的过程中,会出现一下四种情况破坏AVL树的特性,我们可以采取如下相应的旋转。

  1. 左-左型:做右旋。

  2. 右-右型:做左旋转。

  3. 左-右型:先做左旋,后做右旋。

  4. 右-左型:先做右旋,再做左旋。

代码的实现

//节点结构
class AvlNode 
    int data;
    AvlNode L;//左孩子
    AvlNode R;//右孩子
    int height;//记录节点的高度


//AVL结构
public class AvlTree
    //计算每个节点的高度
    static int getHeight(AvlNode node)
        if (node == null)
            return -1;
        else
            return node.height;
        
    

    //对于左左型,右旋操作
    static AvlNode R_Rotate(AvlNode node)
        //暂存这个节点的左子节点
        AvlNode temp = node.L;
        //将这个节点变为左孩子的右节点
        node.L = temp.R;
        temp.R = node;

        //重新计算这个根节点的左右节点的高度
        node.height = Math.max(getHeight(node.L),getHeight(node.R)) + 1;
        temp.height = Math.max(getHeight(temp.L),getHeight(temp.R)) + 1;

        return temp;
    

    //右右型,左旋
    static AvlNode L_Rotate(AvlNode node)
        //暂存这个节点的左子节点
        AvlNode temp = node.R;
        //将这个节点变为左孩子的右节点
        node.R = temp.L;
        temp.L = node;

        //重新计算这个根节点的左右节点的高度
        node.height = Math.max(getHeight(node.L),getHeight(node.R)) + 1;
        temp.height = Math.max(getHeight(temp.L),getHeight(temp.R)) + 1;

        return temp;
    

    //左-右型,进行左旋,再右旋
    static AvlNode L_R_Rotate(AvlNode node) 
        //对左子树进行左旋
        node.L = L_Rotate(node.L);
        //此时变成了左左型,再进行一次右旋即可
        return R_Rotate(node);
    

    //右-左型,进行右旋,再左旋
    static AvlNode R_L_Rotate(AvlNode node) 
        //对右子树进行右旋
        node.R = R_Rotate(node.R);
        //此时变成了右右型,再进行一次左旋即可
        return L_Rotate(node);
    
    
    //插入数值操作
    static AvlNode insert(int data, AvlNode T) 
        if (T == null) 
            T = new AvlNode();
            T.data = data;
            T.L = T.R = null;
         else if(data < T.data) 
            //向左孩子递归插入
            T.L = insert(data, T.L);
            //进行调整操作
            //如果左孩子的高度比右孩子大2
            if (getHeight(T.L) - getHeight(T.R) == 2) 
                //左-左型
                if (data < T.L.data) 
                    T = R_Rotate(T);
                 else 
                    //左-右型
                    T = R_L_Rotate(T);
                
            
         else if (data > T.data) 
            T.R = insert(data, T.R);
            //进行调整
            //右孩子比左孩子高度大2
            if(getHeight(T.R) - getHeight(T.L) == 2)
                //右-右型
                if (data > T.R.data) 
                    T = L_Rotate(T);
                 else 
                    T = L_R_Rotate(T);
                
        
        //否则,这个节点已经在树上存在了,我们什么也不做

        //重新计算T的高度
        T.height = Math.max(getHeight(T.L), getHeight(T.R)) + 1;
        return T;
    

虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋右旋来进行调整,使之再次成为一颗符合要求的平衡树。

显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树

前置知识之红黑树

红黑树在原有的二叉树上增加了一些新的特新:

  • 根节点是黑色的。

  • 每个叶子节点是黑色的空节点(NIL),也就是叶子节点不存储数据

  • 任何相邻的节点不能同时为红色,也就是说,红色节点是被黑色节点隔开的

  • 如果一个结点是红色,那么它的子节点是黑色(也就是每个红色节点有两黑色子节点)

  • 对于每个节点,从该节点到所有可达的叶子节点中黑色的节点数目相等(简称黑高)

性质4和性质5两个性质作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍!!!

原因:

当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。举例说明一下,请看下图:

可以认为红黑树是一种近似平衡的概念

平衡二叉查找树的初衷,是为了解决二叉查找树因为动态更新导致的性能退化问题。所以,“平衡”的意思可以等价为性能不退化。“近似平衡”就等价为性能不会退化的太严重。棵极其平衡的二叉树(满二叉树或完全二叉树)的高度大约是 log2n,所以如果要证明红黑树是近似平衡的,只需要分析,红黑树的高度是否比较稳定地趋近 log2n 就好了

也就是说为了高效的增加和删除有了二叉树,但是会出现极端情况造成查询性能降低,于是有了平衡二叉树,但是这种树结构太过于严格,导致增加,修改和删除新能下降,于是可以理解为红黑树是这两种方案的这种,具有不错的增修删的性能,也具有非常好的查询性能。

对于每一个节点,我们假定标记为红色或者黑色,这样在节点变化时候可以通过重新标记节点颜色来调整树,如果不能平衡再通过旋转达到平衡状态。这个过程就是红黑树保持平衡的重要两点。

红黑树的性质:http://blog.csdn.net/cyp331203/article/details/42677833(看这位大神)

java之map接口

java集合类源码分析之Map

JAVA 基础之容器集合(Collection和Map)

Java之集合

java集合之Map

JAVA集合框架之Map