12.【入门】队列和栈-链表、数组实现

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解013【入门】队列和栈-链表、数组实现_哔哩哔哩_bilibili

一、队列

定义

队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构。它允许在队尾进行插入操作,并在队头进行删除操作。队列的常见操作包括入队(enqueue)和出队(dequeue)。

操作

1. 入队(Enqueue)
  • 作用:将一个元素插入到队列的末尾(队尾)。
  • 示例
    • 队列初始:[A, B, C]
    • 执行入队操作插入 D[A, B, C, D]

2. 出队(Dequeue)
  • 作用:从队列的前端(队头)移除并返回第一个元素。
  • 示例
    • 队列初始:[A, B, C]
    • 执行出队操作:移除 A,队列变为 [B, C]

3. 查看队头元素(Peek 或 Front)
  • 作用:查看队列的前端元素,但不移除它。
  • 示例
    • 队列初始:[A, B, C]
    • 执行 Peek 操作:返回 A,队列保持不变 [A, B, C]

4. 获取队列长度(Size)
  • 作用:返回队列中当前元素的个数。
  • 示例
    • 队列初始:[A, B, C]
    • 执行 Size 操作:返回 3

5. 判断队列是否为空(IsEmpty)
  • 作用:检查队列是否为空,返回布尔值(true 或 false)。
  • 示例
    • 队列初始:[](空队列)
    • 执行 IsEmpty 操作:返回 true

6. 清空队列(Clear)
  • 作用:移除队列中的所有元素,使其变为空队列。
  • 示例
    • 队列初始:[A, B, C]
    • 执行 Clear 操作:队列变为 []

7. 遍历队列(Traverse)
  • 作用:按顺序访问队列中的每个元素。
  • 示例
    • 队列初始:[A, B, C]
    • 遍历操作:依次访问 ABC

应用场景

1.任务调度与排队系统
  • 操作系统任务调度:操作系统使用队列来管理等待执行的任务或进程,确保按先来后到的顺序执行。
  • 打印任务队列:打印机通常使用队列来管理多个打印任务,按顺序处理每个任务。
  • 银行叫号系统:银行使用队列来管理客户排队顺序,确保先到先服务。

2.广度优先搜索(BFS)
  • 在图遍历算法中,BFS利用队列来记录待访问的节点,按照层次顺序访问节点,确保每一层的所有节点都被访问后,再访问下一层。

3.多线程与并发处理
  • 在多线程环境中,队列用于协调任务的分配和执行顺序,确保线程间有序共享资源,避免竞争条件和死锁。

4.实时系统与资源管理
  • 交通信号灯控制:利用队列管理车辆通行顺序,确保交通流畅。
  • CPU调度:在实时系统中,使用队列来管理任务的执行顺序,确保关键任务按时完成。

5.消息传递与通信系统
  • 消息队列:在分布式系统中,消息队列用于异步传递消息,确保消息按顺序处理,提高系统可靠性和可扩展性。
  • 网络数据包处理:网络设备使用队列来管理接收到的数据包,按顺序处理每个数据包,确保数据传输的正确性和及时性。

6.缓存与内存管理
  • 在缓存替换策略中,使用队列来管理缓存项的访问顺序,确保最久未使用的项被优先替换,提高缓存命中率。

7.图形处理与动画
  • 在图形处理中,队列用于管理绘制命令的执行顺序,确保图形元素按正确顺序呈现,生成平滑的动画效果。

8.编译器与解释器
  • 在语法分析阶段,队列用于管理输入的符号,确保分析过程按顺序进行,正确解析语句结构。

代码实现

  • 普通队列(链表和数组实现)
  • 算法原理
    一、使用Java内部LinkedList实现队列(Queue1
  • 数据结构基础
    • 利用LinkedList来实现队列。LinkedList是一个双向链表结构。虽然单向链表对于队列操作  (先进先出)来说已经足够,但Java中的LinkedList是双向的。
  • 操作原理
    • isEmpty方法:
      • 直接调用LinkedListisEmpty方法,用于判断队列是否为空。其原理是检查链表中是否有节点,时间复杂度为O(1)。
    • offer方法:
      • 调用LinkedListoffer方法,在队列尾部添加元素。在LinkedList中,这是一个常数时间操作(如果不考虑链表可能的扩容等情况),时间复杂度为O(1)。
    • poll方法:
      • 调用LinkedListpoll方法,从队列头部移除并返回元素。在LinkedList中,这也是一个常数时间操作(不考虑链表的特殊情况),时间复杂度为O(1)。
    • peek方法:
      • 调用LinkedListpeek方法,返回队列头部的元素但不移除它。时间复杂度为O(1)。
    • size方法:
      • 调用LinkedListsize方法,返回队列中元素的个数。时间复杂度为O(1)。
    二、使用自定义单向链表实现队列(Queue2
  • 数据结构基础
    • 自定义了一个单向链表结构,其中包含节点类ListNode,每个节点有一个值val和指向下一个节点的引用next
    • 队列有两个指针,front指向队列头部,tail指向队列尾部。
  • 操作原理
    • enqueue方法(入队操作):
      • 如果队列为空(isEmptytrue),则新节点既是头部也是尾部(front = newNode; tail = newNode;)。
      • 如果队列不为空,则将新节点添加到尾部(tail.next = newNode; tail = newNode;),并且队列大小size加1。
    • dequeue方法(出队操作):
      • 取出头部节点的值(int value = front.val;),然后将头部指针指向下一个节点(front = front.next;)。
      • 如果出队后队列为空(front == null),则将尾部指针也设为nulltail = null;),并且队列大小size减1。
    • isEmpty方法:
      • 通过检查队列大小size是否为0来判断队列是否为空,时间复杂度为O(1)。
    • size方法:
      • 直接返回队列大小size,时间复杂度为O(1)。
    • peek方法:
      • 返回队列头部节点的值(front.val),时间复杂度为O(1)。
    三、使用数组实现队列(Queue3
  • 数据结构基础
    • 使用一个数组queue来存储队列元素,有两个指针l(表示队列头部的索引)和r(表示队列尾部的索引)。
  • 操作原理
    • isEmpty方法:
      • 通过比较lr是否相等来判断队列是否为空。如果相等,表示队列为空,时间复杂度为O(1)。
    • offer方法
      • 将元素添加到数组中,索引为r的位置,然后r指针后移一位(queue[r++] = num;)。
    • poll方法
      • 取出索引为l的元素(return queue[l++];),然后l指针后移一位。
    • head方法
      • 返回索引为l的元素,即队列头部元素,时间复杂度为O(1)。
    • tail方法
      • 返回索引为r - 1的元素,即队列尾部元素,时间复杂度为O(1)。
    • size方法
      • 通过计算r - l来得到队列中元素的个数,时间复杂度为O(1)。
package queuestackandcircularqueue;

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


public class QueueDemo {
    // 直接用java内部的实现
    // 其实内部就是双向链表,常数操作慢
    public static class Queue1 {

        // java中的双向链表LinkedList
        // 单向链表就足够了
        public Queue<Integer> queue = new LinkedList<>();

        // 调用任何方法之前,先调用这个方法来判断队列内是否有东西
        public boolean isEmpty() {
            return queue.isEmpty();
        }

        // 向队列中加入num,加到尾巴
        public void offer(int num) {
            queue.offer(num);
        }

        // 从队列拿,从头拿
        public int poll() {
            return queue.poll();
        }

        // 返回队列头的元素但是不弹出
        public int peek() {
            return queue.peek();
        }

        // 返回目前队列里有几个数
        public int size() {
            return queue.size();
        }



        //使用自己定义的单向链表来实现队列
        //定义一个单向链表
        public static class ListNode {
            public int val;
            public ListNode next;

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

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

        public static class Queue2 {
            ListNode front = null;
            ListNode tail = null;
            int size = 0;

            //入队操作
            public void enqueue(int value) {
                ListNode newNode = new ListNode(value);
                if(isEmpty()) {
                    front = newNode;
                    tail = newNode;
                }else {
                    tail.next = newNode;
                    tail = newNode;
                }
                size++;
            }

            //出队操作
            public int dequeue() {
                int value = front.val;
                front = front.next;
                if(front == null) {
                    tail = null;
                }
                size--;
                return value;
            }

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

            //获取队列大小
            public int size() {
                return size;
            }

            //获取队列头部元素,但是不弹出
            public int peek() {
                return front.val;
            }

        }

    }


    // 使用数组实现队列
    // 实际刷题时更常见的写法,常数时间好
    // 如果可以确定加入操作的总次数不超过n,那么可以用
    // 一般笔试、面试都会有一个明确数据量,所以这是最常用的方式
    public static class Queue3 {

        public int[] queue;
        public int l;
        public int r;

        // 加入操作的总次数上限是多少,一定要明确
        public Queue3(int n) {
            queue = new int[n];
            l = 0;
            r = 0;
        }

        // 调用任何方法之前,先调用这个方法来判断队列内是否有东西
        public boolean isEmpty() {
            return l == r;
        }

        public void offer(int num) {
            queue[r++] = num;
        }

        public int poll() {
            return queue[l++];
        }

        public int head() {
            return queue[l];
        }

        public int tail() {
            return queue[r - 1];
        }

        public int size() {
            return r - l;
        }

    }

}
  • 环形队列(数组实现)
  • 算法原理
    • 数据结构基础

      • 数组表示
        • 使用一个数组queue来存储循环队列中的元素。
      • 指针定义
        • 定义了两个指针l(表示队列头部的索引)和r(表示队列尾部的索引),以及一个变量size(表示队列中元素的个数)和limit(表示队列的容量上限)。
    • 操作原理

      • 构造函数:
        • 在构造函数CircularQueue(int k)中,初始化数组queue的大小为k,并将lrsize都初始化为0,同时记录队列容量上限limit = k
      • enQueue方法(入队操作):
        • 首先判断队列是否已满(isFull方法)。
        • 如果队列已满(size == limit),则返回false,不进行任何操作。
        • 如果队列未满,则将元素value放入队列尾部(queue[r] = value)。
        • 然后更新r指针,如果r已经到达数组的末尾(r == limit - 1),则将r设置为0,否则r指针后移一位(r = r == limit - 1 ? 0 : (r + 1);)。
        • 最后队列大小size加1,并返回true
      • deQueue方法(出队操作):
        • 首先判断队列是否为空(isEmpty方法)。
        • 如果队列为空(size == 0),则返回false,不进行任何操作。
        • 如果队列不为空,则更新l指针,如果l已经到达数组的末尾(l == limit - 1),则将l设置为0,否则l指针后移一位(l = l == limit - 1? 0 : (l + 1);)。
        • 队列大小size减1,并返回true
      • Front方法(获取队首元素)
        • 首先判断队列是否为空(isEmpty方法)。
        • 如果队列为空,则返回 -1,表示没有元素。
        • 如果队列不为空,则返回索引为l的元素,即队列头部元素(return queue[l];)。
      • Rear方法(获取队尾元素):
        • 首先判断队列是否为空(isEmpty方法)。
        • 如果队列为空,则返回 -1,表示没有元素。
        • 如果队列不为空,则需要根据r指针的位置来确定队尾元素。
        • 如果r在0位置,说明r已经将整个数组遍历一遍后又跳回0位置,那么上一个添加的数在数组的最后一个位置(limit - 1),即返回queue[limit - 1]
        • 如果r不在0位置,说明数组还没有完全遍历完成,那么上一个添加的数在r的前一个位置(r - 1),即返回queue[r - 1]
      • isEmpty方法:
        • 通过比较size是否为0来判断队列是否为空,时间复杂度为O(1)。
      • isFull方法:
        • 通过比较size是否等于limit来判断队列是否已满,时间复杂度为O(1)。
package queuestackandcircularqueue;

// 设计循环队列
// 测试链接 : https://leetcode.cn/problems/design-circular-queue/
class CircularQueue {

    public int[] queue;

    public int l, r, size, limit;

    // 同时在队列里的数字个数,不要超过k
    public CircularQueue(int k) {
        queue = new int[k];
        l = r = size = 0;
        limit = k;
    }

    // 如果队列满了,什么也不做,返回false
    // 如果队列没满,加入value,返回true
    public boolean enQueue(int value) {
        if (isFull()) {
            return false;
        } else {
            queue[r] = value;
            // r++, 结束了,跳回0
            r = r == limit - 1 ? 0 : (r + 1);
            size++;
            return true;
        }
    }

    // 如果队列空了,什么也不做,返回false
    // 如果队列没空,弹出头部的数字,返回true
    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        } else {
            // l++, 结束了,跳回0
            l = l == limit - 1 ? 0 : (l + 1);
            size--;
            return true;
        }
    }

    // 返回队列头部的数字(不弹出),如果没有数返回-1
    public int Front() {
        if (isEmpty()) {
            return -1;
        } else {
            return queue[l];
        }
    }

    //返回最后添加的数字
    public int Rear() {
        if (isEmpty()) {
            return -1;
        } else {
            //如果r在0位置,说明r已经将整个数组遍历一遍后又跳回0位置,那就说明上一个添加的数在数组的最后一个位置
            //如果r不在0位置,说明数组还没有完全遍历完成,那么就说明上一个添加的数在r的前一个位置
            int last = r == 0 ? (limit - 1) : (r - 1);
            return queue[last];
        }
    }

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

    public boolean isFull() {
        return size == limit;
    }

}

二.栈

定义

栈(Stack)是一种先进后出(LIFO, Last In First Out)的数据结构。它只允许在栈顶进行插入和删除操作,具有“后进先出”的特性。栈在程序设计和算法实现中有着广泛的应用。

操作

1. 压栈(Push)
  • 作用:将一个元素添加到栈的顶端(栈顶)。
  • 示例
    • 栈初始:[A, B, C]
    • 执行压栈操作插入 D[A, B, C, D]

2. 弹栈(Pop)
  • 作用:从栈顶移除并返回第一个元素。
  • 示例
    • 栈初始:[A, B, C]
    • 执行弹栈操作:移除 C,栈变为 [A, B]

3. 查看栈顶元素(Peek 或 Top)
  • 作用:查看栈顶元素,但不移除它。
  • 示例
    • 栈初始:[A, B, C]
    • 执行 Peek 操作:返回 C,栈保持不变 [A, B, C]

4. 获取栈的大小(Size)
  • 作用:返回栈中当前元素的个数。
  • 示例
    • 栈初始:[A, B, C]
    • 执行 Size 操作:返回 3

5. 判断栈是否为空(IsEmpty)
  • 作用:检查栈是否为空,返回布尔值(true 或 false)。
  • 示例
    • 栈初始:[](空栈)
    • 执行 IsEmpty 操作:返回 true

6. 清空栈(Clear)
  • 作用:移除栈中的所有元素,使其变为空栈。
  • 示例
    • 栈初始:[A, B, C]
    • 执行 Clear 操作:栈变为 []

7. 遍历栈(Traverse)
  • 作用:按顺序访问栈中的每个元素。
  • 示例
    • 栈初始:[A, B, C]
    • 遍历操作:依次访问 ABC

应用场景

1. 函数调用管理
  • 作用:操作系统使用栈来管理函数调用,确保函数返回时能够正确恢复上下文。
  • 示例
    • 在程序运行过程中,当一个函数调用另一个函数时,当前函数的执行状态(如返回地址、局部变量等)会被压入栈中。
    • 当被调用的函数执行完毕后,栈顶的执行状态被弹出,程序恢复到调用前的状态,继续执行下一个函数。
    • 这种机制确保了函数调用的正确性和顺序性。

2. 表达式求值与括号匹配
  • 作用:栈常用于判断表达式的括号是否匹配,确保语法正确。
  • 示例
    • 在计算表达式时,栈用于暂时存储运算符和操作数,按照运算规则进行计算。
    • 括号匹配:遍历表达式,遇到左括号压入栈,遇到右括号弹出栈顶的左括号。如果在遍历过程中发现右括号没有对应的左括号,或者遍历结束后栈不为空,说明括号不匹配。

3. 递归实现
  • 作用:递归函数通过栈保存每次调用的返回地址和局部变量,确保能够正确返回。
  • 示例
    • 递归函数每次调用时,当前函数的执行状态被压入栈中。
    • 递归终止条件满足时,栈顶的状态被弹出,程序返回上一层递归调用,继续执行。
    • 这种机制使得递归函数可以正确地层层返回,完成计算。

4. 回溯算法
  • 作用:在深度优先搜索(DFS)中,栈用于保存路径,帮助回溯到之前的节点,尝试其他路径。
  • 示例
    • 在迷宫求解或图的遍历中,栈记录当前路径。
    • 当前路径无法到达目标节点时,弹出栈顶节点,回溯到前一个节点,尝试其他可能的路径。

5. 网页浏览器的前进和后退功能
  • 作用:浏览器使用栈来记录用户访问的页面顺序,实现前进和后退功能。
  • 示例
    • 当用户访问一个新页面时,当前页面的URL被压入栈中。
    • 点击“后退”按钮时,栈顶的URL被弹出,浏览器跳转到上一个页面。
    • 点击“前进”按钮时,如果存在被后退的页面,可以再次压入栈中,实现前进功能。

6. 图形编辑器的撤销与重做功能
  • 作用:使用栈来记录用户的操作步骤,支持撤销和重做功能。
  • 示例
    • 每次用户执行一个操作时,操作的状态被压入栈中。
    • 点击“撤销”按钮时,栈顶的操作被弹出,恢复到上一个状态。
    • 点击“重做”按钮时,需要另一个栈来记录撤销的操作,将其重新压入主栈,执行重做。

7. 排序算法
  • 作用:某些排序算法(如快速排序)利用栈来辅助实现递归过程。
  • 示例
    • 快速排序是一种基于分治法的排序算法,通过递归实现。
    • 每次递归调用时,当前区间的起始和结束索引被压入栈中。
    • 递归终止条件满足时,栈顶的区间被弹出,继续处理下一个区间。

8. 计算器的表达式求值
  • 作用:计算器使用栈来处理中缀表达式,将其转换为后缀表达式进行计算。
  • 示例
    • 将中缀表达式转换为后缀表达式时,栈用于存储运算符。
    • 根据运算符的优先级,决定运算符的压栈和弹栈顺序,生成正确的后缀表达式。
    • 然后,使用栈对后缀表达式进行求值,确保运算顺序正确。

9. 内存管理
  • 作用:在某些内存管理策略中,栈用于管理动态内存分配和释放。
  • 示例
    • 动态内存分配(如C语言中的mallocfree函数)可能使用栈来记录已分配内存块的信息。
    • 当内存被释放时,栈顶的内存块信息被弹出,内存被归还给操作系统。

10. 编译器与解释器
  • 作用:编译器和解释器使用栈来进行语法分析和表达式求值。
  • 示例
    • 语法分析阶段,栈用于存储预期的语法元素,确保输入的语法结构正确。
    • 表达式求值阶段,栈用于存储操作数和运算符,确保按正确的顺序进行计算。

代码实现

  • 算法原理 
一.Stack1(基于Java内部Stack实现)的算法原理
  • 数据结构基础

    • 它基于Java内部的Stack类实现栈功能。Stack类在Java中是一种特殊的Vector,而Vector是一种动态数组结构。
  • 操作原理
    • isEmpty操作
      • 直接调用Stack类的isEmpty方法。这个方法会检查内部数组中元素的数量是否为0。时间复杂度为O(1),因为它只是简单地检查一个变量(记录元素数量)是否为0。
    • push操作
      • 调用Stackpush方法。在Stack(基于Vector)中,push操作将元素添加到数组的末尾。如果数组已满,会进行扩容操作(这可能涉及到创建一个新的更大的数组,复制旧数组元素到新数组,是一个相对耗时的操作,但在不考虑扩容的情况下,时间复杂度为O(1))。
    • pop操作
      • 调用Stackpop方法。它会移除并返回数组末尾的元素。在Stack中,这个操作通常是简单地调整数组末尾指针(在不考虑动态数组调整的特殊情况时),时间复杂度为O(1)。
    • peek操作
      • 调用Stackpeek方法。该方法返回数组末尾的元素但不移除它。操作简单地获取数组末尾元素的值,时间复杂度为O(1)。
    • size操作
      • 调用Stacksize方法。它直接返回记录栈中元素数量的变量的值,时间复杂度为O(1)。
二.Stack2(基于数组实现)的算法原理
  • 数据结构基础
    • 使用一个固定大小为n的整数数组stack来存储栈元素,同时有一个变量size来记录当前栈中元素的数量。
  • 操作原理
    • isEmpty操作
      • 通过检查size是否等于0来判断栈是否为空。这是一个简单的比较操作,时间复杂度为O(1)。
    • push操作
      • 将元素num放入数组stack中索引为size的位置,然后将size的值加1。这个操作直接对数组进行写入操作,时间复杂度为O(1)。
    • pop操作
      • 返回数组stack中索引为size - 1的元素,然后将size的值减1。这个操作直接从数组中读取并调整size变量,时间复杂度为O(1)。
    • peek操作
      • 返回数组stack中索引为size - 1的元素,也就是栈顶元素。这是一个简单的数组读取操作,时间复杂度为O(1)。
    • size操作
      • 直接返回size的值,时间复杂度为O(1)。
package queuestackandcircularqueue;

import java.util.Stack;

public class StackDemo {
    // 直接用java内部的实现
    // 其实就是动态数组,不过常数时间并不好
    public static class Stack1 {

        public Stack<Integer> stack = new Stack<>();

        // 调用任何方法之前,先调用这个方法来判断栈内是否有东西
        public boolean isEmpty() {
            return stack.isEmpty();
        }

        public void push(int num) {
            stack.push(num);
        }

        public int pop() {
            return stack.pop();
        }

        public int peek() {
            return stack.peek();
        }

        public int size() {
            return stack.size();
        }

    }

    // 实际刷题时更常见的写法,常数时间好
    // 如果可以保证同时在栈里的元素个数不会超过n,那么可以用
    // 也就是发生弹出操作之后,空间可以复用
    // 一般笔试、面试都会有一个明确数据量,所以这是最常用的方式
    public static class Stack2 {

        public int[] stack;
        public int size;

        // 同时在栈里的元素个数不会超过n
        public Stack2(int n) {
            stack = new int[n];
            size = 0;
        }

        // 调用任何方法之前,先调用这个方法来判断栈内是否有东西
        public boolean isEmpty() {
            return size == 0;
        }

        public void push(int num) {
            stack[size++] = num;
        }

        public int pop() {
            return stack[--size];
        }

        public int peek() {
            return stack[size - 1];
        }

        public int size() {
            return size;
        }

    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值