25.【必备】哈希表、有序表和比较器的用法

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解026【必备】哈希表、有序表和比较器的用法_哔哩哔哩_bilibili

一.HashSet和HashMap

对比表格

对比维度 HashSet HashMap
存储方式 只存储元素的值,不存储键值对 存储键值对,每个元素由键值对(key-value)组成
底层数据结构 基于哈希表实现,只存储键,不存储值 基于哈希表实现,使用数组和链表或红黑树来实现存储和检索
元素的唯一性 元素是唯一的,重复元素会被自动去重 键是唯一的,但值可以重复
性能表现 适用于需要存储唯一元素的场景 适用于需要通过键来获取值的场景
元素访问 只能迭代所有的元素 可以通过键来获取对应的值,也可以迭代所有的键值对
接口实现 实现了Set接口 实现了Map接口
用途 用于存储不重复的元素 用于存储键值对,允许根据键来查找值
元素类型 存储单一的元素类型,如整数、字符串等 存储键值对,键和值可以是不同的类型
迭代顺序 不保证迭代顺序恒久不变 不保证键值对的顺序
线程安全 不是线程安全的,需要在多线程访问时显式同步 非synchronized,但collection框架提供方法能保证synchronized
允许null值 允许有null值 允许键和值为null

结论

  • HashSet:适用于只需要存储不重复元素的场景,例如去重、查找关键词等。它提供了快速的查找、插入和删除操作,并且不允许有重复的元素。
  • HashMap:适用于需要存储键值对的场景,例如缓存、数据库等。它提供了快速的查找、插入和删除操作,并且允许根据键来查找值。

使用场景

  • HashSet:当你需要存储一组唯一的值,并且需要快速查找、插入和删除元素时,可以使用HashSet。例如,在一个搜索系统中,你可以使用HashSet来存储关键词,以便快速查找和去重。
  • HashMap:当你需要存储一组键值对,并且需要快速查找、插入和删除元素时,可以使用HashMap。例如,在一个缓存系统中,你可以使用HashMap来存储键值对,以便快速查找和更新缓存。

注意事项

  • 在使用HashSet和HashMap时,需要注意重写对象的equals()和hashCode()方法,以确保正确的比较和存储。
  • HashSet和HashMap都不是线程安全的,如果在多线程环境中使用,需要进行额外的同步处理。
  • HashMap在存储键值对时,键是唯一的,如果插入重复的键,会覆盖原来的值。

代码实现

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

对比表格

对比维度 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的键排序特性来构建索引结构,方便数据的快速查找。

  • 在游戏开发中,存储游戏角色的属性(以属性名为键,属性值为值),并且按照特定顺序(如按属性重要性排序)进行管理。

注意事项

  • 在使用TreeSet和TreeMap时,若要使用自定义对象作为元素(TreeSet)或者键(TreeMap),需要正确定义比较方法(实现Comparable接口或者提供Comparator),以确保元素或者键能够正确排序。
  • 对于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对象,如s1s2等,这些对象构成了后续操作的数据集合。
二、比较器相关算法原理
(一)EmployeeComparator
  • 比较规则
    • EmployeeComparator类实现了Comparator接口,用于定义Employee对象之间的比较规则。
    • compare方法中,通过return o1.age - o2.age来比较两个Employee对象o1o2的年龄。
    • 如果o1.age - o2.age < 0,表示o1的年龄小于o2的年龄,按照排序规则,o1应该排在o2之前;如果o1.age - o2.age = 0,表示o1o2年龄相同;如果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对象ab的年龄。当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.companyb.company是否不相等。如果a.company - b.company < 0,表示a的公司编号小于b的公司编号,a应排在b之前;如果a.company = b.company,则进一步通过a.age - b.age来比较年龄,以确定顺序。
三、TreeSet相关算法原理
(一)treeSet1
  • 基于EmployeeComparatorTreeSet操作
    • 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)按照字典序进行比较。
    • 在Java中,字符串的字典序比较是基于字符的Unicode值。从字符串的第一个字符开始比较,如果对应字符的Unicode值不同,则根据Unicode值的大小关系确定字符串的顺序;如果前面的字符都相同,则继续比较下一个字符,直到找到不同的字符或者到达字符串的末尾。如果一个字符串是另一个字符串的前缀,则较短的字符串小于较长的字符串。

代码实现

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));
    }
}

你可能感兴趣的:(数据结构与算法,散列表,数据结构,算法)