Java中的链接哈希集

102 阅读5分钟

在这篇文章中,我们将了解LinkedHashSet类,它从1.4版开始就包含在java.util中。

目录

  1. 介绍和继承
  2. Java中的集合
  3. Java中的哈希集
  4. Java中的LinkedHashSet
  5. 使用实例
  6. 复杂性

介绍和继承

链接哈希集是一种存储和检索数据的高效方式。从1.4版本开始,每个Java版本都包含了LinkedHashSet这个类。要使用这个类,你首先需要导入java.util.LinkedHashSet。

LinkedHashSet扩展了HashSet类并实现了Set、Cloneable和Serializable接口。在内部,LinkedHashSet使用一个哈希表来存储数据,并使用一个双链表来维持条目顺序。在下面的章节中,我们将分解LinkedHashSet所继承的集合、哈希表和双链表的重要特性。

Java中的集合

一个Set是一个没有任何重复的元素的集合,并且最多只有一个空元素。Java中的Set接口为任何继承自该接口的类描述了Set的特性。AbstractSet类是这个接口的最小实现,它使其他类更容易实现这个接口。Set接口中有许多方法,但最重要的是add(E e)、remove(Object o)、contains(Object o)和size(),它们分别添加一个元素、移除一个元素、检查一个元素是否包含在集合中,以及检查集合中元素的总数。

值得注意的是,集合中的元素是没有顺序的。因此,如果你使用for-each循环来打印Set中的每个元素,这些元素将以某种任意的顺序打印,这可能与它们被插入Set的顺序不同。

Java中的HashSet

HashSet是Set的一个实现,它使用一个哈希表。因此,HashSet扩展了AbstractSet,并由HashMap<K,V>的一个实例来支持。我们已经讨论了Set的功能,所以让我们把注意力转向HashMap<K,V>的使用。

基本上,一个地图只是一个键值对的集合。你可以把字典看作是地图的一种类型,其中每个词是一个键,每个定义是一个值 (事实上,Java 的 HashMap 与 Python 的 Dictionary 非常相似)。HashMap 是一种依赖散列的地图。散列是使用某种算法将一个键作为输入并输出一个叫做 "散列 "的整数值的过程。

例如,假设你有一个键值对<1, "one">,你想把它插入一个哈希图,其中的哈希函数是key%10。那么,1%10=1,所以这个特定键的哈希值是1。然后,值 "one "可能被插入到存放地图中所有值的数组的第一个索引中。像其他地图一样,插入发生在恒定的O(1)时间内。然而,HashMaps的真正优势在于,在最好的情况下,搜索和检索也发生在O(1)时间内。如果你想找到与键1相关的值,那么你可以简单地计算该键的哈希值并检查值数组中的相应位置。

这种最好的情况只发生在没有碰撞的情况下,也就是说,如果每个键都有一个唯一的哈希值。然而,在我们的例子中,我们可以看到,1、11、21、31等都会产生相同的哈希值。处理这些碰撞的一个常用技术叫做线性链。如果数组中的一个位置已经被占用(被其他具有相同哈希值的键值对占用),那么我们将以线性方式检查数组的其余部分,直到我们遇到一个空位。同样地,当我们搜索一个键的值时,我们可能要在数组中进行迭代,以找到我们插入该键值对的位置,这是线性链的结果。在最坏的情况下,这将花费O(n)时间。

HashSet采用了HashMap来加快元素的检索速度。当每个元素e被添加到集合中时,一个条目<e,e>被添加到内部HashMap中。然后,为了在集合中找到e,HashSet可以使用HashMap的快速检索,而不是像普通的Set那样迭代每个元素。

链接哈希集

到目前为止,我们已经了解到HashSet是一个没有重复的无序元素集合,它使用HashMap来在近乎恒定的时间内定位元素。LinkedHashSet是对HashSet的扩展,它在方程中加入了一个双链表并解决了顺序问题。在一个双链列表中,列表中的每个项目,或者说 "节点",都包含两个指针,分别指向它前面的节点和后面的节点。在LinkedHashSet中,每个元素都作为一个节点被存储在双链表中,按照它们被插入的顺序。这很有用,因为很多时候,你会希望能够根据元素的输入顺序看到集合中的所有元素。HashMap仍然用于查找单个元素,但是如果你打印LinkedHashSet中的每个元素,你会发现它们总是按照它们被插入的顺序打印。

下面是LinkedHashSet中的一些重要方法。

  • add(E e): 向集合中添加一个元素
  • remove(Object o): 从集合中删除一个元素
  • contains(Object o): 检查一个元素是否存在于集合中。
  • iterate(): 返回一个集合元素的Iterator对象,按照它们被输入的顺序。

使用实例

让我们看一下Java中的一个例子。

import java.util.LinkedHashSet;

class LinkedHashSetDemo {

    public static void main(String[] args) {
        
        //Create a LinkedHashSet of Strings
        LinkedHashSet<String> lhs = new LinkedHashSet<>();

        //Add elements to our LinkedHashSet
        lhs.add("Hello");
        lhs.add("Error");
        lhs.add("World!");


        //Print elements in order of entry
        System.out.print("My Set: ");
        for (String string : lhs) {
            System.out.print(string+" ");
        }
        System.out.println();

        //Test "contains" method
        System.out.println("\"Error\" detected: "+lhs.contains("Error"));

        //Remove "Error" from the set
        System.out.println("Removing \"Error\": "+lhs.remove("Error"));

        //Test contains again
        System.out.println("\"Error\" detected: "+lhs.contains("Error"));

        //See what happens when we try to remove an element that is not in the set
        System.out.println("Removing \"Error\": "+lhs.remove("Error"));

        //Print all elements of modified set
        System.out.print("My Set: ");
        for (String string : lhs) {
            System.out.print(string+" ");
        }
    }

} 

复杂度

我们已经简单的提到了复杂性,特别是在HashSet部分,但是我们将在这里更详细的研究它。

LinkedHashSet的运行时间复杂度与HashSet相同。这意味着在最好的情况下,插入、移除和搜索都是在恒定的O(1)时间内发生的。但是,最好的情况下有多大可能呢?

嗯,这完全取决于碰撞的数量。哈希表中发生的碰撞越多,就越需要线性链(或其他解决碰撞的过程)。正如我们已经看到的,这减慢了搜索和删除的速度。为了尽可能地避免碰撞,有一些实现细节需要考虑。

  1. 选择一个好的哈希函数。一个好的哈希函数不会对许多不同的输入给出相同的输出。modulo函数作为散列函数相当好用,特别是当除数(modulus运算符之后的数字)相对较大时。然而,hashCode()方法被写进了Java的总体对象类中,所以你可能不必担心这个细节问题。
  2. 选择一个初始容量和一个负载系数。初始容量设定了你最初可以向哈希表插入多少个值的限制。换句话说,它是可以生成的不同哈希值的数量,也就是哈希函数的范围。对于%10,初始容量是10(0到9,包括)。更高的初始容量可能会提高时间复杂度,但会占用更多空间。负载因子是一个浮动值,决定何时增加哈希表的容量。当这种情况发生时,表的大小通常会增加一倍,整个数据集被重新洗牌。由于现在有两倍的空间用于相同数量的数据,碰撞通常会减少,操作也更接近于最佳状态。如果负载系数太低,重新洗牌,这是一个昂贵的过程,会发生得太频繁。如果重洗太高,你可能会遇到更多的碰撞。一般认为0.75的负载因子(重洗将在75%的容量下发生)是减少开销存储和减少时间复杂度之间的良好平衡。

LinkedHashSet类有两个构造函数,你可以设置初始容量或者同时设置初始容量和负载因子。一般来说,它的性能和HashSet类一样好,在大约O(1)时间内添加、删除和查找元素。LinkedHashSet确实需要稍多的空间来存储双链表,但是通常在LinkedHashSet中迭代的速度要比普通的HashSet快,因为它不需要检查哈希表中的每一个空间,它可以直接在列表中迭代。