数据结构和算法(3)-向量,列表与序列

序列( Sequence),就是依次排列的多个对象。两种典型的序列:向量( Vector)和列表( List)。

3.1 向量与数组

通过[0, n-1]之间的每一个整数,都可以直接访问到唯一的元素 e,而这个整数就等于 S 中位于 e 之前的元素个数⎯⎯在此,我们称之为该元素的秩( Rank)。

3.1.1 向量 ADT

操作方法 功能描述
getSize(): 报告向量中的元素数目
输入:无
输出:非负整数
isEmpty(): 判断向量是否为空
输入:无
输出:布尔量
getAtRank®: 若 0 ≤ r < getSize(),则返回秩为 r 的那个元素
否则,报错
输入:一个整数
输出:对象
replaceAtRank(r, e): 若 0 ≤ r < getSize(),则将秩为 r 的元素替换为 e,并返回原来的元素
否则,报错
输入:一个整数和一个对象
输出:对象
insertAtRank(r, e): 若 0 ≤ r ≤ getSize(),则将 e 插入向量中,作为秩为 r 的元素(原秩不小于 r 的元素顺次后移);并返回该元素
否则,报错
输入:一个整数和一个对象
输出:对象
removeAtRank®: 若 0 ≤ r < getSize(),则删除秩为 r 的那个元素并返回之(原秩大于 r 的元素顺次前移)
否则,报错
输入:一个整数
输出:对象

一种直截了当的方法就是采用数组来实现向量:下标为r的数组项,就对应于秩为r的元素。

3.1.2 基于数组的简单实现

    public class ExceptionBoundaryViolation extends RuntimeException {
    	public ExceptionBoundaryViolation(String message) {
    		super(message);
    	}
    }

向量接口:

public interface Vector {
	// 返回向量中元素数目
	public int getSize();
	// 判断向量是否为空
	public boolean isEmpty();
	/**查改增删*/
	// 取秩为r的元素
	public Object getAtRank(int r) throws ExceptionBoundaryViolation;
	// 将秩为r的元素替换为obj
	public Object replaceAtRank(int r, Object obj) throws ExceptionBoundaryViolation;
	// 插入obj,作为秩为r的元素;返回该元素
	public Object insertAtRank(int r, Object obj) throws ExceptionBoundaryViolation;
	// 删除秩为r的元素
	public Object removeAtRank(int r) throws ExceptionBoundaryViolation;
}
/*
* 基于数组的向量实现
*/
public class Vector_ExtArray implements Vector {

	private int N = 8;// 数组的容量,可不断增加
	private int n;// 向量的实际规模
	private Object A[];// 对象数组

	public Vector_ExtArray() {
		A = new Object[N];
		n = 0;
	}

	@Override
	public int getSize() {
		// TODO Auto-generated method stub
		return n;
	}

	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return (0 == n) ? true : false;
	}

	@Override
	public Object getAtRank(int r) throws ExceptionBoundaryViolation {
		if (0 > r || r >= n)
			throw new ExceptionBoundaryViolation("意外:秩越界");
		return A[r];
	}

	@Override
	public Object replaceAtRank(int r, Object obj) throws ExceptionBoundaryViolation {
		if (0 > r || r >= n)
			throw new ExceptionBoundaryViolation("意外:秩越界");
		/*
		 * A[r] = obj; return A[r];
		 */

		Object bak = A[r];
		A[r] = obj;
		return bak;
	}

	@Override
	public Object insertAtRank(int r, Object obj) throws ExceptionBoundaryViolation {
		if (0 > r || r > n)
			throw new ExceptionBoundaryViolation("意外:秩越界");
		if (N < n) {
			// 数组容量小于实际规模
			N *= 2;
			Object B[] = new Object[N];// 开辟一个容量加倍的数组
			for (int i = 0; i < n; i++) {
				B[i] = A[i];
			}
			A = B;// 用B替换A(原A[]将被自动回收)
		}
		for (int i = n; i > r; i--)
			A[i] = A[i - 1];// 后续元素顺次后移
		A[r] = obj;// 插入
		n++;// 更新当前规模
		return obj;
	}

	@Override
	public Object removeAtRank(int r) throws ExceptionBoundaryViolation {
		if (0 > r || r >= n)
			throw new ExceptionBoundaryViolation("意外:秩越界");
		Object bak = A[r];
		for (int i = r; i < n - 1; i++)
			A[i] = A[i + 1];// 后续元素顺次前移

		n--;// 更新当前规模
		return bak;
	}

}

基于可扩充数组实现的向量,每次数组扩容的分摊运行时间为 O(1)。

3.1.4 java.util.ArrayList 类和 java.util.Vector 类

向量ADT与java.util.ArrayList类的对比

向量 ADT 中的方法 java.util.ArrayList 类的方法
getSize() getSize()
isEmpty() isEmpty()
getAtRank() get()
replaceAtRank() set()
insertAtRank() add()
removeAtRank() remove()

3.2 列表

3.2.1 基于节点的操作

试考察一个(单向或双向)链表 S。如果直接照搬秩的概念,对链表的访问速度会很慢⎯⎯为了在链表结构中确定特定元素的秩,我们不得不顺着元素间的 next(或 prev)引用,从前端(双向链表也可以从后端)开始逐一扫描各个元素,直到发现目标元素。在最坏情况下,这需要线性的时间。

3.2.2 由秩到位置

位置( Position)的概念就是对元素的不同形式的抽象和统一,也是对列表元素之间相对“位置”的形式化刻画。正是在这种抽象的基础上,我们才可以安全地为列表扩充一整套基于节点的操作。

3.2.3 列表 ADT

列表ADT支持的方法:

操作方法 功能与描述
first(): 若 S 非空,则给出其中第一个元素的位置
否则,报错
输入:无
输出:位置
last(): 若 S 非空,则给出其中最后一个元素的位置
否则,报错
输入:无
输出:位置
getPrev(p): 若 p 不是第一个位置,则给出 S 中 p 的前驱所在的位置
否则,报错
输入:位置
输出:位置
getNext(p): 若 p 不是最后一个位置,则给出 S 中 p 的前驱所在的位置
否则,报错
输入:位置
输出:位置
列表支持的动态修改操作
replace(p, e): 将处于位置 p 处的元素替换为 e,并返回被替换的元素
输入:一个位置和一个对象
输出:对象
insertFirst(e): 将元素 e 作为第一个元素插入 S 中,并返回 e 的位置
输入:一个对象
输出:位置
insertLast(e): 将元素 e 作为最后一个元素插入 S 中,并返回 e 的位置
输入:一个对象
输出:位置
insertBefore(p, e): 将元素 e 插入于 S 中位置 p 之前,并返回 e 的位置
输入:一个位置和一个对象
输出:位置
insertAfter(p, e): 将元素 e 插入于 S 中位置 p 之后,并返回 e 的位置
输入:一个位置和一个对象
输出:位置
remove§: 删除 S 中位置 p 处的元素
输入:一个位置
输出:对象

上述列表 ADT 的定义中,有些操作的功能是重复的。比如,insertBefore(first(), e) 的效果与 insertFirst(e)完全相同, insertAfter(last(), e) 的效果与 insertLast(e)也完全相同。之所以允许这类功能的冗余,是为了增加代码的可读性。

列表 ADT 接口

/*
* 列表ADT接口
*/
public interface List {
	//查询列表当前的规模
	public int getSize();
	//判断列表是否为空
	public boolean isEmpty();
	//返回第一个元素(的位置)
	public Position first();
	//返回最后一个元素(的位置)
	public Position last();
	//返回紧接给定位置之后的元素(的位置)
	public Position getNext(Position p)
	throws ExceptionPositionInvalid, ExceptionBoundaryViolation;
	//返回紧靠给定位置之前的元素(的位置)
	public Position getPrev(Position p)
	throws ExceptionPositionInvalid, ExceptionBoundaryViolation;
	//将e作为第一个元素插入列表
	public Position insertFirst(Object e);
	//将e作为最后一个元素插入列表
	public Position insertLast(Object e);
	//将e插入至紧接给定位置之后的位置
	public Position insertAfter(Position p, Object e)
	throws ExceptionPositionInvalid;
	//将e插入至紧靠给定位置之前的位置
	public Position insertBefore(Position p, Object e)
	throws ExceptionPositionInvalid;
	//删除给定位置处的元素,并返回之
	public Object remove(Position p)
	throws ExceptionPositionInvalid;
	//删除首元素,并返回之
	public Object removeFirst();
	//删除末元素,并返回之
	public Object removeLast();
	//将处于给定位置的元素替换为新元素,并返回被替换的元素
	public Object replace(Position p, Object e)
	throws ExceptionPositionInvalid;
	//位置迭代器
	public Iterator positions();
	//元素迭代器
	public Iterator elements();
}

3.2.4 基于双向链表实现的列表

在第 2.5.3 节中,为了实现双向链表结构,我们曾经基于位置ADT实现了双向链表节点类型DLNode( 代码二.16)。 在那里,每一节点对应于一个位置,而getElem()方法只需返回该节点所保存的元素。也就是说,节点本身就担当了位置的角色:那里并没有直接使用变量prev和next,而是通过方法getPrev()、 getNext()、 setPrev()和setNext()间接地对它们进行访问或修改。

List_DLNode 类⎯⎯List 接口的实现

空列表也有首末节点,首末节点并不真正存储数据;只是为了方便结构的表达;

/*
* 基于双向链表实现列表结构
*/
public class List_DLNode implements List {
	protected int numElem;//列表的实际规模
	protected DLNode header, trailer;//哨兵:首节点+末节点
	//构造函数
	public List_DLNode() {
		numElem = 0;//空表
		header = new DLNode(null, null, null);//首节点
		trailer = new DLNode(null, header, null);//末节点
		header.setNext(trailer);//首、末节点相互链接
	}
	/**************************** 辅助方法 ****************************/
	//检查给定位置在列表中是否合法,若是,则将其转换为*DLNode
	protected DLNode checkPosition(Position p) throws ExceptionPositionInvalid {
		if (null == p)
		throw new ExceptionPositionInvalid("意外:传递给List_DLNode的位置是null");
		if (header == p)
		throw new ExceptionPositionInvalid("意外:头节点哨兵位置非法");
		if (trailer == p)
		throw new ExceptionPositionInvalid("意外:尾结点哨兵位置非法");
		DLNode temp = (DLNode)p;
		return temp;
	}
	/**************************** ADT方法 ****************************/
	//查询列表当前的规模
	public int getSize() { return numElem; }
	//判断列表是否为空
	public boolean isEmpty() { return (numElem == 0); }
	//返回第一个元素(的位置)
	public Position first() throws ExceptionListEmpty {
		if (isEmpty())
		throw new ExceptionListEmpty("意外:列表空");
		return header.getNext();
	}
	//返回最后一个元素(的位置)
	public Position last() throws ExceptionListEmpty {
		if (isEmpty())
		throw new ExceptionListEmpty("意外:列表空");
		return trailer.getPrev();
	}
	//返回紧靠给定位置之前的元素(的位置)
	public Position getPrev(Position p) throws ExceptionPositionInvalid, ExceptionBoundaryViolation {
		DLNode v = checkPosition(p);
		DLNode prev = v.getPrev();
		if (prev == header)
		throw new ExceptionBoundaryViolation("意外:企图越过列表前端");
		return prev;
	}
	//返回紧接给定位置之后的元素(的位置)
	public Position getNext(Position p) throws ExceptionPositionInvalid, ExceptionBoundaryViolation {
		DLNode v = checkPosition(p);
		DLNode next = v.getNext();
		if (next == trailer)
		throw new ExceptionBoundaryViolation("意外:企图越过列表后端");
		return next;
	}
	//将e插入至紧靠给定位置之前的位置
	public Position insertBefore(Position p, Object element) throws ExceptionPositionInvalid {
		DLNode v = checkPosition(p);
		numElem++;
		DLNode newNode = new DLNode(element, v.getPrev(), v);
		v.getPrev().setNext(newNode);
		v.setPrev(newNode);
		return newNode;
	}
	//将e插入至紧接给定位置之后的位置
	public Position insertAfter(Position p, Object element) throws ExceptionPositionInvalid {
		DLNode v = checkPosition(p);
		numElem++;
		DLNode newNode = new DLNode(element, v, v.getNext());
		v.getNext().setPrev(newNode);
		v.setNext(newNode);
		return newNode;
	}
	//将e作为第一个元素插入列表
	public Position insertFirst(Object e) {
		numElem++;
		DLNode newNode = new DLNode(e, header, header.getNext());
		header.getNext().setPrev(newNode);
		header.setNext(newNode);
		return newNode;
	}
	//将e作为最后一个元素插入列表
	public Position insertLast(Object e) {
		numElem++;
		DLNode newNode = new DLNode(e, trailer.getPrev(), trailer);
		if (null == trailer.getPrev()) System.out.println("!!!Prev of trailer is
		NULL!!!");
		trailer.getPrev().setNext(newNode);
		trailer.setPrev(newNode);
		return newNode;
	}
	//删除给定位置处的元素,并返回之
	public Object remove(Position p) throws ExceptionPositionInvalid {
		DLNode v = checkPosition(p);
		numElem--;
		DLNode vPrev = v.getPrev();
		DLNode vNext = v.getNext();
		vPrev.setNext(vNext);
		vNext.setPrev(vPrev);
		Object vElem = v.getElem();
		//将该位置(节点)从列表中摘出,以便系统回收其占用的空间
		v.setNext(null);
		v.setPrev(null);
		return vElem;
	}
	//删除首元素,并返回之
	public Object removeFirst()
	{ return remove(header.getNext()); }
	//删除末元素,并返回之
	public Object removeLast()
	{ return remove(trailer.getPrev()); }
	//将处于给定位置的元素替换为新元素,并返回被替换的元素
	public Object replace(Position p, Object obj) throws ExceptionPositionInvalid {
		DLNode v = checkPosition(p);
		Object oldElem = v.getElem();
		v.setElem(obj);
		return oldElem;
	}
	//位置迭代器
	public Iterator positions()
	{ return new IteratorPosition(this); }
	//元素迭代器
	public Iterator elements()
	{ return new IteratorElement(this); }
}

在这里插入图片描述

3.3 序列

通用的序列 ADT:将向量 ADT 与列表 ADT 中的所有方法集成起来

3.3.1 序列 ADT

操作方法 功能描述
rank2Pos(r): 若 0 ≤ r < getSize(),则返回秩为 r 的元素所在的位置;否则,报错
输入:一个(作为秩的)整数
输出:位置
pos2Rank(p): 若 p 是序列中的合法位置,则返回存放于 p 处的元素的秩;否则,报错
输入:一个位置
输出:(作为秩的)整数
/*
* 序列接口
*/
public interface Sequence extends Vector, List {
	//若0 <= r < getSize(),则返回秩为r的元素所在的位置;否则,报错
	public Position rank2Pos(int r) throws ExceptionBoundaryViolation;
	//若p是序列中的合法位置,则返回存放于p处的元素的秩;否则,报错
	public int pos2Rank(Position p) throws ExceptionPositionInvalid;
}

3.3.2 基于双向链表实现序列

实现序列最自然、最直接的方式,就是利用双向链表。这样,来自列表 ADT 的每个方法都依然可以保持原先 O(1)的时间复杂度。当然,来自向量 ADT 的方法尽管也可以借助双向链表来实现,但其效率却会受到影响。实际上,如果希望保持原列表 ADT 中各方法的高效率(即通过位置类来确定操作元素的位置),就不可能显式地在序列中保留和维护各元素的秩。为了完成诸如 getAtRank(r)之类的操作,我们不得不从列表的某一端起逐一扫描各个元素,直到发现秩为 r 的元素。于是,在最坏情况下,这些操作的时间复杂度将注定为Ω(n)。

r <= getSize()/2,从前往后;否则从后往前
注意首末节点,头尾节点

/*
* 基于双向链表实现序列
*/
public class Sequence_DLNode extends List_DLNode implements Sequence {
	//检查秩r是否在[0, n)之间
	protected void checkRank(int r, int n) throws ExceptionBoundaryViolation {
		if (r < 0 || r >= n)
		throw new ExceptionBoundaryViolation("意外:非法的秩" + r + ",应该属于[0, " + n +")");
	}
	//若0 <= r < getSize(),则返回秩为r的元素所在的位置;否则,报错--O(n)
	public Position rank2Pos(int r) throws ExceptionBoundaryViolation {
		DLNode node;
		checkRank(r, getSize());
		if (r <= getSize()/2) {//若秩较小,则
			node = header.getNext( );//从前端开始
			for (int i=0; i<r; i++) node = node.getNext();//逐一扫描
		} else {//若秩较大,则
			node = trailer.getPrev();//从后端开始
			for (int i=1; i<getSize()-r; i++) node = node.getPrev();//逐一扫描
		}
		return node;
	}
	//若p是序列中的合法位置,则返回存放于p处的元素的秩;否则,报错--O(n)
	public int pos2Rank(Position p) throws ExceptionPositionInvalid {
		DLNode node = header.getNext();
		int r = 0;
		while (node != trailer) {
			if (node == p) return(r);
				node = node.getNext(); r++;
			}
			throw new ExceptionPositionInvalid("意外:作为参数的位置不属于序列");
		}
		//取秩为r的元素--O(n)
		public Object getAtRank(int r) throws ExceptionBoundaryViolation {
		checkRank(r, getSize());
		return rank2Pos(r).getElem();
	}
	//将秩为r的元素替换为obj--O(n)
	public Object replaceAtRank(int r, Object obj) throws ExceptionBoundaryViolation {
		checkRank(r, getSize());
		return replace(rank2Pos(r), obj);
	}
	//插入obj,作为秩为r的元素--O(n);返回该元素
	public Object insertAtRank(int r, Object obj) throws ExceptionBoundaryViolation {
		checkRank(r, getSize()+1);
		if (getSize() == r) insertLast(obj);
		else insertBefore(rank2Pos(r), obj);
		return obj;
	}
	//删除秩为r的元素--O(n)
	public Object removeAtRank(int r) throws ExceptionBoundaryViolation {
		checkRank(r, getSize());
		return remove(rank2Pos(r));
	}
}



3.4 迭代器

迭代器是软件设计的一种模式,它是对“逐一访问所有元素”这类操作的抽象。
迭代器本身也是一个序列S,在任何时候,迭代器中都有唯一的当前元素。迭代器还必须提供某种机制,使得我们可以不断转向S中的下一元素,并将其置为新的当前元素。

3.4.1 简单迭代器的 ADT

迭代器ADT支持的操作:

操作方法 功能描述
hasNext(): 检查迭代器中是否还有剩余的元素
输入:无
输出:布尔标志
getNext(): 返回迭代器中的下一元素
输入:无
输出:对象

Java 中的简单迭代器

Java 已经通过 java.util.Iterator 接口提供了一个迭代器。这一接口还有一个额外的功能⎯⎯一旦转向新的对象,就将此前的对象从集合中删除。

在列表和其它 ADT 中引入迭代器

对象集合ADT支持的操作:

操作方法 功能描述
elements(): 返回集合中所有元素的一个迭代器
输入:无
输出:迭代器

对于列表、序列之类支持位置概念的 ADT,我们还提供如下方法:

操作方法 功能描述
positions(): 返回集合中所有位置的一个迭代器
输入:无
输出:迭代器

3.4.2 迭代器接口

/*
* 迭代器ADT接口
*/
public interface Iterator {
	boolean hasNext();//检查迭代器中是否还有剩余的元素
	Object getNext();//返回迭代器中的下一元素
}

3.4.3 迭代器的实现

利用快照建立迭代器

实现迭代器的一种直接办法,就是给容器中的所有元素照张“快照”,并用某一数据结构将其记录下来,当然,这种数据结构必须支持后续对这些元素的遍历操作。比如,可以借助栈结构来存放“快照” ⎯⎯将所有元素(的引用)压入某一栈中。于是,!isEmpty()方法就等效于 hasNext()方法,而 pop()操作则等效于 getNext()操作。

IteratorPosition()的实现

借助列表类本身⎯⎯来实现IteratorPosition():这种实现只需保留并跟踪迭代器的当前元素,因此无论是构造方法还是hasNext()和getNext()方法,都可以在O(1)时间内完成。

/*
* 基于列表实现的位置迭代器
*/
public class IteratorPosition implements Iterator {
	private List list;//列表
	private Position nextPosition;//当前(下一个)位置
	//默认构造方法
	public IteratorPosition() { list = null; }
	//构造方法
	public IteratorPosition(List L) {
		list = L;
		if (list.isEmpty())//若列表为空,则
		nextPosition = null;//当前位置置空
		else//否则
		nextPosition = list.first();//从第一个位置开始
	}
	//检查迭代器中是否还有剩余的位置
	public boolean hasNext() { return (nextPosition != null); }
	//返回迭代器中的下一位置
	public Object getNext() throws ExceptionNoSuchElement {
		if (!hasNext()) throw new ExceptionNoSuchElement("意外:没有下一位置");
		Position currentPosition = nextPosition;
		if (currentPosition == list.last())//若已到达尾位置,则
		nextPosition = null;//不再有下一个位置
		else//否则
		nextPosition = list.getNext(currentPosition);//转向下一位置
		return currentPosition;
	}
}

IteratorElement() 的实现

/*
* 基于列表实现的元素迭代器
*/
public class IteratorElement implements Iterator {
	private List list;//列表
	private Position nextPosition;//当前(下一个)元素的位置
	//默认构造方法
	public IteratorElement() { list = null; }
	//构造方法
	public IteratorElement(List L) {
		list = L;
		if (list.isEmpty())//若列表为空,则
		nextPosition = null;//当前元素置空
		else//否则
		nextPosition = list.first();//从第一个元素开始
	}
	//检查迭代器中是否还有剩余的元素
	public boolean hasNext() { return (null != nextPosition); }
	//返回迭代器中的下一元素
	public Object getNext() throws ExceptionNoSuchElement {
		if (!hasNext()) throw new ExceptionNoSuchElement("意外:没有下一元素");
		Position currentPosition = nextPosition;
		if (currentPosition == list.last())//若已到达尾元素,则
		nextPosition = null;//不再有下一元素
		else//否则
		nextPosition = list.getNext(currentPosition);//转向下一元素
		return currentPosition.getElem();
	}
}

3.4.4 Java 中的列表及迭代器

在使用迭代器的过程中,如果原容器中的内容正在被(比如另一个线程)修改,就很可能会造成危险的后果。若需在容器中的某一“位置”进行插入、删除或替换之类的操作,最好是通过一个位置对象来指明。实际上, java.util.Iterator 的大多数实现都提供了故障快速修复( Fail-fast)的机制⎯⎯在利用迭代器遍历某一容器的过程中,一旦发现该容器的内容有所改变,迭代器就会抛出ConcurrentModificationException 意外错并立刻退出。


来源于:Java数据结构,邓俊辉

posted @ 2018-09-26 23:37  XueXueLai  阅读(315)  评论(0编辑  收藏  举报