跟随狂神学Java
作者:joker2yue 链接:https://github.com/Joker2Yue/Joker2Yue-Blog 来源:Github 著作权归原作者所有。商业转载请联系原作者获得授权,非商业转载请注明出处。
第二十四天:HashSet、TreeSet、自平衡二叉树
知识最大的敌人不是无知,而是错觉。
狂神未更新,转动力节点(bilibili.com)
学习内容 HashSet集合 特点:无序,不可重复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.joker_yue.javalearn.DataStruct;import java.util.HashSet;import java.util.Set;public class HashSetTest { public static void main (String[] args) { Set<String> str = new HashSet <>(); str.add("hello3" ); str.add("hello4" ); str.add("hello1" ); str.add("hello2" ); str.add("hello3" ); str.add("hello3" ); str.add("hello3" ); str.add("hello3" ); for (String s:str){ System.out.println(s); } } }
上述代码的输出结果为
1 2 3 4 hello1 hello4 hello2 hello3
TreeSet集合
特点:无序,不可重复,但是储存的元素可以自动按照大小排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.joker_yue.javalearn.DataStruct;import java.util.Set;import java.util.TreeSet;public class TreeSetTest { public static void main (String[] args) { Set<String> str = new TreeSet <>(); str.add("A" ); str.add("B" ); str.add("Z" ); str.add("Y" ); str.add("Z" ); str.add("K" ); str.add("M" ); for (String s : str) { System.out.println(s); } } }
上述代码的输出结果为
Map接口常用方法
java.util.Map
Map和Collection没有继承关系
Map集合以key和value的方式存储数据,我们称之为键值对 key和value都是引用数据类型 key和value都是存储对象的内存地址 key起到主导的地位,value是key的一个附属品
方法
说明
V remove(Object key)
V get(Object key)
通过key获取value
V put(K key, V value)
向Map中添加键值对
void clear()
清空Map集合
boolean containsKey(Object key)
判断Map中是否包含某个key
boolean containsValue(Object value)
判断Map中是否包含某个value
boolean isEmpty()
判断集合是否为空
Set keySet()
获取Map集合所有的key(所有key是一个Set集合)
int size()
获取Map集合中所有键值对的数量
Collection values()
获取Map集合中所有的value,返回一个Collection集合
Set<Map.Entry<K,V>> entrySet()
将Map集合转换为Set集合
注意:将Map集合通过entrySet()方法转换成的这个Ser集合,Set集合中元素的类型是Map. Entry<K,V>
Map.Entry和String一样,都是一种类型的名字,只不过,Map.Entry是静态内部类,是Map中的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.joker_yue.javalearn.Maps;import java.util.Collection;import java.util.HashMap;import java.util.Map;public class MapTest01 { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "zhangsan" ); map.put(2 , "lisi" ); map.put(3 , "wangwu" ); map.put(4 , "zhaoliu" ); String value = map.get(2 ); System.out.println(value); System.out.println("键值对的数量:" + map.size()); map.remove(2 ); System.out.println("键值对的数量:" + map.size()); System.out.println(map.containsKey(4 )); System.out.println(map.containsValue("wangwu" )); Collection<String> values = map.values(); for (String s : values) { System.out.println(s); } map.clear(); System.out.println("键值对的数量:" + map.size()); System.out.println(map.isEmpty()); } }
Map集合的遍历 我们可以先提取出所有的key,然后再匹配key提出value
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.joker_yue.javalearn.Maps;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;public class MapTest02 { public static void main (String[] args) { Map<Integer,String> map = new HashMap <>(); map.put(1 ,"zhangsan" ); map.put(2 ,"lisi" ); map.put(3 ,"wangwu" ); map.put(4 ,"zhaoliu" ); Set<Integer> keys = map.keySet(); Iterator<Integer> it = keys.iterator(); while (it.hasNext()){ it.next(); Integer key = it.next(); String value = map.get(key); System.out.println(key + "=" +value); } for (Integer key : keys) { System.out.println(key + "=" +map.get(key)); } } }
我们也可以通过Node节点中的方法来进行迭代
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.joker_yue.javalearn.Maps;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;public class MapTest02 { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "zhangsan" ); map.put(2 , "lisi" ); map.put(3 , "wangwu" ); map.put(4 , "zhaoliu" ); Set<Map.Entry<Integer, String>> set = map.entrySet(); Iterator<Map.Entry<Integer, String>> it2 = set.iterator(); while (it2.hasNext()) { Map.Entry<Integer, String> node = it2.next(); Integer key = node.getKey(); String value = node.getValue(); System.out.println(key + "=" + value); } } }
我们可以将上述while改写成foreach
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.joker_yue.javalearn.Maps;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;public class MapTest02 { public static void main (String[] args) { Map<Integer, String> map = new HashMap <>(); map.put(1 , "zhangsan" ); map.put(2 , "lisi" ); map.put(3 , "wangwu" ); map.put(4 , "zhaoliu" ); Set<Map.Entry<Integer, String>> set = map.entrySet(); for (Map.Entry<Integer, String> node : set) { System.out.println(node.getKey()+"-->" +node.getValue()); } } }
这样的话,通过foreach的方式效率较高,原因是通过while循环我们首先是要获取一个key,再通过key来获取value。而foreach的话是将两项操作放在一起
哈希表数据结构
HashMap集合底层是哈希表(散列表)的数据结构
哈希表是一个怎样的结构?
数组:在查询方面效率很高,随机增删效率很低
单项链表:在随机增删方面效率很高,在查询方面效率很低
哈希表将以上两种数据结构结合在一起,充分发挥其各自的优点
HashMap集合底层的源代码
1 2 3 4 5 6 7 8 9 10 11 12 public class HashMap { Node<K,V>[] table; static class Node <K,V>{ final int hash; final K key; V value; Node<K,V> next; } }
哈希表:一维数组,这个数组中的每一个元素是一个单项链表
最主要掌握的是
map.put(k,v)
v = map.get(k)
如果需要在字典中找’中’这个字,会有几种查询方式?
可以一页一页找,直到找到为止
可以按照通过索引找到大致位置,然后向后翻阅直到找到
HashMap集合的key部分特点:
无序,不可重复
为什么无序?因为不一定挂到哪个单向链表上
不可重复是如何保证的?equals方法来保证HashMap集合key不可重复
如果key重复了,value会覆盖
放在HashMap集合key部分的元素其实就是放在HashSet集合中了
所以,HashSet集合中的元素也需要同时重写HashCode()+equals()方法
哈希表HashMap使用不当时无法发挥性能
假设所有的HashCode()方法返回值固定为某个值,那么会导致底层哈希表变成纯单向链表
这种情况我们称之为散列分布不均匀
什么是散列分布均匀?
假设我们有100个元素,10个单向链表,那么每个链表上有10个节点,这是最好的,是散列均匀分布的
假设将所有的HashCode()返回值都设置为不一样的值,可以吗,有什么问题?
不行,将会导致底层变成一维数组
散列分布均匀需要重写HashCode()方法时有一定的技巧
重点:放在HashMap集合key部分的元素,以及放在HashMap集合中的元素,需要同时重写HashCode()和equals方法
HashMap集合的默认初始化容量是16,默认加载因子是0.75
这个默认加载因子是当HashMap集合底层数组的容量达到75%时,数组开始扩容
HashMap初始化的时候, 容量是2的倍数,这也是官方推荐的,因为达到散列均匀为了提高HashMap集合的存取效率,所必须的
对于哈希数据结构来说,
如果a1和a2的hash值相同,一定是放到同一个单向链表上
当然如果用a1和a2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时就会发生“哈希碰撞”
HashSet初始化为16,扩容之后是原容量的2倍
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.joker_yue.javalearn.Maps;import java.util.HashMap;import java.util.Map;import java.util.Set;public class HashMapTest01 { public static void main (String[] args) { Map<Integer,String> map = new HashMap <>(); map.put(1111 ,"zhangsan" ); map.put(6666 ,"lisi" ); map.put(7777 ,"wangwu" ); map.put(2222 ,"zhaoliu" ); map.put(2222 ,"king" ); System.out.println(map.size()); Set<Map.Entry<Integer,String>> set = map.entrySet(); for (Map.Entry<Integer,String> entry:set ) { System.out.println(entry.getKey()+"=" +entry.getValue()); } } }
上述代码的执行结果如下,证明了HashMap的无序、不可重复的特点
1 2 3 4 5 4 7777wangwu 1111zhangsan 6666lisi 2222king
同时重写hashCode()和equals()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.joker_yue.javalearn.Maps;public class Student { String name; Student(String name){ this .name = name; } public boolean equals (Object obj) { if (obj == null ) return false ; if (obj instanceof Student){ if (((Student) obj).name==this .name) return true ; } return false ; } }
1 2 3 4 5 6 7 8 9 10 11 package com.joker_yue.javalearn.Maps;public class HashMapTest02 { public static void main (String[] args) { Student s1 = new Student ("zhangsan" ); Student s2 = new Student ("zhangsan" ); System.out.println(s1.equals(s2)); } }
上面是未重写hashCode和equals方法的情况,接下来我们重写,会如何?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.joker_yue.javalearn.Maps;import java.util.Objects;public class Student { String name; Student(String name){ this .name = name; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Student student = (Student) o; return name.equals(student.name); } @Override public int hashCode () { return Objects.hash(name); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.joker_yue.javalearn.Maps;import java.util.HashSet;import java.util.Set;public class HashMapTest02 { public static void main (String[] args) { Student s1 = new Student ("zhangsan" ); Student s2 = new Student ("zhangsan" ); System.out.println(s1.equals(s2)); System.out.println("s1的HashCode = " +s1.hashCode()); System.out.println("s2的HashCode = " +s2.hashCode()); Set<Student> students = new HashSet <>(); students.add(s1); students.add(s2); System.out.println(students.size()); } }
Java对HashMap的改进 在JDK8后,Java对HashMap进行了改进,设置了树的门限值为8(static final int TREEIFY_THRESHLD = 8
),这意味着单向链表上有着超过8个元素,再继续存的话会将单向链表转换为红黑树(自平衡二叉树),当红黑树上元素少于6时(static final int UNITREEIFY_THRESHOLD = 6
),会重新将其转换为单向链表
HashMap和HashTable的区别
HashMap的key可不可以为空值null?可以,但是只能有一个为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.joker_yue.javalearn.Maps;import java.util.HashMap;import java.util.Map;public class HashMapTest03 { public static void main (String[] args) { Map map = new HashMap (); map.put(null ,null ); System.out.println(map.size()); map.put(null ,100 ); System.out.println(map.size()); System.out.println(map.get(null )); } }
在key==null的时候,将key的hash值置为0,从而解决了当key为null时,走hashCode方法导致空指针异常。
但是HashTable无法将key或者value设置为null,会抛出空指针异常
HashTable都带有synchornized线程安全的
线程安全有其他方案,这个HashTable对线程的处理导致效率较低,使用较少了
HashTable的初始化容量是11,默认加载因子是0.75。HashTable的扩容是原容量乘以二加一
属性类Properties类
Properties是一个Map集合,继承自HashTableProperties的key和value都是String类型
Properties被称为属性类对象
Properties是线程安全的
常用方法:
返回类型
方法
说明
Object
setProperty(String key, String value)
调用HashTable的方法put
String
getProperty(String key)
用指定的键再此属性列表中搜索属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.joker_yue.javalearn.Maps;import java.util.Properties;public class PropertiesTest01 { public static void main (String[] args) { Properties pro = new Properties (); pro.setProperty("username" ,"root" ); pro.setProperty("password" ,"12345" ); pro.setProperty("url" ,"www.baidu.com" ); pro.setProperty("driver" ,"com.mysql.jdbc.Driver" ); String username = pro.getProperty("username" ); String driver = pro.getProperty("driver" ); String url = pro.getProperty("url" ); String password = pro.getProperty("password" ); System.out.println(url); System.out.println(driver); System.out.println(username); System.out.println(password); } }
TreeSet对String是可排序的
TreeSet集合底层实际上是一个TreeMap
TreeMap集合底层是一个二叉树
放到TreeSet集合key部分的元素 等同于 放到TreeSet的Key部分了
TreeSet集合中的元素,无序不可重复,但是可以按照元素大小自动排序,称为可排序集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.joker_yue.javalearn.Maps;import java.util.TreeSet;public class TreeSetTest02 { public static void main (String[] args) { TreeSet<String> ts = new TreeSet <>(); ts.add("zhangsan" ); ts.add("lisi" ); ts.add("wangwu" ); ts.add("zhaoliu" ); ts.add("wangba" ); for (String s:ts){ System.out.println(s); } } }
上述代码的输出结果为
1 2 3 4 5 lisi wangba wangwu zhangsan zhaoliu
TreeSet无法对自定义类型排序 我们现在声明一个类Person
填入age,我们现在想按照age大小排序
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.joker_yue.javalearn.Maps;import java.util.TreeSet;public class TreeSetTest03 { public static void main (String[] args) { Person p1 = new Person (20 ); Person p2 = new Person (19 ); Person p3 = new Person (30 ); Person p4 = new Person (25 ); TreeSet<Person> persons = new TreeSet <>(); persons.add(p1); persons.add(p2); persons.add(p3); persons.add(p4); for (Person p : persons){ System.out.println(p.toString()); } } } class Person { int age; public Person (int age) { this .age = age; } public String toString () { return "Person[age=" +age+"]" ; } }
结果报错,因为其是不可比较的,需要自定义java.lang.comparable
接口
1 2 3 4 5 6 7 8 9 10 D:\Java\jdk-18.0 .1 .1 \bin\java.exe "-javaagent:D:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2022.2.1\lib\idea_rt.jar=2150:D:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2022.2.1\bin" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath E:\Program\Idea\Java\helloworld\src\com\joker_yue\out\production\joker_yue com.joker_yue.javalearn.Maps.TreeSetTest03 Exception in thread "main" java.lang.ClassCastException: class com .joker_yue.javalearn.Maps.Person cannot be cast to class java .lang.Comparable (com.joker_yue.javalearn.Maps.Person is in unnamed module of loader 'app' ; java.lang.Comparable is in module java.base of loader 'bootstrap' ) at java.base/java.util.TreeMap.compare(TreeMap.java:1569 ) at java.base/java.util.TreeMap.addEntryToEmptyMap(TreeMap.java:776 ) at java.base/java.util.TreeMap.put(TreeMap.java:785 ) at java.base/java.util.TreeMap.put(TreeMap.java:534 ) at java.base/java.util.TreeSet.add(TreeSet.java:255 ) at com.joker_yue.javalearn.Maps.TreeSetTest03.main(TreeSetTest03.java:19 ) 进程已结束,退出代码1
自定义java.lang.comparable
接口 回想一下C++的自定义sort排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.joker_yue.javalearn.Maps;import java.util.Comparator;import java.util.TreeSet;public class TreeSetTest04 { public static void main (String[] args) { Custom c1 = new Custom (20 ); Custom c2 = new Custom (19 ); Custom c3 = new Custom (30 ); Custom c4 = new Custom (25 ); TreeSet<Custom> customs = new TreeSet <>(); customs.add(c1); customs.add(c2); customs.add(c3); customs.add(c4); for (Custom c : customs) { System.out.println(c.toString()); } } } class Custom implements Comparable <Custom> { int age; public Custom (int age) { this .age = age; } @Override public int compareTo (Custom o) { return this .age - o.age; } @Override public String toString () { return "Custom{" + "age=" + age + '}' ; } }
我们现在可以进行排序是因为我们继承了接口并重写了comparableTo()方法
如果我们要比较String类型,可以直接使用String的compareTo()方法
实现比较器接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.joker_yue.javalearn.Maps;import java.util.Comparator;import java.util.TreeSet;public class TreeSetTest05 { public static void main (String[] args) { TreeSet<WuGui> wugui = new TreeSet <>(new WuGuiComparator ()); wugui.add(new WuGui (1000 )); wugui.add(new WuGui (20 )); wugui.add(new WuGui (500 )); for (WuGui wg : wugui) { System.out.println(wg.toString()); } } } class WuGui { int age; public WuGui (int age) { this .age = age; } @Override public String toString () { return "小乌龟[" + "age=" + age + ']' ; } } class WuGuiComparator implements Comparator <WuGui> { @Override public int compare (WuGui o1, WuGui o2) { return o1.age - o2.age; } }
当然,我们还可以使用匿名内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.joker_yue.javalearn.Maps;import java.util.Comparator;import java.util.TreeSet;public class TreeSetTest05 { public static void main (String[] args) { TreeSet<WuGui> wugui = new TreeSet <>(new Comparator <WuGui>() { @Override public int compare (WuGui o1, WuGui o2) { return o1.age - o2.age; } }); wugui.add(new WuGui (1000 )); wugui.add(new WuGui (20 )); wugui.add(new WuGui (500 )); for (WuGui wg : wugui) { System.out.println(wg.toString()); } } } class WuGui { int age; public WuGui (int age) { this .age = age; } @Override public String toString () { return "小乌龟[" + "age=" + age + ']' ; } }
所以,我们在放到TreeSet或者TreeMap集合key部分的元素想要做到排序,有两种方案:
放在集合中的元素实现java.lang.Comparable接口
在构造函数TreeSet或者TreeMap集合的时候给它一个比较器对象
如何选择Comparable和comparator?
当比较规则不会轻易发生改变的时候,建议comparable接口
如果需要频繁切换比较规则的时候,建议Comparable接口
Comparable接口的设计符合OCP原则(Open Closed Principle,开闭原则,高内聚低耦合)
自平衡二叉树
TreeSet/TreeMap自平衡二叉树遵循左小右大原则
遍历二叉树的时候有三种方式
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
注意:前中后的遍历方式说的是根的位置
TreeSet集合采用的是中序遍历方式
Iterator迭代器采用的是中序遍历方式
Collections工具类
对List进行排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.joker_yue.javalearn.Maps;import java.util.ArrayList;import java.util.Collections;import java.util.Comparator;import java.util.List;public class CollectionsTest { public static void main (String[] args) { List<String> list = new ArrayList <>(); Collections.synchronizedList(list); list.add("x" ); list.add("abr" ); list.add("a" ); list.add("abc" ); Collections.sort(list); for (String s : list) { System.out.println(s); } List<WuGui2> wuGuis = new ArrayList <>(); wuGuis.add(new WuGui2 (100 )); wuGuis.add(new WuGui2 (8000 )); Collections.sort(wuGuis); for (WuGui2 wg : wuGuis) { System.out.println(wg); } } } class WuGui2 implements Comparable <WuGui2> { int age; public WuGui2 (int age) { this .age = age; } @Override public String toString () { return "小乌龟[" + "age=" + age + ']' ; } @Override public int compareTo (WuGui2 o) { return this .age - o.age; } }
对Set进行排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.joker_yue.javalearn.Maps;import java.util.*;public class CollectionsTest { public static void main (String[] args) { Set<String> set = new HashSet <>(); set.add("king" ); set.add("kingsoft" ); set.add("king2" ); set.add("king1" ); List<String> myList = new ArrayList <>(set); Collections.sort(myList); for (String s : myList){ System.out.println(s); } } }
下面这种方法也能排序
Collections.sort(list集合,比较器对象);