MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java TreeMap的排序规则应用

2021-02-267.2k 阅读

Java TreeMap的排序规则应用

TreeMap简介

在Java集合框架中,TreeMap是一个重要的实现类,它实现了NavigableMap接口,而NavigableMap又继承自SortedMap接口。TreeMap基于红黑树(Red-Black Tree)数据结构实现,这使得它具有良好的有序性。与HashMap不同,TreeMap中的元素是根据键的自然顺序或者我们自定义的顺序进行排序的。

红黑树是一种自平衡的二叉查找树,它的每个节点都带有颜色属性,颜色要么是红色,要么是黑色。通过一系列的规则(如节点是红色或黑色、根节点是黑色、每个叶节点(NIL节点,空节点)是黑色、如果一个节点是红色的,则它的子节点必须是黑色的、从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点等)来维持树的平衡,从而保证了TreeMap在插入、删除和查找操作上的时间复杂度为O(log n),其中n是树中节点的数量。

自然排序

自然排序的原理

当我们使用TreeMap的无参构造函数创建TreeMap对象时,它会按照键的自然顺序进行排序。所谓自然顺序,就是指实现了Comparable接口的类所定义的顺序。在Java中,许多基本数据类型的包装类(如IntegerDoubleString等)以及一些常用的类(如Date)都已经实现了Comparable接口,因此可以直接作为TreeMap的键并按照自然顺序排序。

例如,对于Integer类型的键,自然顺序就是从小到大的顺序;对于String类型的键,自然顺序是按照字典序(基于字符的Unicode值)排序。

代码示例

import java.util.Map;
import java.util.TreeMap;

public class TreeMapNaturalOrderExample {
    public static void main(String[] args) {
        // 创建一个TreeMap,键为Integer类型
        TreeMap<Integer, String> treeMap = new TreeMap<>();

        // 向TreeMap中添加元素
        treeMap.put(3, "Three");
        treeMap.put(1, "One");
        treeMap.put(2, "Two");

        // 遍历TreeMap,输出结果会按照键的自然顺序(从小到大)排列
        for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
            System.out.println(entry.getKey() + " : " + entry.getValue());
        }
    }
}

在上述代码中,我们创建了一个TreeMap,键的类型为Integer。虽然我们添加元素的顺序是312,但是当我们遍历TreeMap时,输出结果会按照键的自然顺序(从小到大)排列,即1 : One2 : Two3 : Three

再看一个String类型键的例子:

import java.util.Map;
import java.util.TreeMap;

public class TreeMapStringNaturalOrderExample {
    public static void main(String[] args) {
        TreeMap<String, Integer> treeMap = new TreeMap<>();

        treeMap.put("banana", 3);
        treeMap.put("apple", 1);
        treeMap.put("cherry", 2);

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

这里,String类型的键按照字典序排序,输出结果为apple : 1banana : 3cherry : 2

自定义排序

实现Comparator接口

有时候,我们需要按照自定义的规则对TreeMap中的键进行排序。这时,我们可以通过实现Comparator接口来定义自己的比较逻辑。Comparator接口中有一个compare(T o1, T o2)方法,我们需要在这个方法中定义如何比较两个对象。

例如,假设我们有一个自定义的类Person,并且希望根据Person的年龄对TreeMap中的键进行排序。

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p1.getAge() - p2.getAge();
    }
}

public class TreeMapCustomOrderExample {
    public static void main(String[] args) {
        // 使用AgeComparator来创建TreeMap
        TreeMap<Person, String> treeMap = new TreeMap<>(new AgeComparator());

        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Bob", 20);
        Person p3 = new Person("Charlie", 30);

        treeMap.put(p1, "Info of Alice");
        treeMap.put(p2, "Info of Bob");
        treeMap.put(p3, "Info of Charlie");

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

在上述代码中,我们定义了一个AgeComparator类实现了Comparator接口,在compare方法中,通过比较Person对象的年龄来确定顺序。然后,我们使用这个AgeComparator来创建TreeMap,这样TreeMap中的元素就会按照Person的年龄从小到大进行排序。

使用匿名内部类定义Comparator

除了像上面那样定义一个单独的类来实现Comparator接口,我们还可以使用匿名内部类的方式来定义Comparator

import java.util.Map;
import java.util.TreeMap;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TreeMapAnonymousComparatorExample {
    public static void main(String[] args) {
        // 使用匿名内部类定义Comparator来创建TreeMap
        TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getAge() - p2.getAge();
            }
        });

        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Bob", 20);
        Person p3 = new Person("Charlie", 30);

        treeMap.put(p1, "Info of Alice");
        treeMap.put(p2, "Info of Bob");
        treeMap.put(p3, "Info of Charlie");

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

这种方式更加简洁,适合比较逻辑较为简单且只在当前代码块中使用的情况。

使用Lambda表达式定义Comparator

在Java 8及以后,我们还可以使用Lambda表达式来定义Comparator,这使得代码更加简洁明了。

import java.util.Map;
import java.util.TreeMap;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class TreeMapLambdaComparatorExample {
    public static void main(String[] args) {
        // 使用Lambda表达式定义Comparator来创建TreeMap
        TreeMap<Person, String> treeMap = new TreeMap<>((p1, p2) -> p1.getAge() - p2.getAge());

        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Bob", 20);
        Person p3 = new Person("Charlie", 30);

        treeMap.put(p1, "Info of Alice");
        treeMap.put(p2, "Info of Bob");
        treeMap.put(p3, "Info of Charlie");

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

在这个例子中,(p1, p2) -> p1.getAge() - p2.getAge()就是一个Lambda表达式,它简洁地定义了Person对象之间的比较逻辑。

逆序排序

使用Collections.reverseOrder()

如果我们希望对TreeMap中的键进行逆序排序,除了在Comparatorcompare方法中编写逆序的比较逻辑外,还可以使用Collections.reverseOrder()方法。这个方法会返回一个逆序的Comparator

import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapReverseOrderExample {
    public static void main(String[] args) {
        // 使用Collections.reverseOrder()来创建逆序的TreeMap
        TreeMap<Integer, String> treeMap = new TreeMap<>(Collections.reverseOrder());

        treeMap.put(3, "Three");
        treeMap.put(1, "One");
        treeMap.put(2, "Two");

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

在上述代码中,通过Collections.reverseOrder()方法创建的Comparator会使TreeMap中的键按照从大到小的顺序排列。输出结果为3 : Three2 : Two1 : One

自定义逆序Comparator

我们也可以自己编写一个逆序的Comparator。以Person类为例,假设我们要按照年龄从大到小排序。

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

class ReverseAgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return p2.getAge() - p1.getAge();
    }
}

public class TreeMapCustomReverseOrderExample {
    public static void main(String[] args) {
        // 使用自定义的逆序Comparator来创建TreeMap
        TreeMap<Person, String> treeMap = new TreeMap<>(new ReverseAgeComparator());

        Person p1 = new Person("Alice", 25);
        Person p2 = new Person("Bob", 20);
        Person p3 = new Person("Charlie", 30);

        treeMap.put(p1, "Info of Alice");
        treeMap.put(p2, "Info of Bob");
        treeMap.put(p3, "Info of Charlie");

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

ReverseAgeComparator类的compare方法中,我们通过p2.getAge() - p1.getAge()实现了年龄从大到小的排序。

排序规则对操作的影响

插入操作

当向TreeMap中插入一个新的键值对时,TreeMap会根据当前的排序规则找到合适的位置插入新的键。如果键已经存在,则会更新对应的值。由于TreeMap基于红黑树实现,插入操作的时间复杂度为O(log n),其中n是TreeMap中元素的数量。在插入过程中,红黑树会通过旋转和颜色调整等操作来维持树的平衡,以保证排序规则始终得到满足。

例如,在自然排序的TreeMap中插入新元素时,它会按照键的自然顺序找到插入位置。假设我们有一个自然排序的TreeMap,键为Integer类型,当前树中有元素13,当我们插入键为2的元素时,TreeMap会找到合适的位置将其插入,使得树仍然保持有序。

删除操作

删除操作同样会受到排序规则的影响。当从TreeMap中删除一个键值对时,TreeMap会先根据排序规则找到要删除的键所在的节点。然后,通过红黑树的删除操作(可能涉及节点的替换、旋转和颜色调整等)将该节点从树中移除,并维持树的平衡和排序规则。删除操作的时间复杂度也是O(log n)。

例如,在一个按照年龄排序的TreeMap(键为Person对象)中,如果我们删除一个Person对象,TreeMap会根据年龄排序规则找到对应的节点并删除,同时保证剩余元素仍然按照年龄顺序排列。

查找操作

查找操作在TreeMap中也依赖于排序规则。TreeMap可以高效地查找特定键的值,因为它可以利用排序后的结构进行二分查找。例如,在一个自然排序的TreeMap中查找一个Integer类型的键时,TreeMap会从根节点开始,根据键的大小与当前节点的键进行比较,决定向左子树还是右子树继续查找,直到找到目标键或者确定目标键不存在。查找操作的时间复杂度同样为O(log n)。

注意事项

键的一致性

在使用TreeMap时,必须保证所有插入的键都遵循相同的排序规则。如果在插入过程中,键的类型不一致或者键的比较逻辑发生变化,可能会导致不可预测的结果。例如,如果我们在一个按照年龄排序的TreeMap(键为Person对象)中,突然插入一个非Person类型的对象作为键,或者在插入部分Person对象后修改了Person类中compare方法的逻辑,就可能破坏TreeMap的有序性。

性能考虑

虽然TreeMap在插入、删除和查找操作上具有较好的时间复杂度O(log n),但与HashMap相比,由于需要维护树的平衡和排序,TreeMap在插入和删除操作上可能会稍微慢一些。因此,如果我们不需要元素的有序性,并且更注重插入和删除的性能,HashMap可能是更好的选择。另外,在创建TreeMap时,如果我们预计元素数量较多,可以考虑预先设置合适的初始容量,以减少动态扩容带来的性能开销。

空键问题

TreeMap不允许键为null。这是因为TreeMap是基于排序的,而null值无法参与比较操作。如果尝试向TreeMap中插入键为null的键值对,会抛出NullPointerException。例如:

import java.util.TreeMap;

public class TreeMapNullKeyExample {
    public static void main(String[] args) {
        TreeMap<String, Integer> treeMap = new TreeMap<>();
        // 下面这行代码会抛出NullPointerException
        treeMap.put(null, 1);
    }
}

在编写代码时,我们需要注意这一点,避免出现不必要的异常。

通过深入理解和合理应用TreeMap的排序规则,我们可以在Java编程中更加灵活高效地处理需要有序存储和操作的数据集合。无论是自然排序还是自定义排序,TreeMap都为我们提供了强大的功能来满足不同的业务需求。同时,我们也需要注意使用过程中的一些细节和性能问题,以确保程序的正确性和高效性。