本节课我们介绍一下迭代器模式,然后在二叉树里实现一下迭代器。
迭代器模式
在容器里存放了大量的同类型对象,我们如果想逐个访问容器中的对象,就必须要知道容器的结构。比如,ArrayList 和 LinkedList 的遍历方法一定是不一样的,如果再加上HashMap, TreeMap,以及我们现在正在研究的BinarySearchTree,对于容器的使用者而言,肯定是一个巨大的负担。作为容器的使用者,我肯定希望所有的容器能提供一个统一的接口,这个接口可以遍历容器中的所有内容,又可以把容器的细节屏蔽掉。
幸运的是,确实存在这样的一个统一的接口,这个接口就是 java.util.Iterator。先看类图,下面的这幅图所表示的意思就是,在一个容器类上实现createIterator方法,这个方法可以创建一个迭代器,使用这个迭代器就可以遍历容器中的所有元素了。
Iterator上定义了各种方法,用于遍历。图中只列出了一小部分。我们可以看一下JDK中是怎么定义的:
public
有两个default方法。default 这个关键字,我们以前没接触过。这是从1.8引入的,为了和以前的接口兼容,这个我们先不去管它,还有 forEachRemaing是一个高阶函数,它可以接受一个lambda表达式,这个我们会在后面的 Java 函数式编程中慢慢介绍,也先不去管它。所以 Iterator中也就只有两个方法,一个是hasNext,告诉使用者,还有没有下一个元素,如果为false,说明遍历已经完成了。还有一个是next,目的是取下一个元素。
我们先来体验一下。
List中的迭代器
我们先使用这个迭代器遍历一下List,例如:
public
请大家再试一下ArrayList,体会一下 Iterator 的用法。可以看到,我们根本就不需要关心后面具体的数据结构,只需要使用 Iterator,不管后面是数组还是链表,都可以很方便地遍历整个容器。
大家想一下,如果要让自己来实现这样的一个Iterator,你会怎么写?这里给出ArrayList的源代码,可以参考一下:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
我把 remove 等方法删掉了,这个方法是在 ListIterator定义的,我们先不去管它。这里只留下了 hasNext 和 next 方法。
可以看到,ArrayList 的 Iterator 就是在数组中逐个前进,逻辑非常简单。
二叉树的Iterator
我们前边几节,实现了一个自己的容器,BinarySearchTree,如果想给这个容器加上迭代器,那应该怎么办呢?
把问题转化一下,如果已经停在一个结点上,next 方法其实就是要寻找二叉树的中序后继。如果某结点有右孩子的话,如图2.2(a),(这是我从以前的讲议里截的图,章节序号都是讲义里的,大家可以忽略这个章节号)
蓝色结点的中序后继就是右孩子上的最小值,即红色结点。如果这个结点没有右孩子,就要去找他的父结点,如果该结点是父亲的左孩子,如图2.2(b),那么父亲结点就是该结点的中序后继,最复杂的情况是如果该结点是父亲的右孩子,那么就继续往祖先结点找,直到找到该结点是在某个祖先的左孩子上,如图2.2(c),如果一直到根结点都未找到,那么可以肯定的是这个结点是二叉树中的最大值结点,无中序后继。
可以看到,我们设计的这个算法中,经常要访问节点的父亲结点,由子结点查找父亲结点是一个大问题。可以这样解决,在Node的定义中,再加一个父结点的引用就可以了。这样,我们就可以通过子节点方便地找到父结点了:
class
Node发生了变化,BST中的 insert 也会相应地发生变化,请读者自行补充。
我们再回到寻找中序后继这个问题上来,根据前边分析三种情况,我们可以分别进行处理。下面是在BST中查找中序后继的方法。
public Node<T> successor(Node<T> n) {
if (n == null)
return null;
Node<T> p;
if (n.right != null) {
p = n.right;
while (p.left != null) {
p = p.left;
}
return p;
}
else {
p = n.parent;
while (p != null && p.left != n) {
n = p;
p = n.parent;
}
return p;
}
}
好了,今天的课程就到这里了。作业:
1. 自己看一下ArrayList的 iterator的实现,LinkedList, ArrayDeque, ListIterator也一起看一下。
2. 使用文章中提供的 successor 方法,实现BST的 iterator 方法,并且向BST中插入几个数字,再使用 iterator 进行遍历,查看遍历结果是不是从小到大有序的。(这个结果与中序遍历的结果相同)
完整的BinarySearchTree我已经放在小密圈里了。圈子里的朋友尽量自己做,做不出来再看答案。
上一节课:数据结构(六):二叉树的遍历
下一节课:二叉树的删除和应用举例
目录:课程目录