算法理解(四)栈和队列

一、是什么

1.1 栈

栈(Stack)是一种线性数据结构,具有“后进先出(LIFO, Last In, First Out)”的特性。栈就像是一种只能在一端进行插入和删除操作的容器,通常这一端被称为栈顶(Top),另一端被称为栈底(Bottom)

栈的特点

  1. 后进先出(LIFO):栈遵循后进先出的原则。也就是说,最后压入栈的元素最先被弹出,最先压入栈的元素最后被弹出。就像一摞盘子一样,最后放上去的盘子必须先拿下来。

  2. 单端操作:所有的插入(push)和删除(pop)操作都只能在栈顶进行,不允许在栈底进行。

  3. 动态变化:栈的大小可以根据需要动态变化,不像数组那样大小固定。

  4. 操作时间复杂度:栈的主要操作(如pushpop)的时间复杂度都是 O(1)O(1)O(1),因为这些操作只涉及对栈顶元素的插入和删除。

栈的基本操作

  1. Push(入栈):将一个元素压入栈顶。例如,push(x)将元素 x 压入栈中。

  2. Pop(出栈):从栈顶移除一个元素,并返回该元素。例如,pop() 将栈顶的元素弹出。

  3. Peek(查看栈顶元素):返回栈顶的元素,但不移除它。例如,peek()top() 可以查看当前栈顶元素的值。

  4. isEmpty(判空):检查栈是否为空。如果栈中没有元素,则返回 True,否则返回 False

  5. Size(栈的大小):返回栈中元素的个数。

栈的性质

  1. 有序性:栈中的元素按插入顺序排列,但访问顺序是反的(后进先出)。

  2. 封闭性:除了栈顶元素,无法直接访问栈中的其他元素。

  3. 空间效率:栈的存储空间可以是静态的(如数组实现)或者动态的(如链表实现)。链表实现的栈可以动态扩展,但需要额外的指针存储空间。

栈的应用

栈在计算机科学中有广泛的应用,特别是在需要回溯、递归、路径搜索等场景中:

  1. 表达式求值:例如,中缀表达式转后缀表达式以及后缀表达式的求值。

  2. 函数调用:在程序执行过程中,函数调用是通过栈实现的。每当一个函数被调用时,当前的执行状态会被压入栈中,待函数执行完毕后再恢复。

  3. 括号匹配:用栈来检查表达式中的括号是否匹配。

  4. 浏览器的前进和后退:浏览器历史记录是用两个栈来管理的,一个存储前进历史,一个存储后退历史。

  5. 深度优先搜索(DFS):在图和树的遍历中,栈用于保存访问路径。

栈的实现方式

  1. 数组实现:使用固定大小的数组来实现栈,简单高效,但栈的容量是固定的,需要预先设定。

  2. 链表实现:使用链表来实现栈,栈的容量可以动态变化,但需要额外的指针来管理链表节点,稍微增加了内存使用。

1.2队列

队列(Queue)是一种线性数据结构,遵循“先进先出(FIFO, First In, First Out)”的原则。队列就像是一种只能在一端插入、在另一端删除的容器,常用于模拟排队等候的场景。

队列的特点

  1. 先进先出(FIFO):队列遵循先进先出的原则。也就是说,最早进入队列的元素将最先被移除,最后进入的元素最后被移除。可以将队列想象成一条排队等待的队伍,排在最前面的最先离开。

  2. 双端操作:队列的插入(入队)操作发生在队尾(Rear),删除(出队)操作发生在队头(Front)。

  3. 动态变化:队列的大小可以根据需要动态变化,不像数组那样大小固定。

  4. 操作时间复杂度:队列的主要操作(如 enqueuedequeue)的时间复杂度都是 O(1)O(1)O(1),因为这些操作只涉及在队尾插入和在队头删除。

队列的基本操作

  1. Enqueue(入队):将一个元素插入到队列的队尾。例如,enqueue(x) 将元素 x 添加到队列末尾。

  2. Dequeue(出队):从队列的队头移除一个元素,并返回该元素。例如,dequeue() 将队列的第一个元素移除并返回。

  3. Front(查看队头元素):返回队列的队头元素,但不移除它。例如,front() 可以查看当前队头的元素。

  4. isEmpty(判空):检查队列是否为空。如果队列中没有元素,则返回 True,否则返回 False

  5. Size(队列的大小):返回队列中元素的个数。

队列的性质

  1. 有序性:队列中的元素按照它们进入队列的顺序排列,先入先出。

  2. 封闭性:只能通过队头访问最早进入的元素,通过队尾添加新元素,无法直接访问队列中的其他元素。

  3. 空间效率:队列的存储可以是静态的(如数组实现)或者动态的(如链表实现)。链表实现的队列可以动态扩展,但需要额外的指针存储空间。

队列的应用

队列在计算机科学中有很多应用场景,特别是在任务调度、数据缓冲、异步数据传输等场景中:

  1. 任务调度:操作系统的任务调度、CPU执行进程的管理通常使用队列结构。

  2. 广度优先搜索(BFS):在图和树的遍历中,使用队列来存储访问路径。

  3. 缓冲区(Buffer):队列用于处理数据流,保证数据的顺序处理。例如,在输入输出缓冲区、打印队列中,数据按顺序进入和处理。

  4. 异步数据传输:队列用于在异步系统中传递消息或数据,以确保数据按顺序传输和处理。

  5. 操作撤销(Undo)机制:在某些应用中,撤销操作会使用双向队列(Deque)来记录历史操作。

队列的种类

  1. 普通队列(Simple Queue):标准的FIFO队列。

  2. 循环队列(Circular Queue):队列的一个变种,最后一个位置指向第一个位置,形成一个循环结构,解决了普通队列因出队操作而浪费存储空间的问题。

  3. 优先队列(Priority Queue):每个元素都有优先级,出队时优先级最高的元素先出队,而不是按照先入先出的顺序。常用于任务调度和路径规划。

  4. 双端队列(Deque, Double-ended Queue):允许在队列的两端进行插入和删除操作,即可以在队头和队尾都进行入队和出队。

队列的实现方式

  1. 数组实现:使用固定大小的数组来实现队列,简单高效,但队列的容量是固定的,需要预先设定。使用数组时需要处理好队列的“环绕”问题(可以通过循环队列解决)。

  2. 链表实现:使用链表来实现队列,队列的容量可以动态变化,但需要额外的指针来管理链表节点,稍微增加了内存使用。

二、为什么

2.1 栈

栈(Stack)作为一种重要的数据结构,主要因为其独特的“后进先出(LIFO)”特性而在计算机科学中具有广泛的应用。它在特定场景中提供了其他数据结构(如数组、链表、队列)不具备的优势。

为什么需要栈数据结构?

栈提供了一种简单而有效的方式来处理一系列按顺序执行的任务,尤其是在需要回溯、递归、表达式求值和临时存储的场景中。以下是一些需要使用栈的原因:

  1. 管理函数调用

    • 在程序执行过程中,函数调用的管理是通过栈来实现的。每当一个函数被调用时,当前执行状态(如返回地址和局部变量)会被压入栈中。当函数返回时,状态从栈中弹出并恢复。这样的机制使得递归调用能够正确工作。
  2. 表达式求值与语法解析

    • 栈在中缀表达式转后缀表达式、后缀表达式求值、语法解析等场景中有广泛应用。例如,在编译器中,栈用于解析表达式和语法树。
  3. 括号匹配和校验

    • 检查括号匹配(如 (){}[])的正确性是栈的经典应用之一。当遇到一个左括号时,将其压入栈中;当遇到一个右括号时,从栈中弹出一个左括号并检查匹配性。这样可以轻松验证表达式中的括号是否正确匹配。
  4. 深度优先搜索(DFS)和图遍历

    • 在图论算法中,深度优先搜索(DFS)可以使用栈来存储访问路径,从而避免使用递归的方式导致栈溢出问题。
  5. 浏览器的前进后退功能

    • 栈可以用来实现浏览器的前进和后退功能。当前页面被压入栈中,前进和后退操作可以依靠栈的后进先出特性来实现。

栈与其他数据结构的比较和优势

相比其他常见的数据结构(如数组、链表、队列等),栈在以下场景中具有特定的优势:

  1. 与数组的比较

    • 数组是一种基于索引的随机访问数据结构,可以通过下标快速访问任意元素(时间复杂度为 O(1)O(1)O(1))。然而,数组在处理需要“后进先出”访问模式的问题时并不高效,例如在函数调用栈中。
    • 在LIFO场景中比数组更加高效,插入和删除操作的时间复杂度均为 O(1)O(1)O(1),而数组在末尾插入或删除操作需要移动元素(特别是在较大的数组中),导致时间复杂度可能为 O(n)O(n)O(n)。
  2. 与链表的比较

    • 链表是一种灵活的动态数据结构,可以在任意位置进行插入和删除。与栈相比,链表可以在头部、尾部或者中间进行插入和删除。
    • 的优势在于其实现简单且专注于LIFO操作,而链表实现的栈(即单链表或双链表)会增加指针管理的复杂性,消耗更多的内存。
  3. 与队列的比较

    • 队列是一种先进先出(FIFO)数据结构,适用于需要按顺序处理元素的场景,如任务调度、数据缓冲等。
    • 适用于需要反向处理顺序的场景(如回溯算法),提供了一种快速简便的方式来存储和访问数据的最后状态。
  4. 与双端队列(Deque)的比较

    • 双端队列(Deque) 允许在两端插入和删除元素,支持比栈更灵活的操作。
    • 的简单性在于它只允许在一端进行插入和删除,这种限制反而使得栈在需要严格LIFO的场景中表现更加高效和直观。

栈的优势总结

  • 操作简单高效:栈的基本操作(入栈、出栈)都是 O(1)O(1)O(1) 时间复杂度。
  • 空间高效:只需简单的指针操作(或数组索引),没有额外的内存开销(相比链表)。
  • 特定场景的适用性:在函数调用、表达式求值、语法解析、深度优先搜索等场景中具有不可替代的优势。
  • 数据安全性:由于栈只能访问栈顶元素,不允许随机访问,能够提供一定的“封装性”,避免数据的误操作。

栈是一种简单而强大的数据结构,在计算机科学的各个领域发挥着重要作用。其独特的特性使其在特定问题和场景中比其他数据结构更具优势。

2.2 队列

队列(Queue)是一种非常重要的数据结构,其独特的“先进先出(FIFO, First In, First Out)”特性使其在许多计算机科学和工程应用中不可或缺。相比其他数据结构,队列在某些场景中能够更好地解决特定问题。以下是需要使用队列的原因以及它相对于其他数据结构的优势。

为什么需要队列数据结构?

队列是一种适合处理按顺序处理任务的数据结构,特别适用于任务调度、异步数据传输、广度优先搜索(BFS)等场景。以下是一些使用队列的具体原因:

  1. 任务调度与处理顺序管理

    • 在多任务操作系统中,CPU任务调度器会使用队列来管理任务。每个任务在就绪状态时进入队列,按照到达的顺序执行。队列保证了先进入队列的任务先被处理,后进入的任务后处理。
  2. 数据缓冲与异步数据传输

    • 队列用于管理数据流(如I/O数据缓冲区、网络请求队列),确保数据按顺序传输和处理。例如,打印队列会按顺序处理多个打印任务。
  3. 广度优先搜索(BFS)和图遍历

    • 在图或树的遍历中,广度优先搜索(BFS)算法使用队列来跟踪访问节点的顺序,以确保在探索邻居节点之前先完全探索当前节点。
  4. 实现异步通信

    • 队列用于在生产者和消费者模式中实现异步通信。生产者将任务或数据项放入队列,消费者从队列中取出并处理它们。
  5. 多级缓冲

    • 在CPU和硬盘之间的数据传输中,队列用于多级缓冲。队列在不同速度的设备之间起到缓冲作用,以避免速度不匹配引起的数据丢失或堵塞。

队列与其他数据结构的比较和优势

相比其他常见的数据结构(如数组、链表、栈等),队列在处理需要按顺序处理的数据或任务时具有特定的优势:

  1. 与数组的比较

    • 数组允许随机访问元素,但不适合频繁的插入和删除操作。数组的插入和删除操作(特别是在头部插入或删除)通常需要移动大量元素,时间复杂度为 O(n)O(n)O(n)。
    • 队列在头部删除和尾部插入的时间复杂度为 O(1)O(1)O(1)。这使得队列非常适合处理需要频繁插入和删除的数据流。
  2. 与链表的比较

    • 链表在任意位置的插入和删除操作都很高效,但它不提供FIFO顺序管理。单链表也不支持从尾部高效地插入或删除操作。
    • 队列(特别是用双向链表实现的队列)可以高效地进行在尾部插入和头部删除操作,同时维护FIFO顺序。
  3. 与栈的比较

    • 是一种LIFO(后进先出)结构,适用于需要反向处理顺序的数据或任务(如递归、表达式求值)。
    • 队列则适用于需要按照进入顺序依次处理的场景(如任务调度、数据缓冲)。它保证了先进先出的特性,提供了栈无法实现的顺序访问。
  4. 与双端队列(Deque)的比较

    • **双端队列(Deque)**允许在两端进行插入和删除操作,比普通队列更灵活。
    • 队列则更简单,仅允许在一端插入,另一端删除。这种限制使得它更专注于FIFO操作,应用程序逻辑更清晰。

队列的优势总结

  • 操作简单高效:队列的 enqueue(入队)和 dequeue(出队)操作都在常数时间 O(1)O(1)O(1) 内完成,特别适合频繁插入和删除的场景。
  • 顺序处理:队列维护了元素的进入顺序,非常适合按顺序处理任务或数据的场景,如任务调度、数据流处理。
  • 资源管理与调度:在多任务处理、I/O处理等系统级应用中,队列能够有效地管理资源和任务调度,提供公平性和效率。
  • 应用广泛:队列适用于多个领域,如图遍历(BFS)、生产者-消费者模型、打印任务管理、网络请求管理等,应用场景非常广泛。
  • 避免数据丢失:在网络传输、操作系统调度等场景中,使用队列可以避免因设备速度不匹配而导致的数据丢失。

总之,队列是一种简单而高效的数据结构,在需要按照顺序管理数据和任务的场景中具有不可替代的优势。

三、怎么办

3.1 栈

(一)栈的实现

用数组实现栈
class ArrayStack {
    private int maxSize; // 栈的最大容量
    private int[] stack; // 数组用于存储栈元素
    private int top;     // 栈顶指针

    // 构造函数,初始化栈
    public ArrayStack(int size) {
        this.maxSize = size;
        this.stack = new int[maxSize];
        this.top = -1; // 栈顶指针初始化为-1,表示栈为空
    }

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == -1;
    }

    // 判断栈是否已满
    public boolean isFull() {
        return top == maxSize - 1;
    }

    // 入栈操作
    public void push(int value) {
        if (isFull()) {
            System.out.println("栈已满,无法入栈!");
            return;
        }
        stack[++top] = value; // 将值压入栈顶,栈顶指针上移
    }

    // 出栈操作
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法出栈!");
        }
        return stack[top--]; // 返回栈顶元素,栈顶指针下移
    }

    // 查看栈顶元素
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法查看栈顶元素!");
        }
        return stack[top];
    }

    // 打印栈元素
    public void printStack() {
        if (isEmpty()) {
            System.out.println("栈为空!");
            return;
        }
        System.out.println("栈元素:");
        for (int i = top; i >= 0; i--) {
            System.out.println(stack[i]);
        }
    }

    public static void main(String[] args) {
        ArrayStack stack = new ArrayStack(5); // 创建一个栈,最大容量为5

        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.printStack(); // 输出:栈元素:3, 2, 1

        System.out.println("出栈元素:" + stack.pop()); // 输出:3
        System.out.println("栈顶元素:" + stack.peek()); // 输出:2
        stack.printStack(); // 输出:栈元素:2, 1
    }
}
用链表实现栈
class LinkedStack {
    private class Node { // 节点类
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
        }
    }

    private Node top; // 栈顶指针

    // 判断栈是否为空
    public boolean isEmpty() {
        return top == null;
    }

    // 入栈操作
    public void push(int value) {
        Node newNode = new Node(value); // 创建新节点
        newNode.next = top;             // 将新节点的next指向当前栈顶
        top = newNode;                  // 更新栈顶指针为新节点
    }

    // 出栈操作
    public int pop() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法出栈!");
        }
        int value = top.data; // 获取栈顶元素
        top = top.next;       // 将栈顶指针指向下一个节点
        return value;
    }

    // 查看栈顶元素
    public int peek() {
        if (isEmpty()) {
            throw new RuntimeException("栈为空,无法查看栈顶元素!");
        }
        return top.data;
    }

    // 打印栈元素
    public void printStack() {
        if (isEmpty()) {
            System.out.println("栈为空!");
            return;
        }
        System.out.println("栈元素:");
        Node current = top;
        while (current != null) {
            System.out.println(current.data);
            current = current.next;
        }
    }

    public static void main(String[] args) {
        LinkedStack stack = new LinkedStack(); // 创建一个链表实现的栈

        stack.push(1);
        stack.push(2);
        stack.push(3);
        stack.printStack(); // 输出:栈元素:3, 2, 1

        System.out.println("出栈元素:" + stack.pop()); // 输出:3
        System.out.println("栈顶元素:" + stack.peek()); // 输出:2
        stack.printStack(); // 输出:栈元素:2, 1
    }
}

(二)栈的题目

        算法题中对栈的考察通常利用其“后进先出”(LIFO, Last In First Out)特性来解决各种问题。以下是一些经典的算法题目,它们可以很好地测试对栈的理解和应用。

1. 有效的括号

题目描述: 给定一个只包含字符 (, ), {, }, [] 的字符串,判断输入字符串是否有效。有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
import java.util.Stack;

public class ValidParentheses {
    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<>(); // 创建一个栈用于存储括号
        for (char c : s.toCharArray()) { // 遍历字符串中的每个字符
            if (c == '(') {  // 如果是左括号,压入栈中
                stack.push(')');
            } else if (c == '{') {
                stack.push('}');
            } else if (c == '[') {
                stack.push(']');
            } 
            // 如果是右括号,检查栈是否为空或栈顶元素是否与之匹配
            else if (stack.isEmpty() || stack.pop() != c) {
                return false; // 不匹配则返回false
            }
        }
        return stack.isEmpty(); // 栈为空表示所有括号匹配成功
    }

    public static void main(String[] args) {
        ValidParentheses vp = new ValidParentheses();
        System.out.println(vp.isValid("(){}[]")); // 输出:true
        System.out.println(vp.isValid("([{}])")); // 输出:true
        System.out.println(vp.isValid("({[)]}")); // 输出:false
        System.out.println(vp.isValid("({})["));  // 输出:false
    }
}
2. 最小栈(Min Stack)

题目描述: 设计一个支持 push()pop()top()getMin() 操作的栈,并且 getMin() 操作总是在常数时间内完成。

import java.util.Stack;

class MinStack {
    private Stack<Integer> stack;     // 正常栈
    private Stack<Integer> minStack;  // 最小值栈

    /** 初始化栈 */
    public MinStack() {
        stack = new Stack<>();
        minStack = new Stack<>();
    }

    /** 入栈操作 */
    public void push(int x) {
        stack.push(x); // 将元素压入正常栈
        if (minStack.isEmpty() || x <= minStack.peek()) {
            minStack.push(x); // 如果最小值栈为空或新元素小于等于最小值栈的栈顶,则压入最小值栈
        }
    }

    /** 出栈操作 */
    public void pop() {
        if (stack.pop().equals(minStack.peek())) {
            minStack.pop(); // 如果弹出的元素等于最小值栈的栈顶,则也从最小值栈中弹出
        }
    }

    /** 获取栈顶元素 */
    public int top() {
        return stack.peek();
    }

    /** 获取最小值 */
    public int getMin() {
        return minStack.peek();
    }

    public static void main(String[] args) {
        MinStack minStack = new MinStack();
        minStack.push(-2);
        minStack.push(0);
        minStack.push(-3);
        System.out.println(minStack.getMin()); // 输出: -3
        minStack.pop();
        System.out.println(minStack.top());    // 输出: 0
        System.out.println(minStack.getMin()); // 输出: -2
    }
}
3. 逆波兰表达式求值(Evaluate Reverse Polish Notation)

题目描述: 根据逆波兰表示法(后缀表达式)求表达式的值。有效的操作符包括 +, -, *, /。每个操作数可以是整数,也可以是另一个表达式。

import java.util.Stack;

public class EvaluateRPN {
    public int evalRPN(String[] tokens) {
        Stack<Integer> stack = new Stack<>();
        for (String token : tokens) { // 遍历每个字符串
            if ("+-*/".contains(token)) { // 如果是操作符,弹出两个栈顶元素进行操作
                int b = stack.pop();
                int a = stack.pop();
                switch (token) {
                    case "+":
                        stack.push(a + b);
                        break;
                    case "-":
                        stack.push(a - b);
                        break;
                    case "*":
                        stack.push(a * b);
                        break;
                    case "/":
                        stack.push(a / b);
                        break;
                }
            } else { // 如果是数字,则直接压入栈中
                stack.push(Integer.parseInt(token));
            }
        }
        return stack.pop(); // 返回栈顶元素
    }

    public static void main(String[] args) {
        EvaluateRPN rpn = new EvaluateRPN();
        String[] tokens = {"2", "1", "+", "3", "*"};
        System.out.println(rpn.evalRPN(tokens)); // 输出:9 ( (2 + 1) * 3 )

        String[] tokens2 = {"4", "13", "5", "/", "+"};
        System.out.println(rpn.evalRPN(tokens2)); // 输出:6 ( 4 + (13 / 5) )
    }
}

4. 用栈实现队列

题目描述: 用栈实现队列的操作。实现 MyQueue 类:

  • void push(int x):将元素 x 推到队列的末尾。
  • int pop():从队列的开头移除并返回元素。
  • int peek():返回队列开头的元素。
  • boolean empty():如果队列为空,返回 true;否则返回 false
import java.util.Stack;

class MyQueue {
    private Stack<Integer> stackIn;  // 输入栈
    private Stack<Integer> stackOut; // 输出栈

    /** 初始化队列 */
    public MyQueue() {
        stackIn = new Stack<>();
        stackOut = new Stack<>();
    }

    /** 入队操作 */
    public void push(int x) {
        stackIn.push(x); // 元素入输入栈
    }

    /** 出队操作 */
    public int pop() {
        if (stackOut.isEmpty()) { // 如果输出栈为空,将输入栈中的所有元素弹出并压入输出栈
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.pop(); // 从输出栈中弹出元素
    }

    /** 获取队首元素 */
    public int peek() {
        if (stackOut.isEmpty()) { // 如果输出栈为空,将输入栈中的所有元素弹出并压入输出栈
            while (!stackIn.isEmpty()) {
                stackOut.push(stackIn.pop());
            }
        }
        return stackOut.peek(); // 返回输出栈的栈顶元素
    }

    /** 判断队列是否为空 */
    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }

    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        queue.push(1);
        queue.push(2);
        System.out.println(queue.peek());  // 输出: 1
        System.out.println(queue.pop());   // 输出: 1
        System.out.println(queue.empty()); // 输出: false
    }
}
5. 每日温度(Daily Temperatures)

题目描述: 根据每日气温列表 T,请重新生成一个列表,要求其对应位置的输出是需要再等待多少天才能等到一个更暖和的气温。如果气温在这之后都不会升高,请在该位置用 0 来代替。

import java.util.Stack;

public class DailyTemperatures {
    public int[] dailyTemperatures(int[] T) {
        int[] result = new int[T.length];
        Stack<Integer> stack = new Stack<>(); // 栈存储温度索引

        for (int i = 0; i < T.length; i++) {
            while (!stack.isEmpty() && T[i] > T[stack.peek()]) { // 当当前温度高于栈顶温度
                int idx = stack.pop();
                result[idx] = i - idx; // 计算天数差
            }
            stack.push(i); // 压入当前索引
        }
        return result;
    }

    public static void main(String[] args) {
        DailyTemperatures dt = new DailyTemperatures();
        int[] temperatures = {73, 74, 75, 71, 69, 72, 76, 73};
        int[] result

3.2 队列

(一)队列的实现

在Java中,可以用数组或链表来实现队列。队列遵循“先进先出”(FIFO, First In First Out)原则,下面是用数组和链表分别实现队列的代码示例。

1. 用数组实现队列

使用数组实现队列需要管理头指针(front)和尾指针(rear),以及一个容量(capacity)来防止溢出。

class ArrayQueue {
    private int[] queue;   // 数组用于存储队列元素
    private int front;     // 队列头指针
    private int rear;      // 队列尾指针
    private int size;      // 队列当前大小
    private int capacity;  // 队列容量

    // 构造函数,初始化队列
    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        queue = new int[capacity];
        front = 0;
        rear = -1;
        size = 0;
    }

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

    // 判断队列是否已满
    public boolean isFull() {
        return size == capacity;
    }

    // 入队操作
    public void enqueue(int value) {
        if (isFull()) {
            System.out.println("队列已满,无法入队!");
            return;
        }
        rear = (rear + 1) % capacity; // 循环使用数组空间
        queue[rear] = value;          // 将元素放入队尾
        size++;                       // 更新队列大小
    }

    // 出队操作
    public int dequeue() {
        if (isEmpty()) {
            System.out.println("队列为空,无法出队!");
            return -1;
        }
        int value = queue[front];      // 获取队首元素
        front = (front + 1) % capacity; // 移动队首指针
        size--;                        // 更新队列大小
        return value;
    }

    // 获取队首元素
    public int peek() {
        if (isEmpty()) {
            System.out.println("队列为空,无法查看队首元素!");
            return -1;
        }
        return queue[front];
    }

    public static void main(String[] args) {
        ArrayQueue queue = new ArrayQueue(5);

        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);
        queue.enqueue(40);
        queue.enqueue(50);

        System.out.println("队首元素: " + queue.peek()); // 输出:10

        System.out.println("出队元素: " + queue.dequeue()); // 输出:10
        System.out.println("出队元素: " + queue.dequeue()); // 输出:20

        queue.enqueue(60); // 入队 60

        System.out.println("队首元素: " + queue.peek()); // 输出:30
    }
}

2. 用链表实现队列

使用链表实现队列更为灵活,不需要预先确定队列的大小。可以使用一个单链表,其中有一个指向头节点(head)和一个指向尾节点(tail)的指针。

class LinkedQueue {
    private class Node { // 节点类
        int data;
        Node next;

        public Node(int data) {
            this.data = data;
            this.next = null;
        }
    }

    private Node front; // 队首指针
    private Node rear;  // 队尾指针

    // 构造函数,初始化队列
    public LinkedQueue() {
        front = null;
        rear = null;
    }

    // 判断队列是否为空
    public boolean isEmpty() {
        return front == null;
    }

    // 入队操作
    public void enqueue(int value) {
        Node newNode = new Node(value); // 创建一个新节点
        if (rear == null) {             // 如果队列为空
            front = rear = newNode;     // 新节点为队首和队尾
        } else {
            rear.next = newNode;        // 否则添加到队尾
            rear = newNode;             // 更新队尾指针
        }
    }

    // 出队操作
    public int dequeue() {
        if (isEmpty()) {
            System.out.println("队列为空,无法出队!");
            return -1;
        }
        int value = front.data;   // 获取队首元素
        front = front.next;       // 移动队首指针
        if (front == null) {      // 如果出队后队列为空
            rear = null;          // 队尾也指向null
        }
        return value;
    }

    // 获取队首元素
    public int peek() {
        if (isEmpty()) {
            System.out.println("队列为空,无法查看队首元素!");
            return -1;
        }
        return front.data;
    }

    public static void main(String[] args) {
        LinkedQueue queue = new LinkedQueue();

        queue.enqueue(10);
        queue.enqueue(20);
        queue.enqueue(30);

        System.out.println("队首元素: " + queue.peek()); // 输出:10

        System.out.println("出队元素: " + queue.dequeue()); // 输出:10
        System.out.println("出队元素: " + queue.dequeue()); // 输出:20

        queue.enqueue(40); // 入队 40

        System.out.println("队首元素: " + queue.peek()); // 输出:30
    }
}

总结

这两种实现方式有各自的优缺点:

  • 数组实现队列:适合需要固定大小的队列,具有O(1)时间复杂度的入队和出队操作,但可能会浪费空间。
  • 链表实现队列:没有大小限制,内存使用更为灵活,但需要更多的内存空间来存储节点的指针。

这两种方法都遵循了队列的基本操作和原则,适用于不同的使用场景。

(二)队列的题目

队列在算法题中通常用来处理需要按顺序处理的数据,特别是那些需要“先进先出”(FIFO, First In First Out)顺序的问题。下面是一些经典的算法题目,它们考察队列的应用。

1. 用队列实现栈

题目描述: 使用队列来实现栈的基本操作:push(), pop(), top()empty()。要求使用两个队列来实现。

import java.util.LinkedList;
import java.util.Queue;

class MyStack {
    private Queue<Integer> queue1;
    private Queue<Integer> queue2;

    /** 初始化栈 */
    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    /** 入栈操作 */
    public void push(int x) {
        queue1.offer(x); // 将元素加入队列1
        while (!queue2.isEmpty()) {
            queue1.offer(queue2.poll()); // 将队列2中的元素加入队列1
        }
        // 交换队列1和队列2的引用
        Queue<Integer> temp = queue1;
        queue1 = queue2;
        queue2 = temp;
    }

    /** 出栈操作 */
    public int pop() {
        return queue2.poll(); // 从队列2中弹出栈顶元素
    }

    /** 获取栈顶元素 */
    public int top() {
        return queue2.peek(); // 查看队列2中的栈顶元素
    }

    /** 判断栈是否为空 */
    public boolean empty() {
        return queue2.isEmpty();
    }

    public static void main(String[] args) {
        MyStack stack = new MyStack();
        stack.push(1);
        stack.push(2);
        System.out.println(stack.top());  // 输出: 2
        System.out.println(stack.pop());  // 输出: 2
        System.out.println(stack.empty()); // 输出: false
    }
}
2. 滑动窗口最大值(Sliding Window Maximum)

题目描述: 给定一个整数数组 nums 和一个滑动窗口大小 k,请你在每个滑动窗口中找出最大值。返回一个包含每个滑动窗口的最大值的数组。

import java.util.Deque;
import java.util.LinkedList;

public class SlidingWindowMaximum {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if (nums == null || nums.length == 0) return new int[0];

        int n = nums.length;
        int[] result = new int[n - k + 1];
        Deque<Integer> deque = new LinkedList<>(); // 存储数组的索引

        for (int i = 0; i < n; i++) {
            // 移除不在滑动窗口内的元素
            if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
                deque.pollFirst();
            }
            // 移除队列中小于当前元素的值,因为它们不可能成为滑动窗口的最大值
            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }
            // 将当前元素的索引加入队列
            deque.offerLast(i);
            // 从滑动窗口开始时,更新结果数组
            if (i >= k - 1) {
                result[i - k + 1] = nums[deque.peekFirst()];
            }
        }
        return result;
    }

    public static void main(String[] args) {
        SlidingWindowMaximum swm = new SlidingWindowMaximum();
        int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
        int k = 3;
        int[] result = swm.maxSlidingWindow(nums, k);
        for (int val : result) {
            System.out.print(val + " "); // 输出: 3 3 5 5 6 7
        }
    }
}
3. 按层遍历二叉树(Level Order Traversal)

题目描述: 给定一个二叉树,返回其按层次遍历的节点值。即从根节点开始,每一层的节点值从左到右顺序输出。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class LevelOrderTraversal {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> result = new ArrayList<>();
        if (root == null) return result;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            List<Integer> level = new ArrayList<>();
            int size = queue.size();

            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
            result.add(level);
        }
        return result;
    }

    public static void main(String[] args) {
        // 测试二叉树
        TreeNode root = new TreeNode(3);
        root.left = new TreeNode(9);
        root.right = new TreeNode(20);
        root.right.left = new TreeNode(15);
        root.right.right = new TreeNode(7);

        LevelOrderTraversal lot = new LevelOrderTraversal();
        List<List<Integer>> result = lot.levelOrder(root);
        for (List<Integer> level : result) {
            System.out.println(level);
        }
    }

    // 二叉树节点类
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }
}
4. 二叉树的右视图(Binary Tree Right Side View)

题目描述: 给定一个二叉树,返回其从右侧看每一层的节点值。即每层最右边的节点值。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class BinaryTreeRightSideView {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (i == size - 1) {
                    result.add(node.val); // 记录当前层最右边的节点
                }
                if (node.left != null) queue.offer(node.left);
                if (node.right != null) queue.offer(node.right);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        // 测试二叉树
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.right = new TreeNode(5);
        root.right.right = new TreeNode(4);

        BinaryTreeRightSideView brsv = new BinaryTreeRightSideView();
        List<Integer> result = brsv.rightSideView(root);
        System.out.println(result); // 输出: [1, 3, 4]
    }

    // 二叉树节点类
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }
}
5. 编码解码字符串(Encode and Decode Strings)

题目描述: 设计一个算法来对字符串进行编码和解码。编码格式是:每个字符串前面加上其长度。

import java.util.ArrayList;
import java.util.List;

public class EncodeDecodeStrings {
    // 编码
    public String encode(List<String> strs) {
        StringBuilder encodedString = new StringBuilder();
        for (String str : strs) {
            encodedString.append(str.length()).append('#').append(str);
        }
        return encodedString.toString();
    }

    // 解码
    public List<String> decode(String s) {
        List<String> decodedStrings = new ArrayList<>();
        int i = 0;
        while (i < s.length()) {
            int delimiterIndex = s.indexOf('#', i);
            int length = Integer.parseInt(s.substring(i, delimiterIndex));
            i = delimiterIndex + 1;
            decodedStrings.add(s.substring(i, i + length));
            i += length;
        }
        return decodedStrings;
    }

    public static void main(String[] args) {
        EncodeDecodeStrings codec = new EncodeDecodeStrings();
        List<String> strs = List.of("hello", "world");
        String encoded = codec.encode(strs);
        System.out.println("Encoded: " + encoded);

        List<String> decoded = codec.decode(encoded);
        System.out.println("Decoded: " + decoded);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值