本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
网课链接:算法讲解026【必备】哈希表、有序表和比较器的用法_哔哩哔哩_bilibili
对比维度 | HashSet | HashMap |
---|---|---|
存储方式 | 只存储元素的值,不存储键值对 | 存储键值对,每个元素由键值对(key-value)组成 |
底层数据结构 | 基于哈希表实现,只存储键,不存储值 | 基于哈希表实现,使用数组和链表或红黑树来实现存储和检索 |
元素的唯一性 | 元素是唯一的,重复元素会被自动去重 | 键是唯一的,但值可以重复 |
性能表现 | 适用于需要存储唯一元素的场景 | 适用于需要通过键来获取值的场景 |
元素访问 | 只能迭代所有的元素 | 可以通过键来获取对应的值,也可以迭代所有的键值对 |
接口实现 | 实现了Set接口 | 实现了Map接口 |
用途 | 用于存储不重复的元素 | 用于存储键值对,允许根据键来查找值 |
元素类型 | 存储单一的元素类型,如整数、字符串等 | 存储键值对,键和值可以是不同的类型 |
迭代顺序 | 不保证迭代顺序恒久不变 | 不保证键值对的顺序 |
线程安全 | 不是线程安全的,需要在多线程访问时显式同步 | 非synchronized,但collection框架提供方法能保证synchronized |
允许null值 | 允许有null值 | 允许键和值为null |
import java.util.HashMap;
import java.util.HashSet;
// 名为Code01_HashSetAndHashMap的类
public class Code01_HashSetAndHashMap {
public static void main(String[] args) {
// 对于Java中的包装类(如Integer、Long、Double、Float、Byte、Short、Character、Boolean)以及String类
// 有一些特殊的比较特性
// 创建两个内容相同但内存地址不同的String对象
String str1 = new String("Hello");
String str2 = new String("Hello");
// "=="比较的是对象的内存地址,由于str1和str2是不同的对象实例,所以结果为false
System.out.println(str1 == str2);
// "equals"方法在String类中被重写,用于比较字符串内容,所以结果为true
System.out.println(str1.equals(str2));
// 创建一个HashSet,用于存储String类型的元素
HashSet set = new HashSet<>();
// 向HashSet中添加str1
set.add(str1);
// 由于HashSet在判断元素是否存在时,是根据元素的hashCode和equals方法来判断的
// "Hello"和str1的内容相同,所以会返回true
System.out.println(set.contains("Hello"));
// 同理,str2的内容和str1相同,所以也会返回true
System.out.println(set.contains(str2));
// 向HashSet中添加str2,由于HashSet的元素唯一性,不会重复添加,所以集合大小仍然为1
set.add(str2);
System.out.println(set.size());
// 从HashSet中移除str1
set.remove(str1);
// 清空HashSet中的所有元素
set.clear();
// 判断HashSet是否为空,由于已经清空,所以结果为true
System.out.println(set.isEmpty());
System.out.println("===========");
// 创建一个HashMap,键和值都是String类型
HashMap map1 = new HashMap<>();
// 向HashMap中添加键值对,键为str1,值为"World"
map1.put(str1, "World");
// 由于HashMap在判断键是否存在时,也是根据键的hashCode和equals方法来判断的
// "Hello"和str1的内容相同,所以会返回true
System.out.println(map1.containsKey("Hello"));
// 同理,str2的内容和str1相同,所以也会返回true
System.out.println(map1.containsKey(str2));
// 获取键为str2对应的值,由于键str1和str2内容相同,所以能获取到值"World"
System.out.println(map1.get(str2));
// 由于HashMap中不存在键为"你好"的键值对,所以结果为true
System.out.println(map1.get("你好") == null);
// 从HashMap中移除键为"Hello"的键值对
map1.remove("Hello");
// 输出HashMap的大小,由于移除了一个键值对,所以大小为0
System.out.println(map1.size());
// 清空HashMap中的所有键值对
map1.clear();
// 判断HashMap是否为空,由于已经清空,所以结果为true
System.out.println(map1.isEmpty());
System.out.println("===========");
// 创建一个HashMap,键为Integer类型,值为Integer类型
HashMap map2 = new HashMap<>();
// 向HashMap中添加键值对
map2.put(56, 7285);
map2.put(34, 3671263);
map2.put(17, 716311);
map2.put(24, 1263161);
// 如果键的范围是固定的、可控的,那么可以使用数组来替代哈希表的功能
// 创建一个长度为100的整数数组
int[] arr = new int[100];
// 通过数组索引来模拟哈希表的键值对存储,将值赋给对应的索引位置
arr[56] = 7285;
arr[34] = 3671263;
arr[17] = 716311;
arr[24] = 1263161;
// 在笔试场景中,当满足上述条件时,哈希表的功能往往可以被数组替代
System.out.println("在笔试场合中哈希表往往会被数组替代");
System.out.println("===========");
// 创建一个内部类Student的两个实例,这两个实例的age和name属性相同
Student s1 = new Student(17, "张三");
Student s2 = new Student(17, "张三");
// 创建一个HashMap,键为Student类型,值为String类型
HashMap map3 = new HashMap<>();
// 向HashMap中添加键值对,键为s1,值为"这是张三"
map3.put(s1, "这是张三");
// 由于HashMap判断键是否存在是根据键对象的hashCode和equals方法
// 而默认情况下Student类没有重写这两个方法,s1和s2是不同的对象实例,所以这里返回true
System.out.println(map3.containsKey(s1));
// 这里返回false,因为默认的对象比较是基于内存地址,s2和s1内存地址不同
System.out.println(map3.containsKey(s2));
// 向HashMap中添加键为s2的键值对,由于Student类没有合适的hashCode和equals方法重写
// 所以会当作不同的键处理,此时map3的大小变为2
map3.put(s2, "这是另一个张三");
System.out.println(map3.size());
// 获取键为s1对应的值并输出
System.out.println(map3.get(s1));
// 获取键为s2对应的值并输出
System.out.println(map3.get(s2));
}
// 内部类Student,用于表示学生,包含年龄和姓名两个属性
public static class Student {
public int age;
public String name;
// 学生类的构造函数,用于初始化年龄和姓名属性
public Student(int a, String b) {
age = a;
name = b;
}
}
}
对比维度 | TreeSet | TreeMap |
---|---|---|
存储结构 | 基于红黑树,存储单个元素 | 基于红黑树,存储键值对 |
元素唯一性 | 不允许重复元素 | 键不允许重复,值可以重复 |
排序依据 | 元素按照自然顺序(如数字大小、字母顺序)或者自定义比较器确定的顺序排列 | 键按照自然顺序或者自定义比较器确定的顺序排列 |
空值处理 | 不允许存储null元素 | 键不允许为null,值可以为null |
访问方式 | 只能遍历元素 | 可以通过键来访问对应的值,也可以遍历键值对 |
接口实现 | 实现Set接口 | 实现Map接口 |
主要用途 | 用于存储一组有序且不重复的元素,如存储排序后的数据集 | 用于存储一组有序的键值对,如存储配置信息(键为配置项,值为配置内容) |
性能特点 | 插入、删除、查找等操作的时间复杂度为O(log n) | 插入、删除、键查找等操作的时间复杂度为O(log n) |
TreeSet:适合在需要对元素进行排序并且保证元素唯一性的场景下使用。例如,在一个需要对用户输入的单词进行排序并且去除重复单词的程序中,可以使用TreeSet。
TreeSet:操作效率在数据量较大时能保持相对稳定,因为其基于红黑树结构,每次操作的时间复杂度为对数级别的O(log n)。
TreeMap:适用于需要根据键来进行排序存储键值对的场景。比如在一个对学生成绩进行管理的系统中,以学生姓名为键,成绩为值,使用TreeMap可以按照姓名顺序存储和查询相关信息。
TreeMap:同样由于基于红黑树结构,其键相关操作(插入、删除、查找)的时间复杂度为O(log n),在处理大量数据时也能表现出较好的性能。
TreeSet:
当需要对一组数据进行排序并且不希望有重复数据时,例如统计一篇文章中的不同单词。
在数据挖掘中,对收集到的数据进行初步的排序和去重处理。
TreeMap:
在数据库索引的实现中,可以利用TreeMap的键排序特性来构建索引结构,方便数据的快速查找。
在游戏开发中,存储游戏角色的属性(以属性名为键,属性值为值),并且按照特定顺序(如按属性重要性排序)进行管理。
import java.util.PriorityQueue;
import java.util.TreeMap;
import java.util.TreeSet;
// 名为Code02_TreeSetAndTreeMap的类
public class Code02_TreeSetAndTreeMap {
public static void main(String[] args) {
// 创建一个TreeMap,其底层是红黑树结构,键为Integer类型,值为String类型
TreeMap treeMap = new TreeMap<>();
// 向TreeMap中添加键值对,键为数字,值为对应的描述字符串
treeMap.put(5, "这是5");
treeMap.put(7, "这是7");
treeMap.put(1, "这是1");
treeMap.put(2, "这是2");
treeMap.put(3, "这是3");
treeMap.put(4, "这是4");
treeMap.put(8, "这是8");
// 检查TreeMap是否包含键为1的键值对,输出结果为true或false
System.out.println(treeMap.containsKey(1));
// 检查TreeMap是否包含键为10的键值对,输出结果为true或false
System.out.println(treeMap.containsKey(10));
// 获取键为4对应的字符串值并输出
System.out.println(treeMap.get(4));
// 将键为4的值更新为"张三是4"
treeMap.put(4, "张三是4");
// 再次获取键为4对应的字符串值并输出,此时应为"张三是4"
System.out.println(treeMap.get(4));
// 从TreeMap中移除键为4的键值对
treeMap.remove(4);
// 检查获取键为4的值是否为null,由于已移除,结果应为true
System.out.println(treeMap.get(4) == null);
// 获取TreeMap中的最小键并输出
System.out.println(treeMap.firstKey());
// 获取TreeMap中的最大键并输出
System.out.println(treeMap.lastKey());
// 获取TreeMap中所有键小于等于4的键中最大的那个键并输出
System.out.println(treeMap.floorKey(4));
// 获取TreeMap中所有键大于等于4的键中最小的那个键并输出
System.out.println(treeMap.ceilingKey(4));
System.out.println("========");
// 创建一个TreeSet,其底层是红黑树结构,元素为Integer类型
TreeSet set = new TreeSet<>();
// 向TreeSet中添加元素,TreeSet会自动去重,这里添加了3和4,但实际只会各保存一个
set.add(3);
set.add(3);
set.add(4);
set.add(4);
// 输出TreeSet的大小,应为2
System.out.println("有序表大小 : " + set.size());
// 循环取出TreeSet中的最小元素(通过pollFirst方法)并输出,直到TreeSet为空
while (!set.isEmpty()) {
System.out.println(set.pollFirst());
// 如果使用pollLast则是取出最大元素
}
System.out.println("========");
// 创建一个PriorityQueue(默认是小根堆),元素为Integer类型
PriorityQueue heap1 = new PriorityQueue<>();
// 向小根堆中添加元素,小根堆会根据元素大小自动调整堆结构
heap1.add(3);
heap1.add(3);
heap1.add(4);
heap1.add(4);
// 输出小根堆的大小,应为4
System.out.println("堆大小 : " + heap1.size());
// 循环取出小根堆的堆顶元素(最小元素)并输出,直到小根堆为空
while (!heap1.isEmpty()) {
System.out.println(heap1.poll());
}
System.out.println("========");
// 创建一个定制的PriorityQueue(大根堆),通过传入比较器来实现
// 比较器的逻辑是(b - a),使得元素按照从大到小的顺序排列在堆中
PriorityQueue heap2 = new PriorityQueue<>((a, b) -> b - a);
// 向大根堆中添加元素,大根堆会根据自定义的比较器调整堆结构
heap2.add(3);
heap2.add(3);
heap2.add(4);
heap2.add(4);
// 输出大根堆的大小,应为4
System.out.println("堆大小 : " + heap2.size());
// 循环取出大根堆的堆顶元素(最大元素)并输出,直到大根堆为空
while (!heap2.isEmpty()) {
System.out.println(heap2.poll());
}
}
}
在Java中,比较器(Comparator)是一个接口,用于定义对象之间的比较规则。它允许我们按照自定义的方式对对象进行比较,而不是依赖对象自身的自然顺序(如果有)。
Employee
类及对象创建Employee
类结构
Employee
的内部类,包含两个属性company
(代表公司编号)和age
(代表年龄)。通过构造函数public Employee(int c, int a)
可以方便地创建Employee
对象并初始化这两个属性。main
方法中,创建了多个Employee
对象,如s1
、s2
等,这些对象构成了后续操作的数据集合。EmployeeComparator
类EmployeeComparator
类实现了Comparator
接口,用于定义Employee
对象之间的比较规则。compare
方法中,通过return o1.age - o2.age
来比较两个Employee
对象o1
和o2
的年龄。o1.age - o2.age < 0
,表示o1
的年龄小于o2
的年龄,按照排序规则,o1
应该排在o2
之前;如果o1.age - o2.age = 0
,表示o1
和o2
年龄相同;如果o1.age - o2.age > 0
,表示o1
的年龄大于o2
的年龄,o2
应该排在o1
之前。lambda
表达式的比较器Arrays.sort(arr, (a, b) -> b.age - a.age)
使用lambda
表达式定义了一个比较器。b.age - a.age
来比较两个Employee
对象a
和b
的年龄。当b.age - a.age < 0
时,表示b
的年龄小于a
的年龄,所以b
应该排在a
之前,从而实现了按照年龄从大到小的排序。Arrays.sort(arr, (a, b) -> a.company!= b.company? (a.company - b.company) : (a.age - b.age))
定义了一个更复杂的比较器。a.company
和b.company
是否不相等。如果a.company - b.company < 0
,表示a
的公司编号小于b
的公司编号,a
应排在b
之前;如果a.company = b.company
,则进一步通过a.age - b.age
来比较年龄,以确定顺序。TreeSet
相关算法原理treeSet1
EmployeeComparator
的TreeSet
操作
TreeSet treeSet1 = new TreeSet<>(new EmployeeComparator());
创建了一个TreeSet
,并使用EmployeeComparator
作为比较器。treeSet1
中添加Employee
对象时,TreeSet
会根据EmployeeComparator
中定义的年龄比较规则来确定元素的顺序。TreeSet
不允许重复元素,根据EmployeeComparator
的比较规则(年龄相同即为相同元素),当尝试添加一个已经存在的Employee
对象(如treeSet1.add(new Employee(2, 27));
)时,TreeSet
会认为这是一个重复元素而不添加,所以treeSet1
的大小保持不变。treeSet2
TreeSet
操作
TreeSet treeSet2 = new TreeSet<>((a, b) -> a.company!= b.company? (a.company - b.company) : a.age!= b.age? (a.age - b.age) : a.toString().compareTo(b.toString()))
定义了一个更复杂的比较器。a.company - b.company < 0
,表示a
的公司编号小于b
的公司编号,a
应排在b
之前;如果公司编号相同,则进一步比较年龄,若a.age - b.age < 0
,表示a
的年龄小于b
的年龄,a
应排在b
之前;如果年龄也相同,则通过a.toString().compareTo(b.toString())
比较对象的字符串表示来确定顺序。treeSet2
添加元素时,会根据这个复杂的比较规则确定元素顺序和唯一性。在treeSet2.add(new Employee(2, 27));
时,由于之前的比较规则更细致,这个操作不会被视为添加重复元素,所以treeSet2
的大小会增加。字符串比较
str1 = "abcde"
和str2 = "ks"
,str1.compareTo(str2)
和str2.compareTo(str1)
按照字典序进行比较。import java.util.Arrays;
import java.util.Comparator;
import java.util.TreeSet;
// 这是一个名为Code03_Comparator的类
public class Code03_Comparator {
// 内部类Employee,表示员工类,包含公司编号和年龄两个属性
public static class Employee {
public int company;
public int age;
// 员工类的构造函数,用于初始化公司编号和年龄
public Employee(int c, int a) {
company = c;
age = a;
}
}
// 内部类EmployeeComparator实现了Comparator接口,用于定义Employee对象的比较规则
public static class EmployeeComparator implements Comparator {
// 重写compare方法,比较规则为根据员工年龄比较
// 如果o1的年龄小于o2的年龄,返回负数,表示o1的优先级更高(按照年龄从小到大排序)
// 如果o1的年龄大于o2的年龄,返回正数,表示o2的优先级更高
// 如果年龄相等,返回0
@Override
public int compare(Employee o1, Employee o2) {
return o1.age - o2.age;
}
}
public static void main(String[] args) {
// 创建多个Employee对象
Employee s1 = new Employee(2, 27);
Employee s2 = new Employee(1, 60);
Employee s3 = new Employee(4, 19);
Employee s31 = new Employee(4, 19);
Employee s4 = new Employee(3, 23);
Employee s5 = new Employee(1, 35);
Employee s6 = new Employee(3, 55);
Employee[] arr = {s1, s2, s3, s4, s5, s6};
// 使用自定义的比较器EmployeeComparator对数组进行排序
Arrays.sort(arr, new EmployeeComparator());
// 遍历排序后的数组并输出每个员工的公司编号和年龄
for (Employee e : arr) {
System.out.println(e.company + ", " + e.age);
}
System.out.println("=====");
// 使用lambda表达式定义比较规则,按照年龄从大到小排序
Arrays.sort(arr, (a, b) -> b.age - a.age);
for (Employee e : arr) {
System.out.println(e.company + ", " + e.age);
}
System.out.println("=====");
// 使用lambda表达式定义比较规则,先按照公司编号从小到大排序,如果公司编号相同再按照年龄从小到大排序
Arrays.sort(arr, (a, b) -> a.company!= b.company? (a.company - b.company) : (a.age - b.age));
for (Employee e : arr) {
System.out.println(e.company + ", " + e.age);
}
// 创建一个TreeSet,使用EmployeeComparator作为比较器
TreeSet treeSet1 = new TreeSet<>(new EmployeeComparator());
for (Employee e : arr) {
treeSet1.add(e);
}
// 输出TreeSet的大小,由于TreeSet不允许重复元素,这里会是6
System.out.println(treeSet1.size());
// 尝试添加一个已经存在的Employee对象(根据比较器的定义,年龄相同即为相同元素),TreeSet会去重
treeSet1.add(new Employee(2, 27));
// 输出TreeSet的大小,仍然为6
System.out.println(treeSet1.size());
System.out.println("===");
// 创建一个新的TreeSet,使用更复杂的比较规则
// 先按照公司编号比较,如果相同再按照年龄比较,如果年龄也相同则按照对象的toString方法比较(这里可以用来区分不同对象)
TreeSet treeSet2 = new TreeSet<>((a, b) -> a.company!= b.company? (a.company - b.company)
: a.age!= b.age? (a.age - b.age) : a.toString().compareTo(b.toString()));
for (Employee e : arr) {
treeSet2.add(e);
}
// 输出TreeSet的大小,由于比较规则更细致,这里会是6
System.out.println(treeSet2.size());
// 尝试添加一个已经存在的Employee对象(根据前面的比较规则,这个对象与之前的不同),TreeSet不会去重
treeSet2.add(new Employee(2, 27));
// 输出TreeSet的大小,现在为7
System.out.println(treeSet2.size());
System.out.println("===");
// 简单演示字符串的字典序比较
String str1 = "abcde";
String str2 = "ks";
// 比较str1和str2,按照字典序,如果str1小于str2,返回负数
System.out.println(str1.compareTo(str2));
// 比较str2和str1,按照字典序,如果str2大于str1,返回正数
System.out.println(str2.compareTo(str1));
}
}