Java学习day061 具体的集合(二)(数组列表、散列集、树集、队列与双端队列、优先级队列)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day061 具体的集合(二)(数组列表、散列集、树集、队列与双端队列、优先级队列)


1.数组列表

前面介绍了 List 接口和实现了这个接口的LinkedList类。List 接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是用迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用于链表,但对数组却很有用。集合类库提供了一种大家熟悉的ArrayList类,这个类也实现了List接口。ArrayList封装了一个动态再分配的对象数组。

对于一个经验丰富的Java程序员来说,在需要动态数组时,可能会使用Vector类。为什么要用ArrayList取代Vector呢?原因很简单:Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。这种情况还是很常见的。而ArrayList方法不是同步的,因此,建议在不需要同步时使用ArrayList,而不要使用Vector。


2.散列集

链表和数组可以按照人们的意愿排列元素的次序。但是,如果想要査看某个指定的元素,却又忘记了它的位置,就需要访问所有元素,直到找到为止。如果集合中包含的元素很多,将会消耗很多时间。如果不在意元素的顺序,可以有几种能够快速査找元素的数据结构。其缺点是无法控制元素出现的次序。它们将按照有利于其操作目的的原则组织数据。

有一种众所周知的数据结构,可以快速地査找所需要的对象,这就是散列表(table)。散列表为每个对象计算一个整数,称为散列码(hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。下表列出了几个散列码的示例,它们是由String类的hashCode方法产生的。         

                                                  

如果自定义类,就要负责实现这个类的hashCode方法。有关hashCode方法的详细内容请参看之前的内容。注意,自己实现的hashCode方法应该与equals方法兼容,即如果a_equals(b)为true,a与b必须具有相同的散列码。

现在,最重要的问题是散列码要能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中的其他对象无关。在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。要想査找表中对象的位置,就要先计算它的散列码,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如,如果某个对象的散列码为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)。或许会很幸运,在这个桶中没有其他元素,此时将元素直接插人到桶中就可以了。

                                                  

当然,有时候会遇到桶被占满的情况,这也是不可避免的。这种现象被称为散列冲突(hashcollision)。这时,需要用新对象与桶中的所有对象进行比较,査看这个对象是否已经存在。如果散列码是合理且随机分布的,桶的数目也足够大,需要比较的次数就会很少。

如果想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。如果要插入到散列表中的元素太多,就会增加冲突的可能性,降低运行性能。

如果大致知道最终会有多少个元素要插人到散列表中,就可以设置桶数。通常,将桶数设置为预计元素个数的75%~150%。有些研究人员认为:尽管还没有确凿的证据,但最好将桶数设置为一个素数,以防键的集聚。标准类库使用的桶数是2的幂,默认值为16(为表大小提供的任何值都将被自动地转换为2的下一个幂)。

当然,并不是总能够知道需要存储多少个元素的,也有可能最初的估计过低。如果散列表太满,就需要再散列(rehashed)。如果要对散列表再散列,就需要创建一个桶数更多的表,并将所有元素插入到这个新表中.,然后丢弃原来的表。装填因子(loadfactor)决定何时对散列表进行再散列。例如,如果装填因子为0.75(默认值),而表中超过75%的位置已经填人元素,这个表就会用双倍的桶数自动地进行再散列。对于大多数应用程序来说,装填因子为0.75是比较合理的。

散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set是没有重复元素的元素集合。set的add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。

Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中査找元素,而不必查看集合中的所有元素。

散列集迭代器将依次访问所有的桶。由于散列将元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。只有不关心集合中元素的顺序时才应该使用HashSet。

下面的程序将从System.in读取单词,然后将它们添加到集中,最后,再打印出集中的所有单词。将文本文件放置在同一个文件夹内,并从命令行 shell 运行:

java SetTest < alice30.txt

这个程序将读取输人的所有单词,并且将它们添加到散列集中。然后遍历散列集中的不同单词,最后打印出单词的数量。单词以随机的顺序出现。在更改集中的元素时要格外小心。如果元素的散列码发生了改变,元素在数据结构中的位置也会发生变化。

下面是程序的代码:

/**
 *@author  zzehao
 */
import java.util.*;

public class SetTest
{
	public static void main(String[] args)
	{
		Set<String> words = new HashSet<>();//HashSet implements Set
		long totalTime = 0;

		try(Scanner in = new Scanner(System.in))
		{
			while (in.hasNext())
			{
				String word = in.next();
				long callTime = System.currentTimeMillis();
				words.add(word);
				callTime = System.currentTimeMillis()-callTime;
				totalTime += callTime;
			}
		}

		Iterator<String> iter = words.iterator();
		for(int i = 1;i<=20 && iter.hasNext();i++)
			System.out.println(iter.next());
		System.out.println("...");
		System.out.println(words.size()+" distinct words. "+ totalTime +" milliseconds. ");
	}
}

运行的结果是:


3.树集

TreeSet类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合(sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。例如,假设插入3个字符串,然后访问添加的所有元素。

SortedSet<String>sorter = newTreeSet<>(); //TreeSet implements SortedSet
sorter.add("Bob");
sorter.add("Aniy");
sorter.add("Carl");
for (String s : sorter) System.println(s);

这时,每个值将按照顺序打印出来:Amy Bob Carl。正如TreeSet类名所示,排序是用树结构完成的(当前实现使用的是红黑树(red-black tree)。每次将一个元素添加到树中时,都被放置在正确的排序位置上。因此,迭代器总是以排好序的顺序访问每个元素。将一个元素添加到树中要比添加到散列表中慢,如下表的比较,但是,与检查数组或链表中的重复元素相比还是快很多。如果树中包含n个元素,査找新元素的正确位置平均需要log2n次比较。例如,如果一棵树包含了1000个元素,添加一个新元素大约需要比较10次。

    

要使用树集,必须能够比较元素。这些元素必须实现 Comparable接口,或者构造集时必须提供一个 Comparator。回头看一看表可能会有疑虑:是否总是应该用树集取代散列集。毕竟,添加一个元素所花费的时间看上去并不很长,而且元素是自动排序的。到底应该怎样做将取决于所要收集的数据。如果不需要对数据进行排序,就没有必要付出排序的开销。更重要的是,对于某些数据来说,对其排序要比散列函数更加困难。散列函数只是将对象适当地打乱存放,而比较却要精确地判别每个对象。要想具体地了解它们之间的差异,还需要研究一个收集矩形集的任务。如果使用TreeSet,就需要提供Comparator<Rectangle>。如何比较两个矩形呢?比较面积吗?这行不通。可能会有两个不同的矩形,它们的坐标不同,但面积却相同。树的排序必须是全序。也就是说,任意两个元素必须是可比的,并且只有在两个元素相等时结果才为0。确实,有一种矩形的排序(按照坐标的词典顺序排列)方式,但它的计算很牵强且很繁琐。相反地,Rectangle类已经定义了散列函数,它直接对坐标进行散列。

下面的程序中创建了两个Item对象的树集。第一个按照部件编号排序,这是Item对象的默认顺序。第二个通过使用一个定制的比较器来按照描述信息排序。

/**
 *@author  zzehao
 */
import java.util.*;

public class TreeSetTest
{
	public static void main(String[] args)
	{
		SortedSet<Item> parts = new TreeSet<>();
		parts.add(new Item("Toaster",1234));
		parts.add(new Item("Widget",4562));
		parts.add(new Item("Modem",9912));
		System.out.println(parts);

		NavigableSet<Item> sortByDescription = new TreeSet<>(Comparator.comparing(Item::getDescription));

		sortByDescription.addAll(parts);
		System.out.println(sortByDescription);
	}
}
/**
 *@author  zzehao
 */
import java.util.*;

public class Item implements Comparable<Item>
{
	private String description;
	private int partNumber;

	public Item(String aDescription,int aPartNumber)
	{
		description = aDescription;
		partNumber = aPartNumber;
	}

	public String getDescription()
	{
		return description;
	}
	
	public String toString()
	{
		return "[descripion=" +description+ ", partNumber="+partNumber+ "]";
	}

	public boolean equals(Object otherObject) 
	{
		if (this == otherObject) return true; 
		if (otherObject == null) return false; 
		if (getClass() != otherObject.getClass()) return false; 
		Item other =(Item) otherObject; 
		return Objects.equals(description, other.description) && partNumber == other.partNumber; 
	}
	
	public int hashCode() 
	{
		return Objects.hash(description, partNumber); 
	}
	
	public int compareTo(Item other) 
	{
		int diff = Integer.compare(partNumber,other.partNumber); 
		return diff != 0 ? diff : description.compareTo(other.description);
	}
}

运行的结果是:

    


4.队列与双端队列

前面已经讨论过,队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。在JavaSE6中引人了Deque接口,并由ArrayDeque和LinkedList类实现。这两个类都提供了双端队列,而且在必要时可以增加队列的长度。

                  

                     


5.优先级队列

优先级队列(priority queue)中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆(heap)。堆是一个可以自我调整的二叉树,对树执行添加(add)和删除(remore)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。

与TreeSet—样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可以保存在构造器中提供的Comparator对象。

使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每当启动一个新的任务时,都将优先级最高的任务从队列中删除(由于习惯上将1设为“最高”优先级,所以会将最小的元素删除)。

下面的程序显示了一个正在运行的优先级队列。与TreeSet中的迭代不同,这里的迭代并不是按照元素的排列顺序访问的。而删除却总是删掉剩余元素中优先级数最小的那个元素。

/**
 *@author  zzehao
 */
import java.util.*;
import java.time.*;

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

运行的结果:


 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值