数据结构-栈和队列

994 阅读4分钟

ba2e5cd85f4a56eeed1fd0feeecbb90f939bc41ecc76-6Ja1d3_fw658.jfif

这两个数据结构,熟悉编程的小伙伴肯定听过或者很熟悉。今天我们来详细介绍一下这两个,从基础开始系统的介绍,新手和老司机都会有所收获

什么是栈

  • 堆叠(stack)又称为栈或堆叠,是计算机科学中的一种抽象资料型别,只允许在有序的线性资料集合的一端(称为堆叠顶端,top)进行加入数据(push)和移除数据(pop)的运算。
  • 因而按照后进先出(LIFO, Last In First Out)的原理运作,堆叠常用一维数组或连结串列来实现
  • 栈也是一种线性结构
  • 相比数组,栈对应的操作是数组的子集
image.png

栈的应用

  • 我们常常使用的 “撤销” 操作
    • 例如我们在编码的过程中,可以每次的操作都放入栈中。依次敲入a b c d(其中a在栈底,d在栈顶)这时候我们想撤销到上一步
    • 在撤销的时候,利用栈的特性,从栈顶元素开始出栈
    • 撤销第一次 将d弹出,我们的文本变成abc
    • 撤销第二次,将c弹出,我们的文本变成ab
    • 借助栈的特性 就实现了撤销的动作,(当然实际中可能没有这么简单,这里只是做一个简单例子)
  • 还有一个比较重要的应用-程序调用栈
    • 方法a中有调用方法b,方法b中有调用方法c。以这种方法之间存在调用来举例
    • 程序执行从a开始,到调用b方法的时候会暂停a方法的执行,转到b方法的执行中,这个时候会压入栈内一个元素例如a1。同理在b方法中执行c方法的时候,将压入栈内一个b1。
    • c方法执行结束之后,从栈内弹出元素,b1,程序就会知道接着执行b方法。同理,b方法执行完毕之后,弹出a1,继续执行a方法,a方法执行完毕之后,栈内无元素,方法执行完毕。
    • 这里做个简单介绍。感兴趣小伙伴可以搜索专门的文章来查看

动手编码

  • 自定义实现一个栈
  • 借助上一篇的数组结构,只不过栈只能从一端加入元素和释放元素
public class MyStack<E>{

    private E[] data;
    private int size;

    //构造函数,初始化一个数组结构
    //数组头当作栈低,数组末尾当作栈顶
    public MyStack(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    //将数据放入栈中,也即像数组的末尾添加元素
    public void push(E e){
        if(size == data.length){
            resize(2 * data.length);
        }
        data[size] = e;
        size ++;
    }

    //从栈中弹出元素,也即删除数组末尾元素
    public E pop(){
        size --;
        E ret = data[size];
        data[size] = null;

        if(size == data.length / 4 && data.length / 2 != 0){
            resize(data.length / 2);
        }
        return ret;
    }
    
    // 将数组空间的容量变成newCapacity大小
    private void resize(int newCapacity){

        E[] newData = (E[])new Object[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }
    
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append("Stack: ");
        res.append('[');
        for(int i = 0 ; i < array.getSize() ; i ++){
            res.append(array.get(i));
            if(i != array.getSize() - 1)
                res.append(", ");
        }
        res.append("] top");
        return res.toString();
    }
}

  • 想必有一部分小伙伴已经发现了,这里的 push和pop方法的实现和上一篇数组的add方法和remove方法一样,因为是基于数组来实现的,这也证实了开头的定义,是数组功能的子集

数组实现栈的时间复杂度

  • push:O(1)
  • pop: O(1)
  • 两个方法都是O(1)的想必小伙伴们没有什么疑问吧,因为都是通过索引快速操作。
  • 如果有疑问,可以看看我的上篇文章-“数据结构-数组” 里面有对时间复杂度的介绍

队列

什么是队列

  • 是计算机科学中的一种抽象资料型别,是先进先出(FIFO, First-In-First-Out)的线性表。
  • 在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作。
  • 队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加
  • 综上所述,队列也是一个线性结构
  • 相比较数组,队列的操作也是数组的子集

image.png

动手编码

public class MyQueue<E> {

    private E[] data;
    private int size;

    //构造方法,数组的头为队首,数组的尾为队尾
    public MyQueue(int capacity){
        data = (E[])new Object[capacity];
        size = 0;
    }

    //入队操作,也即从数组的末尾 加元素,和入栈的操作一样
    public void push(E e){
        if(size == data.length){
            resize(2 * data.length);
        }
        data[size] = e;
        size ++;
    }

    //出队操作,也即从数组的头 删除一个元素
    public E dequeue(){
        E ret = data[0];
        for(int i = 1 ; i < size ; i ++){
            data[i - 1] = data[i];
        }
        size --;
        data[size] = null;

        if(size == data.length / 4 && data.length / 2 != 0){
            resize(data.length / 2);
        }
        return ret;
    }
    
    // 将数组空间的容量变成newCapacity大小
    private void resize(int newCapacity){

        E[] newData = (E[])new Object[newCapacity];
        for(int i = 0 ; i < size ; i ++)
            newData[i] = data[i];
        data = newData;
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append("Queue: ");
        res.append("front [");
        for(int i = 0 ; i < array.getSize() ; i ++){
            res.append(array.get(i));
            if(i != array.getSize() - 1)
                res.append(", ");
        }
        res.append("] tail");
        return res.toString();
    }
}

  • 我知道,当小伙伴看到出队这个操作的时候,我想整个人都是懵的,因为从数组头删除一个元素,还需要将后面的所有元素向前移动一个空间,这典型的O(n) 操作啊
  • 别急,我们先分析完时间复杂度再说

数组实现队列的时间复杂度

  • push:O(1)
  • pop: O(n)
  • 出队是一个O(n)的操作,我想大家肯定是忍不了吧,效率太差
  • 那么怎么能把出队这个操作变成O(1) 呢,那就是想删除头元素的时候,其余元素不必向前移动一个空间,循环队列 它来了

循环队列

  • 什么是循环队列呢,简单的解释是:队列首尾相接,形成一个环形
  • 当出队元素的时候,仅仅删除对应的元素,其余元素不移动。入队元素的时候,如果末尾已经满了,由于是首尾相连的,会向队首依次添加
  • 所以出现两个新的名字 front:代表队首元素的索引位置,rear:代表队尾元素的位置
  • 当 front = rear的时候 说明队列为空 front = rear+1 的时候 说明队列满

image.png

循环队列实现

public class MyQueue<E> {

    private E[] data;
    private int front, rear;
    private int size;

    //构造方法,和上面的队列基本一致,只是因为循环队列会有一个空间空余,所以这里容量要+1
    public MyQueue(int capacity){
        data = (E[])new Object[capacity + 1];
        front = 0;
        rear = 0;
        size = 0;
    }

    //入队操作,小伙伴可以细细体会一下这个取余操作
    public void push(E e){
        if((rear + 1) % data.length == front)
            resize(getCapacity() * 2);

        data[rear] = e;
        rear = (rear + 1) % data.length;
        size ++;
    }

    //出队
    public E pop(){
        if(front == rear){
            throw new IllegalArgumentException("队列为空");
        }
        E ret = data[front];
        data[front] = null;
        front = (front + 1) % data.length;
        size --;
        
        int capacity = data.length - 1;
        if(size == capacity / 4 && capacity / 2 != 0)
            resize(capacity / 2);
        return ret;
    }
}
  • 到此,循环队列实现完成了,小伙伴可以体会一下 循环 这个感觉,类似一个环~
  • 这样出队这个操作的时间复杂度就是O(1) 了

到此,本文就结束了,将栈和队列放在一起,一来是因为 这两个都是线性结构,二来 通过对比队列的实现细节,来发现其中缺点。大家可以想象循环队列的优缺点。试想有没有其他的实现方案