本文将介绍三种数据类型,分别是背包(Bag)、队列(Queue)和栈(Stack)。它们的不同之处在于删除或者访问对象的顺序不同。
其次是介绍【链式数据结构】的重要性,特别是经典数据结构【链表】,有了它们我们才能高效实现背包、队列和栈。
集合结构介绍及API
背包
背包是一种不支持从中删除元素的集合数据类型——它的目的就是帮助用例收集元素并迭代遍历所有收集到的元素(用例也可以检查背包是否为空或者获取背包中元素的数量)。迭代顺序不确定且与用例无关。
先进先出队列
先进先出队列(简称队列)是一种基于先进先出(FIFO)策略的集合类型。
下压栈
下压栈(或简称栈)是一种基于后进先出(LIFO)策略的集合类型。
栈的数组实现
在Java中,数组一旦被创建,其大小是无法改变的,因此栈使用的空间只能是这个最大容量的一部分。使用数组实现栈的基本思路是首先创建默认长度的数组用来存储元素(可以是任意长度),使用一个计数器N(初值为0)存储栈中元素的个数。
向栈中加入元素时,首先判断栈中元素是否已经达到数组长度,如果达到,就创建一个更大的数组,将旧数组中的元素依次复制过去,然后使用新数组替代旧数组,将新加入的元素存储在数组中,更新计数器N的值。
在栈中移除最上层的元素时,首先将elements[N - 1](elements为成员变量,即存储元素使用的数组)赋值为空,这是为了防止对象游离(根据java内存回收机制,如果一个对象仍然有引用指向它,就不会被回收),然后将N减一。完成移除操作后,如果栈中元素小于等于数组长度的四分之一(整型的除法向下取整,所以elements.length / 4的含义是整除4),就将数组大约缩减一半(即创建一个长度为旧数组一半的新数组取代它)。
public class ResizeArrayStack<Item> implements Iterable<Item>{
private int N;
private Item[] elements;
private static int DEFAULT_CAPACITY = 2;
//......
/**
* 往堆里增加元素
* @param item 元素
*/
public void push(Item item){
if(N == elements.length){
resize(N * 2);
}
elements[N++] = item;
}
/**
* 弹出堆最上层的元素
* @return 堆最上层的元素
*/
public Item pop(){
if(isEmpty()) throw new NoSuchElementException("stack underflow");
Item result = elements[--N];
if(N > 0 && N == elements.length / 4){
resize(elements.length / 2);
}
return result;
}
private void resize(int l){
elements = Arrays.copyOf(elements, l);
}
public boolean isEmpty(){
return N == 0;
}
//......
}
这里有两个辅助函数isEmpty()和resize(int l)。isEmpty非常简单,如果计数器N的值为0即代表栈为空,反之,栈不为空。resize方法调用了Arrays.copyOf(Object[] src, int newlength)方法复制数组,第一个参数为源数组(即将被复制的数组),第二个参数为新数组的长度(如果小于源数组,则相当于截取源数组的一部分,如果大于源数组,多出的部分使用默认值填充,如Int型数组默认值是0,对象数组默认值是null),函数返回对应参数要求的新数组,将其赋值给成员变量elements。
队列的数组实现
队列的数组实现相对于栈的数组实现比较复杂。它需要维护first和last两个指针,以记录队列中元素存储在数组中的下标范围。具体代码如下:
package QueueStackAndBag;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class ResizeArrayQueue<Item> implements Iterable<Item>{
private int N;
private Item[] elements;
private static int DEFAULT_CAPACITY = 2;
private int first;
private int last;
/**
* 没有指定stack初始容量,则调用默认的容量
*/
ResizeArrayQueue(){
this(DEFAULT_CAPACITY);
}
/**
* 初始化
* @param capacity 初始容量
*/
ResizeArrayQueue(int capacity){
elements = (Item[]) new Object[capacity];
}
/**
*
* @return 队列是否为空
*/
public boolean isEmpty(){
return N == 0;
}
/**
* 返回队列的长度
* @return N
*/
public int size(){
return N;
}
/**
* 往队列里增加元素
* @param item 元素
*/
public void enqueue(Item item){
if(N == elements.length){
resize(N * 2);
}
N++;
elements[last++] = item;
if(last == elements.length) last = 0;
}
/**
* 弹出队列最上层的元素
* @return 队列最上层的元素
*/
public Item dequeue(){
if(isEmpty()) throw new NoSuchElementException("queue underflow");
Item result = elements[first];
elements[first++] = null;
N--;
if(first == elements.length) first = 0;
if(N > 0 && N == elements.length / 4){
resize(elements.length / 2);
}
return result;
}
public Item first(){
if(isEmpty()) throw new NoSuchElementException("stack underflow");
return elements[first];
}
/**
* 重置elements数组的长度
* @param l 新的elements长度
*/
private void resize(int l){
Item[] newElements = (Item[])new Object[l];
for(int i = 0; i < N; i++){
newElements[i] = elements[(first + i) % elements.length];
}
first = 0;
last = N;
elements = newElements;
}
class StackReverseIterator implements Iterator<Item>{
int k = first;
int count = 0;
@Override
public boolean hasNext() {
return count < N;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item result = elements[k++];
if(k == N) k = 0;
count++;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Item> iterator() {
return new StackReverseIterator();
}
public static void main(String[] args){
ResizeArrayQueue<String> queue = new ResizeArrayQueue<>();
System.out.println("isEmpty:" + queue.isEmpty());
queue.enqueue("hello world!");
System.out.println("dequeue:" + queue.dequeue());
for(int i = 0; i < 10; i++){
queue.enqueue("test" + (i + 1));
}
for(int i = 0; i < 5; i++){
queue.dequeue();
}
// 测试遍历
System.out.println("==forEach==");
for(String s : queue){
System.out.println(s);
}
System.out.println("==methods==");
System.out.println("isEmpty:" + queue.isEmpty());
System.out.println("first:" + queue.first());
System.out.println("dequeue:" + queue.dequeue());
System.out.println("size:" + queue.size());
System.out.println("dequeue:" + queue.dequeue());
// 测试异常
System.out.println("dequeue:" + queue.dequeue());
}
}
链表
为了实现链表,我们首先创建Node类代表链表中的一个节点。一个节点包含元素(item)和下一个节点(next)这两个成员变量。为了让节点可以储存不同类型的元素,我们这里使用了泛型(命名为Item)。
private class Node {
Item item;
Node next;
}
创建链表
通过节点的成员属性next,我们可以把第二个节点的引用存储在第一个节点的next属性上,第三个节点的引用存储在第二个节点的next属性上,以此类推。
由此我们可以知道,只要拥有第一个节点的引用,就可以顺着它遍历出整个链表,所以链表的基本结构为如下:
import java.util.Iterator;
public class SimpleNodeList<Item> implements Iterable<Item>{
private Node first;
private int N;
private class Node{
//......见上文
}
}
我们使用成员属性first来存储第一个节点的引用,一个计数器N来存储链表的长度。将前文所述的Node类作为链表类的子类。
插入节点
对应的代码:
public void push(Item item){
first = new Node(item, first);
N++;
}
删除头部节点
对应的代码:
public Item pop(){
if(isEmpty()) throw new RuntimeException();
Item result = first.item;
first = first.next;
N--;
return result;
}
pop()函数的作用是删除头部节点并返回其中的元素。这里的辅助函数isEmpty()同前文的一样,如果计数器N为0则返回真,否则返回假。删除节点之前先进行检查,是否为空链表,如果是空链表则抛出异常(根据具体使用场景的需要,也可以设计成返回空)。
栈的链表实现
import java.util.Iterator;
public class LinkedStack<Item> implements Iterable<Item>{
private Node first;
private int N;
private class Node{
Item value;
Node next;
Node(Item value){
this.value = value;
}
Node(Item value, Node next){
this(value);
this.next = next;
}
}
public boolean isEmpty(){
return first == null;
}
public int size(){
return N;
}
public void push(Item item){
first = new Node(item, first);
N++;
}
public Item pop(){
if(isEmpty()) throw new RuntimeException();
Item result = first.value;
first = first.next;
N--;
return result;
}
public Item peek(){
if(isEmpty()) throw new RuntimeException();
return first.value;
}
private class StackIterator implements Iterator<Item>{
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
Item result = current.value;
current = current.next;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Item> iterator() {
return new StackIterator();
}
public static void main(String[] args){
LinkedStack<String> stack = new LinkedStack<>();
System.out.println("=====methods====");
System.out.println("isEmpty:" + stack.isEmpty());
stack.push("test1!");
stack.push("test2!");
System.out.println("dequeue:" + stack.pop());
stack.push("hello world!");
System.out.println("first:" + stack.peek());
System.out.println("size:" + stack.size());
System.out.println("isEmpty:" + stack.isEmpty());
System.out.println("=====forEach====");
for(String s : stack){
System.out.println(s);
}
}
}
关于栈的API的介绍在本文开头部分。peek()方法和pop()方法类似,区别是它仅仅返回栈顶节点存储的元素,而不把从栈中删去。同时LinkedStack实现了Iterable接口 ,使其可以使用foreach语句遍历。
队列的链表实现
对于栈的链表实现,我们在入栈(向栈顶添加元素)的时候创建新节点并作为链表的头结点。因此,上一个加入栈中的元素一定存储在头节点中,所以出栈的时候只需要弹出链表的头结点即可。
而链表入列和出列的操作正好和栈是相反的,栈是后进先出,而队列是先进先出。我们的实现方法如下,入列的时候创建新节点并加入到链表尾部,出列的时候依然弹出链表头节点,并返回存储的元素。所以队列类的成员变量相比栈多了一个last,用于存储链表最后一个节点。部分类结构如下:
import java.util.Iterator;
public class LinkedQueue<Item> implements Iterable<Item>{
private Node first;
private Node last;
private int N;
private class Node{
//......
}
}
入列的时候把新创建的节点加到链表尾端,并更新连接(旧的尾端节点的next属性指向新节点,然后把新节点的引用赋给last成员属性)。
public void enqueue(Item item){
if(isEmpty()){
first = last = new Node(item);
}else{
last = last.next = new Node(item);
}
N++;
}
出列方法与栈的pop()方法类似,只是方法名不一样。
完整的代码如下:
import java.util.Iterator;
public class LinkedQueue<Item> implements Iterable<Item>{
private Node first;
private Node last;
private int N;
private class Node{
Item value;
Node next;
Node(Item value){
this.value = value;
}
Node(Item value, Node next){
this(value);
this.next = next;
}
}
public boolean isEmpty(){
return first == null;
}
public int size(){
return N;
}
public void enqueue(Item item){
if(isEmpty()){
first = last = new Node(item);
}else{
last.next = new Node(item);
last = last.next;
}
N++;
}
public Item dequeue(){
if(isEmpty()) throw new RuntimeException();
Item result = first.value;
first = first.next;
N--;
return result;
}
public Item first(){
if(isEmpty()) throw new RuntimeException();
return first.value;
}
private class QueueIterator implements Iterator<Item>{
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
Item result = current.value;
current = current.next;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Item> iterator() {
return new QueueIterator();
}
public static void main(String[] args){
LinkedQueue<String> queue = new LinkedQueue<>();
System.out.println("=====methods====");
System.out.println("isEmpty:" + queue.isEmpty());
queue.enqueue("test1!");
queue.enqueue("test2!");
System.out.println("dequeue:" + queue.dequeue());
queue.enqueue("hello world!");
System.out.println("first:" + queue.first());
System.out.println("size:" + queue.size());
System.out.println("isEmpty:" + queue.isEmpty());
System.out.println("=====forEach====");
for(String s : queue){
System.out.println(s);
}
}
}
背包的链表实现
背包的链表实现与栈及其类似,可以将其理解为“退化”了的栈。
package QueueStackAndBag;
import java.util.Iterator;
public class LinkedBag<Item> implements Iterable<Item>{
private Node first;
private int N;
private class Node{
Item value;
Node next;
Node(Item value){
this.value = value;
}
Node(Item value, Node next){
this(value);
this.next = next;
}
}
public boolean isEmpty(){
return first == null;
}
public int size(){
return N;
}
public void add(Item item){
first = new Node(item, first);
N++;
}
private class LinkedBagIterator implements Iterator<Item>{
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
Item result = current.value;
current = current.next;
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Item> iterator() {
return new LinkedBagIterator();
}
public static void main(String[] args){
LinkedBag<String> nodeList = new LinkedBag<>();
System.out.println("=====methods====");
System.out.println("isEmpty:" + nodeList.isEmpty());
nodeList.add("test1!");
nodeList.add("test2!");
nodeList.add("hello world!");
System.out.println("size:" + nodeList.size());
System.out.println("isEmpty:" + nodeList.isEmpty());
System.out.println("=====forEach====");
for(String s : nodeList){
System.out.println(s);
}
}
}
详细代码:https://github.com/617976080/algorithms-4th-code
参考书籍:《算法(第4版)》Robert Sedgewick / 美Kevin Wayne编写,人民邮电出版社在2012年出版。