Java教程

算法入门 - 基于动态数组的栈和队列(Java版本)

本文主要是介绍算法入门 - 基于动态数组的栈和队列(Java版本),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

之前我们学习了动态数组的实现,接下来我们用它来实现两种数据结构——栈和队列。首先,我们先来看一下栈。

什么是栈?

栈是计算机的一种数据结构,它可以临时存储数据。那么它跟数组有何区别呢?
我们知道,在数组中无论添加元素还是删除元素,都可以根据索引位置或值进行操作,栈是否也支持这样的操作呢?答案是不行,栈最大的特点就是后进先出(Last In First Out, LIFO):

栈虽然看似简单,但是在计算机世界中有着非常重要的作用。比如在连续调用时,就利用了栈的特性:

    public static void addNum(){
        System.out.println("加法运算");
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入整数a:");
        int a = scanner.nextInt();
        System.out.print("请输入整数b:");
        int b = scanner.nextInt();
        System.out.println("a + b = " + (a + b));
    }

    public static void main(String[] args) {
        addNum();
    }

这里,在调用 addNum 方法时,内部又依次调用了两次 Scanner 实现输入。所以可以这么看,先调用了 addNum 方法,但是必须等待两次 Scanner 调用完成后,addNum 方法才能结束:

了解了栈后进先出的特点,我们就可以使用动态数组进行模拟了。

使用动态数组模拟栈

模拟的关键点在于“后进”和“先出”,也就是说,如果使用数组模拟的话,入栈时需要从数组尾部添加元素(addLast),出栈时也从尾部出栈(removeLast):

所以这样一来,数组头部实际上是栈底,数组尾部是栈顶。
接下来我们就用代码实现。

代码实现

首先定义栈的接口,规范栈的操作:

package com.algorithm.stack;

public interface Stack <E> {
    void push(E element);   // 入栈
    E pop();                // 出栈
    E peek();               // 查看栈顶元素
    int getSize();          // 获取栈长度
    boolean isEmpty();      // 判断栈是否为空

}

按照刚才说的,只要分别使用动态数组的 addLast() 和 removeLast() 方法替代栈的 push() 和 pop() 方法即可:

package com.algorithm.stack;

import com.algorithm.dynamicarrays.Array;

// 使用动态数组实现栈结构
// 栈底: index = 0; 栈顶: index = size - 1 (push: O(1), pop: O(1))
// 如果栈顶设在index=0的位置,push和pop都将面临较大开销(O(n))
public class ArrayStack<E> implements Stack<E>{
    private Array<E> arr;    // 使用之前实现的Array动态数组模拟栈

    public ArrayStack(int capacity){
        arr = new Array<>(capacity);
    }

    public ArrayStack(){
        arr = new Array<>();
    }

    // 从栈顶压入
    @Override
    public void push(E element){
        arr.addLast(element);
    }

    // 从栈顶弹出
    @Override
    public E pop(){
        return arr.removeLast();
    }

    // 从栈顶返回
    @Override
    public E peek(){
        return arr.getLast();
    }

    // 栈长度
    @Override
    public int getSize(){
        return arr.getSize();
    }

    // 栈容量
    public int getCapacity(){
        return arr.getCapacity();
    }

    // 判断栈是否为空
    @Override
    public boolean isEmpty(){
        return arr.isEmpty();
    }

    @Override
    public String toString(){
        StringBuilder str = new StringBuilder();
        str.append(String.format("Stack: size = %d, capacity = %d\n[", getSize(), getCapacity()));
        for (int i=0; i<getSize(); i++) {
            str.append(arr.get(i));
            if (i < getSize() - 1) {
                str.append(", ");
            }
        }
        str.append("] top");    // 标识出栈顶位置
        return str.toString();
    }

    // main函数测试
    public static void main(String[] args) {
        ArrayStack<Integer> arrayStack = new ArrayStack<>();
        for (int i =0; i<5; i++){
            arrayStack.push(i);
            System.out.println(arrayStack);
        }
        // pop
        arrayStack.pop();
        System.out.println(arrayStack);
    }
}

/*
输出结果:
Stack: size = 1, capacity = 10
[0] top
Stack: size = 2, capacity = 10
[0, 1] top
Stack: size = 3, capacity = 10
[0, 1, 2] top
Stack: size = 4, capacity = 10
[0, 1, 2, 3] top
Stack: size = 5, capacity = 10
[0, 1, 2, 3, 4] top
Stack: size = 4, capacity = 10
[0, 1, 2, 3] top
*/

结果符合预期。

栈的时间复杂度分析

入栈对应的数组操作是 addLast(),我们可以通过查看该方法的具体实现进行分析:

    /**
     * 在指定位置添加元素
     * 指定位置处的元素需要向右侧移动一个单位
     * @param index   索引
     * @param element 要添加的元素
     */
    public void add(int index, E element) {
        if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and <= size!");
        // 数组满员触发扩容
        if (getSize() == getCapacity()) {
            resize(2 * getCapacity());  // 扩容为原数组的2倍
        }
        // 从尾部开始,向右移动元素,直到index
        for (int i = getSize() - 1; i >= index; i--) {
            data[i + 1] = data[i];
        }
        // 添加元素
        data[index] = element;
        size++;
    }

    // 数组尾部添加元素
    public void addLast(E element) {
        add(getSize(), element);
    }

    /**
     * 删除指定位置元素
     * 通过向左移动一位,覆盖指定位置处的元素,实现删除元素(data[size - 1] = null)
     * @param index 索引
     */
    public E remove(int index) {
        if (index < 0 || index > size) throw new IllegalArgumentException("Illegal index, index must > 0 and < size!");
        // 数组长度为0时抛出异常
        if (getSize() == 0) throw new IllegalArgumentException("Empty array!");
        E removedElement = data[index];
        // 向左移动元素
        for (int i = index; i < getSize() - 1; i++) {
            data[i] = data[i + 1];
        }
        // 将尾部空闲出的位置置为空,释放资源
        data[getSize() - 1] = null;
        size--;
        // size过小触发数组缩减
        if (size == getCapacity() / 4 && getCapacity() / 2 != 0) resize(getCapacity() / 2);
        return removedElement;
    }

    // 删除尾部元素
    public E removeLast() {
        return remove(getSize() - 1);
    }

可以看出,每次从数组尾部添加元素时,add() 方法的 for 循环都无法满足条件,等同于直接在 size 处添加元素,所以时间复杂度为 O(1)。如果再考虑数组满员后触发的 resize 操作,相当于是进行了 n+1 次 add() 操作后才会触发 n次操作的 resize(移动n个元素至新数组),所以每次 add() 操作平均耗时为 \(\frac{2n+1}{n+1} \approx 2\),是一个与数组长度 n 无关的数,所以也可以看做是 O(1) 复杂度的。
同理,出栈对应的 removeLast() 的时间复杂度也是 O(1)。

什么是队列?

理解了栈后,队列就更简单了。实际上,队列是我们日常生活中几乎每天都会碰到的。我们去超市买东西结账时需要排队,去银行办理业务时需要排队,做核酸、打疫苗就更需要排队了

这篇关于算法入门 - 基于动态数组的栈和队列(Java版本)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!