数据结构与算法 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();
    }
} 
posted @ 2021-08-12 09:34  喝一口七喜  阅读(106)  评论(0)    收藏  举报