第3章 栈 和 队 列 3.1栈 3.1.1栈的基本概念 栈是一种特殊的线性表,其插入、删除操作只能在表的尾部进行。在栈中允许进行插入、删除操作的一端称为栈顶,另一端称为栈底。在栈{a0,a1,…,an-1}中a0称为栈底元素,an-1称为栈顶元素。通常,栈的插入操作叫入栈,栈的删除操作叫出栈。 由于栈的插入和删除操作只允许在栈顶进行,每次入栈的元素即成为栈顶元素,每次最先出栈的总是栈顶元素,所以栈是一种后进先出的线性表。就像一摞盘子,每次将一个盘子摞在最上面,每次从最上面取一只盘子,不能从中间插进或者抽出。 3.1.2栈的抽象数据类型描述 栈中的数据元素和数据间的逻辑关系与线性表相同,是由n个具有相同数据类型的数据元素构成的有限序列,栈的抽象数据类型的Java描述如下: 1package ch03; 2public interface IStack { 3public void clear();//将栈置空 4public boolean isEmpty(); //判断栈是否为空 5public int length(); //返回栈的数据元素个数 6public Object peek(); //返回栈顶元素 7public void push(Object x); //将数据元素x入栈 8public Object pop(); //将栈顶元素出栈并返回 9public void display(); //输出栈中的所有元素 10} 栈的抽象数据Java接口包含了栈的主要基本操作,如果要使用这个接口还需要具体的类来实现。栈的Java接口的实现方法主要有以下两种: (1) 基于顺序存储的实现,为顺序栈; (2) 基于链式存储的实现,为链栈。 3.1.3顺序栈 1. 顺序栈类的描述 顺序栈用数组实现,因为入栈和出栈操作都是在栈顶进行,所以增加变量top来指示栈顶元素的位置,top指向栈顶元素存储位置的下一个存储单元的位置,空栈时top=0。 顺序栈类的Java语言描述如下: 1package ch03; 2public class SqStack implements IStack{ 3private Object[] stackElem; //顺序栈存储空间 4private int top; //指向栈顶元素的下一个存储单元位置 5private int maxSize; //栈的最大存储单元个数 6 7//构造最大存储单元个数为maxSize的空栈 8public SqStack(int maxSize){ 9top=0; 10this.maxSize=maxSize; 11stackElem=new Object[maxSize]; 12} 13//将栈置空 14public void clear() { 15top=0; 16} 17//判断栈是否为空 18public boolean isEmpty() { 19return top==0; 20} 21//返回栈中数据元素的个数 22public int length() { 23 24return top; 25} 26//返回栈顶元素 27public Object peek() { 28if(!isEmpty()) 29return stackElem[top-1]; 30else 31return null; 32} 33//入栈 34public void push(Object x) { 35 36} 37//出栈 38public Object pop() { 39 40} 41//输出栈中的所有数据元素 42public void display(){ 43for(int i=top-1;i>=0;i--){ 44System.out.print(stackElem[i]+" "); 45} 46} 47} 2. 顺序栈基本操作的实现 1) 入栈操作 入栈操作push(x)是将数据元素x作为栈顶元素插入顺序栈中,主要操作如下: (1) 判断顺序栈是否为满,若满则抛出异常。 (2) 将x存入top所指的存储单元位置。 (3) top加1。 图3.1显示了进行入栈操作时栈的状态变化。 图3.1入栈操作 【算法3.1】入栈操作。 1//入栈 2public void push(Object x) { 3if(top==maxSize) 4throw new Exception("栈已满"); 5stackElem[top]=x; 6top++; 7} 图3.2出栈操作 2) 出栈操作 出栈操作pop()是将栈顶元素从栈中删除并返回,主要步骤如下。 (1) 判断顺序栈是否为空,若空则返回null。 (2) top减1。 (3) 返回top所指的栈顶元素的值。 图3.2显示了进行出栈操作时栈的状态变化。 【算法3.2】出栈操作。 1public Object pop() { 2if(isEmpty()) 3return null; 4top--; 5return stackElem[top]; 6} 分析可得,入栈和出栈操作的实现为顺序表的尾插入和尾删除,时间复杂度为O(1)。 【例3.1】利用顺序栈实现括号匹配的语法检查。 解: 括号匹配是指程序中出现的括号,左、右括号的个数是相同的,并且需要先左后右依次出现。括号是可以嵌套的,一个右括号与其前面最近的一个左括号匹配,使用栈保存多个嵌套的左括号。 public void isMatched(String str) throws Exception{ for(int i=0;i1时则需要将序号小于n的n-1个圆盘移动到y上,再将序号为n的圆盘移动到z上,然后将y上的n-1个圆盘移动到z上。如何将n-1个圆盘移动到z上是一个和原问题相似的问题,只是规模变小,可以用同样的方法求解。代码如下: public static void hanoi(int n,char x,char y,char z){ if(n==1) move(1,x,z); else{ hanoi(n-1,x,z,y); //将x上序号为1至n-1的圆盘从x移动到y move(n,x,z); //将序号为n的圆盘从塔座x移动到塔座z hanoi(n-1,y,x,z); //将y上序号为1至n-1的圆盘从y移动到z } } //将序号为n的圆盘从塔座x移动到塔座z public static void move(int n,char x,char z){ System.out.println("将圆盘:"+n+"从塔座"+x+"移动到塔座"+z+" "); } public static void main(String []args){ hanoi(3,'x','y','z'); } 3.2队列 3.2.1队列的基本概念 队列是一种特殊的线性表,其插入操作只能在表的尾部进行,删除操作只能在表头进行。在队列中允许进行插入操作的一端称为队尾,允许进行删除操作的另一端称为队首。在队列{a0,a1,…,an-1}中a0称为队首元素,an-1称为队尾元素。通常,队列的插入操作叫入队,队列的删除操作叫出队。没有数据元素的队列称为空队列。 由于插入和删除操作分别在队尾和队首进行,最先入队的元素总是最先出队,因此队列具有先进先出的特点。 3.2.2队列的抽象数据类型描述 队列中的数据元素和数据间的逻辑关系与线性表相同,是由n个具有相同数据类型的数据元素构成的有限序列,队列的抽象数据类型的Java描述如下: 1package ch03; 2public interface IQueue { 3public void clear();//将队列置空 4public boolean isEmpty(); //判断队列是否为空 5public int length(); //返回队列的数据元素个数 6public Object peek(); //返回队首元素 7public void offer(Object x) throws Exception; //将数据元素x插入到队列成为队尾元素 8public Object poll(); //将队首元素删除并返回其值 9public void display(); //输出队列中的所有数据元素 10} 队列的抽象数据Java接口包含了队列的主要基本操作,如果要使用这个接口,还需要具体的类来实现。队列的Java接口的实现方法主要有以下两种。 (1) 基于顺序存储的实现,为顺序队列。 (2) 基于链式存储的实现,为链队列。 3.2.3顺序队列 1. 顺序队列类的描述及实现 顺序队列的存储结构与顺序栈类似,可用数组实现,因为入队和出队操作分别在队尾和队首进行,所以增加变量front来指示队首元素的位置,rear指示队尾元素的下一个存储单元的位置。顺序队列进行入队操作的状态变化如图3.7所示,进行出队操作后的状态变化如图3.8所示。 图3.7入队操作 图3.8出队操作 顺序队列类的Java语言描述如下: 1package ch03; 2public class SqQueue implements IQueue { 3private Object[] queueElem; //队列的存储空间 4private int front; //指向队首元素 5private int rear; //指向队尾元素的下一个存储单元 6private int maxSize; //队列的最大存储单元个数 7 8//构造最大存储单元个数为maxSize的空队列 9public SqQueue(int maxSize){ 10front=rear=0; 11queueElem=new Object[maxSize]; 12this.maxSize=maxSize; 13} 14//将队列置空 15public void clear() { 16front=rear=0; 17} 18//判断队列是否为空 19public boolean isEmpty() { 20return rear==front; 21} 22//返回队列的长度 23public int length() { 24return rear-front; 25} 26//读取队首元素并返回其值 27public Object peek() { 28if(isEmpty()) 29return null; 30return queueElem[front]; 31} 32//入队 33public void offer(Object x) throws Exception { 34if(rear==maxSize) 35throw new Exception("队列已满"); 36queueElem[rear]=x; 37rear++; 38} 39//出队 40public Object poll() { 41if(rear==front) 42return null; 43Object p=queueElem[front]; 44front++; 45return p; 46} 47//输出队列中的所有数据元素 48public void display() { 49if(!isEmpty()){ 50for(int i=front;i " + c); }else{ hanoi(num - 1, a, c, b); //借助c把第 num 个以外的圆盘从a移动到b System.out.println(num + " " + a + " -> " + c); //把第num个圆盘从a移动到c hanoi(num - 1, b, a, c); //借助a把第 num 个以外的圆盘从b移动到c } } } 3.4.2吃巧克力 小Q的父母要出差N天,走之前给小Q留下了M块巧克力。小Q决定每天吃的巧克力数量不少于前一天吃的一半,但是他又不想在父母回来之前的某一天没有巧克力吃,请问他第一天最多能吃多少块巧克力? import java.util.Scanner; public class U3H2 { public static void main(String[] args) { System.out.println("请输入巧克力个数M:"); Scanner sc1 = new Scanner(System.in); int M = sc1.nextInt(); System.out.println("请输入出差天数N:"); Scanner sc2 = new Scanner(System.in); int N = sc2.nextInt(); //第i天吃a[i]个巧克力 //为了保证最后一天有至少巧克力吃 if (M - Eat(N) > 0) { System.out.println("第一天最多吃" + (M - Eat(N)) + "巧克力"); } else { System.out.println("巧克力不足,请重新输入"); } } private static int Eat(int s) { int sum = 1; for (int i = 0; i < s - 2; i++) { sum += 2 * sum; } return sum; } } 视频讲解 3.4.3表达式求值 传入一个由整数、运算符和符合语法的括号组成的表达式,输出运算结果。 示例: 输入: (((10 * (6 / (9 + 3) * 11)) + 17) + 5)/9 输出: 8.555555555555555 提示: 利用栈将表达式转换为逆波兰表达式(后缀表达式)后进行运算。 import java.util.Collections; import java.util.Stack; public class U3H3 { private Stack postfixStack = new Stack<>(); //后缀式栈 private Stack opStack = new Stack<>(); //运算符栈 private int [] operatPriority = new int[] {0,3,2,1,-1,1,0,2}; //运用运算符ASCII码-40做索引的运算符优先级 public static void main(String[] args) { U3H3 cal = new U3H3(); String s = "5+12*(3+5)/7"; double result = cal.calculate(s); System.out.println(result); } /** * 按照给定的表达式计算 * @param expression 要计算的表达式,如5+12*(3+5)/7 * @return */ public double calculate(String expression) { Stack resultStack = new Stack(); prepare(expression); Collections.reverse(postfixStack); //将后缀式栈反转 String firstValue ,secondValue,currentValue; //参与计算的第一个值、第二个 值和算术运算符 while(!postfixStack.isEmpty()) { currentValue = postfixStack.pop(); if(!isOperator(currentValue.charAt(0))) {//如果不是运算符则存入操作 数栈中 resultStack.push(currentValue); } else {//如果是运算符则从操作数栈中取两个值和该数值一起参与运算 secondValue = resultStack.pop(); firstValue = resultStack.pop(); String tempResult = calculate(firstValue, secondValue, currentValue.charAt(0)); resultStack.push(tempResult); } } return Double.valueOf(resultStack.pop()); } /** * 数据准备阶段将表达式转换为后缀式栈 * @param expression */ private void prepare(String expression) { opStack.push(','); //运算符放入栈底元素逗号,此符号优先级最低 char[] arr = expression.toCharArray(); int currentIndex = 0; //当前字符的位置 int count = 0; //上次算术运算符到本次算术运算符的字符长度 char currentOp ,peekOp; //当前操作符和栈顶操作符 for(int i=0;i 0) { postfixStack.push(new String(arr,currentIndex,count)); //取两个运算符之间的数字 } peekOp = opStack.peek(); if(currentOp == ')') {//遇到反括号则将运算符栈中的元素移除到后缀式栈中,直到遇到左括号 while(opStack.peek() != '(') { postfixStack.push(String.valueOf(opStack.pop())); } opStack.pop(); } else { while(currentOp != '(' && peekOp != ',' && compare(currentOp,peekOp) ) { postfixStack.push(String.valueOf(opStack.pop())); peekOp = opStack.peek(); } opStack.push(currentOp); } count = 0; currentIndex = i+1; } else { count++; } } if(count > 1 || (count == 1 && !isOperator(arr[currentIndex]))) {//最后一个字符不是括号或者其他运算符则加入后缀式栈中 postfixStack.push(new String(arr,currentIndex,count)); } while(opStack.peek() != ',') { postfixStack.push(String.valueOf( opStack.pop())); //将操作符栈中的剩余元素添加到后缀式栈中 } } /** * 判断是否为算术符号 * @param c * @return */ private boolean isOperator(char c) { return c == '+' || c == '-' || c == '*' || c == '/' || c == '(' ||c == ')'; } /** * 利用ASCII码-40做下标的算术符号优先级 * @param cur * @param peek * @return */ public boolean compare(char cur,char peek) {// 如果是peek优先级高于cur,返回true,默认都是peek优先级要低 boolean result = false; if(operatPriority[(peek)-40] >= operatPriority[(cur) - 40]) { result = true; } return result; } /** * 按照给定的算术运算符做计算 * @param firstValue * @param secondValue * @param currentOp * @return */ private String calculate(String firstValue,String secondValue,char currentOp) { String result = ""; switch(currentOp) { case '+': result = String.valueOf(ArithHelper.add(firstValue, secondValue)); break; case '-': result = String.valueOf(ArithHelper.sub(firstValue, secondValue)); break; case '*': result = String.valueOf(ArithHelper.mul(firstValue, secondValue)); break; case '/': result = String.valueOf(ArithHelper.div(firstValue, secondValue)); break; } return result; } } class ArithHelper { // 默认除法运算精度 private static final int DEF_DIV_SCALE = 16; // 这个类不能实例化 private ArithHelper() { } /** * 提供精确的加法运算 * * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 */ public static double add(double v1, double v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(Double.toString(v1)); java.math.BigDecimal b2 = new java.math.BigDecimal(Double.toString(v2)); return b1.add(b2).doubleValue(); } public static double add(String v1, String v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(v1); java.math.BigDecimal b2 = new java.math.BigDecimal(v2); return b1.add(b2).doubleValue(); } /** * 提供精确的减法运算 * * @param v1 被减数 * @param v2 减数 * @return 两个参数的差 */ public static double sub(double v1, double v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(Double.toString(v1)); java.math.BigDecimal b2 = new java.math.BigDecimal(Double.toString(v2)); return b1.subtract(b2).doubleValue(); } public static double sub(String v1, String v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(v1); java.math.BigDecimal b2 = new java.math.BigDecimal(v2); return b1.subtract(b2).doubleValue(); } /** * 提供精确的乘法运算 * * @param v1 被乘数 * @param v2 乘数 * @return 两个参数的积 */ public static double mul(double v1, double v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(Double.toString(v1)); java.math.BigDecimal b2 = new java.math.BigDecimal(Double.toString(v2)); return b1.multiply(b2).doubleValue(); } public static double mul(String v1, String v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(v1); java.math.BigDecimal b2 = new java.math.BigDecimal(v2); return b1.multiply(b2).doubleValue(); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,精确到小数点后10位,以后的数字四舍五入 * * @param v1 被除数 * @param v2 除数 * @return 两个参数的商 */ public static double div(double v1, double v2) { return div(v1, v2, DEF_DIV_SCALE); } public static double div(String v1, String v2) { java.math.BigDecimal b1 = new java.math.BigDecimal(v1); java.math.BigDecimal b2 = new java.math.BigDecimal(v2); return b1.divide(b2, DEF_DIV_SCALE, java.math.BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入 * * @param v1 被除数 * @param v2 除数 * @param scale小数点后保留几位 * @return 两个参数的商 */ public static double div(double v1, double v2, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } java.math.BigDecimal b1 = new java.math.BigDecimal(Double.toString(v1)); java.math.BigDecimal b2 = new java.math.BigDecimal(Double.toString(v2)); return b1.divide(b2, scale, java.math.BigDecimal.ROUND_HALF_UP).doubleValue(); } /** * 提供精确的小数位四舍五入处理 * * @param v 需要四舍五入的数字 * @param scale 小数点后保留几位 * @return 四舍五入后的结果 */ public static double round(double v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } java.math.BigDecimal b = new java.math.BigDecimal(Double.toString(v)); java.math.BigDecimal one = new java.math.BigDecimal("1"); return b.divide(one, scale, java.math.BigDecimal.ROUND_HALF_UP).doubleValue(); } public static double round(String v, int scale) { if (scale < 0) { throw new IllegalArgumentException("The scale must be a positive integer or zero"); } java.math.BigDecimal b = new java.math.BigDecimal(v); java.math.BigDecimal one = new java.math.BigDecimal("1"); return b.divide(one, scale, java.math.BigDecimal.ROUND_HALF_UP).doubleValue(); } } 小结 (1) 栈是一种特殊的线性表,它只允许在栈顶进行插入和删除操作,具有后进先出的特性,各种运算的时间复杂度为O(1)。栈采用顺序存储结构或者链式存储结构。 (2) 队列是一种特殊的线性表,它只允许在表头进行删除操作、在表尾进行插入操作,具有先进先出的特性,各种运算的时间复杂度为O(1)。队列采用顺序存储结构或者链式存储结构。 (3) 循环队列是将顺序队列的首尾相连,防止“假溢出”现象的发生。 (4) 优先级队列是在普通队列的基础之上将队列中的数据元素按照关键字的值进行有序排列。在表头进行删除操作,插入操作按照优先级插入到队列的合适位置。 习题3 一、 选择题 1. 对于栈操作数据的原则是()。 A. 先进先出B. 后进先出C. 后进后出D. 不分顺序 2. 在做入栈运算时应先判别栈是否(①),在做出栈运算时应先判别栈是否(②)。当栈中元素为n个,做进栈运算时发生上溢,则说明该栈的最大容量为(③)。 为了增加内存空间的利用率和减少溢出的可能性,由两个栈共享一片连续的内存空间时应将两栈的(④)分别设在这片内存空间的两端,这样当(⑤)时才产生上溢。 ①、②: A. 空B. 满C. 上溢D. 下溢 ③: A. n-1 B. n C. n+1 D. n/2 ④: A. 长度B. 深度C. 栈顶D. 栈底 ⑤: A. 两个栈的栈顶同时到达栈空间的中心点 B. 其中一个栈的栈顶到达栈空间的中心点 C. 两个栈的栈顶在栈空间的某一位置相遇 D. 两个栈均不空,且一个栈的栈顶到达另一个栈的栈底 3. 一个栈的输入序列为1,2,3,…,n,若输出序列的第一个元素是n,输出的第i(1≤i≤n)个元素是()。 A. 不确定B. n-i+1 C. i D. n-i 4. 若一个栈的输入序列为1,2,3,…,n,输出序列的第一个元素是i,则第j个输出元素是()。 A. i-j-1 B. i-j C. j-i+1 D. 不确定的 5. 若已知一个栈的入栈序列是1,2,3,…,n,其输出序列为p1,p2,p3,…,pn,若pn是n,则pi是()。 A. iB. n-i C. n-i+1 D. 不确定 6. 有6个元素6、5、4、3、2、1顺序入栈,下列不是合法的出栈序列是()。 A. 5,4,3,6,1,2 B. 4,5,3,1,2,6C. 3,4,6,5,2,1D. 2,3,4,1,5,6 7. 设栈的输入序列是1,2,3,4,则()不可能是其出栈序列。 A. 1,2,4,3B. 2,1,3,4C. 1,4,3,2 D. 4,3,1,2 E. 3,2,1,4 8. 一个栈的输入序列为1,2,3,4,5,则下列序列中不可能是栈的输出序列的是()。 A. 2,3,4,1,5B. 5,4,1,3,2C. 2,3,1,4,5D. 1,5,4,3,2 9. 设一个栈的输入序列是1,2,3,4,5,则下列序列中是栈的合法输出序列的是()。 A. 5,1,2,3,4B. 4,5,1,3,2C. 4,3,1,2,5D. 3,2,1,5,4 10. 某堆栈的输入序列为a,b,c,d,下面序列中不可能是它的输出序列的是()。 A. a,c,b,dB. b,c,d,aC. c,d,b,aD. d,c,a,b 11. 设a、b、c、d、e、f以所给的次序入栈,若在入栈操作时,允许出栈操作,则下面得不到的序列为()。 A. f,e,d,c,b,aB. b,c,a,f,e,dC. d,c,e,f,b,aD. c,a,b,d,e,f 12. 设有3个元素X、Y、Z顺序入栈(入的过程中允许出栈),下列得不到的出栈排列是()。 A. X,Y,ZB. Y,Z,XC. Z,X,YD. Z,Y,X 13. 输入序列为A,B,C,变为C,B,A时经过的栈操作为()。 A. push,pop,push,pop,push,popB. push,push,push,pop,pop,pop C. push,push,pop,pop,push,popD. push,pop,push,push,pop,pop 14. 若一个栈以向量V[1..n]存储,初始栈顶指针top为n+1,则下面x入栈的正确操作是()。 A. top=top+1;V[top]=xB. top=top-1; V[top]=x C. V[top]=x; top=top+1D. V[top]=x; top=top-1 15. 若栈采用顺序存储方式存储,现两栈共享空间V[1..m],top[i]代表第i个栈(i=1,2)的栈顶,栈1的底在V[1]、栈2的底在V[m],则栈满的条件是()。 A. |top[2]-top[1]|=0 B. top[1]+1=top[2] C. top[1]+top[2]=mD. top[1]=top[2] 二、 填空题 1. 向量、栈和队列都是结构,可以在向量的位置插入和删除元素; 对于栈只能在插入和删除元素; 对于队列只能在插入和删除元素。 2. 栈是一种特殊的线性表,允许插入和删除运算的一端称为,不允许插入和删除运算的一端称为。 3. 是被限定为只能在表的一端进行插入运算、在表的另一端进行删除运算的线性表。 4. 在一个循环队列中,队尾指针指向队首元素的位置。 5. 在具有n个单元的循环队列中,队满时共有个元素。 6. 向栈中压入元素的操作是先,后。 7. 从循环队列中删除一个元素,其操作是先,后。 8. 顺序栈用data[1..n]存储数据,栈顶指针是top,则值为x的元素入栈的操作是。 9. 表达式23+((12*3-2)/4+34*5/7)+108/9的后缀表达式是。 10. 引入循环队列的目的是为了克服。 三、 算法设计题 1. 把十进制整数转换为二至九进制数并输出。 2. 堆栈在计算机语言的编译过程中用来进行语法检查,试编写一个算法检查一个Java语言程序中的大括号、方括号和圆括号是否配对,若能够全部配对则返回逻辑真,否则返回逻辑假。 3. 斐波那契(Fibonacci)数列的定义为,它的第1项和第2项分别为0和1,以后各项为其前两项之和。若斐波那契数列中的第n项用Fib(n)表示,则计算公式如下: Fib(n)=n-1(n=1或2) Fib(n-1)+Fib(n-2)(n>2) 试编写出计算Fib(n)的递归算法和非递归算法。