集合
集合(collection)是收集并组织其他对象的对象,它定义了访问及管理称作集合元素(element)的其他对象的一种具体方式。集合的使用者——常常是软件系统中的另一个类或对象,它必须按照规定的方式与集合进行交互。集合主要分为两类:线性的和非线性的。线性集合(linear collection)就是将集合中的元素排成一行。非线性集合(nonlinear collection)则会按照层次或是网络等方式或根本没有组织方式来组织元素。
集合中元素之间的组织方式通常由两个因素决定:它们加入集合的次序和元素之间的某些固有的关系。例如一个线性集合可能将新元素添加到末尾,所以元素之间的次序就由它们加入集合的次序决定。另一个线性集合可能会按照字典(字母表)顺序排列元素,就是利用了元素之间的固有关系。
抽象
集合是隐藏了其实现细节的一个抽象名称。抽象(abstraction)是一个重要的软件工程概念。在大型的软件系统中,任何人都不可能在短时间内迅速了解系统的所有细节。为了便于开发管理,系统通常会被划分成规定了一定目的并相互间交互的抽象的子系统。子系统会被分派给不同的开发人员或开发团队,由他们开发子系统来契合需求。处理抽象比同时处理过多的细节相比容易很多,举个例子,我们不必详细了解汽车的所有细节,如火花塞、活塞、发动机的运行等。相反,我们可以只关注汽车的“接口”:方向盘、脚踏板和一些控制器。这些控制器就是抽象名称,它们隐藏了内部细节,却能让我们轻松控制非常复杂的机器。
抽象数据类型(abstract data type,ADT)是其值和操作都没有在程序设计语言中定义的一种数据类型。它是抽象的,因为其实现细节必须要定义,而且要对用户隐藏。数据结构(data structure)是用来实现集合的程序结构集合。例如一个集合可能由数组这样的定长结构来实现。
Java程序设计语言带有一个很大的类库,可用来开发软件。类库中包含应用程序接口(application programming interface,API)。Java Collections API提供了一些集合类,但是提供的标准类可能不是以我们所希望的方式实现的,所以集合的设计与实现始终是学习软件开发的重要一部分。
栈集合
栈就是集合的一个典型例子。栈(stack)是一个线性集合,其元素的添加以及删除都在一端进行。我们很早就知道了栈的处理方式是后进先出(last in, first out,LIFO),即最后进入栈中的元素最先移出栈。使用栈来处理事件的一个法则就是反序。下图展示了栈的处理方式。我们将栈作为一个抽象数据类型,不关心它在创建时具体使用的实现技术,而专注于它的接口的操作。用栈的术语来讲,插入元素称为入栈(push),删除元素称为出栈(pop)。还可以查看(peek)栈顶元素的值,需要的时候可以使用它但并不会从栈中删除它。还有一些其他的常规操作,如判断栈是否为空,是否不空,获取栈目前所含的元素的个数等。
栈最重要的特性就是所有的操作都在它的一端(栈顶)进行。如果在解决一个问题时需要访问集合的中间元素或是集合底部的元素,就不适合使用栈。
在计算领域,栈的使用非常频繁。例如,字处理器中的回退操作常用栈来实现。当对文档进行修改时(添加数据、删除数据修改公式等),字处理器向栈中压入某个标识符以记录每步操作。当回退一个操作时,字处理器弹出栈中最后执行的操作,并反向执行。在绝大多数字处理编辑器中,许多操作都可以用这种方式恢复。
下面的程序为栈集合定义了一个Java接口。它是javafoundations包的一部分。
package javafoundations;
public interface Stack<T>
{
public void push(T element);
public T pop();
public T peek();
public boolean isEmpty();
public int size();
public String toString();
}
注意栈接口定义为Stack< T >,是对泛型 T 进行操作的。接口中方法的各参数的类型及返回值也常常表示为泛型T。实现这个接口时,要给予取代T的一个具体类型。栈有不同的实现方法,下面我们讨论两种常见类型的栈,即使用数组或链表结构来存储栈中所含的对象。
从Java5.0开始,Java允许基于泛型来定义类,即可以定义一个类,它保存、操作并管理直到实例化时才确定类型的对象。
数组栈
我们首先回顾一下Java数组的几个关键特性。存储在数组中的元素下标为0到n-1,其中n是数组总的单元数。一个数组就是一个对象,应该分别对其中所保存的对象进行实例化。对象数组实际上指的是指向对象的引用的数组。
定义一个类ArrayStack< T >来表示基于数组实现的、可保存泛型T对象的栈集合。当实例化ArrayStack对象时,将指定泛型T所代表的类型。
package javafoundations;
import javafoundations.exceptions;
public class ArrayStack<T> implements Stack<T>
{
private final int DEFAULT_CAPACITY = 10;
private int count;
private T[] stack;
public ArrayStack()
{
count = 0;
stack = (T[])(new Object[DEFAULT_CAPACITY]);
}
public void push(T element)
{
if (count == stack.length)
expandCapacity();
stack[count] = element;
count++;
}
public <T> T pop() throws EmptyCollectionException
{
if (stack.isEmpty)
throws new EmptyCollectionException(“No element available.”);
count--;
T element = stack[count];
stack[count] = null;
return element;
}
public String toString()
{
String result = “<top of stack>\n”;
for (int index = count-1; index >= 0; index--)
result += stack[index] + “\n”;
return result + “<bottom of stack>“;
}
private void expandCapacity()
{
T[] larger = (T[])(new Object[stack.length*2]);
for (int index = 0; index < stack.length; index++)
larger[index] = stack[index];
stack = larger;
}
}
数组栈中,将一个元素入栈,只需将其插入到数组中由变量count指定的下一个可用位置。操作前判定数组是否已满,如果已满则扩展它。出栈操作删除并返回栈顶元素。对于数组来说就是返回下标为count-1的元素。在这之前必须保证栈中至少有一个元素,如果栈是空的,则抛出EmptyCollectionException异常。
链表栈
链式结构(linked structure)是使用对象引用变量来建立对象之间联系的一种数据结构。我们知道,对象引用变量保存的是对象的地址,它指出对象在内存中存放在什么位置。通常我们不会关注对象引用变量中的具体地址,而只是通过它来访问对象。因此使用到这个引用变量时,我们不显示它保存的具体地址而是说它指向一个对象。现在我们考虑这样一种情况:将类中定义的数据成员实例化为指向同一类的另一个对象。例如假定有一个类Person,它包含人名、地址等信息,并且还包含了一个指向另一个Person对象的引用变量。
public class Person
{
private String name;
private String address;
private Person next;
}
我们只用这一个类,就可以创建出一个链式结构。一个Person对象含有一个指向第二个Person对象的链。第二个对象含有指向另一个Person的链,一个包含另一个,以此类推。有时会称这种类型的对象为自指示(self-referential)。这类关系组成了链表(linked list)的基础,所谓链表就是一个链式结构,一个对象指向下一个对象,建立了表中对象之间的线性关系。存储在链表中的对象常称为表的结点(node)。
注意必须使用一个单独的引用变量指向表的第一个结点。结点的next引用为null时表示表的结束。
链表只是链式结构中的一种。如果类中建立多个指向对象的引用,则可以建立更复杂的结构。
与具有固定大小的数组不同,链表没有容量上限,除非计算机的内存耗尽。链表被看作是一个动态(dynamic)结构,因为它的大小总随着所保存的元素个数在变大或变小。在Java中,所有的对象都动态创建在称为系统堆(heap)或者空闲存储区(free store)的内存区域中。
程序LinkedStack< T >类用链式结构实现了栈。
package javafoundations;
import javafoundations.exceptions.*;
public class LinkedStack<T> implements Stack<T>
{
private int count;
private LinearNode<T> top;
public LinkedStack()
{
count = 0;
top = null;
}
public T pop() throws EmptyCollectionException
{
if (count == 0)
throws new EmptyCollectionException(“Pop operation failed.”
+ “The stack is empty.”);
T result = top.getElement();
top = top.getNext();
count--;
return result;
}
public String toString()
{
String result = “<top of stack>\n”;
LinearNode current = top;
while (current != null)
{
result += current.getElement() + “\n”;
current = current.getNext();
}
return result + “<bottom of stack>”;
}
}
package javafoundations;
public class LinearNode<T>
{
private LinearNode<T> next;
private T element;
public LinearNode()
{
}
}
package javafoundations;
public class LinearNode<T>
{
private LinearNode<T> next;
private T element;
public LinearNode()
{
next = null;
element = null;
} //新建一个空的结点
public LinearNode(T elem)
{
next = null;
element = elem;
} //返回下一个结点
public LinearNode<T> getnext()
{
return next;
}
public void setNext(LinearNode<T> node)
{
next = node;
}
public T getElement()
{
return element;
}
public void setElement(T elem)
{
element = elem;
}
}
链式结构的栈实现push操作包含下列步骤:
1.创建一个新结点,其中包含一个指向要放置到栈中对象的引用。
2.设置新结点的next引用指向当前的栈顶(如果栈为空,则它为null)。
3.设置top引用指向新结点/
4.栈中元素的个数count加1。
链式实现中的pop操作包含下列步骤:
1.确保栈非空。
2.设置临时引用指向栈顶元素。
3.设置top引用指向栈顶结点的next引用。
4.栈中元素个数count减1。
5.返回临时引用指向的元素。