前言:
我们已经学了大部分的关于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是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
一般常见的搜索方式有:
- 直接遍历,时间复杂度O(N),元素如果比较多效率会非常慢。
- 二分查找,时间复杂度O(logN),但搜索前必须要求是有序的
上述搜索比较适合静态类型的查找,即一般不会对区间进行插入和删除操作了,而现实中的查找比如:不重复集合,即需要先搜索关键字是否已经在集合中。
可能在查找时会进行一些插入和删除的操作,即动态查找,所以以上方式就不太合适了,Map和Set是一种适合动态查找的集合容器。
一般把搜索的数据称为关键字(Key),和关键字对应的值(Value),将其称之为Key-value的键值对,所以模型会有两种:
- 纯Key模型。比如:有一个英文词典,快速查找一个单词是否在词典中;快速查找某个名字在不在通讯录中。
- 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类:
- TreeSet类(底层为TreeMap类:二叉搜索树)
- HashSet类
所以可以得出一个结论: 搜索树-> TreeMap TreeSet
HashMap类:
哈希冲突:
接下来我们重点讲解HashMap类,它的背后是哈希表(这里的哈希表是 :哈希表 + 链表 + 红黑树)。
我们也都接触过哈希表,不过当我们插入重复元素时应该如何解决呢?我们先来了解一个概念:哈希冲突。
哈希冲突:不同的关键字通过相同哈希计算出相同的哈希地址,该现象成为 哈希冲突 或 哈希碰撞。
由于哈希表底层数组的容量往往是小于实际要存储关键字的数量的,这就导致一个问题,冲突发生是必然的,但我们能做的应该是尽量降低冲突率。
引起哈希冲突一个原因可能是:哈希函数设计不合理。
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间。
- 哈希函数计算出来的地址能够均匀的分布在整个空间中。
- 哈希函数应该比较简单。
比如计数排序就是哈希表的典型应用。
解决哈希冲突方法:
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是哈希表(不需要比较)。本文章可能还有很多不足,希望大伙多多指正。