数据结构与算法 java描述 笔记-第二章 栈与队列
第二章 栈与队列
栈
是存放对象的一种特殊容器,在插入与删除对象时,这种结构遵循后进先出(Last-in-first-out, LIFO)的原则⎯⎯也就是说,对象可以任意插入栈中,但每次取出的都是此前插入的最后一个对象。
ADT
| 操作方法 | 功能描述 |
|---|---|
| push(x) | 将对象 x 压至栈顶 输入:一个对象 输出:无 |
| pop() | 若栈非空,则将栈顶对象移除,并将其返回 否则,报错 输入:无 输出:对象 |
| getSize() | 返回栈内当前对象的数目 输入:无 输出:非负整数 |
| isEmpty() | 检查栈是否为空 输入:无 输出:布尔标志 |
| top() | 若栈非空,则返回栈顶对象(但并不移除) 否则,报错 输入:无 输出:栈顶对象 |
接口
//当试图对空栈应用pop或top方法时,抛出
public class ExceptionStackEmpty extends RuntimeException {
public ExceptionStackEmpty(String err) {
super(err);
}
}
public interface Stack {
public int getSize();//返回栈中元素数目
public Object top() throws ExceptionStackEmpty;//取栈顶元素(但不删除)
public void push (Object ele);//入栈
public Object pop() throws ExceptionStackEmpty;//出栈
}
基于数组的简单实现
思路
为了实现栈接口,我们可以用一个数组来存放其中的元素。
使用一个容量为 N 的数组 S,再加上一个变量 top 来只是当前栈顶的位置。
由于数组的容量固定,因此有可能会出现栈溢出的情况。

引入一个新的异常ExceptionStackFull,这一 异常并非栈ADT本身的要求,而只是针对数组实现而设置的。
public class ExceptionStackFull extends RuntimeException {
public ExceptionStackFull(String err) {
super(err);
}
}
public class Stack_Array implements Stack {
public static final int CAPACITY = 1024;//数组的默认容量
protected int capacity;//数组的实际容量
protected Object[] S;//对象数组
protected int top = -1;//栈顶元素的位置
//按默认容量创建栈对象
public Stack_Array() {
this(CAPACITY);
}
//按指定容量创建栈对象
public Stack_Array(int cap) {
capacity = cap;
S = new Object[capacity];
}
//获取栈当前的规模
public int getSize() {
return (top + 1);
}
//测试栈是否为空
public boolean isEmpty() {
return (top < 0);
}
//入栈
public void push(Object obj) throws ExceptionStackFull {
if (getSize() == capacity)
throw new ExceptionStackFull("意外:栈溢出");
S[++top] = obj;
}
//取栈顶元素
public Object top() throws ExceptionStackEmpty {
if (isEmpty())
throw new ExceptionStackEmpty("意外:栈空");
return S[top];
}
//出栈
public Object pop() throws ExceptionStackEmpty {
Object elem;
if (isEmpty())
throw new ExceptionStackEmpty("意外:栈空");
elem = S[top];
S[top--] = null;
return elem;
}
}
借助栈进行数组倒置
public static Integer[] reverse(Integer[] a) {
Stack_Array S = new Stack_Array(a.length);
Integer[] b = new Integer[a.length];
for (int i = 0; i < a.length; i++)
S.push(a[i]); //所有元素顺序入栈
for (int i = 0; i < a.length; i++)
b[i] = (Integer) (S.pop()); //逆序退栈
return b;
}
栈应用实例
括号匹配算法
算法:ParenMatch(X, n)
输入:长度为n数组X,对应于某一算术表达式
输出:判断X中的括号是否匹配
{
初始化栈S;
for (i=0 to n-1) {//依次扫描X中的各个符号
if (X[i]为左括号) //若遇到左括号,则
S.push(X[i]); //入栈
else if (X[i]为右括号) { //若遇到右括号,则
if (S.isEmpty()) //若此时栈为空,则
return false; //不用检查后续字符,即可报告“不匹配”
if (S.pop()与X[i]不匹配) //否则,若弹出的栈顶与当前括号不匹配,则
return false; //报告“不匹配”
}
}
//至此,整个表达式已经扫描完毕
if (S.isEmpty())
return true; //若此时栈空,则报告“匹配”
else
return false; //否则,报告“不匹配”
}
队列
队列也是对象的一种容器,其中对象的插入和删 除遵循“先进先出”(First-In-First-Out, FIFO)的原则⎯⎯也就是说,每次删除的只能是最先插入的 对象。
ADT
| 操作方法 | 功能描述 |
|---|---|
| enqueue(x) | 将元素 x 加到队列末端 输入:一个对象 输出:无 |
| dequeue() | 若队列非空,则将队首元素移除,并将其返回 否则,报错 输入:无 输出:对象 |
| getSize() | 返回队列内当前对象的数目 输入:无 输出:非负整数 |
| isEmpty() | 检查队列是否为空 输入:无 输出:布尔标志 |
| front() | 若队列非空,则返回队首元素(但并不移除) 否则,报错 输入:无 输出:队头对象 |
接口
public interface Queue {
public int getSize();//返回队列中元素数目
public boolean isEmpty();//判断队列是否为空
public Object front()//取队首元素(但不删除)
throws ExceptionQueueEmpty;
public void enqueue(Object obj)
throws ExceptionQueueFull;//入队
public Object dequeue()//出队
throws ExceptionQueueEmpty;
public void Traversal();//遍历
}
基于数组的实现
顺序数组
一种自然的办法就是仿照栈的实现,以 Q[0]作为队首,其它对象顺序往后存放。
然而如此一来, 每次首元素出队之后,都需要将后续的所有元素向前顺移一个单元⎯⎯若队长为 n,这项工作需要 O(n)时间,因此效率很低。
循环数组
为了避免数组的整体移动,可以引入如下两个变量 f 和 r:
f:始终等于 Q 的首元素在数组中的下标,即指向下次出队元素的位置
r:始终等于 Q 的末元素的下标加一,即指向下次入队元素的位置
一开始,f = r = 0,此时队空。
每次有对象入队时,将其存放于 Q[r],然后 r 加一,以指向下一 单元。
每次有对象出队之后,也将 f 加一,指向新的队首元素。
front()、enqueue() 和 dequeue()方法的每一次调用都只需常数时间。
若队列数组的容量为 N,则在经过 N 次入队操作后,r 所指向的单元必然超出 数组的范围
就是在每次 f 或 r 加一后,都要以数组的长度做取模运算,以 保证其所指单元的合法性。
相当于把数组的头和尾相联,构成一个环状结构。
public class Queue_Array implements Queue {
public static final int CAPACITY = 1000;//数组的默认容量
protected int capacity;//数组的实际容量
protected Object[] Q;//对象数组
protected int f = 0;//队首元素的位置
protected int r = 0;//队尾元素的位置
//构造方法(空队列)
public Queue_Array() {
this(CAPACITY);
}
//按指定容量创建对象
public Queue_Array(int cap) {
capacity = cap;
Q = new Object[capacity];
}
//查询当前队列的规模
public int getSize() {
return (capacity - f + r) % capacity;
}
//判断队列是否为空
public boolean isEmpty() {
return (f == r);
}
//入队
public void enqueue(Object obj) throws ExceptionQueueFull {
if (getSize() == capacity - 1)
throw new ExceptionQueueFull("Queue overflow.");
Q[r] = obj;
r = (r + 1) % capacity;
}
//出队
public Object dequeue() {
Object elem;
if (isEmpty())
throw new ExceptionQueueEmpty("意外:队列空");
elem = Q[f];
Q[f] = null;
f = (f + 1) % capacity;
return elem;
}
//取(并不删除)队首元素
public Object front() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:队列空");
return Q[f];
第二章 栈与队列 §2.2 队列
46
}
//遍历(不属于ADT)
public void Traversal() {
for (int i = f; i < r; i++)
System.out.print(Q[i] + " ");
System.out.println();
}
}
队列应用实例
循环分配器
算法:RoundRobin
{
e = Q.dequeue();
Serve(e);
Q.enqueue(e);
}
Josephus 环
一群小孩围成一圈,有一个刚出锅的山芋在他们之间 传递。其中一个孩子负责数数,每数一次,拿着山芋的孩子就把山芋转交给右边的邻居。一旦数到 某个特定的数,拿着山芋的孩子就必须退出,然后重新数数。如此不断,最后剩下的那个孩子就是 幸运者。
n 个孩子玩这个游戏,最后的幸运者是谁?
public class Josephus {
//利用队列结构模拟Josophus环
public static Object Josephus(Queue Q, int k) {
if (Q.isEmpty())
return null;
while (Q.getSize() > 1) {//不断迭代
Q.Traversal();//显示当前的环
for (int i = 0; i < k; i++)//将山芋向前传递k次
Q.enqueue(Q.dequeue());
Object e = Q.dequeue();//拿着山芋的孩子退出
System.out.println("\n\t" + e + "退出");
}
return Q.dequeue();//最后剩下的那个孩子
}
//将一组对象组织为一个队列
public static Queue buildQueue(Object a[]) {
Queue Q = new Queue_Array();
for (int i = 0; i < a.length; i++)
Q.enqueue(a[i]);
return Q;
}
//测试用main方法
public static void main(String[] args) {
String[] kid = {"Alice", "Bob", "Cindy", "Doug", "Ed",
"Fred", "Gene", "Hope", "Irene", "Jack",
"Kim", "Lance", "Mike", "Nancy", "Ollie"};
System.out.println("最终的幸运者是" + Josephus(buildQueue(kid), 5));
}
}
链表
单链表
链表(Linked list),就是按线性次序排列的一组数据节点。
每个节点都是 一个对象,它通过一个引用element指向对应的数据元素,同时还通过一个引用next指向下一节点。
若 P 的 next 引用指向 S,则 P 就是 S 的直接前驱,而 S 是 P 的直接后继。

public class Node implements Position {
private Object element;//数据对象
private Node next;//指向后继节点
/**************************** 构造函数 ****************************/
public Node() {
this(null, null);
}//指向数据对象、后继节点的引用都置空
public Node(Object e, Node n) {
element = e;
next = n;
}//指定数据对象及后继节点
/**************************** Position接口方法 ****************************/
//返回存放于该位置的元素
public Object getElem() {
return element;
}
//将给定元素存放至该位置,返回此前存放的元素
public Object setElem(Object e) {
Object oldElem = element;
element = e;
return oldElem;
}
/**************************** 单链表节点方法 ****************************/
//取当前节点的后继节点
public Node getNext() {
return next;
}
//修改当前节点的后继节点
public void setNext(Node newNext) {
next = newNext;
}
}
首节点的插入与删除
1.要创建该节点,然后将其next 引用指向当前的首节点
2.然后将队列的head引用指向新插入的节点

首节点的删除
1.单链表,为了删除首节点,我们首先将首节点的next引用复制一 份,然后才删除该节点
2.最后将表头引用设置为先前复制的引用

末节点的插入
1.为了将一个新节点作为末节点插入,我们首先要创建该节点,将其next引用 置为空,并将当前末节点的next引用指向该新节点.
2.最后将tail引用指向新节点

基于单链表实现栈
public class Queue_List implements Queue {
protected Node head;//指向表首元素
protected Node tail;//指向表末元素
protected int size;//队列中元素的数目
//构造方法(空队列)
public Queue_List() {
head = tail = null;
size = 0;
}
//查询当前队列的规模
public int getSize() {
return size;
}
//判断队列是否为空
public boolean isEmpty() {
return (0 == size) ? true : false;
}
//入队
public void enqueue(Object obj) {
Node node = new Node();
node.setElem(obj);
node.setNext(null);//新节点作为末节点插入
if (0 == size)
head = node;//若此前队列为空,则直接插入
else
tail.setNext(node);//否则,将新节点接至队列末端
tail = node;//更新指向末节点引用
size++;//更新规模
}
//出队
public Object dequeue() throws ExceptionQueueEmpty {
if (0 == size)
throw new ExceptionQueueEmpty("意外:队列空");
Object obj = head.getElem();
head = head.getNext();
size--;
if (0 == size) tail = null;//若队列已空,须将末节点引用置空
return obj;
}
//取(并不删除)队首元素
public Object front() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:队列空");
return head.getElem();
}
//遍历(不属于ADT)
public void Traversal() {
Node p = head;
while (null != p) {
System.out.print(p.getElem() + " ");
p = p.getNext();
}
}
}
基于单链表实现队列
import java.util.Stack;
public class Stack_List implements Stack {
protected Node top;//指向栈顶元素
protected int size;//栈中元素的数目
//构造方法(空栈)
public Stack_List() {
top = null;
size = 0;
}
//查询当前栈的规模
public int getSize() {
return size;
}
//判断是否栈空
public boolean isEmpty() {
return (top == null) ? true : false;
}
//压栈
public void push(Object elem) {
Node v = new Node(elem, top);//创建一个新节点,将其作为首节点插入
top = v;//更新首节点引用
size++;//更新规模记录
}
//读取(但不删除)栈顶
public Object top() throws ExceptionStackEmpty {
if (isEmpty())
throw new ExceptionStackEmpty("意外:栈空");
return top.getElem();
}
//弹出栈顶
public Object pop() throws ExceptionStackEmpty {
if (isEmpty())
throw new ExceptionStackEmpty("意外:栈空");
Object temp = top.getElem();
top = top.getNext();//更新首节点引用
size--;//更新规模记录
return temp;
}
}
位置
抽象出位置(Position)这一概念,使我们既能够保持链表结构高效性,而又不致违背面向对 象的设计原则。
ADT
| 操作方法 | 功能描述 |
|---|---|
| getElem() | 返回存放于当前位置的元素 输入:无 输出:对象 |
| setElem(e) | 将元素 e 放入当前位置,并返回此处原先存放的元素 输入:一个元素 输出:一个元素 |
接口
public interface Position {
public Object getElem();//返回存放于该位置的元素
public Object setElem(Object e);//将给定元素存放至该位置,返回此前存放的元素
}
双端队列
双端队列(Double-ended queue),简称为Deque。顾名思义, 也就是前端与后端都支持插入和删除操作的队列。
ADT
| 操作方法 | 功能描述 |
|---|---|
| insertFirst(x) | 将对象 x 作为首元素插入 输入:一个对象 输出:无 |
| insertLast(x) | 将对象 x 作为末元素插入 输入:一个对象 输出:无 |
| removeFirst() | 若队列非空,则将首元素删除,并将其内容返回 否则,报错 输入:无 输出:对象 |
| removeLast() | 若队列非空,则将末元素删除,并将其内容返回 否则,报错 输入:无 输出:对象 |
| first() | 若队列非空,则返回首元素的内容 否则,报错 输入:无 输出:对象 |
| last() | 若队列非空,则返回末元素的内容 否则,报错 输入:无 输出:对象 |
| getSize() | 报告队列中的元素数目 输入:无 输出:非负整数 |
| sEmpty() | 判断队列是否为空 输入:无 输出:布尔量 |
接口
public interface Deque {
public int getSize();//返回队列中元素数目
public boolean isEmpty();//判断队列是否为空
public Object first() throws ExceptionQueueEmpty;//取首元素(但不删除)
public Object last() throws ExceptionQueueEmpty;//取末元素(但不删除)
public void insertFirst(Object obj);//将新元素作为首元素插入
public void insertLast(Object obj);//将新元素作为末元素插入
public Object removeFirst() throws ExceptionQueueEmpty;//删除首元素
public Object removeLast() throws ExceptionQueueEmpty;//删除末元素
public void Traversal();//遍历
}
双向链表
public class DLNode implements Position {
private Object element;//数据对象
private DLNode prev;//指向前驱节点
private DLNode next;//指向后继节点
/**************************** 构造函数 ****************************/
public DLNode() {
this(null, null, null);
}
public DLNode(Object e, DLNode p, DLNode n) {
element = e;
prev = p;
next = n;
}
//注意三个参数的次序:数据对象、前驱节点、后继节点
/**************************** Position接口方法 ****************************/
//返回存放于该位置的元素
public Object getElem() {
return element;
}
//将给定元素存放至该位置,返回此前存放的元素
public Object setElem(Object e) {
Object oldElem = element;
element = e;
return oldElem;
}
/**************************** 双向链表节点方法 ****************************/
//找到后继位置
public DLNode getNext() {
return next;
}
//找到前驱位置
public DLNode getPrev() {
return prev;
}
//修改后继位置
public void setNext(DLNode newNext) {
next = newNext;
}
//修改前驱位置
public void setPrev(DLNode newPrev) {
prev = newPrev;
}
}
基于双向链表实现的双端队列
哨兵
最前端和最后 端各设置一个哑元节点(Dummy node )。这两个节点分别称作头节点(Header node )和尾节点 (Trailer node),起哨兵(Sentinel)的作用。也就是说,它们并不存储任何实质的数据对象,头(尾)节点的next(prev)引用指向首(末)节点,而prev(next)引用为空。如此构成的双向链表 结构。

首、末节点的插入



首、末节点的删除
一般地,若要在相邻的一对节点 p 和 q 之间插入新元素 e,我们只需生成一个新节点 t,将 e 存放至其中,然后将 t 的 prev 和 next 引用分别指向 p 和 q,然后让 p(q)的 next(prev)引用指 向 t。
反过来。若要删除节点 t,我们可以找到其直接前驱节点 p 和直接后继节点 q,然后将 p(q) 的 next(prev)引用指向 q(p)。对于节点 t,我们无需做任何处理⎯⎯既然它不再被引用,就将被 内存回收器回收。



一般节点的插入与删除



实现
import java.util.Deque;
public class Deque_DLNode implements Deque {
protected DLNode header;//指向头节点(哨兵)
protected DLNode trailer;//指向尾节点(哨兵)
protected int size;//队列中元素的数目
//构造函数
public Deque_DLNode() {
header = new DLNode();
trailer = new DLNode();
header.setNext(trailer);
trailer.setPrev(header);
size = 0;
}
//返回队列中元素数目
public int getSize() {
return size;
}
//判断队列是否为空
public boolean isEmpty() {
return (0 == size) ? true : false;
}
//取首元素(但不删除)
public Object first() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:双端队列为空");
return header.getNext().getElem();
}
//取末元素(但不删除)
public Object last() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:双端队列为空");
return trailer.getPrev().getElem();
}
//在队列前端插入新节点
public void insertFirst(Object obj) {
DLNode second = header.getNext();
DLNode first = new DLNode(obj, header, second);
second.setPrev(first);
header.setNext(first);
size++;
}
//在队列后端插入新节点
public void insertLast(Object obj) {
DLNode second = trailer.getPrev();
DLNode first = new DLNode(obj, second, trailer);
second.setNext(first);
trailer.setPrev(first);
size++;
}
//删除首节点
public Object removeFirst() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:双端队列为空");
DLNode first = header.getNext();
DLNode second = first.getNext();
Object obj = first.getElem();
header.setNext(second);
second.setPrev(header);
size--;
return (obj);
}
//删除末节点
public Object removeLast() throws ExceptionQueueEmpty {
if (isEmpty())
throw new ExceptionQueueEmpty("意外:双端队列为空");
DLNode first = trailer.getPrev();
DLNode second = first.getPrev();
Object obj = first.getElem();
trailer.setPrev(second);
second.setNext(trailer);
size--;
return (obj);
}
//遍历
public void Traversal() {
DLNode p = header.getNext();
while (p != trailer) {
System.out.print(p.getElem() + " ");
p = p.getNext();
}
System.out.println();
}
}

浙公网安备 33010602011771号