本章涉及到三种数据存储类型:栈、队列和优先级队列。首先介绍着三种结构与数组的区别,然后依次介绍每种数据结构。
1 不同的结构类型
本章所讲的数据结构和算法与前面章节提到的有很大不同,下面是三个区别。
1.1 程序员的工具
数组是前面介绍过的数据存储结构,和本书后面介绍的其他结构(链表、树)一样,都适用于数据库应用中做数据记录。它常用于记录那些对应于现实世界的对象和活动的数据,如职员档案、目录和商务数据等等。这些结构便于数据的访问:它们易于进行插入、删除和查找特定数据项的操作。
然而,本章要讲解的数据结构和算法更多的是作为程序员的工具来运用,它们主要作为构思算法的辅助工具,而不是完全的数据存储工具,这些数据结构的生命周期比那些数据库类型的结构要短得多。在程序操作执行期间它们才被创建,通常用它们去执行某项特殊的任务,当完成任务之后,它们就被销毁。
1.2 受限访问
在数组中,若知道数据项的下标,便可以立即访问该数据项,或者通过顺序搜索数据项,访问到数组中的各数据项。而在本章中的数据结构中,访问是受限制的,即在特定时刻只有一个数据项可以被读取或者被删除。
1.3 更加抽象
栈、队列和优先级队列是比数组和其他数据存储结构更为抽象的结构。主要通过接口对栈、队列和优先级队列进行定义,这些接口表明通过它们可以完成的操作,而它们的主要实现机制对用户来说是不可见的。
例如,栈的主要机制可以用数组来实现,本章的示例就是这样处理的;但它也可以用链表来实现,优先级队列的内部实现可以用数组或一种特别的树————来实现。
2 栈
栈只允许访问一个数据项:即最后插入的数据项。移除这个数据项后才能访问倒数第二个插入的数据项,依次类推。就像条状装的阿尔卑斯糖一样,每一个小的圆的阿尔卑斯糖叠加,然后用纸包好成条状,只能先吃最后放的那个糖,然后才能吃倒数第二个糖。本节中将看到如何利用栈来检验源程序中的小括号、中括号和大括号是否匹配的问题。
栈也是那些应用了相当复杂的数据结构算法的便利工具。
大部分微处理器运用基于栈的体系结构,当调用一个方法时,把它的返回地址和参数压入栈,当方法结束返回时,那些数据出栈,栈操作就嵌入在微处理器中。
栈的操作有New(新建)、Push(入栈)、Pop(出栈)和Peek(查看)
这里的栈是利用数组来实现的,因此可以看到存储数据项的数组。虽然它是基于数组的,但是因为栈的访问是受限访问,所以不能利用下标访问各数据项。实际上,栈的概念和实现它的内部数据结构应该是完全分开的,栈也可以利用其他的存储结构比如链表来实现。栈的容量通常会比较小,是临时的数据结构,解析一个很长的算术表达式只需要一个十几个单元的栈就够了。
2.1 栈的实现
下面利用数组实现一个栈:
1 package chapter4; 2 3 class StackX { 4 //栈的最大空间 5 private int maxSize; 6 //用于实现栈的数组 7 protected long[] stackArray; 8 //栈顶 9 private int top; 10 public StackX(int maxSize){ 11 this.maxSize = maxSize; 12 //创建数组 13 stackArray = new long[maxSize]; 14 //top = -1表示栈里没有元素 15 top = -1; 16 }; 17 //入栈操作 18 public void push(long i){ 19 //先把栈顶指针向上移动一个位置 20 ++top; 21 //把新值赋给栈顶指针指向的位置 22 stackArray[top] = i; 23 }; 24 //出栈操作 25 public long pop(){ 26 //返回栈顶指针当前指向的值,也就是即将被"移除"的值 27 //然后将栈顶指针回移一个位置即可 28 return stackArray[top--]; 29 }; 30 //查看栈 31 public long peek(){ 32 //返回当前栈顶指针所指的值 33 return stackArray[top]; 34 }; 35 //判断栈是否为空 36 public boolean isEmpty(){ 37 return (top == -1); 38 } 39 //判断栈是否已满 40 public boolean isFull(){ 41 return (top == this.maxSize - 1); 42 } 43 }; 44 45 public class StackApp{ 46 public static void main(String[] args){ 47 //创建一个新栈 48 StackX s = new StackX(10); 49 //将一些值推入栈中 50 s.push(2); 51 s.push(7); 52 s.push(27); 53 //输出入栈后的结果 54 System.out.println("入栈后的值:"); 55 for(int i = 0; i < s.stackArray.length; i++){ 56 System.out.print(s.stackArray[i] + " "); 57 } 58 System.out.println(); 59 //查看当前栈的情况 60 System.out.println("当前栈指针指向值:" + s.peek()); 61 //将元素出栈 62 System.out.println("出栈的值:" + s.pop()); 63 //查看当前栈的情况 64 System.out.println("当前栈指针指向值:" + s.peek()); 65 } 66 };
输出结果:
入栈后的值:
2 7 27 0 0 0 0 0 0 0
当前栈指针指向值:27
出栈的值:27
当前栈指针指向值:7
程序分析:上面利用数组实现了一个栈,并进行了创建栈、入栈、出栈、查看栈的四种基本操作,但是没有进行异常处理。比如满栈添加元素,或者空栈弹出一个数据项。只需要利用isFull来判断即可。
栈实例1:单词逆序
因为栈是后入先出的,所以就可以利用这个特性很容易将单词逆序,具体方法就是利用pop函数将元素逆序喷出~~
我们将上面的程序稍微改造一下即可:
package chapter4; import java.io.*; class StackX { //栈的最大空间 private int maxSize; //用于实现栈的数组 protected char[] stackArray; //栈顶 private int top; public StackX(int maxSize){ this.maxSize = maxSize; //创建数组 stackArray = new char[maxSize]; //top = -1表示栈里没有元素 top = -1; }; //入栈操作 public void push(char i){ //先把栈顶指针向上移动一个位置 ++top; //把新值赋给栈顶指针指向的位置 stackArray[top] = i; }; //出栈操作 public char pop(){ //返回栈顶指针当前指向的值,也就是即将被"移除"的值 //然后将栈顶指针回移一个位置即可 return stackArray[top--]; }; //查看栈 public char peek(){ //返回当前栈顶指针所指的值 return stackArray[top]; }; //判断栈是否为空 public boolean isEmpty(){ return (top == -1); } //判断栈是否已满 public boolean isFull(){ return (top == this.maxSize - 1); } }; class Reverser{ private String input; private String output; public Reverser(String input){ this.input = input; }; //进行逆序 public String doRev(){ int stackSize = input.length(); StackX s = new StackX(stackSize); for(int i = 0; i < stackSize; i++){ char ch = input.charAt(i); s.push(ch); }; output = ""; while(!(s.isEmpty())){ char ch = s.pop(); output = output + ch; } return output; } } public class StackApp{ public static String getString()throws IOException{ InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static void main(String[] args)throws IOException{ String input, output; while(true){ System.out.println("请输入一个字符串:"); System.out.flush(); //读取一个字符串 input = getString(); //遇到Enter键则表示输入完毕 if (input.equals("")){ break; } //进行逆转 Reverser rs = new Reverser(input); output = rs.doRev(); //输出逆序结果 System.out.println("逆序后结果为:" + output); } } };
输出结果:
请输入一个字符串:
Hello World
逆序后结果为:dlroW olleH
请输入一个字符串:
栈实例2:分隔符匹配
栈通常用于解析某种类型的文本串,通常,文本串是用于计算机语言写的代码行,而解析它们的程序就是编译器。比如说一个表达式a{b[c]},这里有两个大括号和两个中括号。
原理分析:
分隔符匹配程序从字符串中不断读取字符,每次读取一个字符,若发现它是左分隔符,将它压入栈中。当从输入中读到一个右分隔符时,弹出栈顶的左分隔符,并且查看它是否和右分隔符相匹配。如果它们不匹配(比如,一个左大括号和一个右大括号),则程序保存。如果没有做分隔符和右分隔符匹配,或者一直存在着没有被匹配的分隔符,程序被报错,分隔符没有被匹配表现为把所有的字符读入之后,栈中仍留有着分隔符。注意:非分隔符是不插入栈中的。
以a{b[c]}为例
所读字符 栈中字符
a
{ {
b {
[ {[
c {[
] {
}
这个方法的可行性在于,最后出现的左边分隔符需要最先匹配,符合栈后进先出的特点。
package chapter4; import java.io.*; class StackX { //栈的最大空间 private int maxSize; //用于实现栈的数组 protected char[] stackArray; //栈顶 private int top; public StackX(int maxSize){ this.maxSize = maxSize; //创建数组 stackArray = new char[maxSize]; //top = -1表示栈里没有元素 top = -1; }; //入栈操作 public void push(char i){ //先把栈顶指针向上移动一个位置 ++top; //把新值赋给栈顶指针指向的位置 stackArray[top] = i; }; //出栈操作 public char pop(){ //返回栈顶指针当前指向的值,也就是即将被"移除"的值 //然后将栈顶指针回移一个位置即可 return stackArray[top--]; }; //查看栈 public char peek(){ //返回当前栈顶指针所指的值 return stackArray[top]; }; //判断栈是否为空 public boolean isEmpty(){ return (top == -1); } //判断栈是否已满 public boolean isFull(){ return (top == this.maxSize - 1); } }; class BracketChecker{ private String input; public BracketChecker(String input){ this.input = input; }; //分隔符检查 public void check(){ int stackSize = input.length(); StackX s = new StackX(stackSize); for(int i = 0; i < stackSize; i++){ char ch = input.charAt(i); switch(ch){ case '{': case '[': case '(': s.push(ch); break; case '}': case ']': case ')': if(!(s.isEmpty())){ char chx = s.pop(); if((ch == '{' && chx != '}')|| (ch == '[' && chx != ']')|| (ch =='(' && chx != ')') ){ System.out.println("Error: " + ch + " at" + i); } }else{ System.out.println("Error: " + ch + " at" + i); } break; default:{ break; } } if(!(s.isEmpty())){ System.out.println("Error:missing the right delimiter"); } }; } } public class StackApp{ public static String getString()throws IOException{ InputStreamReader isr = new InputStreamReader(System.in); BufferedReader br = new BufferedReader(isr); String s = br.readLine(); return s; } public static void main(String[] args)throws IOException{ String input, output; while(true){ System.out.println("请输入一个字符串:"); System.out.flush(); //读取一个字符串 input = getString(); //遇到Enter键则表示输入完毕 if (input.equals("")){ break; } //进行逆转 BracketChecker bc = new BracketChecker(input); bc.check(); } } };
2.2 栈的效率
上面用数组实现了栈,数据项入栈和出栈的时间复杂度都为常数O(1)。这也就是说,栈操作所耗时间都不依赖于栈中数据项的个数,因此操作时间很短,栈不需要比较和移动操作。
3 队列
队列(queue)这个单词是英国人说的排(line)(一种等待服务的方式)。队列是一种数据结构,有点类似栈,只是在队列中第一个插入的数据项也会最先被移除,也就是先进先出FIFO。而在栈中,最后插入的数据项最先移除,最先插入的最后移除,LIFO。队列就像排队买票一样,最先到达的最先买票。
队列也可以模拟真实世界的环境,在计算机或网络操作系统中,有各种队列工作,打印作业有队列等待,键盘敲击输入,有一个存储键入内容的队列。
队列有两个指针,一个是队头指针,一个是队尾指针,队头指针用于控制数据项的删除,删除时,指针往后移,队尾指针用于控制数据项的插入,插入时指针往后移动。这样就可以控制先进先出。这样设计有一个问题是 队尾指针很快会移动到数组的末端(数组下标最大),并且数组开始的地方因为已经移除了部分数据项,所以开头会有空数据项,这样新的数据项就无法从队尾插入。为了避免队列没有满,但是却无法插入新数据项的问题,可以让队头队尾指针绕回数组开始的位置,这就是循环队列。
3.1 循环队列
循环队列的特点是队头指针和队尾指针都是循环式的,当插入更多的数据项时,队尾指针如预计的那样向上移动。注意在队尾指针回绕之后,它限制处在队头指针的下面,这就颠倒了初始的位置。
删除足够多的数据项后,对头指针也回绕,这时队列的指针回到了初始运行时的位置状态,队头指针在队尾指针的下面。总之,插入看队尾指针,删除看队头指针,无论怎么绕来绕去,这个是不变的。
3.2 循环队列的数组实现
下面我们来实现队列的新建、插入、删除和查看四种操作。
1 package chapter4; 2 3 class Queue{ 4 private int maxSize; 5 private long[] queueArray; 6 private int front; 7 private int rear; 8 private int nElems; 9 //初始化队列 10 public Queue(int maxSize){ 11 this.maxSize = maxSize; 12 queueArray = new long[this.maxSize]; 13 front = 0; 14 rear = -1; 15 nElems = 0; 16 }; 17 //用队尾指针标识插入数据项 18 public void insert(long i){ 19 queueArray[++rear] = i; 20 //如果队尾指针已经达到数组末端,则循环至开头 21 if(rear == this.maxSize - 1){ 22 rear = -1; 23 } 24 nElems++; 25 }; 26 //删除数据项 27 public long remove(){ 28 long temp = queueArray[front++]; 29 if(front == this.maxSize){ 30 front = 0; 31 } 32 nElems--; 33 return temp; 34 }; 35 //查看队列 36 public long peek(){ 37 return queueArray[front]; 38 }; 39 //检查队列是否已满 40 public boolean isFull(){ 41 return (nElems == this.maxSize); 42 }; 43 //检查队列是否为空 44 public boolean isEmpty(){ 45 return (nElems == 0); 46 }; 47 public int size(){ 48 return nElems; 49 }; 50 }; 51 public class QueueApp { 52 public static void main(String[] args){ 53 //创建一个队列 54 Queue q = new Queue(5); 55 //插入5个数据项 56 q.insert(2); 57 q.insert(7); 58 q.insert(27); 59 q.insert(77); 60 q.insert(87); 61 //删除数据项 62 while(!(q.isEmpty())){ 63 long n = q.remove(); 64 System.out.println(n); 65 } 66 }; 67 };
输出结果:
2
7
27
77
87
上面我们用数组实现了一个循环队列,并且从运行结果来看,插入顺序和删除的顺序是一致的。这就是队列的特点。另外,插入时候没有进行队列是否已满检查。
3.2.1 队列的效率
和栈一样,队列中插入数据项和移除数据项的时间复杂度为O(1)
3.1 循环队列
循环队列的特点是队头指针和队尾指针都是循环式的,当插入更多的数据项时,队尾指针如预计的那样向上移动。注意在队尾指针回绕之后,它限制处在队头指针的下面,这就颠倒了初始的位置。
删除足够多的数据项后,对头指针也回绕,这时队列的指针回到了初始运行时的位置状态,队头指针在队尾指针的下面。总之,插入看队尾指针,删除看队头指针,无论怎么绕来绕去,这个是不变的。
3.2 循环队列的数组实现
下面我们来实现队列的新建、插入、删除和查看四种操作。
输出结果:
2
7
27
77
87
上面我们用数组实现了一个循环队列,并且从运行结果来看,插入顺序和删除的顺序是一致的。这就是队列的特点。另外,插入时候没有进行队列是否已满检查。
3.2.1 队列的效率
和栈一样,队列中插入数据项和移除数据项的时间复杂度为O(1)
3.3 优先级队列
优先级队列是比栈和队列更专用的数据结构。但是在很多情况下都很有用。和普通队列一样,优先级队列有一个队头和一个队尾,并且也是从队头移除数据项。优先级队列应该称为有序队列。因为插入时候会按照顺序将数据项插入,这样队头总是最小的数据项。优先级队列在某些计算机系统中也有很多应用,例如抢先式多任务操作系统中,程序排列在优先级队列中,这样优先级最高的程序就会得到时间片并得以执行。
优先级队列可以通过堆和数组实现,这里介绍数组的实现。
输出结果:
2
7
26
27
65
77
87
我们看到,插入后的队列总是按顺序的。
4 小结
1.栈、队列和优先级队列是经常用于简化某些程序操作的数据结构。
2.在这些数据结构中,只有一个数据项可以被访问。
3.栈允许访问最后一个插入的数据项
4.栈中重要的操作是在栈顶插入一个数据项,以及从栈顶移除一个数据项。
5.队列只允许访问第一个插入的数据项。
6.队列的重要操作是在队尾插入数据项和在队头移除数据项。
7.队列可以实现为循环队列,它基于数组,数组下标可以从数组末端回绕到数组的开始位置。
8.优先级队列允许访问最小或者最大的数据项,优先级插入数据时候是有序插入的。
9.这些数据结构可以用数组实现,也可以用其他机制,比如链表来实现。