Java核心技术 卷1-总结-13
- 具体的集合
- 散列集
- 树集
- 队列与双端队列
- 优先级队列
- 映射
- 基本映射操作
具体的集合
散列集
链表和数组可以有序的存储元素。但是,如果想要查看某个指定的元素,却又忘记了它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多,将会消耗很多时间。
散列表(hash table)可以快速地查找所需要的对象。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。 散列码是由String类的hashCode方法产生的。
如果自定义类,就要负责实现这个类的hashCode 方法。 注意,自己实现的hashCode方法应该与equals方法兼容,即如果a.equals(b)
为true
,a与b必须具有相同的散列码。
在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。
要想查找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的散列码为76268,并且有128个桶,对象应该保存在第108号桶中(76268 除以128余108)。在这个桶中如果没有其他元素,就可将元素直接插入到桶中。如果桶中存在其他元素,就会产生散列冲突(hash collision)。这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,发生哈希冲突的可能性就会降低。
注意:在Java SE8中,桶中元素大于等于8且桶的数量超过64时,链表会变为平衡二叉树。如果选择的散列函数不当,或者如果有恶意代码试图在散列表中填充多个有相同散列码的值,这样就能提高性能。
桶数是指用于收集具有相同散列值的桶的数目。 如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将被自动地转换为2的下一个幂)。
并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子(load factor)决定何时对散列表进行再散列。 例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行再散列。
散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set是没有重复元素的元素集合。 set 的add
方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add
方法添加元素。contains
方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。
散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用HashSet。
树集
TreeSet类与散列集十分类似,不过TreeSet是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。 例如,假设插入3个字符串,然后访问添加的所有元素。
SortedSet<String> sorter = new TreeSet<>(); // TreeSet implements SortedSet
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for (String s : sorter) {
System.println(s);
}
这时,每个值将按照顺序打印出来:Amy Bob Carl。TreeSet排序是用树结构完成的(当前实现使用的是红黑树(red-black tree)。 每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。
将一个元素添加到树中要比添加到散列表中慢, 参见下表中的比较,
文 档 | 单词总数 | 不同的单词个数 | HashSet | TreeSet |
---|---|---|---|---|
Alice in Wonderland | 28195 | 5909 | 5秒 | 7秒 |
The Count of Monte Cristo | 466300 | 37545 | 75秒 | 98秒 |
但是,与检查数组或链表中的重复元素相比还是快很多。如果树中包含n个元素,查找新元素的正确位置平均需要logn次比较。 注意:要使用树集,必须能够比较元素。这些元素必须实现Comparable接口,或者构造集时必须提供一个Comparator。
是否应该用树集取代散列集取决于所要收集的数据。如果不需要对数据进行排序,就没有必要付出排序的开销。 更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。散列函数只是将对象适当地打乱存放, 而比较却要精确地判别每个对象。
队列与双端队列
队列可以有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。 在Java SE 6中引人了Deque接口,并由 ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。
优先级队列
优先级队列(priority queue)中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。 然而,优先级队列并没有对所有的元素进行排序。优先级队列使用了一个高效的数据结构,称为堆(heap)。 堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remore)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供的Comparator对象。使用优先级队列的典型示例是任务调度。 每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除。
与TreeSet中的迭代不同,PriorityQueue的迭代并不是按照元素的排列顺序访问的。而删除却总是删掉剩余元素中优先级数最小的那个元素。
import java.time.LocalDate;
import java.util.PriorityQueue;
/**
* This program demonstrates the use of a priority queue.
*/
public class PriorityQueueTest {
public static void main(String[] args) {
PriorityQueue<LocalDate> pq = new PriorityQueue<>();
pq.add(LocalDate.of(1906,12,9));//G.Hopper
pq.add(LocalDate.of(1815,12,10));// A.Lovelace
pq.add(LocalDate.of(1903,12,3));//J.von Neumann
pq.add(LocalDate.of(1910,6,22));// K.Zuse
System.out.println("Iterating over elements...");
for (LocalDate date : pq) {
System.out.println(date);
}
System.out.println("Removing elements...");
while(!pq.isEmpty()) {
System.out.println(pq.remove());
}
}
}
输出
Iterating over elements...
1815-12-10
1906-12-09
1903-12-03
1910-06-22
Removing elements...
1815-12-10
1903-12-03
1906-12-09
1910-06-22
映射
集是一个集合,它可以快速地查找现有的元素。但是,要查看一个元素,需要有要查找元素的精确副本。这不是一种非常通用的查找方式。通常,我们知道某些键的信息,并想要查找与之对应的元素。映射(map)数据结构就是为此设计的。映射用来存放键/值对。如果提供了键,就能够查找到值。
基本映射操作
Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
如果不需要按照排列顺序访问键,就最好选择散列,否则选择树映射。
下列代码将为存储的员工信息建立一个散列映射:
Map<String, Employee> staff= new HashMap<>();// HashMap implements
Map Employee harry = new Employee("Harry Hacker");
staff.put("987-98-9996", harry);
每当往映射中添加对象时,必须同时提供一个键。 在这里,键是一个字符串,对应的值是Employee对象。要想检索一个对象,必须使用一个键。
String id = "987-98-9996";
e = staff.get(id);// gets harry
如果在映射中没有与给定键对应的信息,get
将返回null
。
null返回值可能并不方便。有时可以有一个好的默认值,用作为映射中不存在的键。然后使用getOrDefault
方法。
Map<String,Integer> scores = . . .;
int score=scores.get(id, 0);//Gets 0 if the id is not present
键必须是唯一的。不能对同一个键存放两个值。
remove
方法用于从映射中删除给定键对应的元素。size
方法用于返回映射中的元素数量。要迭代处理映射的键和值,最容易的方法是使用forEach方法。可以提供一个接收键和值的lambda表达式。映射中的每一项会依序调用这个表达式:
scores.forEach((k, v)->
System.out.println("key=" + k + ",value=" + v));