数据结构与算法4——栈和队列

  本章涉及到三种数据存储类型:栈、队列和优先级队列。首先介绍着三种结构与数组的区别,然后依次介绍每种数据结构。

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 };
View Code

输出结果:
入栈后的值:
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); 
        }
    }
};
View Code

输出结果:
请输入一个字符串:
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();
        }
    }
};
View Code

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 };
View Code

输出结果:
2
7
27
77
87
上面我们用数组实现了一个循环队列,并且从运行结果来看,插入顺序和删除的顺序是一致的。这就是队列的特点。另外,插入时候没有进行队列是否已满检查。

3.2.1 队列的效率 

  和栈一样,队列中插入数据项和移除数据项的时间复杂度为O(1)

 

3.1 循环队列                         

 

  循环队列的特点是队头指针和队尾指针都是循环式的,当插入更多的数据项时,队尾指针如预计的那样向上移动。注意在队尾指针回绕之后,它限制处在队头指针的下面,这就颠倒了初始的位置。
  删除足够多的数据项后,对头指针也回绕,这时队列的指针回到了初始运行时的位置状态,队头指针在队尾指针的下面。总之,插入看队尾指针,删除看队头指针,无论怎么绕来绕去,这个是不变的。

 

 

3.2 循环队列的数组实现        

 

  下面我们来实现队列的新建、插入、删除和查看四种操作。

 

View Code

 

输出结果:
2
7
27
77
87
上面我们用数组实现了一个循环队列,并且从运行结果来看,插入顺序和删除的顺序是一致的。这就是队列的特点。另外,插入时候没有进行队列是否已满检查。

 

3.2.1 队列的效率 

 

  和栈一样,队列中插入数据项和移除数据项的时间复杂度为O(1)

 

3.3 优先级队列                  

 

  优先级队列是比栈和队列更专用的数据结构。但是在很多情况下都很有用。和普通队列一样,优先级队列有一个队头和一个队尾,并且也是从队头移除数据项。优先级队列应该称为有序队列。因为插入时候会按照顺序将数据项插入,这样队头总是最小的数据项。优先级队列在某些计算机系统中也有很多应用,例如抢先式多任务操作系统中,程序排列在优先级队列中,这样优先级最高的程序就会得到时间片并得以执行。
  优先级队列可以通过堆和数组实现,这里介绍数组的实现。

 

View Code

 

输出结果:
2
7
26
27
65
77
87
我们看到,插入后的队列总是按顺序的。

 

4 小结                                                                        

 

1.栈、队列和优先级队列是经常用于简化某些程序操作的数据结构。
2.在这些数据结构中,只有一个数据项可以被访问。
3.栈允许访问最后一个插入的数据项
4.栈中重要的操作是在栈顶插入一个数据项,以及从栈顶移除一个数据项。
5.队列只允许访问第一个插入的数据项。
6.队列的重要操作是在队尾插入数据项和在队头移除数据项。
7.队列可以实现为循环队列,它基于数组,数组下标可以从数组末端回绕到数组的开始位置。
8.优先级队列允许访问最小或者最大的数据项,优先级插入数据时候是有序插入的。
9.这些数据结构可以用数组实现,也可以用其他机制,比如链表来实现。

 

原文地址:https://www.cnblogs.com/people/p/3255563.html