【数据结构】三、栈、队列笔记

栈、队列笔记

一、栈

1、定义:

栈,存储货物或供旅客住宿的地方,可引申为仓库、中转站,所以引入到计算机领域里,就是指数据暂时存储的地方,所以才有进栈、出栈的说法。

栈是一种表类型的数据结构,是一种只能在一段进行插入和删除操作的特殊线性表。它按照先进后出的原则存储数据,先进后出的数据被压入栈底(push),最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(pop)。

在这里插入图片描述


2、实现:

由于栈是一个表,因此任何实现表的方法都能实现栈。故ArrayList与LinkedList都可以实现栈操作。而笔者通过这两种方式来实现栈这一数据结构:

ArrayList的方式实现:

成员变量:

    private MyArrayList theArray;
    private int topOfStack;

构造方法:

    public Stack_A() {
        theArray = new MyArrayList();
        topOfStack = -1;
    }

由于是由ArrayList实现的栈,因此引用了之前自己写的ArrayList。而topOfStack是栈中指向栈顶元素的指针,对于空栈而言它是-1。

其他方法

这里只将Stack中的isEmpty()、peek()、push()和pop()方法进行实现,因此以下将详细的描述,而ArrayList类部分的代码请见 ([数据结构] 二、数组笔记_芋yu头的博客-CSDN博客)。

    public boolean isEmpty(){
        return theArray.isEmply();
    }

    public T peek(){
        return (T) theArray.get(topOfStack);  //类型擦除
    }

    public boolean push(T ele){
        topOfStack++;
        return theArray.add(ele);
    }

    public T pop(){
        T ele = peek();
        theArray.remove();
        topOfStack--;
        return ele;
    }

peek()方法是返回栈顶部的元素,而该方法由于用到了泛型,以此JVM在执行时会进行类型擦除为Object类型,故此时需要向下转型成T类型。

push()方法是将元素压入栈中。为将某个元素ele推入栈中,我们使topOfStack增1然后置theArray[topOfStack] = ele。(这里是调用MyArrayList中的add()方法,在数组末尾添加元素)

pop()方法是将元素弹出栈中。为了弹出栈元素,我们置返回值为theArray[topOfStack] = ele然后使topOfStack减1。(这里是调用MyArrayList中的remove()方法,在数组末尾删除元素)

注意:这些操作都是以常数时间运行,而且是非常快的常数时间运行。因此栈是计算机科学在数组之后的最基本的数据结构。

LinkedList的方式实现:

成员变量:

    SingleLinkedList s;
    int topOfStack;

这里笔者使用单链表的方式来实现栈结构。

构造方法:

    public Stack_L() {
        s = new SingleLinkedList();
        topOfStack = -1;
    }

其他方法:

public boolean isEmply(){
        return s.isEmpty();
    }

    public T peek(){
        return (T) s.get(topOfStack);  //泛型类型擦除
    }

    // 后进先出
    public boolean push(T ele){
        topOfStack++;
        return s.add(ele);
    }

    public T pop(){
        T ele = peek();
        s.remove(s.size() - 1);
        topOfStack--;
        return ele;
    }		

具体的方法与数组实现的方式类似,这里不重复赘述。

3、应用:

  • 平衡符号:编译器检查符号是否对称
  • 后缀表达式:计算机处理具有优先级的数学符号运算
  • 方法调用:当前方法执行完成跳转到另一方法中(递归的调用)

笔者用栈来完成“后缀表达式”的应用,故详细介绍该部分内容

后缀表达式:

逆波兰式(Reverse Polish notation,RPN,或逆波兰记法),也叫后缀表达式(将运算符写在操作数之后)。

在这里插入图片描述
处理后缀表达式分为两步:将中缀表达式转化为后缀表达式、后缀表达式的计算。

后缀表达式的转化

中缀转化成后缀表达式主要处理符号,即通过栈的方式存储并按照相应规则弹出,数字则按顺序输出。代码如下:

public static ArrayList transfer(String input){
            char[] i = input.toCharArray();
            Stack<Character> s = new Stack<>();
            ArrayList output = new ArrayList();

            int idx = 0;
            while(i[idx] != ' '){
                // 符号处理
                if (i[idx] > 39 && i[idx] < 48 || i[idx] == 94){
                    //防止空栈异常
                    if (s.empty()){
                        s.push('O');
                    }
                    //小括号的处理.
                    if (i[idx] == ')'){
                        while (s.peek() != '('){
                            output.add(s.pop());
                        }
                        s.pop();
                        idx++;
                        popStack(i, s, output, idx);
                        continue;
                    }
                    //优先级出栈
                    while(!priority(i[idx], s.peek())){
                        output.add(s.pop());
                    }

                    //Input中的符号元素若优先级大于栈中的符号,压栈
                    if (priority(i[idx], s.peek())){
                        s.push(i[idx]);
                    }
                }
                //数字处理
                else
                    output.add(i[idx]);

                idx++;
                popStack(i, s, output, idx);
            }
            return output;
        }

首先,输入为用户输入的字符串String,输出则将转化为后缀表达式的结果存储在ArrayList。由于该程序主要对于简单的带有整数的中缀表达式的转换,因此这里用到字符char的处理,故将输入的字符串转化成char[]数组;同时使用栈来处理符号。

由于这里涉及到用户输入,因此想到使用ASCII码来区分符号与数字。故查询ASCII码发现数字为48-57。数字计算符号主要的ASCII码范围为40-47。(不包含幂运算情况)

观察代码可发现while循环内分别处理符号与数字,当读到一个操作数时,立即把它放到输出中。操作符不立即输出,而是将其推入推入栈中,由于计算从一个空栈开始,为了防止后面在比较符号时出现EmptyStackException,故先在栈中压入一个“O”。

可观察到这里需要调用了priority()私有方法来判断符号的优先级。该优先级的判断是如果输入的运算符号(不包括括号)不比栈顶元素的优先级低,则将其压入栈中。例如中缀表达式子为 a + b * c,则后缀表达式为a b c * +, 乘法的运算优先级比加法的运算优先级高,故将乘号压入栈中。

如果输入的运算符号优先级比栈顶元素的优先级低,则从栈顶开始逐一将比输入符号的优先级低的弹出栈,并输出。例如a / b * c + d,先将除号压入栈中,当乘号输入时,比较乘号与除号具有相同的优先级,故乘号压入栈中;而当加号输入时,由于加号的优先级比乘除低,故从栈顶开始将元素弹出,结果为a b c * /,此时栈为空,故将加号压入栈中。

现在来看括号,若是左括号( ,则其具有最高优先级,执行压栈操作,且括号内的操作符不按照以上的优先级,而是只有输入为右括号时,才执行弹栈操作,并且将栈元素直到(所有元素弹出(括号内的操作符按照先后顺序压入栈中)。例如中缀表达式为a * ( b *c + d ),首先,将乘号压入栈中,当输入为左括号时,由于左括号优先级最高,故也将其压入栈中。而括号内的符号按照顺序压入栈中,故先将乘号压入栈中,输入为加号时不需要将乘号弹出,而是继续将加号压入栈中,直到出现右括号时把直到左括号内所有的操作符依次弹出栈(这里不输出括号)。后缀表达式为:a b c * d + *。

若输入为空时,将栈中的符号全部弹出并输出,直到栈变成空栈。

实现过程中调用了私有方法priority()方法,笔者这里通过量化来比较操作符的优先级。

    // 当栈内无元素时,添加O防止空栈异常。
        private static boolean priority(char a, char b){
            int ap = 0;
            int bp = 0;
            if (a == '*' || a == '/'){
                ap += 1;
            }
            if (b == '*' || b == '/'){
                bp += 1;
            }
            if (a == '^'){
                ap += 3;
            }
            if (b == '^'){
                bp += 2;
            }
            if (a == '('){
                ap += 4 ;
            }
            if (b == '('){
                bp -= 4;
            }
            if (b == 'O'){
                bp -= 1;
            }
            return ap > bp;
        }

后缀表达式的计算:

后缀表达式的计算规则为当见到一个数就把它推入栈中;在遇到一个运算符时该算符就作用在该栈弹出的两个数(符号)上,再将所得结果推入栈中。笔者通过对各个符号的ASCII码来判断执行相应的运算操作。

public static int calculate(ArrayList<Character> output){
            Stack<Integer> s = new Stack();
            for (int idx = 0; idx < output.size(); idx++){
                if (output.get(idx) > 47 && output.get(idx) != 94){
                    s.push(output.get(idx) - 48);
                }
                else if (output.get(idx) == 43){
                    int a = s.pop();
                    int b = s.pop();
                    s.push(a + b);
                }
                else if (output.get(idx) == 45){
                    int a = s.pop();
                    int b = s.pop();
                    s.push(b - a);
                }
                else if (output.get(idx) == 42){
                    int a = s.pop();
                    int b = s.pop();
                    s.push(a * b);
                }
                else if (output.get(idx) == 47){
                    int a = s.pop();
                    int b = s.pop();
                    s.push(b / a);
                }
                else if (output.get(idx) == 94){
                    int a = s.pop();
                    int b = s.pop();
                    s.push((int) Math.pow(b, a));
                }
            }
            return s.pop();
        }

补充:

幂运算也在该程序之中,其不同点是:有些操作符是从左到右结合的,如:a - b - c 转化为 ab - c - ,而幂运算是从右到左结合的:
2 ( 2 2 ) = 2 8 = 256 2^(2^2) = 2^8=256 2(22)=28=256

代码中的体现为输入的的优先级比栈中的的低。


二、队列

1、定义:

队列也是表,其使用队列时插入在一端进行而删除则在另一端进行。其核心为“先进先出”。

队列的基本操作是enqueue,它是在表的末端插入一个元素,和dequeue,它是删除在表的开头的元素,如图所示:

在这里插入图片描述


2、队列的数组实现:

由于在java中有专门实现队列的接口Queue,且其实现的原理于List不太相同,因此这里只是简单的模拟普通的队列:

对于每一个队列数据结构,我们保留一个数组theArray以及位置front和back,它们代表队列的两端。同时记录实际存在于队列的元素的个数currentSize。

为了使一个元素ele入队(enqueue),则让currentSize和back增1,然后置theArray[back] = ele。

若使元素出队(dequeue),我们置返回值为theArray[front],且currentSize减1,然后使front增1。

为了避免不停的入队使底层的数组容量溢出,故这里设置了一个循环数组,即只要front或back到达数组的尾端,它就又回到开头。

具体实现代码如下:

public class Queue<T> {
    private T[] theArray;
    private static final int CAPACITY = 10;
    private int currentSize;
    private int front;
    private int back;

    public Queue() {
        theArray = (T[]) new Object[CAPACITY];
        currentSize = 0;
        front = 0;
        back = CAPACITY - 1;
    }

    public boolean isEmpty(){
        return currentSize == 0;
    }

    public int size(){
        return CAPACITY;
    }
    // 先进先出
    public void enqueue(T ele){
        back++;
        if (back == CAPACITY){
            back = 0;
        }
        theArray[back] = ele;
        currentSize++;
    }

    public T dequeue(){
        if (currentSize == 0){
            new Queue();
        }
        if (front == CAPACITY){
            front = 0;
        }
        currentSize--;
        return theArray[front++];
    }
}

3、队列的应用:

  • 文件服务器:使用计算机的用户按照现到先使用的原则访问文件

  • 排队论。

      }
      theArray[back] = ele;
      currentSize++;
    

    }

    public T dequeue(){
    if (currentSize == 0){
    new Queue();
    }
    if (front == CAPACITY){
    front = 0;
    }
    currentSize–;
    return theArray[front++];
    }
    }


### 3、队列的应用:

- 文件服务器:使用计算机的用户按照现到先使用的原则访问文件
- 排队论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值