寒假看了数据结构与算法分析Java语言描述这本书,但并没有静下心来仔细阅读,准备毕业前重新认认真真看完。想到的第一件事就是通过写博客来加深理解,虽然会花费不少时间,但我觉得利肯定是大于弊的,加油每一天。
Java语言中包含一些基本数据结构。今天总结的是表ADT。
表ADT就是list,在Java中表是由List接口所实现。List接口继承了Conllection接口。先说关于Collection接口。列举一些比较重要的方法:
public interface Collection<T> extends Iterable<T>{
int size();
boolean isEmpty();
void clear();
boolean contains(T x);
boolean add(T x);
boolean remove(T x);
java.util.Iterator<T> iterator();
}
那么Iterable接口是个什么鬼,这是Collection接口必须继承的方法,他只有一个方法,就是
public interface<T> {
Iterator<T> iterator();
}
也就是我们上面见到的第七个方法。
那Iterator又是什么,Iterator也是一个接口,实现了三个方法:
public interface Iterator<T> {
boolean hasNext();
T next();
void remove();
}
Collection接口扩展了Iterable接口。实现Iterable接口的类可以拥有增强的for循环(foreach).
Iterator的思想是:通过iterator方法,每个集合均可以创建并返回给客户一个实现了iterator接口的对象,并将当先位置的概念在对象内部存储下来.每次对next的调用都给出集合的下一项,然后用hasNext来告诉是否存在下一项。当编译器见到一个用于Iterator的对象的增强的for循环时,它用对iterator方法的调用来代替增强的for循环来得到一个Iterator对象。(数据结构与算法分析 java语言描述 p47)
Iterator接口中的方法只有三个,一般只能用来遍历,不过remove方法很值得去尝试使用,相对于Collection里的remove方法来言好处多多:
Collection中的remove方法需要先找出被删除项。那么久需要先获取其位置,之前说过,通过iterator方法返回一个实现了iterator接口的对象,并且当前位置的概念在对象内部存储下来,那么就知道了所删除项的位置,开销就减少了。比如说我们要在一个集合里每隔一项删除一项,最好的方法就是使用迭代器:
public static void remove(List<Integer> list){
Iterator<Integer> itr = list.iterator();
while(itr.hasNext()){
if(itr.next() % 2 == 0)
itr.remove();
}
}
不过需要注意的是:对正在迭代的集合进行结构上的改变是不合法的,其后使用该迭代器时会有ConcurrentModificationException异常被抛出。所以也就是只有在需要一个迭代器时我们才需要获取迭代器,不过如果迭代器调用了它自己的remove方法,那它就依旧是合法的,这也是我们更愿意使用迭代器的remove方法的第二个原因,而这也是为什么上面例程里为什么不用增强的for循环去解决问题的原因(从而会出现list.remove()这种情况),我们不能期待其懂得循环只有在一项不被删除时它才会向前推进,这里会发生ConcurrentModificationException异常。
现在我们可以回到我们今天的主题:List。List接口继承了Collection接口,所以它获取了Collection接口的所有方法,以及其他一些方法,比如说其中一些重要的:
public interface List<T> extends Collection<T>{
T get(int idx);
T set(int idx, T x);
void add(int idx, T x);
void remove(int idx);
ListIterator<AnyType> listIterator(int pos);
}
那么又有疑惑了,ListIterator是什么,查看一下jdk,就会很快知道,ListIterator是一个继承了Iterator接口的接口:
public interface ListIterator<T> extends Iterator<T> {
public boolean hasNext();
T next();
boolean hasPrevious();
T previous();
int nextIndex();
int previousIndex();
void remove();
void set(T);
add(T);
}
previous和hasPrevious使得对表从后向前的遍历得以完成。add方法将一个新的项以当前位置放入表中,当前项是通过把迭代器对next的调用所给出的项和对previous的调用所给出的项之间而抽象出来的概念。set方法改变被迭代器看到的最后一个值,对LinkedList很难实现。关于ArrayList和LinkedList,下面即将开始讲。
List 有两种流行的实现方式:ArrayList和LinkedList,具体的继承关系直接看jdk源码。使用ArrayList的好处在于对get和set花费常数时间,但是对于插入和删除代价昂贵,除非变动在ArrayList末尾。LinkedList提供了双链表实现,所以在插入删除方面代价较小(假设变动项位置已知),在表头或表尾添加删除花费常数时间,LinkedList提供了想要的方法addFirst,removeFirst,addLast,removeLast,getFirst,getLast.但缺点却是对get的调用是昂贵的,除非在靠近端点处。
下面例程计算List中的数之和:
public static int sum(List<Integer> lst){
int total = 0;
for(int i = 0; i < list.size(); i++){
total += lst.get(i);
}
}
在ArrayList中运行时间为O(n),但对于LinkedList则需要O(n^2),因为LinkedList实现get(int idx)需要花费O(n)时间,但解决办法不是没有,使用增强的for循环,从而使整体运行时间为O(n)。之前说过,实现了Iterable接口的类可以拥有增强的for循环,该循环施于这些类之上以观察它们所有的项。其本质是增强的for循环底层实现了iterator迭代,迭代有效地从一项到下一项推进(通过调用它的next方法)。
那么就开始尝试去了解一下ArrayList和LinkedList的源码。但是jdk源码中比较长,我们抽取其中的一部分比较重要的方法,自己去写一个(照着源码写),写出来是对源码最好的分析(这个在书中有, 但为了更好理解,可以同时参考JDK源码一起看)。
public class MyArrayList<E> implements Iterable<E>{
//源码中ArrayList并不是直接实现Iterable接口,直接看jdk源码可知
private static final int DEFAULT_CAPACITY = 10;
private int theSize;
private E[] theItems;
public MyArrayList(){
clear();
}
public clear(){
theSize = 0;
ensureCapacity(DEFAULT_CAPACITY);
}
public int size(){
return theSize;
}
public boolean isEmpty {
return size() == 0;
}
public void trimeToSize(){
ensureCapacity(size());
}
public {
}
public E get(int index){
if(index < 0 || index >= size())
throw new ArrayIndexOutOfBoundsException();
return theItems[index];
}
public E set(int index, E item){
if(index < 0 || index >= size())
throw new ArrayIndexOutOfBoundsException();
E old = theItems[index];
theItems[index] = item;
return old;
}
public ensureCapacity(int size){
E[] old = theItems;
theItems = (E[])new Object[size];
for(int i = 0; i<size(); i++){
theItems[i] = old[i];
}
public void add(int index, E x){
if(size() == theItems.length){
ensureCapacity(size() * 2 +1)
}
for(int i = theSize; i > index; i-- ){
theItems[i] = theItems[i - 1];
}
theItems[index] = x;
theSize ++;
}
public E remove(int index){
E x = theItems[index];
for(int i = theSize; i < size(); i+ ){
theItems[i] = theItems[i + 1];
}
theSize --;
return x;
}
public java.util.Iterator<E> iterator(){
return new ArrayListIterator();
}
private class ArrayListIterator implements java.util.Iterator<E> {
private int current = 0;
public boolean hasNext(){
return current < size();
}
public E next(){
if(! hasNext){
throw new java.util.NoSuchElementException();
}
return theItems[current ++];
}
public void remove(){
//感受内部类的好处
MyArrayList.this.remove(--current);
}
}
}
LinkedList则有些不同,LinkedList是由双链表来实现,而且还需要保留该表两端的引用。只要操作发生在已知位置(端点或者是迭代器指定的位置),我们的操作(add或remove)将花费常数时间。
在下面这个精简的LinkedList类MyLinkedList类中,将会包含以下三个类:
1.MyLinkedList类本身,包括两端的链,表的大小以及一些方法。
2.Node类
3.LinkedListIterator类。
在表的前端我们创建一个头节点作为逻辑上开始的标志,在末端创建节点叫做尾节点。使用这些额外的节点的优点在于,排除了特殊情况简化代码。比如说删除第一个节点,因为删除算法需要访问被删除节点前面的节点。
关于Node类,是MyLinkedList类的一个静态内部类,具体实现:
private static class Node<E>{
public E d;
public Node<E> previous;
public Node<E> next;
public Node(E d, Node<E> previous, Node<E> next){
this.d = d;
this.previous = preivous;
this.next = next;
}
}
现在我们探究MyLinkedList类本身,包含以下几个成员变量和一些重要方法:
private int modCount;
private int theSize;
private Node<E> startFlag;
private NOde<E> endFalg;
public void clear(){
theSize = 0;
startFlag = new Node<E>(null, null, null);
endFlag = new Node<E>(null, startFlag, null);
startFlag.next = endFlag;
modCount++;
}
public void add(int index, E x){
addBefore(getNode(index), x);
}
private void addBefore(Node<E> p, E x ){
Node<E> newNode = new Node<E>(x, p.previous, p);
newNode.previous.next = newNode;
p.previous = newNode;
modCount ++;
theSize ++;
}
private E remove(Node<E> p, E x){
p.next.previous = p.previous;
p.previous.next = p.next;
theSize ++;
modCount ++;
return p.data
}
private Node<E> getNode(int index){
Node<E> p;
if(index <0 || index >=size()){
throw new IndexOutOfBoundsException();
}
if(index < size()/2){
//这里只是遍历查找,并不是做结构上的变化
p = startFlag.next;
for(int i =0; i < index; i++){
p = p.next;
}
}else{
......
}
return p;
}
LinkedListIterator类似ArrayListIterator,但添加了错误检测。
private class LinkedListIterator implements java.util.Iterator<E>{
private Node<E> current = startFlag.next;
private int expectedModCount = modCount;
private boolean okToRemove = false;
public boolean hasNext(){
return current != endFlag;
}
public E next(){
if(modCount != expectedModCount){
throw new ConcurrentModificationException();
}
if(! hasNext()){
throw new NoSuchElementException();
}
E nextItem = current.data;
current = current.next;
okToRemove = true;
return nextItem;
}
public void remove(){
if(modCount != expectedModCount){
throw new ConcurrentModificationException();
}
if(! okToRemove){
throw new IllegalStateException();
}
MyLinkedList.this.remove(current.previous);
okToRemove = false;
expectedModCount++;
}
}
最后还有一句话:remove方法里的current是保持不变的,因为current正在观察的节点不受前面被删除的节点的影响。
好了,list就到此为止了,但我所学会的这些只是基础,以后还应更深入地学习,加油每一天。下一篇准备做栈和队列的笔记。