ArrayList、LinkedList、PriorityQueue、TreeSet

Java常用容器介绍
【以下码字都是抄的笔记】
C++的STL中提供了丰富的容器,帮助我们存储和处理数据。JAVA标准库的java.util工具包中也有相应的容器,这些容器类都继承自两个基本接口Collection和Map.
一般在算法竞赛中,我们会使用到的有
:ArrayList :动态数组 ,一种可以动态增长和缩减的索引序列。
LinkedList :链表,一种可以在任何位置进行高效地插入和删除操作的序列。
PriorityQueue :优先队列,一种可以获得最小元素的集合。
TreeSet :有序集合,一种没有重复元素的有序集合。
TreeMap :有序映射表,一种键值有序排列的映射表。

当我们定义一个数组时需要给予固定的大小,有时候不确定会存多少数据,此时数组开太大浪费内存,太小又怕数据多了存不下,因此我们希望有一个能动态调整容量的数组。C++中提供了vector容器实现这一功能,而JAVA中我们使用ArrayList容器。
注意:Java中也有Vector容器,并且线程是安全的,但效率不如ArrayList,因此在算法竞赛中不会使用Vector。
下面开始介绍这几种常用容器:

ArrayList

ArrayList是一个采用类型参数的泛型类(是泛型类),为了指定保存的元素对象类型,需要用一对尖括号将类名括起来加载后面:
ArrayList.(用前记得import java.util.*;)
下面的代码声明和构造一个保存Integer类对象的ArrayList;

左边可以不写尖括号,但右边必须写上。确保无误,当然最好 是 都写

<>尖括号中的类名应是一个具体的类名,如果我们使用基本数据类型,就需要使用对应的包装器。JAVA提供了9个包装器:Integer、Long、Short、Byte、Double、Float、Character、Boolean、Void。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
比如,我们需要一个储存int类型数据的ArrayList:

使用add方法,可以在ArrayList尾部插入元素

上面的代码中,ArrayList里保存的都是Integer对象,但是我们传进去的却是基本类型int,这是因为JAVA提供了自动装箱、自动拆箱的特性,会给我们自动构成包装器对象,用基本数据类型获取容器中元素的时候也不需要转换。
注意:add方法还有一个重载版本,可以在任意位置插入元素,使得后面的元素都往后移动一个位置:

注意: 该方法比较耗时,在算法竞赛中一般不会使用,如要大量使用该方法,很可能是你用错容器了。
容器的大小,即容器中元素的个数可以用size方法获得:

输出结果为:6
注意:容器的大小(size)和容量(capacity)是两个概念,大小必须满足小于等于容量。刚开始构造ArrayList后,会有一个初始容量。每当插入元素后超过容量时,容量会变为原来的1.5倍。动态数组就是根据这种策略来实现的,每当超过容量时,就开辟一块新的内存,并把原来的数据复制到新的空间里,这样均摊下来,每次调用add方法插入到ArrayList尾部的时间复杂度都是O(1);

如果我们能预估容量,可以把初始容量传递给ArrayList的构造器:

此时初始容量为100,但容器中元素的个数仍为0.
我们也可以调用ensureCapacity方法扩大容量

提前扩大容量是为了避免插入元素时多次开辟内存并复制,可以优化常数。
clear方法是用来清空ArrayList,注意清空后容量依旧不变。如果我们需要释放这部分内存,还应该调用trimToSize方法,该方法是的ArrayList的容量削减到当前的大小(size)。

Java中的数组元素可以用[]下表来访问,而ArrayList却不能,本质上是因为JAVA不支持运算符重载。那我们如何获得指定位置的元素值呢? 这里我们用 get方法。

注意当前size为6,因此可访问的索引范围为0~5,超出范围会抛出java.lang.IndexOutOfBoundsException异常。
同理我们用set方法来修改。第一个参数是修改的位置,第二个参数修改后的值 。

使用remove方法可以从ArrayList中删除元素,该方法有两个重载版本:
1.参数为int类型,表示要删除元素的索引,必须在0~size-1范围内。
2.参数为Object类型,删除容器中值等于该参数的第一个元素,若不存在,则没有任何操作。
删除成功后,被删除元素之后的元素都会往前移动一个位置,我们来看一个例子:
第一次删除索引为3的位置,第二次删除第一个值为3的元素。
注意 我们没有用new Integer(3)来构造一个Integer对象,因为该构造器在JAVA9中被废弃了。 要注意到remove(Object o) 里面是一个对象,Integer是对象,int只是一个基本类型而已。
我们可以用循环来遍历ArrayList:

我们可以用Arrays.asList方法把一个数组转换成ArrayList。

输出为:

这里用到了ArrayList的第三个构造器,可以接受一个collection对象,用该对象的元素构造一个ArrayList。
toArray方法可以把ArrayList转换成一个普通数组:

当我们确定一个ArrayList不再增长时,可以把它重新转换成数组,这样可以方便地使用下标访问了。
AarrayList容器常用方法集合:
方法 功能
add 插入元素
get 获取元素
set 修改元素
remove 删除元素
size 获取容器大小(元素个数)
clear 清空
ensureCapacity 扩大到指定容量
trimTosize 容量削减到当前大小
toArray 转换成数组

LinkedList

之前所讲的ArrayList容器存在一个重大缺陷,从中间插入过着删除一个元素要付出很大的代价,该元素之后的所有元素都需要移动一个位置。而基于双向链表实现的LinkedList容器有效地解决了这个问题,因为从双向链表中间插入或者删除一个元素只需要对操作位置附近的节点更新一下即可。
LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作是一个链表,又可以看作是一个队列。
和之前学习的ArrayList一样,我们可以构造一个空的LinkedList,然后调用add方法往里面添加元素,接着可以调用remove方法删除元素:

add方法有两个重载版本,向LinkedList末尾添加一个元素,或者从指定的位置添加一个元素.
remove方法有两个重载版本:
1.参数为int类型,表示要删除元素的索引,必须在0~size-1范围内。
2.参数为Object类型,删除容器中值等于改参数的第一个元素,若不存在,则没有任何操作。
注意第二中,Object类型,其中 String类型无需包装,别是除类类型外, 基本数据类型都需要用包装类如,Integer,Short,Long等。

由于链表不支持随机访问,根据索引访问某个元素,或者修改某个元素会变得很低效。但我们依然可以用set和get方法修改或者获得某个位置的元素,具体用法和ArrayList是一样的。

注意:千万不要像这样遍历LinkedList,效率很低:

但我们依然可以用 for each 循环 高效地 遍历,因为它是迭代器来遍历LinkedList:

注意注意:访问链表时,一定一定不要用for来输出,要用迭代器或者 for each循环来输出,在顺序表中可以用for来输出,但在链表中一定不要用。
现在我们来学习Java中的迭代器如何使用。
用iterator方法可以获得某个容器的迭代器,类型为Iterator。
在C++中,迭代器指向的是某个元素,而Java的迭代器可以认为是处在两个元素之间的。初始时,迭代器指向第一个元素之前的位置。
Next方法将迭代器跳到下一个位置,并返回越过的元素。通过反复调用next方法, 可以逐个访问容器中的某个元素。但是,如果达到了容器的末尾,next方法将抛出java.util.NoSuchElementException异常。因此需要在调用next前调用hasNext方法判断是否存在下一个元素。
我们可以像下面这样遍历输出集合里的元素:

Iterator接口的remove方法将会删除上次调用next方法时越过的元素,比如我们现在要删除country中第一个元素:

如果想要删除两个相邻的元素,这样写是不合法的:

Remove方法执行后,必须再调用一次next,才能再次remove,否则会抛出java.lang.IllegalStateException异常。

ListIterator是Iterator的子接口,该接口多了一些支持双向链表的功能,包括双向遍历、修改元素、插入元素、获得索引。
因此对于LinkedList容器,我们今后所用的迭代器都是ListIterator,同样可以用ListIterator方法来获得某个LinkedList的迭代器。
和next方法相对应的,previous方法用来反向遍历链表,除方向相反外,其含义和next方法完全一致,调用前应确保hasPrevious方法的结果为true。
举个例子,下面这段代码,先调用next方法越过第一个元素往后跳,再调用previous方法越过第一个元素往前跳,最后删除的元素仍是第一个元素。

ListIterator的另外一个功能是可以调用add方法在迭代器前面那个位置添加元素:
当add发生后,此时迭代器的位置处于新加入的元素和原迭代器后面的元素的中间位置。

注意,add方法,只依赖于迭代器的位置,而remove方法依赖于迭代器的状态。之前讲过,迭代器所处的位置在两个元素之间,因此当容器含有n个元素时,迭代器就会有n+1个可选位置,对应n+1个插入位置。
LinkedList的ListIterator方法可以提供一个参数i,范围是0~size,这样得到的迭代器初始时所处的位置在第i-1个元素和第i个元素之间。
ListIterator的nextIndex方法和previousIndex方法可以返回迭代器所处位置后面和前面的两个索引。举个例子:

ListIterator还有一个set方法,其用法和remove类似,修改的元素时上一次调用next方法或者previous方法越过的那个元素。比如我们现在想把country中第2个元素England改成Britain,可以这样操作:

--------************************************************----------
LinkedList继承了Deque接口,因此有一些和队列相关的方法。
peekFirst方法和peekLast方法分别用于查看头部和尾部的元素,不会改变容器的内容。
pollFirst方法删除头部元素,并返回这个元素,容器为空时返回null。同理pollLast方法删除尾部元素并返回。
offerFirst方法添加元素到头部,offerLast方法添加元素到尾部。
下面的方法掩饰了用offerFirst和offerLast方法一个个插入元素到LinkedList里,再从头部一个个取出元素并输出,知道容器为空。

LinkedList容器常用方法:

方法 功能
offerFirst 从头部插入元素
offerLast 从尾部插入元素
peekFirst 查看头部元素
peekLast 查看尾部元素
pollFirst 取出头部元素
pollLast 取出尾部元素
size 获取容器大小
clear 清空
listIterator 获得链表迭代器

ListIterator容器常用方法:

方法 功能
next 往后越过一个元素
hasNext 判断后面是否还有元素
nextIndex 获得后一个元素的索引
previous 往前越过一个元素
hasPrevious 判断前面是否还有元素
previousIndex 返回前一个元素的索引
add 插入元素
remove 删除元素
set 修改元素

PriorityQueue

有些时候我们需要把一些元素放入某个集合中,然后每次取出优先级最大的元素,这时候就需要用到优先队列。Java提供了PriorityQueue容器(用二叉堆实现),可以每次从容器中取出优先级最高的元素。其中(如果构造器不指定比较器comparator,那么就会使用队列里面元素默认的构造器比较)。
下面就定义了一个存储int类型的PriorityQueue.

那么PriorityQueue如何知道元素的大小关系呢?有两种方法:

  • 元素本身的类实现了Comparable接口并实现了compareTo方法,不如上面例子中的Integer。
  • PriorityQueue的构造器中传入了一个比较器(Comparator)。上面的例子中没有传入比较器,所以使用类本身的compareTo方法来比较。
    我们定义了一个Student类,里面有两个成员,分别是学号int、姓名name.现在目的是把这个类放到PriorityQueue里时,学号小的优先级高。于是让该类继承Comparable接口,并实现compareTo方法。相关代码如下:

第二种方法时使用比较器,比较器是一个实现了Comparator接口的对象,有时也把这种对象成为函数对象。通常我们会定义为匿名内部类的实例。
比如刚才定义的Student类,在不改变类的定义的情况下,我们想让name字典序越小优先级越大,可以这么写:

Comparator接口的compare方法带有两个参数o1和o2.规则也是同样的:

  • 若o1和o2相等,返回0
  • 若o1比o2小,返回一个负整数
  • 若o1比o2大,返回一个正整数
    两个String对象比较字典序使用compareTo方法,其结果正好作为compare方法的返回值,如果要求字典序越大的优先级越大,在前面加个负号即可。
    offer方法可以向PriorityQueue中插入一个元素,remove方法可以删除一个指定的元素(若元素不存在则无任何操作)

(注意):如果这段代码出现Debug那么,是你Student类中没有实现Comparable接口并且没有覆写compareTo方法导致的。 换句话说,如果要用PriorityQueue的话,必须用那两种方式。
上面的代码在PriorityQueue的构造器中没有传入比较器,用的是Student类中compareTo方法来比较大小,也就是学号越小的优先级越大。这里我们用remove方法删除一个元素,必须在Student类里正确覆盖equals方法:

我们想知道当前优先级最大的元素时,可以调用peek方法,该方法仅仅是查看而不会移除元素。
若想在查看的同时移除元素,可以调用poll方法,比如下面的代码每次取出优先级最大的元素,直到PriorityQueue为空

PriorityQueue容器的常用方法:
方法 功能
offer 插入元素
peek 查看最小元素
poll 取出最小元素
remove 删除指定元素
Size 获取容器大小
Clear 情况容器

TreeSet

集合是数学中的一个基本概念,通俗地理解,集合是由一些不重复的数据组成的。比如{1,2,3}就是一个有三个元素的集合。
在Java中,我们常用的集合是HashSet和TreeSet,但因为TreeSet是有序的,功能更强大,所以这里只介绍TreeSet容器。

TreeSet内部使用红黑树实现的,因此插入、删除、查找某个元素的时间复杂度都是O(log n),并且内部元素时有序的。因为其内部有序,我们需要定义元素之间的排序规则,具体方法已经在PriorityQueue中讲过了,这里就不多说了。
add方法可以从TreeSet中插入一个元素,remove方法可以删除一个指定的元素(若元素不存在则无任何操作).
在下面的例子中,按任意顺序插入元素,最终都会按从小到大的顺序排列。而插入重复的元素或者删除不存在的元素,集合中不会有任何变化。

如果是自定义类,删除元素 不需要像 PriorityQueue那样覆盖equals方法。

C++中有多重集合multiset,允许把重复的元素存到集合中,而Java却没有这种容器。如果要实现多重集合,只能自定义类:

上面的代码,我们可以把x相等的元素存放到TreeSet中,只要保证其id不同就可以,可以在构造Node对象的时候用一个计数器保证id各不相同(即插入的时候,排序用id值来排)

first方法用于从TreeSet中查看最小的元素,last方法用于从TreeSet中查看最大的元素(都不改变其大小和顺序)。
TreeSet不支持随机访问遍历,只能通过迭代器进行遍历。下面的代码中,我们使用for each循环遍历,遍历的顺序是从小到大。

contain方法查看TreeSet中指定元素是否存在,该方法返回一个boolean值。
floor方法返回小于等于指定元素的最大元素,ceiling方法返回大于等于指定元素的最小元素,若不存在返回null。
lower方法返回严格小于指定元素的最大元素,higher方法返回严格大于指定元素的最小元素,若不存在返回null。

TreeSet容器的常用方法:
方法 功能
add 插入元素
remove 删除指定元素
size 获取容器大小
clear 清空容器
first 查看最小的元素
last 查看最大的元素
contain 查看指定元素是否存在
floor 返回小于等于指定元素的最大元素
ceiling 返回大于等于指定元素的最小元素
lower 返回严格小于指定元素的最大元素
higher 返回严格大于指定元素的最小元素

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值