背包、队列和栈的实现(基于数组)

前面总结了背包、队列和栈的概念,现在总结背包、队列和栈(基于数组)的实现。

注:代码/图片中的 StdIn 和 StdOut 是《算法(第四版)》中的工具库,在这里功能上分别等价于 Java 中的 System.in 和 System.out。

API 是开发的起点,所以先给出 API:

表 1 背包、队列和栈的 API
img

总思路是先给出一个简单而经典的实现,然后讨论它的改进并实现表 1 中的所有 API。

定容栈

从简单的开始,先实现一种表示容量固定的字符串栈的抽象数据类型,如表 2 所示。

它的 API 和 Stack 的 API 有所不同:它只能处理 String 值、要求用例指定一个容量且不支持迭代。

实现一份 API 的第一步就是选择数据的表示方式。对于 FixedCapacityStackOfStrings,显然可以选择 String 数组。由此可以得到表 2 中底部的实现。

表 2 一种表示定容字符串栈的抽象数据类型
img

表 3 FixedCapacityStackOfStrings 的测试轨迹
img

这种实现的主要性能特点是 push 和 pop 操作所需的时间独立于栈的长度。许多应用会因为这种简洁性而选择它。但几个缺点限制了它作为通用工具的潜力,下面尝试改进它。

泛型定容栈

FixedCapacityStackOfStrings 的第一个缺点是它只能处理 String 对象。对此,可在代码中引入泛型。表 4 中的代码展示了实现的细节。

表 4 一种表示泛型定容栈的抽象数据类型
img

调整数组大小

在 Java 中,数组一旦创建,其大小是无法改变的。这导致创建大容量的栈在栈为空或几乎为空时会浪费大量的内存,而小容量的栈则可能不满足保存大量数据的要求。

所以 FixedCapacityStack 最好能够支持动态调整数组大小,使它既足以保存所有元素,又不至于浪费过多的空间。

为此,可以给 FixedCapacityStack 添加一个 resize(int) 方法:

private void resize(int max)
{ // 将大小为 N <= max 的栈移动到一个新的大小为 max 的数组中
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++)
temp[i] = a[i];
a = temp;
}

然后在 push() 中,检查数组是否太小,太小时调用 resize(int) 增大数组长度:

public void push(Item item)
{ // 将元素压入栈顶
if (N == a.length) resize(2*a.length);
a[N++] = item;
}

类似地,在 pop() 中,首先删除栈顶的元素,之后如果数组太大就调用 resize(int) 将它的长度减半。

public Item pop()
{ // 从栈顶删除元素
Item item = a[--N];
a[N] = null; // 避免对象游离
if (N > 0 && N == a.length/4) resize(a.length/2);
return item;
}

push() 和 pop() 操作中数组大小调整的轨迹见表 5。

表 5 一系列 push() 和 pop() 操作中数组大小调整的轨迹
img

迭代

集合类数据类型的基本操作之一就是,能够使用 Java 的 foreach 语句通过迭代遍历并处理集合中的每个元素。有关在代码中加入迭代的方法见 Java 迭代

栈的实现

有了前面的准备,就能够写出下压栈能动态调整数组大小的实现。

栈的实现(基于数组)

import java.util.Iterator;
public class ResizingArrayStack<Item> implements Iterable<Item>
{
private Item[] a = (Item[]) new Object[1]; // 栈元素
private int N = 0; // 元素数量
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int max)
{ // 将栈移动到一个大小为 max 的新数组
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++)
temp[i] = a[i];
a = temp;
}
public void push(Item item)
{ // 将元素添加到栈顶
if (N == a.length) resize(2*a.length);
a[N++] = item;
}
public Item pop()
{ // 从栈顶删除元素
Item item = a[--N];
a[N] = null; // 避免对象游离
if (N > 0 && N == a.length/4) resize(a.length/2);
return item;
}
public Iterator<Item> iterator()
{ return new ReverseArrayIterator(); }
private class ReverseArrayIterator implements Iterator<Item>
{ // 支持后进先出的迭代
private int i = N;
public boolean hasNext() { return i > 0; }
public Item next() { return a[--i]; }
public void remove() { }
}
}

背包和队列的实现

受上述栈的实现过程的启发,可以类似写出背包和队列的实现。

对于背包,将栈实现中的 push(Item) 方法的方法名改为 add 并删除添加元素的方法即可。

背包的实现(基于数组)

import java.util.Iterator;
public class ResizingArrayBag<Item> implements Iterable<Item>
{
private Item[] a = (Item[]) new Object[1]; // 背包元素
private int N = 0; // 元素数量
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int max)
{ // 将栈移动到一个大小为 max 的新数组
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++)
temp[i] = a[i];
a = temp;
}
public void add(Item item)
{ // 将元素添加到背包
if (N == a.length) resize(2*a.length);
a[N++] = item;
}
public Iterator<Item> iterator()
{ return new ReverseArrayIterator(); }
private class ReverseArrayIterator implements Iterator<Item>
{ // 支持后进先出的迭代
private int i = N;
public boolean hasNext() { return i > 0; }
public Item next() { return a[--i]; }
public void remove() { }
}
}

对于队列,可以使用两个实例变量作为索引,一个变量 head 指向队列的开头,一个变量 tail 指向队列的结尾,如表 6 所示。在删除一个元素时,使用 head 访问它并将 head 加 1;在插入一个元素时,使用 tail 保存它并将 tail 加 1。如果某个索引在增加之后越过了数组的边界则将它重置为 0。

表 6 ResizingArrayQueue 的测试轨迹
img

队列的实现(基于数组)

import java.util.Iterator;
public class ResizingArrayQueue<Item> implements Iterable<Item>
{
private static final int INIT_CAPACITY = 8;
private Item[] a = (Item[]) new Object[INIT_CAPACITY]; // 队列元素
private int N = 0; // 元素数量
private int head = 0; // 指向队列的开头
private int tail = 0; // 指向队列的结尾
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int max)
{ // 将队列移动到一个大小为 max 的新数组
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++) temp[i] = a[(head + i) % a.length];
a = temp;
head = 0;
tail = N;
}
public void enqueue(Item item)
{ // 将元素添加到队尾
if (N == a.length) resize(2*a.length);
a[tail++] = item;
if (tail == a.length) tail = 0;
N++;
}
public Item dequeue()
{ // 从队头删除元素
Item item = a[head];
a[head++] = null; // 避免对象游离
if (head == a.length) head = 0;
N--;
if (N > 0 && N == a.length/4) resize(a.length/2);
return item;
}
public Iterator<Item> iterator()
{ return new ResizingArrayQueueIterator(); }
private class ResizingArrayQueueIterator implements Iterator<Item>
{ // 支持先进先出的迭代
private int i = 0;
public boolean hasNext()
{ return i < N; }
public Item next()
{
Item item = a[(i + head) % a.length];
i++;
return item;
}
}
}

队列的两个索引

注意到队列用了两个索引来处理数据,这和栈以及背包的实现明显不同。为什么要这样写?

受栈实现的启发,可能会写出如下队列的实现:

import java.util.Iterator;
public class ResizingArrayQueue<Item> implements Iterable<Item>
{
private Item[] a = (Item[]) new Object[1]; // 队列元素
private int N = 0; // 元素数量
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int max)
{ // 将队列移动到一个大小为 max 的新数组
Item[] temp = (Item[]) new Object[max];
for (int i = 0; i < N; i++) temp[i] = a[i];
a = temp;
}
public void enqueue(Item item)
{ // 将元素添加到队尾
if (N == a.length) resize(2*a.length);
a[N++] = item;
}
public Item dequeue()
{ // 从队头删除元素
Item item = a[0];
// 将后面的元素整体前移
for (int i = 1; i < N; i++) a[i - 1] = a[i];
N--;
if (N > 0 && N == a.length/4) resize(a.length/2);
return item;
}
public Iterator<Item> iterator()
{ return new ResizingArrayQueueOfItemsIterator(); }
private class ResizingArrayQueueOfItemsIterator implements Iterator<Item>
{ // 支持先进先出的迭代
private int i = 0;
public boolean hasNext() { return i < N; }
public Item next() { return a[i++]; }
}
}

该实现更加直观易懂,但注意到其中的 dequeue 操作会发生数组元素的整体前移,这是相当耗时的,使用两个索引来实现队列则可解决这个问题。

性能

上述基于数组的实现几乎(但还没有)达到了任意集合类数据类型的实现的最佳性能:

❏ 每项操作的用时都与集合大小无关;

❏ 空间需求总是不超过集合大小乘以一个常数。

基于数组的实现的缺点在于某些 push/enqueue/add 和 pop/dequeue 操作会调整数组的大小:这项操作的耗时和栈大小成正比。基于链表的实现则可以克服该缺陷。

总结自《算法(第四版)》1.3 背包、队列和栈

posted @   Higurashi-kagome  阅读(103)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示