第三章 栈与队列

121 阅读13分钟

第三章 栈与队列

参考文章:www.hello-algo.com/

3.1 栈(Stack)

3.1.1 栈的定义与特性

  • 定义:栈(Stack)是一种遵循“后进先出”(LIFO,Last In First Out)原则的线性数据结构。
  • 类比:可以将栈类比为桌面上的一摞盘子,如果想取出底部的盘子,必须先移走上面的盘子。
  • 关键术语
    • 栈顶(Top):允许操作的一端,元素从这里入栈和出栈。
    • 栈底(Bottom):固定的一端,通常不允许直接操作。
    • 入栈(Push):将元素添加到栈顶的操作。
    • 出栈(Pop):从栈顶移除元素的操作。
    • 查看栈顶元素(Peek):访问栈顶元素而不移除它。

3.1.2 栈的常用操作

  • 常用方法
    • push(value):将一个元素压入栈顶,时间复杂度为 O(1)
    • pop():移除并返回栈顶元素,时间复杂度为 O(1)
    • peek():返回栈顶元素但不移除它,时间复杂度为 O(1)
    • size():返回栈中元素的数量,时间复杂度为 O(1)
    • isEmpty():判断栈是否为空,时间复杂度为 O(1)

3.1.3 栈的实现方式

  • 基于数组的实现
    • 使用数组的尾部作为栈顶,通过动态数组(如 Python 的 list 或 Java 的 ArrayList)实现自动扩容。
    • 优点:操作简单,时间复杂度低(均为 O(1))。
    • 缺点:如果栈的大小变化频繁,可能会导致多次扩容操作。
  • 基于链表的实现
    • 使用链表的头部作为栈顶,通过链表节点的插入和删除操作实现栈的功能。
    • 优点:无需担心扩容问题,适合元素数量变化大的场景。
    • 缺点:需要额外的指针操作,代码实现相对复杂。

3.1.4 栈的典型应用

  • 括号匹配:检查代码中的括号是否正确匹配。
  • 逆波兰表达式求值:用于计算后缀表达式的值。
  • 函数调用栈:在程序执行过程中,用于存储函数调用的上下文信息。
  • 回溯算法:在解决组合问题、排列问题等时,用于记录状态并回溯到上一步。
  • 深度优先搜索(DFS):在图或树的遍历中,用于存储访问路径。

3.1.5 代码示例

以下是基于数组和链表实现栈的代码示例(以 Java 为例):

基于数组的栈实现:

package com.liucc.chapter_stack_and_queue;

import java.util.ArrayList;
import java.util.Arrays;

/** 基于动态数组实现的栈 */
class ArrayStack {

    private ArrayList<Integer> stack;

    public ArrayStack(){
        stack = new ArrayList<>();
    }

    /* 获取栈的长度 */
    public int size(){
        return stack.size();
    }
    /* 判断栈是否为空 */
    public boolean isEmpty(){
        return size() == 0;
    }
    /* 查看栈顶元素 */
    public int peek(){
        if (size() == 0) {
            throw new IndexOutOfBoundsException();
        }
        return stack.get(size()-1);
    }
    /* 入栈 */
    public void push(int num){
        stack.add(num);
    }
    /* 出栈 */ 
    public int pop(){
        if (size() == 0) {
            throw new IndexOutOfBoundsException();
        }
        return stack.remove(size()-1);
    }
    /* 转换为数组 */
    public Object[] toArray(){
        return stack.toArray();
    }
}

public class array_stack {
    public static void main(String[] args) {
        /* 初始化栈 */
        ArrayStack stack = new ArrayStack();

        /* 元素入栈 */
        stack.push(1);
        stack.push(3);
        stack.push(2);
        stack.push(5);
        stack.push(4);
        System.out.println("栈 stack = " + Arrays.toString(stack.toArray()));

        /* 访问栈顶元素 */
        int peek = stack.peek();
        System.out.println("栈顶元素 peek = " + peek);

        /* 元素出栈 */
        int pop = stack.pop();
        System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray()));

        /* 获取栈的长度 */
        int size = stack.size();
        System.out.println("栈的长度 size = " + size);

        /* 判断是否为空 */
        boolean isEmpty = stack.isEmpty();
        System.out.println("栈是否为空 = " + isEmpty);
    }
}

基于链表的栈实现:

package com.liucc.chapter_stack_and_queue;

import java.util.Arrays;

/** 双向链表节点 */
public class ListNode {
public int val;
public ListNode next; // 后继结点
public ListNode prev; // 前驱节点

public ListNode(int val) {
    this.val = val;
    next = prev = null;
}
}

/** 基于链表实现的栈 */
class LinkedListStack {
	private ListNode head; // 头节点作为栈顶元素
	private int stackSize; // 栈大小

	public LinkedListStack(){
	    this.head = null;
	}

	/** 获取栈顶元素 */
	public int peek(){
	    if (size() == 0) {
	        throw new IndexOutOfBoundsException();
	    }
	    return head.val;
	}
	/** 获取栈的长度 */ 
	public int size(){
	    return stackSize;
	}
	/** 栈是否为空 */
	public boolean isEmpty(){
	    return size() == 0;
	}
	/** 入栈 */ 
	public void push(int num){
	    ListNode node = new ListNode(num);
	    node.next = head;
	    head = node; // 头节点指向新节点
	    stackSize++;
	}
	/** 出栈 */
	public int pop(){
	    if (size() == 0) {
	        throw new IndexOutOfBoundsException();
	    }
	    int val = head.val;
	    head = head.next; // 指针后移
	    stackSize--;
	    return val;
	}
	/** 栈转为数组 */
	public int[] toArray(){
	    // 1->2->3->4
	    int[] array = new int[size()];
	    ListNode temp = head;
	   for (int i = array.length - 1; i >= 0; i--) {
	        array[i] = temp.val;
	        temp = temp.next;
	   }
	   return array;
	}
}

public class linkedlist_stack {
	public static void main(String[] args) {
    /* 初始化栈 */
    LinkedListStack stack = new LinkedListStack();

    /* 元素入栈 */
    stack.push(1);
    stack.push(3);
    stack.push(2);
    stack.push(5);
    stack.push(4);
    System.out.println("栈 stack = " + Arrays.toString(stack.toArray()));

    /* 访问栈顶元素 */
    int peek = stack.peek();
    System.out.println("栈顶元素 peek = " + peek);

    /* 元素出栈 */
    int pop = stack.pop();
    System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + Arrays.toString(stack.toArray()));

    /* 获取栈的长度 */
    int size = stack.size();
    System.out.println("栈的长度 size = " + size);

    /* 判断是否为空 */
    boolean isEmpty = stack.isEmpty();
    System.out.println("栈是否为空 = " + isEmpty);
	}
}

3.2 队列

3.2.1 队列的定义

  • 队列是一种遵循先入先出(FIFO) 规则的线性数据结构。
  • 队列模拟了排队现象:新元素加入队列尾部(入队),位于队列头部的元素逐个离开(出队)。
  • 队列的头部称为队首,尾部称为队尾

3.2.2 队列的基本操作

  • 入队(push):将元素添加到队尾,时间复杂度为 O(1)
  • 出队(pop):删除队首元素,时间复杂度为 O(1)
  • 访问队首元素(peek):查看队首元素,但不删除,时间复杂度为 O(1)
  • 获取队列长度(size):返回队列中元素的数量。
  • 判断队列是否为空(is_empty):检查队列是否为空。

3.2.3 队列的实现方式

队列可以通过以下两种主要方式实现:

  • 基于链表的实现
    • 使用链表的头节点作为队首,尾节点作为队尾。
    • 入队操作在尾节点后添加新节点,出队操作从头节点删除节点。
    • 优点:动态扩展,适合元素数量不确定的场景。
  • 基于数组的实现
    • 使用数组模拟队列,通过索引操作实现入队和出队。
    • 需要处理数组满或空的情况,可能需要额外的逻辑来循环使用数组空间(循环队列)。
    • 优点:访问速度快,适合元素数量相对固定的场景。

3.2.4 队列的典型应用

  • 广度优先搜索(BFS):在图和树的遍历中,队列用于逐层访问节点。
  • 任务调度:操作系统中,任务按照到达顺序排队等待处理。
  • 缓冲区管理:如打印机队列、数据流的缓冲处理。

3.2.5 示例代码

基于链表实现队列的Java代码示例:

package com.liucc.chapter_stack_and_queue;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Queue;

import com.liucc.linkedlist.ListNode;

/**
 * 基于链表实现的队列
 */
class LinkedListQueue{
    private ListNode front ,rear; // 队首队尾指针
    private int queueSize; // 队列长度

    // 构造器初始化
    public LinkedListQueue(){
        front = null;
        rear = null;
    }
    // 队列是否为空
    public boolean isEmpty(){
        return queueSize == 0;
    }
    // 获取队列长度
    public int size(){
        return queueSize;
    }
    // 访问队首元素
    public int peek(){
        if (isEmpty()) {
            throw new IndexOutOfBoundsException();
        }
        return front.val;
    }
    // 入队
    public void push(int num){
        ListNode node = new ListNode(num);
        // 如果队列为空,队首队尾指针均指向新节点
        if (isEmpty()) {
            front = node;
            rear = node;
        }else{ // 队列不为空,新节点添加到尾节点之后
            rear.next = node;
            rear = node;
        }
        queueSize++;
    }
    // 出队
    public int pop(){
        int peek = peek();
        front = front.next;
        queueSize--;
        return peek;
    }
    // 转化为数组
    public int[] toArray(){
        int[] arrays = new int[size()];
        ListNode temp = front;
        for (int i = 0; i < arrays.length-1; i++) {
            arrays[i] = temp.val;
            temp = temp.next;  // 指针后羿
        }
        return arrays;
    }
}

public class linkedlist_queue {
    public static void main(String[] args) {
        /* 初始化队列 */
        LinkedListQueue queue = new LinkedListQueue();

        /* 元素入队 */
        queue.push(1);
        queue.push(3);
        queue.push(2);
        queue.push(5);
        queue.push(4);
        System.out.println("队列 queue = " + Arrays.toString(queue.toArray()));

        /* 访问队首元素 */
        int peek = queue.peek();
        System.out.println("队首元素 peek = " + peek);

        /* 元素出队 */
        int pop = queue.pop();
        System.out.println("出队元素 pop = " + pop + ",出队后 queue = " + Arrays.toString(queue.toArray()));

        /* 获取队列的长度 */
        int size = queue.size();
        System.out.println("队列长度 size = " + size);

        /* 判断队列是否为空 */
        boolean isEmpty = queue.isEmpty();
        System.out.println("队列是否为空 = " + isEmpty);
    }    
    
}

基于数组的实现

数组直接删除元素的时间复杂度为 O(n),这会导致出队操作效率变低。然而,可以采取以下技巧进行优化:

定义变量 front 指向队首元素的索引,并维护变量 queSize 来存储队列的实际长度。可以通过rear = front + queSize 来计算队尾元素的插入位置。

  • 入队操作:将元素添加到 rear 位置处,size++即可
  • 出队操作:front加 1,size 减 1 即可

这种方式下时间复杂度优化为 O(1)

在这里插入图片描述

在这里插入图片描述

仍存在的问题:在不断出队、入队的过程中,front和 rear 一直向数组尾部移动,到达数组尾部就无法继续移动。

解决:通过环形数组进行解决,front、rear 到达尾部时再继续跳转到数组头部空闲位置。

示例代码

package com.liucc.chapter_stack_and_queue;

import java.util.Arrays;

/**
 * 基于环形数组实现的队列
 */
class ArrayQueue {
    private int[] nums; // 存储队列队列元素的数组
    private int front; // 队首指针
    private int queSize; // 队列实际长度

    // 构造器
    public ArrayQueue(int capacity) {
        nums = new int[capacity]; // 队列容量
    }

    // 队列容量
    public int capacity() {
        return nums.length;
    }

    // 队列实际长度
    public int size() {
        return queSize;
    }

    // 队列是否为空
    public boolean isEmpty() {
        return queSize == 0;
    }

    // 入队
    public void push(int num) {
        // 队满,不可再添加元素
        if (queSize == capacity()) {
            System.out.println("队列已满,不可再添加元素");
            return;
        }
        // 计算新元素应该存放的位置(rear)
        int rear = (front + queSize) % capacity();
        nums[rear] = num;
        queSize++;
    }

    // 出队
    public int pop() {
        int num = peek();
        front = (front + 1) % capacity();
        queSize--;
        return num;
    }

    // 访问队首元素
    public int peek() {
        if (queSize == 0) {
            throw new IndexOutOfBoundsException();
        }
        return nums[front];
    }
    // 转换为数组
    public int[] toArray(){
        int[] arr = new int[queSize];
        int idx = front;
        for (int i = 0; i < queSize; i++) {
            arr[i] = nums[idx];
            idx = (idx+1)%capacity();
        }
        return arr;
    }
}

public class array_queue {
    public static void main(String[] args) {
        /* 初始化队列 */
        ArrayQueue queue = new ArrayQueue(5);

        /* 元素入队 */
        queue.push(1);
        queue.push(3);
        queue.push(2);
        queue.push(5);
        queue.push(4);
        queue.push(6); // 队列满,无法正常添加
        System.out.println("队列 queue = " + Arrays.toString(queue.toArray()));

        /* 访问队首元素 */
        int peek = queue.peek();
        System.out.println("队首元素 peek = " + peek);

        /* 元素出队 */
        int pop = queue.pop();
        System.out.println("出队元素 pop = " + pop + ",出队后 queue = " + Arrays.toString(queue.toArray()));

        /* 获取队列的长度 */
        int size = queue.size();
        System.out.println("队列长度 size = " + size);

        /* 判断队列是否为空 */
        boolean isEmpty = queue.isEmpty();
        System.out.println("队列是否为空 = " + isEmpty);
    }

}
💡

可以发现,基于环形数组实现的队列仍存在局限,数据的长度的固定的,无法自动扩容,但这一点可以采取动态数组进行优化,从而引入扩容机制。

3.3 双向队列

3.3.1 概念

双向队列是一种灵活的数据结构,允许在队列的头部和尾部执行元素的添加或删除操作。它结合了队列和栈的特点,提供了更高的操作灵活性。

3.3.2 常用操作

双向队列支持以下操作,且大多数操作的时间复杂度为 O(1)

方法名描述时间复杂度
push_first()在队首添加元素O(1)
push_last()在队尾添加元素O(1)
pop_first()删除队首元素O(1)
pop_last()删除队尾元素O(1)
peek_first()访问队首元素O(1)
peek_last()访问队尾元素O(1)
size()获取队列长度O(1)
is_empty()判断队列是否为空O(1)

3.3.3 实现方式

双向队列可以通过以下两种方式实现:

  1. 基于双向链表的实现:使用双向链表作为底层数据结构,允许在头部和尾部快速插入和删除节点。
  2. 基于数组的实现:使用数组模拟双向队列,但需要注意数组扩容和收缩的复杂度。

3.3.4 代码实现

基于双向链表的实现

  • 节点结构

    /** 双向链表节点 */
    public class ListNode {
        public int val;
        public ListNode next; // 后继结点
        public ListNode prev; // 前驱节点
    
        public ListNode(int val) {
            this.val = val;
            next = prev = null;
        }
    }
    
  • 双向队列类

    package com.liucc.chapter_stack_and_queue;
    
    import java.util.Arrays;
    
    import com.liucc.linkedlist.ListNode;
    
    /* 基于链表实现的双向队列 */
    class LinkedListDeque{
        private ListNode front, rear; // 队列头节点,尾节点
        private int queSize; // 队列长度
    
        public LinkedListDeque(){
            front = rear = null;
        }
    
        // 队列是否为空
        public boolean isEmpty(){
            return queSize == 0;
        }
        // 队列大小
        public int size(){
            return queSize;
        }
        // 获取队首元素
        public int peekFirst(){
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            }
            return front.val;
        }
        // 获取队尾元素
        public int peekLast(){
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            }
            return rear.val;
        }
        // 入队
        public void push(int num, boolean isFront){
            ListNode node = new ListNode(num);
            // 如果队列为空,front、rear 均指向新节点
            if (isEmpty()) {
                front = node;
                rear = node;
            }else{
                if (isFront) { // 头插法
                    front.prev = node;
                    node.next = front;
                    front = node; // 更新头节点
                }else{ // 尾插法
                    rear.next = node;
                    node.prev = rear;
                    rear = node;
                }
            }
            queSize++;
        }
        // 队首入队
        public void pushFirst(int num){
            push(num, true);
        }
        // 队尾入队
        public void pushLast(int num){
            push(num, false);
        }
        // 出队
        public int pop(boolean isFront){
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            }
            int val = -1;
            if (isFront) {
                // 保存头节点的值
                val = front.val;
                ListNode temp = front.next;
                front.next = null;
                temp.prev = null;
                front = temp; // 更新头节点
            }else{
                val = rear.val;
                ListNode temp = rear.prev;
                rear.prev = null;
                temp.next = null;
                rear = temp; // 更新尾节点
            }
            queSize--;
            return val;
        }
        // 队首出队
        public int popFirst(){
            return pop(true);
        }
        // 队尾出队
        public int popLast(){
            return pop(false);
        }
        // 转为数组
        public int[] toArray(){
            int[] arr = new int[queSize];
            ListNode pointer = front;
            for (int i = 0; i < queSize; i++) {
                arr[i] = pointer.val;
                pointer = pointer.next; // 指针后移
            }
            return arr;
        }
    }
    
    public class linkedlist_deque {
        public static void main(String[] args) {
            /* 初始化双向队列 */
            LinkedListDeque deque = new LinkedListDeque();
            deque.pushLast(3);
            deque.pushLast(2);
            deque.pushLast(5);
            System.out.println("双向队列 deque = " + Arrays.toString(deque.toArray()));
    
            /* 访问元素 */
            int peekFirst = deque.peekFirst();
            System.out.println("队首元素 peekFirst = " + peekFirst);
            int peekLast = deque.peekLast();
            System.out.println("队尾元素 peekLast = " + peekLast);
    
            /* 元素入队 */
            deque.pushLast(4);
            System.out.println("元素 4 队尾入队后 deque = " + Arrays.toString(deque.toArray()));
            deque.pushFirst(1);
            System.out.println("元素 1 队首入队后 deque = " + Arrays.toString(deque.toArray()));
    
            /* 元素出队 */
            int popLast = deque.popLast();
            System.out.println("队尾出队元素 = " + popLast + ",队尾出队后 deque = " + Arrays.toString(deque.toArray()));
            int popFirst = deque.popFirst();
            System.out.println("队首出队元素 = " + popFirst + ",队首出队后 deque = " + Arrays.toString(deque.toArray()));
    
            /* 获取双向队列的长度 */
            int size = deque.size();
            System.out.println("双向队列长度 size = " + size);
    
            /* 判断双向队列是否为空 */
            boolean isEmpty = deque.isEmpty();
            System.out.println("双向队列是否为空 = " + isEmpty);
        }
    }
    

基于环形数组的实现

package com.liucc.chapter_stack_and_queue;

import java.util.Arrays;

/* 基于环形数组实现的双向队列 */
class ArrayDeque {
    private int[] nums; // 用于存储双向队列元素
    private int front; // 计算队首元素索引
    private int queSize; // 队列长度

    // 构造器
    public ArrayDeque(int capacity) {
        nums = new int[capacity];
    }

    // 队列容量
    public int capacity() {
        return nums.length;
    }
    public int size(){
        return queSize;
    }

    // 队列是否为空
    public boolean isEmpty() {
        return queSize == 0;
    }

    // 计算环形数组的索引
    public int index(int i) {
        return (i + capacity()) % capacity();
    }

    // 访问队首元素
    public int peekFirst() {
        if (isEmpty()) {
            throw new IndexOutOfBoundsException();
        }
        return nums[front];
    }

    // 访问队尾元素
    public int peekLast(){
        if (isEmpty()) {
            throw new IndexOutOfBoundsException();
        }
        // 计算队尾元素位置
        int rearIdx = index(front+queSize-1);
        return nums[rearIdx];
    }
    // 队首入队
    public void pushFirst(int num){
        if (queSize == capacity()) {
            System.out.println("双向队列已满");
            return;
        }
        // 计算新头指针索引位置
         front = index(front-1);
        nums[front] = num;
        queSize++;
    }
    // 队尾入队
    public void pushLast(int num){
        if (queSize == capacity()) {
            System.out.println("双向队列已满");
            return;
        }
        // 计算尾指针索引位置
        int idx = index(front+queSize);
        nums[idx] = num;
        queSize++;
    }
    // 队首出队
    public int popFirst(){
        if (isEmpty()) {
            throw new IndexOutOfBoundsException();
        }
        int val = nums[front];
        front = index(front+1);
        queSize--;
        return val;
    }
    // 队尾出队
    public int popLast(){
        if (isEmpty()) {
            throw new IndexOutOfBoundsException();
        }
        int val = peekLast();
        queSize--;
        return val;
    }
    // 转换为数组
    public int[] toArray(){
        int[] arr = new int[queSize];
        int idx = front;
        for (int i = 0; i < queSize; i++) {
            arr[i] = nums[idx];
            idx = index(idx+1);
        }
        return arr;
    }
}

public class array_deque {
    public static void main(String[] args) {
         /* 初始化双向队列 */
         ArrayDeque deque = new ArrayDeque(10);
         deque.pushLast(3);
         deque.pushLast(2);
         deque.pushLast(5);
         System.out.println("双向队列 deque = " + Arrays.toString(deque.toArray()));
 
         /* 访问元素 */
         int peekFirst = deque.peekFirst();
         System.out.println("队首元素 peekFirst = " + peekFirst);
         int peekLast = deque.peekLast();
         System.out.println("队尾元素 peekLast = " + peekLast);
 
         /* 元素入队 */
         deque.pushLast(4);
         System.out.println("元素 4 队尾入队后 deque = " + Arrays.toString(deque.toArray()));
         deque.pushFirst(1);
         System.out.println("元素 1 队首入队后 deque = " + Arrays.toString(deque.toArray()));
 
         /* 元素出队 */
         int popLast = deque.popLast();
         System.out.println("队尾出队元素 = " + popLast + ",队尾出队后 deque = " + Arrays.toString(deque.toArray()));
         int popFirst = deque.popFirst();
         System.out.println("队首出队元素 = " + popFirst + ",队首出队后 deque = " + Arrays.toString(deque.toArray()));
 
         /* 获取双向队列的长度 */
         int size = deque.size();
         System.out.println("双向队列长度 size = " + size);
 
         /* 判断双向队列是否为空 */
         boolean isEmpty = deque.isEmpty();
         System.out.println("双向队列是否为空 = " + isEmpty);
    }
}

3.3.5 应用场景

双向队列兼具栈与队列的逻辑,因此它可以实现这两者的所有应用场景,同时提供更高的自由度

我们知道,软件的“撤销”功能通常使用栈来实现:系统将每次更改操作 push 到栈中,然后通过 pop 实现撤销。然而,考虑到系统资源的限制,软件通常会限制撤销的步数(例如仅允许保存 50 步)。当栈的长度超过 50 时,软件需要在栈底(队首)执行删除操作。但栈无法实现该功能,此时就需要使用双向队列来替代栈。请注意,“撤销”的核心逻辑仍然遵循栈的先入后出原则,只是双向队列能够更加灵活地实现一些额外逻辑。