Map和Set(搜索二叉树和哈希表)

前言:

        我们已经学了大部分的关于Java的集合框架,接下来我们就要来了解剩余的两个Map和Set。

        从图中可以看到它们都是接口,接下来就让我们先来笼统的了解一下。

TreeSet和TreeMap简介:

        TreeSet和TreeMap背后都是一颗搜索树(红黑树)。如果要学好他们,我们要先学二叉搜索树,之后学习AVL树(高度平衡的二叉搜索树),最后学习红黑树。

搜索二叉树:

        我们需要先来了解什么是搜索二叉树:根节点右边每一颗树都比根节点大,左边每一棵树都比根节点小。

        对二叉搜索树进行中序遍历即可得到一个升序的数组。

        但是总会出现一些你不想看到的情况,比如这种极端情况:

        通过以上图形,我们也就知道了这些树直接的关系,那么接下来,我们就要去实现一颗搜索二叉树了。

搜索二叉树的具体实现:

定义一个树类:

        接下来我们就来完成一个搜索二叉树,首先我们先定一个搜索二叉树的类。

public class BinarySearchTree {
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    public TreeNode root;//创建根节点
}

        里面定义一个内部类,就是树的节点。因为是二叉树,索引要记录节点的左边和右边,构造方法只需要给定一个val。为了方便我们找到其他节点,我们定义一个根节点。

查找节点:

//最好情况:完全二叉树O(logN)
//最坏情况:单分支的树O(N)
public boolean search(int key) {
    TreeNode cur = root;
    while (cur != null) {
        if (cur.val < key) {
            cur = cur.left;
            //节点的值小于遍历节点,往左边查找
        } else if (cur.val > key) {
            cur = cur.right;
            //节点的值大于遍历节点,往右边查找
        } else {
            return true;
        }
    }

    return false;
}

插入节点:

        这里我们需要注意,搜索二叉树中是不能存在相同的节点的,所以如果插入的值有重复的,则不会被插入。

//插入
public boolean insert(int key) {
    if (root == null) {
        root = new TreeNode(key);
        return true;
    }
    TreeNode parent = null;
    TreeNode cur = root;
    while (cur != root) {
        if (cur.val > key) {
            parent = cur;
            cur = cur.left;
        } else if (cur.val < key) {
            parent = cur;
            cur = cur.right;
        } else {
            //当插入元素已经存在,二叉搜索树中是不能存在的
            return false;
        }
    }

    if (parent.val > key) {
        parent.left = new TreeNode(key);
    } else {
        parent.right = new TreeNode(key);
    }
    return true;
    //相当于尾插
}

        所以不难发现,搜索二叉树,无论如何都是尾插。 

        之后我们先来测试代码。

测试代码:

public class Test {
    public static void main(String[] args) {
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        //给一个数组
        int[] arr = {5,12,3,2,11,15};
        for (int i = 0; i < arr.length; i++) {
            binarySearchTree.insert(arr[i]);
        }
        
    }
}

删除节点: 

        关于二叉搜索树最复杂的情况是删除节点,我们要分情况讨论。

        也就是所,要分为3种情况:

1.左树为空

  • 再判断cur是否为root节点(单独处理)
  • 最后再判断parent的左边节点还是右边节点(单独处理)

2.右树为空

  • 再判断cur是否为root节点(单独处理)
  • 最后在判断parent的左边节点还是右边节点(单独处理)

3.左右树均不为空,此时我们要么在cur的左树找最右边的节点,要么在cur的右树找最左边的节点,此时我们演示的是找右树最左边的节点。

         当我们要删除一个左右都不为空的节点时,我们为了保证左子树都比当前节点小,右子树都比当前节点大,我们可以找该节点左子树的最右边的节点 或者 右子树最左边的节点,之后替换cur的值,并删除找的的节点即可。

        所以得出结论,存放数据是一定可以比较的。 

搜索二叉树全部代码:

public class BinarySearchTree {
    static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;

        public TreeNode(int val) {
            this.val = val;
        }
    }

    public TreeNode root;//创建根节点

    //最好情况:完全二叉树O(logN)
    //最坏情况:单分支的树O(N)
    public boolean search(int key) {
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val < key) {
                cur = cur.right;
            } else if (cur.val > key) {
                cur = cur.left;
            } else {
                return true;
            }
        }

        return false;
    }

    //插入
    public boolean insert(int key) {
        if (root == null) {
            root = new TreeNode(key);
            return true;
        }
        TreeNode parent = null;
        TreeNode cur = root;
        while (cur != null) {
            if (cur.val > key) {
                parent = cur;
                cur = cur.left;
            } else if (cur.val < key) {
                parent = cur;
                cur = cur.right;
            } else {
                //当插入元素已经存在,二叉搜索树中是不能存在的
                return false;
            }
        }

        if (parent.val > key) {
            parent.left = new TreeNode(key);
        } else {
            parent.right = new TreeNode(key);
        }
        return true;
        //相当于尾插
    }

    //删除节点
    public void remove(int key) {
        TreeNode cur = root;
        TreeNode parent = null;
        //先找到节点
        while (cur != null) {
            if (cur.val < key) {
                parent = cur;
                cur = cur.right;
            } else if (cur.val > key) {
                parent = cur;
                cur = cur.left;
            } else {
                //此时找到了,删除
                removeNode(cur, parent);
                return;
            }
        }

        //此时没有找到,抛出异常
    }

    private void removeNode(TreeNode cur, TreeNode parent) {
        //这里面删除cur
        if (cur.left == null) {
            //左树为空
            if (cur == root) {
                //遍历节点为根节点
                root = cur.right;
            } else if (cur == parent.left) {
                //遍历节点在父节点的左边
                parent.left = cur.right;
            } else {
                //遍历节点在父节点的右边
                parent.right = cur.right;
            }
        } else if (cur.right == null) {
            //右树为空
            if (cur == root) {
                //遍历节点为根节点
                root = cur.left;
            } else if (cur == parent.left) {
                //遍历节点在父节点的左边
                parent.left = cur.left;
            } else {
                //遍历节点在父节点的右边
                parent.right = cur.left;
            }
        } else {
            /*//此时左右都不为空
            //此时我们找右子树的最小值替换
            TreeNode targetParent = cur;
            TreeNode target = cur.right;

            //找最右边的最小值(右树最左边)
            while (target.left != null) {
                targetParent = target;
                target = target.left;
            }

            cur.val = target.val;
            //因为是最左边,所以只会在右边有值
            if (targetParent.left == target) {
                targetParent.left = target.right;
                //右树不是单分支情况
            } else {
                targetParent.right = target.right;
                //右树是单分支的情况
            }*/

            //找左边最大值(左树最右边)
            TreeNode targetParent = cur;
            TreeNode target = cur.left;
            while(target.right != null) {
                targetParent = target;
                target = target.right;
            }

            cur.val = target.val;
            //因为是最右边,所以只会在左边有值
            if(targetParent.right == target) {
                targetParent.right = target.left;
                //左树不是单分支的情况
            } else {
                targetParent.left = target.left;
            }

        }
    }
}
public class Test {
    public static void main(String[] args) {
        BinarySearchTree binarySearchTree = new BinarySearchTree();
        int[] arr = {5,12,3,2,11,15};
        for (int i = 0; i < arr.length; i++) {
            binarySearchTree.insert(arr[i]);
        }
        binarySearchTree.insert(13);
        binarySearchTree.remove(12);
        System.out.println("======");
    }
}

Map和Set用途和区别:

        Map和Set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。

         一般常见的搜索方式有:

  1. 直接遍历,时间复杂度O(N),元素如果比较多效率会非常慢。
  2. 二分查找,时间复杂度O(logN),但搜索前必须要求是有序的

        上述搜索比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:不重复集合,即需要先搜索关键字是否已经在集合中。

        可能在查找时会进行一些插入和删除的操作,即动态查找,所以以上方式就不太合适了,Map和Set是一种适合动态查找的集合容器。

        一般把搜索的数据称为关键字(Key),和关键字对应的值(Value),将其称之为Key-value的键值对,所以模型会有两种:

  1. 纯Key模型。比如:有一个英文词典,快速查找一个单词是否在词典中;快速查找某个名字在不在通讯录中。
  2. Key-value模型。比如:统计文件每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>

        而Map中存储的就是key-value的键值对,Set中只存储了Key。

Map的使用:

        我们先来看一段代码:

Map<String, Integer> map1 = new TreeMap<>();//时间复杂度O(logN)
map1.put("sunny", 3);//相当于这个单词出现了3次
map1.put("the", 5);

        比如此时我们往里面放入元素,因为底层是红黑树所以会进行比较,我们进入源码观察。

        可以发现是根据key来进行比较的。

        当我们去查找一个不存在于Map中的Key时,会返回一个null。

TreeMap<String, Integer> map1 = new TreeMap<>();//时间复杂度O(logN)
map1.put("sunny", 3);//相当于这个单词出现了3次
map1.put("the", 5);

Integer val = map1.get("the2");//获取对应的值
System.out.println(val);

关于Map的相关方法:

        我们来使用这些方法: 

TreeMap<String, Integer> map1 = new TreeMap<>();//时间复杂度O(logN)
map1.put("sunny", 3);//相当于这个单词出现了3次
map1.put("the", 5);

Integer val = map1.get("the2");//获取对应的值
System.out.println(val);

Integer val2 = map1.getOrDefault("the2", 999);//因为没有这个key返回默认值
System.out.println(val2);

Set<String> set = map1.keySet();//获取所有的key
System.out.println(set);

entrySet方法:

        这个方法我们需要重点掌握。Set<Map.Entry<K, V>> entrySet()。

        可以发现其返回的是一个set集合类型,我们把map中所有的节点都放入了该集合当中,其中Entry可以理解为节点的意思(作者能力有限)。 

TreeMap<String, Integer> map1 = new TreeMap<>();//时间复杂度O(logN)
map1.put("sunny", 3);//相当于这个单词出现了3次
map1.put("the", 5);
map1.put("the", 7);//再添加一次相同的键则会覆盖之前的数据

Set<Map.Entry<String, Integer>> entrySet = map1.entrySet();

for (Map.Entry<String, Integer> entry : entrySet) {
    System.out.println("key: " + entry.getKey() + " value: " + entry.getValue());
}

TreeMap和HashMap的区别:

Set接口(TreeSet和TreeMap区别): 

        set可以对集合中的元素去重,我们观察TreeSet底层代码:

         可以发现TreeSet底层代码是由TreeMap实现的,而TreeMap底层是红黑树(目前水平可以理解为二叉搜索树)。

        而我们知道Set接口下有两个Set类:

  1. TreeSet类(底层为TreeMap类:二叉搜索树)
  2. HashSet类

        所以可以得出一个结论: 搜索树-> TreeMap TreeSet

HashMap类:

哈希冲突:

        接下来我们重点讲解HashMap类,它的背后是哈希表(这里的哈希表是 :哈希表 + 链表 + 红黑树)。

        我们也都接触过哈希表,不过当我们插入重复元素时应该如何解决呢?我们先来了解一个概念:哈希冲突。

        哈希冲突:不同的关键字通过相同哈希计算出相同的哈希地址,该现象成为 哈希冲突 或 哈希碰撞。

        由于哈希表底层数组的容量往往是小于实际要存储关键字的数量的,这就导致一个问题,冲突发生是必然的,但我们能做的应该是尽量降低冲突率。

        引起哈希冲突一个原因可能是:哈希函数设计不合理。

        哈希函数设计原则:

  1. 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
  2. 哈希函数计算出来的地址能够均匀的分布在整个空间中。
  3. 哈希函数应该比较简单。

        比如计数排序就是哈希表的典型应用。

解决哈希冲突方法:

1.冲突-避免-负载因子调节(开散列):

        散列表的负载因子定义为:a = 填入表中元素个数 / 散列表的长度

        不难发现,冲突率与负载因子成正比关系,一般负载因子为0.75。

        以上方法需要调整哈希表的长度,称之为开散列。 

2. 冲突-避免-性探测法:

        这些方法不需要调整哈希表大小。

        二:二次探测:

        这样就均匀地分部开了。

开散列(重点):

        闭散列方法比较简单,我们需要重点掌握开散列(负载因子调节)的方法。

        当超过了负载因子(一般为0.75),就会调整散列表的空间。

        开散列法又称链地址法(开链法)。因为我们知道HashMap底层原理是哈希表 + 链表 + 红黑树,我们由于还没有学到红黑树,就先来模拟 哈希表 + 链表 来模拟实现 HashMap (也就是哈希桶)。

        插入链表的时候,JDK1.7及以前,都是采用的头插法,从1.8开始,采用尾插法。

        还是否记得我们之前说的树化条件:数组 + 链表 + 红黑树 (当数组长度 >= 64 && 链表长度 >= 8 以后把链表变成红黑树(注:每个桶背后是红黑树,数组的每个元素是链表的头结点)

模拟实现HashMap(哈希桶):

        我们直接定义一个使用final修饰的负载因子,当计算的负载因子大于这个值时就调整哈希表的大小。这里我们直接给出代码实现:

public class HashBuck {
    static class Node {
        //要有3个域
        public int key;
        public int val;
        public Node next;

        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    public Node[] array;
    public int usedSize;//存放了多少个有效数据

    public static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //定义负载因子

    public HashBuck() {
        array = new Node[10];
    }

    public void put(int key, int val) {
        int index = key % array.length;

        //遍历index下标的链表是否存在key 存在更新value 不存在进行头插法插入数据
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                //更新val
                cur.val = val;
                return;
            }
            cur = cur.next;
        }

        //cur == null 链表遍历完成 没有找到这个key
        Node node = new Node(key, val);//此时进行头插
        node.next = array[index];
        array[index] = node;
        usedSize++;

        
    }
    
    private float doLoadFactor() {
        return usedSize * 1.0f / array.length;
    }
}

        当我们放入的元素超过了负载因子时,就需要扩容:

if (doLoadFactor() > DEFAULT_LOAD_FACTOR) {
    array = Arrays.copyOf(array, 2 * array.length);
}

        此时我们如果直接就赋值数组,并扩容会出现问题:

         因为原来放入的位置就需要重新哈希计算放入,否则拷贝有误,所以给出以下修改:

if (doLoadFactor() > DEFAULT_LOAD_FACTOR) {
     //此时需要扩容
     //array = Arrays.copyOf(array, 2 * array.length);error
     resize();
}

private void resize() {
        Node[] newArray = new Node[2 * array.length];

        //遍历原来数组
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                Node nextPos = cur.next;
                int newIndex = cur.key % newArray.length;//新的下标
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;//头插

                cur = nextPos;
            }
        }
        array = newArray;
    }

        接下来我们给出全部代码:

public class HashBuck {
    static class Node {
        //要有3个域
        public int key;
        public int val;
        public Node next;

        public Node(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    public Node[] array;
    public int usedSize;//存放了多少个有效数据

    public static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //定义负载因子

    public HashBuck() {
        array = new Node[10];
    }

    public void put(int key, int val) {
        int index = key % array.length;

        //遍历index下标的链表是否存在key 存在更新value 不存在进行头插法插入数据
        Node cur = array[index];
        while (cur != null) {
            if (cur.key == key) {
                //更新val
                cur.val = val;
                return;
            }
            cur = cur.next;
        }

        //cur == null 链表遍历完成 没有找到这个key
        Node node = new Node(key, val);//此时进行头插
        node.next = array[index];
        array[index] = node;
        usedSize++;

        if (doLoadFactor() > DEFAULT_LOAD_FACTOR) {
            //此时需要扩容
            //array = Arrays.copyOf(array, 2 * array.length);error
            resize();
        }
    }

    private void resize() {
        Node[] newArray = new Node[2 * array.length];

        //遍历原来数组
        for (int i = 0; i < array.length; i++) {
            Node cur = array[i];
            while (cur != null) {
                Node nextPos = cur.next;
                int newIndex = cur.key % newArray.length;//新的下标
                cur.next = newArray[newIndex];
                newArray[newIndex] = cur;//头插

                cur = nextPos;
            }
        }
        array = newArray;
    }

    private float doLoadFactor() {
        return usedSize * 1.0f / array.length;
    }

    public int get(int key) {
        int index = key % array.length;
        Node cur = array[index];

        while (cur != null) {
            if (cur.key == key) {
                return cur.val;
            }
            cur = cur.next;
        }

        return -1;
    }
}

        测试代码:

public class Test {
    public static void main(String[] args) {
        HashBuck hashBuck = new HashBuck();
        hashBuck.put(1,11);
        hashBuck.put(2,22);
        hashBuck.put(9,99);
        hashBuck.put(5,99);
        hashBuck.put(4,99);
        hashBuck.put(7,99);
        hashBuck.put(3,99);
        hashBuck.put(8,99);
        hashBuck.put(10,99);

        System.out.println(hashBuck.get(9));
    }
}

        为了验证,我们调试代码观察数组大小是否变大了:

        此时我们来使用HashMap类:

public static void main(String[] args) {
    HashMap<String, Integer> map = new HashMap<>();
    map.put("abcd", 2);
    map.put("hello", 10);
    map.put("gao", 3);

    Integer val = map.get("abcd");
    System.out.println(val);

    System.out.println(map);
}

        这里打印的并没有按照顺序,是因为哈希表和哈希算法。我们知道Map里面有Entry方法,我们可以利用它遍历所有Map中的元素:

for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println("key: " + entry.getKey() + " val: " + entry.getValue());
}
//不支持迭代器遍历

        注意:这里我们还是使用这种方法进行遍历,不支持迭代器遍历,是因为迭代器遍历都是实现了Iterable接口的。但是HashMap只是实现了Map接口。

HashMap详解:

        我们这次使用引用类型插入元素,观察以下代码:

class Student {

}

public static void main(String[] args) {
    HashMap<Student, Integer> map = new HashMap<>();
    map.put(new Student(), 2);
    map.put(new Student(), 2);
    //这里的Student里面没有任何内容
    map.put(null, 2);
}

        我们直接运行可以发现没有报错,但是我们的Student类中并没有实现任何关于比较的接口。        

        这是因为HashMap插入元素是不会进行key的元素比较的,所以不用实现Comparable接口比较,而且可以插入null。 

HashMap原码:

        因为这个类很重要,所以我们进入源代码观察:

         我们一定要知道HashMap底层是哈希表(哈希表 + 数组 + 链表),所以我们很有必要来了解底层代码。

        我们再来观察构造方法:

        我们再来观察无参构造器: 

        可以发现无参构造器是没有分配数组大小的,所以我们还要观察其他方法,此时比如我们调用了无参构造器,并往里面添加元素,使用put方法。

        因为key可能是引用类型,所以必须将其变成一个合法的整数。

        之后调用了putVal方法。

        因为table为null,所以又去调用了resize方法。

        所以可以发现,当我们调用无参构造器时,最开始并没有给数组分配空间,当我们放入一个元素时,会开辟16个元素的数组。 

        我们再来观察当本身就已经给数组分配空间的插入元素的情况。

        HashMap一般会保证数组的容量是2的某个次幂。

        但是如果我们此时指定容量,比如11,那么还是否会是2的次幂,我们进入源码观察。

        首先我们要知道,HashMap中size的意思为:HashMap中存放的KV的数量(为链表和树中的KV总和)。 

        capacity表示桶的数量,也就是数组长度,默认值为16。

        threshold表示当HashMap的size大于threshold是会执行resize操作。

        threshold = capacity * loadFactor

树化条件:

        但我们插入第二个元素时,是尾插:

         我们再进入treeifyBin(tab, hash)来观察。

        可以发现,必须数组长度大于等于64 并且 链表长度大于等于8时才会树化。 

HashSet类: 

HashSet的使用:

        Set是集合,顾名思义,可以将元素去重:

public static void main(String[] args) {
    //set不能存储相同的key 可以去重
    HashSet<String> set = new HashSet<>();
    set.add("hello");
    set.add("abcd");
    set.add("hello");

    System.out.println(set);
}

        注意,这里set放入的顺序也是杂乱的,无序的,输出结果和我们放入的顺序并不一致。 

        HashSet底层是一个HashMap。

        每次存储元素时,默认的value就是一个Object对象。

hashCode方法:

        观察以下代码:

class Student {
    public String id;

    public Student(String id) {
        this.id = id;
    }
}
public static void main(String[] args) {
    Student student1 = new Student("613");
    Student student2 = new Student("613");

    System.out.println(student1.hashCode());
    System.out.println(student2.hashCode());

}

        我们直接调用了hashCode方法,但是我们的Student类中并没有这个方法,因为所有的类都继承于Object类,所以我们进入Object中观察这个方法。

 

         我们无法观察此方法的源代码,但是根据结果显示,发现计算的结果并不同,而HashMap存入是根据 整形 / 数组长度放入的,利用该方法就可以得出整形,以至于相同的 key 可以存入

        接下来我们利用编译器生成的hashCode方法来观察(注:会提示很多信息,我们直接默认往下走到头即可)。

class Student {
    public String id;

    public Student(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
public static void main(String[] args) {
    Student student1 = new Student("613");
    Student student2 = new Student("613");

    System.out.println(student1.hashCode());// x % len (x 是hashCode的结果)
    System.out.println(student2.hashCode());

}

         此时使我们自己写的hashCode方法,所以计算生成的结果是一样的,如果我们将student1放入HashMap对象中,并取出student2,因为hashCode结果相同,所以取出的值是student1对应的值。

public static void main(String[] args) {
    Student student1 = new Student("613");
    Student student2 = new Student("613");

    System.out.println(student1.hashCode());// x % len (x 是hashCode的结果)
    System.out.println(student2.hashCode());

    HashMap<Student, Integer> map = new HashMap<>();
    map.put(student1, 2);

    System.out.println(map.get(student2));//并没有放入student2

}

        如果没有重写hashCode方法,直接去拿没有的键就只会拿到null。

模拟实现HashSet类: 

        因为底层原理是HashMap,所以我们还是利用之前写的哈希桶的代码来模拟完成,不过更加粗略。

public class HashBuck<K, V> {
    static class Node<K, V> {
        public K key;
        public V val;

        public Node<K, V> next;

        public Node(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }

    public Node<K, V> [] array;

    public int usedSize;
    public static final float DEFAULT_LOAD_FACTOR = 0.75f;

    public HashBuck() {
        array = (Node<K, V>[]) new Node[10];
    }

    public void put(K key, V val) {
        int hash = key.hashCode();
        //注意这里使用了hashCode方法区计算
        int index = hash % array.length;
        //得出该存放的下标
        Node<K, V> cur = array[index];
        while (cur != null) {
            if (cur.key.equals(key)) {
                cur.val = val;
                return ;
            }
            cur = cur.next;
        }

        Node<K, V> node = new Node(key, val);
        node.next = array[index];
        array[index] = node;
        usedSize++;
    }

    public V getValue(K key) {
        int hash = key.hashCode();
        int index = hash % array.length;
        Node<K, V> cur = array[index];
        while (cur != null) {
//            if (cur.key == key) {
//                return cur.val;
//            }
            //注意key此时是引用类型,不能直接比较
            if (cur.key.equals(key)) {
                return cur.val;
            }
            cur = cur.next;
        }

        return null;
    }
}
class Student {
    public String id;

    public Student(String id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(id, student.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
public static void main(String[] args) {
    Student student1 = new Student("613");
    Student student2 = new Student("613");

    HashBuck<Student, Integer> hashBuck = new HashBuck<>();
    hashBuck.put(student1, 10);
    System.out.println(hashBuck.getValue(student2));
}

        以后在写自定义对象的时候,最好自己实现一下equals和hashCode。

练习:

练习一: 

        还记得我们之前写的只出现一次的数字吗?我们利用Java的集合框架来写,利用HashSet来写。

        基本思路是遍历一遍原数组,之后将所有的元素加入集合中去,当加入的是重复的元素时,看hashSet中有没有该元素,如果有就删除set中的元素,最终就只剩下一个元素就是只出现一次的元素(注意:这里面的元素最多每个也就出现2次)。

        但是因为HashSet中没有get方法,我们必须返回该元素,不能直接使用toString方法,所以还要在遍历一遍数组找到在集合中存在的元素。

public int singleNumber(int[] nums) {
        HashSet<Integer> hashSet = new HashSet<>();
        for (int i = 0; i < nums.length; i++) {
            if (hashSet.isEmpty() || !hashSet.contains(nums[i])) {
                hashSet.add(nums[i]);
            } else {
                hashSet.remove(nums[i]);
            }
        }

        for (int i = 0; i < nums.length; i++) {
            if (hashSet.contains(nums[i])) {
                return nums[i];
            }
        }

        return -1;
    }

练习二: 

        还是熟悉的配方,复制随机链表,这次我们使用HashMap来完成(力扣138题)。

        我们利用HashMap来完成随机节点的复制,是一种新的方法。

/*
// Definition for a Node.
class Node {
    int val;
    Node next;
    Node random;

    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}
*/

class Solution {
    public Node copyRandomList(Node head) {
        //利用HashMap来完成
        HashMap<Node, Node> map = new HashMap<>();
        Node cur = head;

        //1.将新节点与旧节点放入map
        while (cur != null) {
            Node tmp = new Node(cur.val);
            map.put(cur, tmp);
            cur = cur.next;
        }

        //2.将next与random复制
        cur = head;
        while (cur != null) {
            map.get(cur).next = map.get(cur.next);
            map.get(cur).random = map.get(cur.random); 
            cur = cur.next;
        }

        //返回新链表的头结点
        return map.get(head);
    }
}

练习三: 

         当我们在10w个数据中,我们找到所有元素并且不重复,就可以利用HashSet;但是在10w个数据中找重复的数据出现了几次,我们可以利用HashMap。

        接下来我们来看代码,求每个元素出现的次数:

public static void main(String[] args) {
    int[] arr = {1, 2, 3, 3, 2};

    HashMap<Integer, Integer> map = new HashMap<>();
    for (Integer x : arr) {
        if (map.get(x) == null) {
            //第一次存放
            map.put(x, 1);
        } else {
            //其他情况在原来的基础上加 1
            int val = map.get(x);
            map.put(x, val + 1);
        }
    }

    for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
        System.out.println("key: " + entry.getKey() + " val: " + entry.getValue());
    }
}

总结: 

        多加练习就可以记住,这里有一个技巧:HashSet底层是HashMap;TreeSet底层是TreeMap。TreeMap是红黑树(必须比较);HashMap是哈希表(不需要比较)。本文章可能还有很多不足,希望大伙多多指正。

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值