一 Java Collections API 中的表
1.1 Coolection 接口
public interface Coolection<T> extends Iterable<T>{
int size();
boolean isEmpty();
void clear();
boolean contains(T t);
boolean add(T t);
boolean remove(Object o);
java.util.Iterator<T> iterator();
}
Collection 接口扩展了 Iterable 接口,实现 Iterable 接口的那些类可以拥有增强的 For 循环。
public static <T> void print(Collection<T> coll){
for(T item : coll){
System.out.println(item);
}
}
1.2 Iterator 接口
public interface Iterator<T>{
boolean hasNext();
T next();
void remove();
}
实现 Iterable 接口的集合必须提供一个称为 iterator() 的方法,该方法返回一个 Iterator 类型的对象。
Iterator 接口的思路是,通过 iterator 方法,每个集合均可创建并返回给客户一个实现 Iterator 接口的对象,并将当前位置的概念在对象内部存储下来。
当编译器见到一个正在用于 Iterable 的对象的增强 for 循环的时候,它调用 Iterator 的那些方法代替增强 for 循环,以得到一个 Iterator 对象,然后调用 next() 、hasNext(),因此,前面看到的 print() 例程由编译器重写。
public static <T> void print(Collection<T> coll){
Iterator<T> it = coll.iterator();
while( it.hasNext() ){
T item = it.next();
System.out.println(item);
}
}
1.2.1 remove() 方法的用处和优点
a .Collection 的 remove 方法必须首先找出要被删除的项,如果知道所要删除的项的精确位置,那么删除它的开销很可能要小很多。
b. 当直接使用 Iterator (而不是通过增强的 for 循环间接使用)时,要记住一个重要的法则:如果对正在被迭代的集合进行结构上的改变(即对该集合使用add、remove 或 clear 方法),那么迭代器就不再合法(并且在其后使用该迭代器时,会有 Concurrent-ModificationException 异常被抛出)。然而,如果迭代器调用了它自己的 remove() 方法,那么这个迭代器就仍然是合法的。这是有时候我们更愿意用迭代器的 remove 方法的第二个原因。
1.3 List 接口、ArrayList 类和 LinkedList 类
List 接口中一些重要的方法
public interface<E> List extends Collection<E>{
E get(int idx);
E set(int idc);
void add(int idx, E t);
void remove(int idx);
ListIterator<E> listIterator();
ListIterator<E> listIterator(int pos);
}
List ADT 有两种流行的实现方式:
a. ArrayList 类提供了 List ADT 的一种可增长数组的实现。使用 LinkedList 的优点在于,对 get 和 set 的调用都花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在 ArrayList 的末端进行。
b. LinkedList 类则提供了 List ADT 的双链表实现。使用 LinkedList 的优点在于,新项的插入和现有项的删除均开销很小,这里假设变动项位置是已知的。使用 LinkedList 的缺点是它不容易作索引,因此对 get 的调用是昂贵的,除非调用非常接近表的端点(如果对 get 的调用是对接近表后部的项进行,那么搜索的进行可以从表的后部开始)。
这意味着,在表的前端进行添加和删除都是常数时间的操作,由此 LinkedList 更提供了 addFirst 和 removeFirst、addLast 和 removeLast,以及 getFirst 和 getLast 等以有效地添加、删除和昂文表两端的项
为了看出差别,我们考察对一个 List 进行操作的某些方法。首先,设我们通过在末端添加一些项来构造一个 List。
public static void makeList(List<Integer> lst, int N){
lst.clear();
for(int i = 0; i < N; i++){
lst.add(i);
}
}
不管 ArrayList 还是 LinkedList 作为参数被传递,makeList 的运行时间都是 O(N),因为对 add 的每次调用都是在表的末端进行,从而均花费常数时间(可以忽略对 ArrayList 偶尔进行的扩展)。另一方面,如果我们通过在表的前端添加一些项来构造一个 List;
public static void makeList(List<Integer> lst, int N){
lst.clear();
for(int i = 0; i < N; i++){
lst.add(0, i);
}
}
那么,对于 LinkedList 它的运行时间是O(N),但是对于 ArrayList 其运行时间则是O(N的二次方),因为在 ArrayList 中,在前端进行添加是一个 O(N) 操作。
下面一个例程时计算 List 中的数的和
public static int sum(List<Integer> lst){
int total = 0;
for(int i = 0; i < N; i++)
total += lst.get(i);
return total;
}
这里,ArrayList 的运行时间是 O(N),但对于 LinkedList 来说,其运行时间则是 O(N的二次方),因为在 LinkedList 中,对 get 的调用是 O(N) 操作。可是,要是使用一个增强的 for 循环,那么他对任意的一个 List 的运行时间都是 O(N),因为迭代器有效地从一项到下一项推进。
对搜索而言,ArrayList 和 LinkedList 都是低效的,对 collection 的 contains 和 remove 两个方法(他们都以 E 为参数)的调用均花费线性时间。
在 ArrayList 中有一个容量的概念,他表示基础数组的大小。在需要的时候,ArrayList 将自动增加其容量以保证它至少具有表的大小。如果该大小的早期估计存在,那么 ensureCapacity 可以设置容量为一个足够大的量,以避免数组容量以后的扩展。再有,trimToSize 可以在所有的 ArrayList 添加完成之后使用,以避免浪费空间。
1.4 例子:remove 方法对 LinkedList 类的使用
需求:
将一个表中的所有偶数的项删除。比如,如果表中包含6,5,1,4,2,则在该方法调用之后,表中仅有元素5,1。
思考:
是否可以建一个新表,然后拷贝奇数项?这样太浪费空间,我们写一个干净的避免拷贝的表,遇到偶数项时将它从表中删除。对于 ArrayList 这种几乎是在任意位置删除的行为是极其昂贵的代价,不过 LinkedList 给我们提供了可能。
第一种方案
public static void removeEvensVer1(List<Integer> lst){
int i = 0;
while( i < lst.size()){
if( lst.get(i) % 2 == 0)
lst.remove(i);
}
}
分析:
在一个 ArrayList 上,我们知道,remove 的效率不是很高的,因此该程序花费的是二次时间。LinkedList 暴露两个问题。首先,对 get 的效率不高,因此例程花费的是二次时间。而且,对 remove 的调用也同样低效,因为达到位置 i 的代价是昂贵的。
第二种方案
public static void removeEvensVer2(List<Integer> lst){
for( Integer x : lst ){
if( x % 2 == 0)
lst.remove(x);
}
}
分析:
矫正了第一种方案,不是用 get ,而是用迭代器一步步遍历该表,这是高效率的。但是使用 Collection 的 remove 方法是非常低效的,因为 remove 方法必须再次搜索此项,花费线性时间。而且当一项被删除时,list 的结构发生变化,由增强 for 循环使用的基础迭代器是非法的,会报出异常。
第三种方案
public static void removeEvensVer3(List<Integer> lst){
Iterator<Integer> it = lst.iterator();
while( it.hasNext() ){
if( it.next() % 2 == 0 )
it.remove();
}
}
分析:
一种成功的想法,使用迭代器的方式,使用迭代器的 remove 方法。对于一个 LinkedList 来说,对该迭代器的 remove 方法的调用只花费常数时间,而不是二次时间。对于 ArrayList 来说,即便迭代器位于被删除的节点上,其 remove 的代价依然很昂贵,整个程序仍然花费二次时间。
1.5 关于 ListIterator 接口
List 的 ListIterator 扩炸了 List 的 Iterator 的功能。方法 previous 和 hasPrevious 使得对表从后往前遍历得以完成。add 方法将一个新的项放在当前位置。对于 LinkedList 来说,add 时一种常数时间的操作,但对于 ArrayList 来说代价昂贵。set 改变迭代器看到的最后一个值,从而对 LinkedList 很方便。例如,他可以用来对 List 的所有偶数中减去1,而这对于 LinkedList 来说,不用 ListIterator 的 set 方法是很难做到的。
public interface ListIterator<E> extends Iterator<E>{
boolean hasPrevious();
boolean previous();
void add(E e);
void set(E newVal);
}
二 ArrayList 类的实现
建立一个 MyArrayList 类,实现细节:
1. MyArrayList 将保持基础数组,数组的容量,以及存储在 MyArrayList 中的当前项数。
2. MyArrayList 将提供一种机制以改变基础数组的容量。通过获得一个新数组,将老数组拷贝到新数组中来改变数组的容量。允许虚拟机回收老数组。
3. MyArrayList 将提供 get 和 set 的实现。
4. MyArrayList 将提供基本的例程,如 size、isEmpty、和 clear,它们是典型的单行程序;还提供 remove ,以及两种不同版本的 add,如果数组的大小和容量相同,那么这两个 add 增加容量。
5. MyArrayList 将提供一个实现 Iterator 接口的类。这个类将存储迭代序列中的下一项的下标,并提供 next、hasNext 和 remove 等方法的实现。MyArrayList 的迭代器方法直接返回实现 Iterator 接口的该类的新构造的实例。
2.1 基本类
为了把精力集中在编写迭代器类的基本方面,我们不检测可能使得迭代器无效的结构上的修改,也不检测非法的迭代器 remove 方法。
import java.util.Iterator;
/**
* 自定义实现链表问题:
* 1.何时使用theSize,何时使用size();
* 2.为何要扩容至两倍;
*
* @param <AnyType>
*/
public class MyArrayList <AnyType>implements Iterable<AnyType>
{
/**
* 自定义链表的默认容量 10
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 链表的大小——当前链表中的 元素的个数
*/
private int theSize;
/**
* 用于链表实现的数组
*/
private AnyType [] theItems;
/**
* 构造方法
*/
public MyArrayList()
{
doClear();
}
/**
* 清空链表
*/
public void clear()
{
doClear();
}
/**
* 清空链表
*/
private void doClear()
{
//设置链表元素个数为 0
theSize = 0;
//链表初始化方法,确保不发生异常
ensureCapacity(DEFAULT_CAPACITY);
}
/**
* 返回当前链表的元素个数
* @return
*/
public int size()
{
return theSize;
}
/**
* 返回当前链表是不是空表(元素个数是否为0)
* @return
*/
public boolean isEmpty()
{
return theSize == 0;
}
/**
* 将链表数组大小修改为元素个数大小,以避免空间资源浪费
*/
public void trimToSize()
{
ensureCapacity(size());
}
/**
* 获取某个指定索引位置上的元素
* @param idx 索引参数
* @return
*/
public AnyType get(int idx)
{
//如果索引参数小于0或者大于链表元素最大索引(size()-1,从零开始,等于链表元素个数-1);则抛出数组越界异常
if(idx<0||idx>=size())
{
throw new ArrayIndexOutOfBoundsException();
}
return theItems[idx];
}
/**
* 修改链表中指定索引位置处的元素的值为指定的值
* @param idx 索引参数
* @param newVal 新的元素值
* @return 原来的元素值
*/
public AnyType set(int idx,AnyType newVal)
{
if(idx<0||idx>=size())
{
throw new ArrayIndexOutOfBoundsException();
}
AnyType old = theItems[idx];
theItems [idx] = newVal;
return old;
}
/**
* 对链表容量进行扩充,以保证不会发生数组越界。
* @param newCapacity 扩充后的容量
*/
public void ensureCapacity(int newCapacity)
{
//如果需要扩增的容量小于当前链表的元素个数,则表明预期链表容量完全够用,不用扩容,直接返回。
if(newCapacity<theSize)
{
return;
}
//否则,就需要对现在的链表扩容至新容量大小,以保证后续操作不会使数组越界。
AnyType [] old = theItems;
theItems = (AnyType[]) new Object[newCapacity];
for(int i =0;i<size();i++)
{
theItems[i] = old[i];
}
}
/**
* 在指定索引位置新增元素
* @param idx 索引参数
* @param x 新增元素
*/
public void add(int idx,AnyType x)
{
//如果链表数组还剩下一个元素的空位,则将链表容量扩容至两倍
if(theItems.length==size())
{
//+1是考虑 0 的时候的情况
ensureCapacity(size()*2+1);
}
//从索引位置开始,其后每个元素都向后移动一个位置
for(int i = theSize;i>idx;i--)
{
theItems[i]=theItems[i-1];
}
//设置 位置索引为 idx处的元素的值为x
theItems[idx] = x;
//链表大小(元素个数)+1
theSize++;
}
/**
* 向链表最后端添加元素
* @param x 添加的新元素
* @return 是否添加成功
*/
public boolean add(AnyType x)
{
add(size(),x);
return true;
}
/**
* 删除指定索引处的元素
* @param idx 位置索引参数
* @return 被删除的元素值
*/
public AnyType remove(int idx)
{
if(idx<0||idx>=size())
{
throw new ArrayIndexOutOfBoundsException();
}
AnyType removedItem = theItems[idx];
for(int i=idx;i<size()-1;i++)
{
theItems[i]=theItems[i+1];
}
theSize --;
return removedItem;
}
/**
* 链表 迭代器 返回自定义迭代器
*/
public Iterator<AnyType> iterator()
{
return new ArrayListIterator();
}
/**
* 内部类 实现Iterator
*
*/
private class ArrayListIterator implements java.util.Iterator<AnyType>
{
/**
* 迭代器当前指针位置——与链表数组的索引对应
*/
private int current = 0;
/**
* 是否有下一个元素
*/
@Override
public boolean hasNext()
{
return current < size();
}
/**
* 返回下一个元素(当前索引+1 处)
*/
public AnyType next()
{
if(!hasNext())
{
throw new java.util.NoSuchElementException();
}
return theItems[current++];
}
/**
* 迭代器 删除链表元素;循环中直接使用链表方法进行元素删除会抛出异常,需要使用迭代器的删除方法
* 问题:迭代器删除方法同样是调用链表删除元素的方法,为什么使用迭代器删除方法就不会报错,而使用链表删除方法就会报错?
*/
public void remove()
{
MyArrayList.this.remove(--current);
}
}
}
三 LinkedList 类的实现
实现细节:
1. MyLinkedList 类本身,它包含到两端的链、表的大小以及一些方法。
2. Node 类,他可能是一个私有的嵌套类。一个节点包含数据以及到前一个节点的链和到下一个节点的链,还有一些适当的构造方法。
3. LinkedListIterator 类,该类抽象了位置的概念,是一个私有类,并实现接口 Iterator。它提供了方法 next、hasNext 和 remove 的实现。
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class MyLinkedList<T> implements Iterable<T> {
private int theSize;
//modCount 代表自从构造以来对链表所做的改变次数。
private int modCount = 0;
private Node<T> beginMarker;
private Node<T> endMarker;
private static class Node<T>{
public T data;
public Node<T> prev;
public Node<T> next;
public Node(T e, Node<T> p, Node<T> n){
data = e;
prev = p;
next = n;
}
}
public MyLinkedList(){
doClear();
}
public void clear(){
doClear();
}
private void doClear() {
beginMarker = new Node<T>(null, null, null);
endMarker = new Node<T>(null, null, null);
beginMarker.next = endMarker;
theSize = 0;
modCount++;
}
public int size(){
return theSize;
}
public boolean isEmpty(){
return theSize == 0;
}
public boolean add(T t){
add( size(), t);
return true;
}
public void add(int idx, T t){
addBefore(getNode(idx, 0, size()), t);
}
public T get(int idx){
return getNode(idx).data;
}
public T set(int idx, T newVal){
Node<T> p = getNode(idx);
T oldVal = p.data;
p.data = newVal;
return oldVal;
}
public T remove(int idx){
return remove(getNode(idx));
}
private void addBefore(Node<T> p, T t){
Node<T> newNode = new Node<>(t, p.prev, p);
newNode.prev.next = newNode;
p.prev = newNode;
theSize++;
modCount++;
}
private T remove(Node<T> p){
p.next.prev = p.prev;
p.prev.next = p.next;
theSize++;
modCount++;
return p.data;
}
private Node<T> getNode(int idx){
return getNode(idx, 0, size()-1);
}
private Node<T> getNode(int idx, int lower, int upper){
Node<T> p;
if(idx < lower || idx > upper)
throw new IndexOutOfBoundsException();
if(idx < size()/2){
p = beginMarker.next;
for(int i = 0; i < idx; i++)
p = p.next;
}
else{
p = endMarker;
for (int i = size(); i > idx; i--)
p = p.prev;
}
return p;
}
public Iterator<T> iterator() {
return new LinkedListIterator();
}
private class LinkedListIterator implements Iterator<T>{
private Node<T> current = beginMarker.next;
private int expectedModCount = modCount;
private boolean okToRemove = false;
public boolean hasNext(){
return current != endMarker;
}
public T next(){
if(modCount != expectedModCount)
throw new ConcurrentModificationException();
if(!hasNext())
throw new NoSuchElementException();
T nextItem = current.data;
current = current.next;
okToRemove = true;
return nextItem;
}
public void remove(){
if(modCount != expectedModCount)
throw new ConcurrentModificationException();
if(!hasNext())
throw new NoSuchElementException();
MyLinkedList.this.remove(current.prev);
expectedModCount++;
okToRemove = false;
}
}
}