本文内容基于《数据结构与算法分析 Java语言描述》第三版,冯舜玺等译。
1. 抽象数据类型
抽象数据类型(ADT)是带有一组操作的一些对象的集合,诸如表、集合、图以及它们各自的操作一起形成的这些对象都可以被看作是抽象数据类型。
Java类也考虑到ADT的实现,不过适当地隐藏了实现的细节。这样,程序中需要对ADT实施操作的任何其他部分可以通过调用适当的方法来进行。
2. 表ADT
2.1 表的简单数组实现
对表的所有这些操作都可以通过使用数组来实现,数组的实现可以使得printList以线性时间被执行,而findKth操作则话费常数时间,这正是所期望的,不过,插入和删除的花费却隐藏着昂贵的开销。
public class MyArrayTable {
private Object[] arr;
public MyArrayTable(Object[] arr) {
this.arr = arr;
}
public void printList() {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public int find(Object x) {
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(x)) {
return i;
}
}
return -1;
}
public void insert(int pos, Object x) {
Object[] newArr = new Object[arr.length + 1];
for (int i = 0; i < newArr.length; i++) {
if (i == pos) {
newArr[i] = x;
} else if (i < pos) {
newArr[i] = arr[i];
} else {
newArr[i] = arr[i - 1];
}
}
arr = newArr;
}
public void remove(Object x) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(x)) {
count++;
}
}
Object[] newArr = new Object[arr.length - count];
for (int i = 0, j = 0; j < newArr.length; i++) {
if (!arr[i].equals(x)) {
newArr[j] = arr[i];
j++;
}
}
arr = newArr;
}
public Object findKth(int pos) {
return arr[pos];
}
}
2.2 简单链表
为了避免插入和删除的线性开销,需要保证表可以不连续存储,否则表的每个部分都可能需要整体移动。链表由一系列节点组成,这些节点不必在内存中相连,每一个节点均含有表元素和到包含该元素后继元的节点的链,称之为next链,最后一个单元的next链引用null。
如果知道变动将要发生的地方,向链表插入或从链表中删除一项的操作不需要移动很多项,而只设计常数个节点链的改变。
让每一个节点持有一个指向它在表中的前驱节点的链称之为双链表。
class Node {
Object value;
Node next;
public Node(Object value) {
this.value = value;
}
}
public class MyLinkTable {
private Node root;
public MyLinkTable() {
Node node1 = new Node(34);
Node node2 = new Node(12);
Node node3 = new Node(52);
Node node4 = new Node(16);
Node node5 = new Node(12);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
node5.next = null;
root = node1;
}
public void printList() {
Node node = root;
while (node.next != null) {
System.out.print(node.value + " ");
node = node.next;
}
System.out.println(node.value);
}
public int find(Object x) {
int index = -1;
Node node = root;
while (node.next != null) {
index++;
if (node.value.equals(x)) {
return index;
} else {
node = node.next;
}
}
index++;
if (node.value.equals(x)) {
return index;
} else {
return -1;
}
}
public void insert(int pos, Object x) {
int index = 0;
Node node = root;
while (node.next != null) {
if (index == pos - 1) {
Node temp = node.next;
Node newNode = new Node(x);
node.next = newNode;
newNode.next = temp;
return;
} else {
index++;
node = node.next;
}
}
}
public void remove(Object x) {
Node node = root;
if (node.value.equals(x)) {
root = node.next;
} else {
while (node.next != null) {
if (node.next.value.equals(x)) {
node.next = node.next.next;
}
if (node.next != null) {
node = node.next;
}
}
}
}
public Object findKth(int pos) {
int index = 0;
Node node = root;
while (node.next != null) {
if (index == pos) {
return node.value;
}
index++;
node = node.next;
}
return null;
}
}
3. Java Collectons API中的表
3.1 Collection接口
Collections API位于java.util包中。集合的概念在Collection接口中得到抽象,它存储一组类型相同的对象。
Collection接口扩展了Iterable接口。实现Iterable接口的那些类可以拥有增强的for循环,该循环施于这些类之上以观察它们所有的项。
3.2 Iterator接口
实现Iterable接口的集合必须提供一个称为iterator的方法,该方法返回一个Iterator类型的对象。
Iterator的remove方法比Collection接口的remove方法的优点是:Collection的remove方法必须首先找出要被删除的项。
当直接使用Iterator,而不是通过一个增强的for循环间接使用时,如果对正在被迭代的集合进行结构上的改变(add、remove、clear),那么迭代器就不再合法,抛出ConcurrentModificationException异常。如果迭代器调用它自己的remove方法,那么这个迭代器是合法的。
3.3 List接口、ArrayList类和LinkedList类
List ADT有两种流行的实现方式:
- ArrayList类提供了List ADT的一种可增长数组的实现。使用ArrayList的优点在于,对get和set的调用花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在Arrayist的末端进行。
- LinkedList类则提供了List ADT的双链表实现。使用LinkedList的优点在于,新项的插入和现有项的删除均开销很小,这里假设变动项的位置是已知的。使用LinkdeList的缺点是它不容易作索引,因此对get的调用是昂贵的,除非调用非常接近表的端点。
ArrayList中有一个容量的概念,它表示基础数组的大小。在需要的时候,ArrayList将自动增加其容量以保证它至少具有表的大小。默认10,每次扩容成原来的1.5倍。
List<Integer> list1 = new ArrayList<>();
如果该大小的早期估计存在,那么ensureCapacity可以设置容量为一个足够大的量以避免数组容量以后的扩展。再有,trimToSize可以在所有的ArrayList添加操作完成之后使用以避免浪费空间。
List<Integer> list2 = new ArrayList<>(20);
4. 栈ADT
4.1 栈模型
栈是限制插入和删除只能在一个位置上进行的表,改位置是表的末端,叫做栈的顶。栈有时又叫作LIFO(后进先出)表。
栈模型是:存在某个元素位于栈顶,而该元素是唯一的可见元素。
4.2 栈的实现
由于栈是一个表,因此任何实现表的方法都能实现栈。
- 栈的链表实现
使用单链表,通过在表的顶端插入来实现push,通过删除表顶端元素实现pop,top返回表顶端元素。
public class MyLinkedStack {
private Node topNode = null;
public void print() {
Node node = topNode;
if (node == null) {
System.out.println("stack is empty!");
return;
}
while (node.next != null) {
System.out.print(node.value + " ");
node = node.next;
}
System.out.println(node.value);
}
public void push(Object x) {
if (topNode == null) {
topNode = new Node(x);
topNode.next = null;
} else {
Node newTopNode = new Node(x);
newTopNode.next = topNode;
topNode = newTopNode;
}
}
public void pop() {
if (topNode == null) {
System.out.println("stack is empty!");
return;
}
topNode = topNode.next;
}
public Object top() {
if (topNode == null) {
System.out.println("stack is empty!");
return null;
}
return topNode.value;
}
}
- 栈的数组实现
public class MyArrayStack {
private static final int DEFAULT_SIZE = 10;
private Object[] theArray = new Object[DEFAULT_SIZE];
private int topOfStack = -1;
public void print() {
if (topOfStack == -1) {
System.out.println("stack is empty!");
return;
}
for (int i = 0; i <= topOfStack; i++) {
System.out.print(theArray[i] + " ");
}
System.out.println();
}
private void adjustSize() {
if (topOfStack == DEFAULT_SIZE - 1) {
Object[] newTheArray = new Object[theArray.length * 2];
for (int i = 0; i < theArray.length; i++) {
newTheArray[i] = theArray[i];
}
theArray = newTheArray;
}
}
public void push(Object x) {
System.out.println("size: " + theArray.length);
adjustSize();
topOfStack++;
theArray[topOfStack] = x;
}
public void pop() {
if (topOfStack == -1) {
System.out.println("stack is empty!");
return;
}
topOfStack--;
}
public Object top() {
if (topOfStack == -1) {
System.out.println("stack is empty!");
return null;
}
return theArray[topOfStack];
}
}
5. 队列ADT
5.1 队列模型
像栈一样,队列也是表。不同的是,使用队列时插入在一端进行而删除则在另一端进行。
队列的基本操作是enqueue入队,它在表的末端(队尾)插入一个元素,和dequeue出队,它是删除并返回在表的开头(队头)的元素。
5.2 队列的实现
- 队列的链表实现
略。
- 队列的数组实现
public class MyArrayQueue {
private Object[] theArray;
private int front = 0;
private int back = -1;
private int currentSize = 0;
public MyArrayQueue() {
theArray = new Object[10];
}
public void print() {
if (currentSize == 0) {
System.out.println("queue is empty!");
return;
}
for (int i = front; i <= back; i++) {
System.out.print(theArray[i] + " ");
}
System.out.println();
}
public void enqueue(Object x) {
if (back == theArray.length - 1) {
if (currentSize == theArray.length) {
System.out.println("queue is full!");
} else {
back = -1;
}
}
currentSize++;
back++;
theArray[back] = x;
}
public void dequeue() {
if (currentSize == 0) {
System.out.println("queue is empty!");
return;
}
if (front == theArray.length - 1) {
front = -1;
}
currentSize--;
front++;
}
}