数据结构 -- 队列

news2025/1/31 3:15:28

1、Queue队列

先进先出

在这里插入图片描述

 

2、双端队列 --- Deque

Deque的实现类是LinkedList,ArrayDeque,LinkedBlockingDeque。

ArrayDeque底层实现是数组,LinkedList底层实现是链表。

在这里插入图片描述

 双端队列可以作为普通队列使用,也可以作为栈使用。Java官方推荐使用Deque替代Stack使用

103. 二叉树的锯齿形层序遍历

给你二叉树的根节点 root ,返回其节点值的 锯齿形层序遍历 。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

解题思路:

这个题是一个变形的二叉树层序遍历,可以用BFS求解,值得注意的是,每隔一层,输出的值顺序是相反的。

  1. 本来想的是利用双端队列直接排列节点的顺序,奇数层和偶数层的时候,分别从头插入和从尾插入,但是调试了几次用例没通过,元素的添加和取出比较绕
  2. 其实完全可以按照正常的BFS模板去写,奇数层和偶数层的时候,利用一个双端队列作为一个中转,分别从头插入和从尾插入当前节点,然后在转为list链表。
class Solution {
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) {
            return res;
        }

        ArrayDeque<TreeNode> queue = new ArrayDeque<>();
        queue.offerFirst(root);
        int level = 1;
        while (!queue.isEmpty()) {
            // 这里定义一个双端队列
            ArrayDeque<Integer> list = new ArrayDeque<>();
            int size = queue.size();

            for (int i = 0; i < size; i++) {
                TreeNode treeNode = queue.poll();

                // 这里把当前值存入双端队列,根据层数不同,选择插入尾部或者头部
                if (level % 2 != 0) {
                    list.offerLast(treeNode.val);
                } else {
                    list.offerFirst(treeNode.val);
                }

                if (treeNode.left != null) {
                    queue.offerLast(treeNode.left);
                }
                if (treeNode.right != null) {
                    queue.offerLast(treeNode.right);
                }
            }
            // 这个把临时创建的双端队列存入结果数据中
            res.add(new ArrayList<>(list));
            level++;
        }
        return res;
    }
}

3、优先级队列 -- PriorityQueue

队列是先进先出,优先级队列就是在队列的基础上,增加了一个优先级的概念。

1.使用无序数组实现

offer:直接插入在数组最后面

poll:定义一个max指针,先指向最后面,然后不断往前遍历,如果前面的优先级大,那么就更新max的值,找到最大的max,删除该位置元素,然后后面数组向前移动

peek:先找到max指针,然后返回该位置的值

2、使用有序数组实现

offer:把数据插入到数组最后面,与前一个进行比较,如果比他小,则交换位置

poll : 移除最后一个元素

peek: 返回最后一个元素

3、使用堆实现

堆:是一种基于树的数据结构,通常用完全二叉树实现,有如下特性:

  • 大顶堆:任一节点与C与他的父节点P,有P.value ≥ C.value 
  • 小顶堆:任一节点与C与他的父节点P,有P.value ≤ C.value

如图就是一个大顶堆,粉色为索引,蓝色为优先级数据。

从索引0开始存储节点数据有如下规律:

  1. 节点i的父节点索引为 floor((i-1)/2) ,i>0
  2. 节点i的左子节点为 2i+1,右子节点为 2i+2,当然索引都要小于堆容量size
    // 插入元素:
    // 1、假设插入到最后的位置,
    // 2、把要插入的元素优先级和父节点比较,如果比他大,交换位置,一直保持大顶堆
    // 3、直到要插入的元素比父节点小了,或者是根节点了,就把当前元素插到这个位置。
    public boolean offer(Priority offered) {
        if (isFull()) {
            return false;
        }
        int child = size;
        size++;
        int parent = (child - 1) / 2;
        while (child > 0 && offered.priority > array[parent].priority) {
            array[child] = array[parent];
            child = parent;
            parent = (child - 1) / 2;
        }
        array[child] = offered;
        return true;
    }

 

    // 移除元素
    // 1、其实移除的就是根节点元素,但是要保持大顶堆,就需要进行额外操作
    // 2、把最后一个元素替换到根节点的位置,然后和左右子节点比较,把大的节点提上来
    // 3、不选循环提节点的操作,直到节点的位置比子节点都大
    public Priority poll() {
        if (isEmpty()) {
            return null;
        }
        swap(0, size - 1);
        size--;
        Priority removed = array[size];
        down(0);// 根节点下沉
        return removed;
    }

    // 将元素下沉,构建大顶堆
    private void down(int parent) {
        int left = 2 * parent + 1;
        int right = left + 1;
        int max = parent;

        if (left < size && array[left].priority > array[max].priority) {
            max = left;
        }
        if (right < size && array[right].priority > array[max].priority) {
            max = right;
        }
        if (max != parent) {
            swap(parent, max);
            down(max);
        }
    }

    // 交换节点位置
    private void swap(int x, int y) {
        Priority temp = array[x];
        array[x] = temp;
        array[y] = temp;
    }
    // 堆顶元素
    public Priority peek() {
        if (isEmpty()) {
            return null;
        }
        return array[0];
    }

23、合并 K 个升序链表

解题:

之前学习链表的时候,我们知道了怎么合并两个有序链表,那么合并多个有序链表,就循环遍历,两两合并就可以了。

现在,学习了优先级链表,也可以用该数据结构进行解题。

采用小顶堆,Java代码中可以直接使用  PriorityQueue,将给出的多个链表中的头节点放到优先级队列中,然后取出最小的一个节点 (如果这个节点在原来链表中有next节点,那么就把next节点在加入优先级队列中。不断循环,就可以得到排序的链表。)

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode newList = new ListNode();
        ListNode head = newList;

        PriorityQueue<ListNode> priorityQueue = new PriorityQueue<>(new Comparator<ListNode>() {
            @Override
            public int compare(ListNode o1, ListNode o2) {
                return o1.val - o2.val;
            }
        });
        for (ListNode l : lists) {
            if (l != null) {
                priorityQueue.add(l);
            }
        }
        while (!priorityQueue.isEmpty()) {
            ListNode curr = priorityQueue.poll();
            head.next = curr;
            head = head.next;
            if (curr.next != null) {
                priorityQueue.add(curr.next);
            }
        }
        return newList.next;
    }
}

也可以吧所有的节点都加入到优先级链表中,然后在一个个取出来,但是会占用很大的堆空间。

4、阻塞队列

适用于生产者消费者模式,消费的时候,要保证已经有东西生产出来了,生产的时候,要保证队列没有满。

阻塞队列,单锁实现。

public class MyBlockingQueue {
    private final int[] array;

    private int head;

    private int tail;

    private int size;

    // 可重入锁
    private ReentrantLock lock = new ReentrantLock();

    private Condition headWaits = lock.newCondition();

    private Condition tailWaits = lock.newCondition();

    public MyBlockingQueue(int capacity) {
        this.head = 0;
        this.tail = 0;
        this.array = new int[capacity];
    }

    public void offer(int value) throws InterruptedException {
        // 加锁
        lock.lockInterruptibly();
        try {
            while (isFull()) { //while循环,防止虚假唤醒
                // 满了就等待
                tailWaits.await();
            }
            // 添加元素
            array[tail] = value;
            if (++tail == array.length) {
                tail = 0;
            }
            size++;
            // 唤醒poll方法
            headWaits.signal();
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    // 添加一个等待时间
    public boolean offer(int value, Long timeout) throws InterruptedException {
        // 加锁
        lock.lockInterruptibly();
        long nanos = TimeUnit.MICROSECONDS.toNanos(timeout);
        try {
            while (isFull()) { //while循环,防止虚假唤醒
                // 满了就等待
                if (nanos > 0) {
                    // 假设要求等待5s,返回值就是等待后还剩余的时间。
                    // 假设等待了1s后,被唤醒,但是又被其他线程抢了锁,这里就要重新等待,但是不能等待5s,而是4s
                    nanos = tailWaits.awaitNanos(nanos);
                }
                return false;
            }
            // 添加元素
            array[tail] = value;
            if (++tail == array.length) {
                tail = 0;
            }
            size++;
            // 唤醒poll方法
            headWaits.signal();
            return true;
        } finally {
            // 解锁
            lock.unlock();
        }
    }

    public int poll() throws InterruptedException {
        lock.lockInterruptibly();

        try {
            while (isEmpty()) { //while循环,防止虚假唤醒
                // 满了就等待
                headWaits.await();
            }
            int res = array[head];
            array[head] = 0;
            if (++head == array.length) {
                head = 0;
            }
            size--;
            // 唤醒offer方法
            tailWaits.signal();
            return res;
        } finally {
            lock.unlock();
        }
    }

    public int peek() {
        return array[head];
    }

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

    public boolean isFull() {
        return size == array.length;
    }
}

上述代码,offer和poll操作用的是同一把锁,因此这两个操作是相互干扰的,即一个执行的时候,另外一个会阻塞,这样是不合理的。

因此,可以使用两把锁,分别控制offer和poll操作,但是这个会引入一个问题,就是size++/size-- 的问题。因为++/--操作不是原子性的,那么,在多线程的情况下就是不安全的(为什么一把锁的时候是线程安全呢?因为一把锁的时候,offer拿锁,poll会阻塞,因此在size++的过程中,就不可能出现size++的操作,故是线程安全的,而两把锁的时候,offer和poll互不影响,因此就可能在size++的时候出现size-- 导致线程不安全。)

两把锁代码如下,每个锁控制着自己对应的Condition,因此唤醒操作只能在锁存在的时候才可以唤醒。为了避免死锁,需要保证一把锁解开后,才可以去给另一把锁上锁

    private ReentrantLock tailLock = new ReentrantLock();
    private Condition tailWaits = tailLock.newCondition();

    private ReentrantLock headLock = new ReentrantLock();
    private Condition headWaits = headLock.newCondition();

    private AtomicInteger size; // 原子性

    public void offer(int value) throws InterruptedException {
        // 加锁
        tailLock.lockInterruptibly();
        try {
            while (isFull()) { //while循环,防止虚假唤醒
                // 满了就等待
                tailWaits.await();
            }
            // 添加元素
            array[tail] = value;
            if (++tail == array.length) {
                tail = 0;
            }
            size.getAndIncrement();
        } finally {
            // 解锁
            tailLock.unlock();
        }
        
        headLock.lockInterruptibly();
        try {
            // 唤醒poll方法,只能在tailLock锁解锁之后唤醒,避免死锁
            headWaits.signal(); 
        } finally {
            headLock.unlock();
        }
    }

    public int poll() throws InterruptedException {
        headLock.lockInterruptibly();
        int res;
        try {
            while (isEmpty()) { //while循环,防止虚假唤醒
                // 满了就等待
                headWaits.await();
            }
            res = array[head];
            array[head] = 0;
            if (++head == array.length) {
                head = 0;
            }
            size.getAndDecrement();
        } finally {
            headLock.unlock();
        }
        
        tailLock.lockInterruptibly();
        try {
            // 唤醒offer方法,只能在headLock锁解锁之后唤醒,避免死锁
            tailWaits.signal();
        } finally {
            tailLock.unlock();
        }

        return res;
    }

上述代码实现了两把锁分别控制offer和poll,但是还存在一个问题,就是每次offer的时候,为了唤醒poll,还是给headlock加了锁;每次poll的时候为了唤醒offer,还是给taillock加了锁,为了提升效率,可以做如下优化:

思想:级联思想

  • offer的时候:只有队列中元素从0到1,才触发唤醒poll操作,其余的poll线程唤醒交给poll方法自身
  • poll的时候:只有队列中元素从满到不满才触发唤醒offer操作,如遇offer线程唤醒交给offer自身。

优化代码如下:

    public void offer(int value) throws InterruptedException {
        // 加锁
        tailLock.lockInterruptibly();
        int c; // 记录队列增加前的元素个数
        try {
            while (isFull()) { //while循环,防止虚假唤醒
                // 满了就等待
                tailWaits.await();
            }
            // 添加元素
            array[tail] = value;
            if (++tail == array.length) {
                tail = 0;
            }
            c = size.getAndIncrement();

            // 队列中元素不满,唤醒其他offer线程
            if (c < array.length - 1) {
                tailWaits.signal();
            }
        } finally {
            // 解锁
            tailLock.unlock();
        }

        // 队列中元素从0到1,才触发唤醒poll操作
        if (c == 0) {
            headLock.lockInterruptibly();
            try {
                // 唤醒poll方法,只能在tailLock锁解锁之后唤醒,避免死锁
                headWaits.signal();
            } finally {
                headLock.unlock();
            }
        }
    }

    public int poll() throws InterruptedException {
        headLock.lockInterruptibly();
        int res;
        int c; // 记录队列减少前的元素个数
        try {
            while (isEmpty()) { //while循环,防止虚假唤醒
                // 满了就等待
                headWaits.await();
            }
            res = array[head];
            array[head] = 0;
            if (++head == array.length) {
                head = 0;
            }
            c = size.getAndDecrement();

            // 队列中有元素,唤醒其他poll线程
            if (c > 1) {
                headWaits.signal();
            }
        } finally {
            headLock.unlock();
        }
        
        // 队列从满到不满的时候,加锁唤醒offer线程
        if (c == array.length) {
            tailLock.lockInterruptibly();
            try {
                // 唤醒offer方法,只能在headLock锁解锁之后唤醒,避免死锁
                tailWaits.signal();
            } finally {
                tailLock.unlock();
            }
        }

        return res;
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559198.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【flask + sqlalchemy】连接clickhouse数据库的踩的坑,在这里记录一下

文章目录 前言1. 发现问题2. 复盘2.1 上面试一次错误的问题记录2.2 flask使用clickhouse2.2.1 配置2.2.2 orm2.3 如何插入数据 前言 使用clickhouse有一段时间了&#xff0c;现在要重构一个项目&#xff0c;重度依赖clickhouse&#xff0c;现在终于理顺了&#xff0c;记录一下…

数据库管理-第七十七期 再探分布式(20230523)

数据库管理 2023-05-23 第七十七期 再探分布式1 单机分布式2 分布式改造3 尝试改造一个订单系统3.1 表类型和分片键选择3.2 扩展分片3.3 业务扩展 总结 第七十七期 再探分布式 上一次系统探讨分布式数据库还是在第三十六期&#xff0c;经过大半年的“进步”加上中间参加了不少…

Linux启动过程的问题解决

文章目录 Linux启动过程的问题解决忘记root密码的解决因文件系统错误而无法启动 Linux启动过程的问题解决 当我们在使用Linux时&#xff0c;可能会因为某些设置或突然断电等原因导致文件系统出现错误&#xff0c;从而导致Linux无法正常启动。但这并不意味着我们需要重新安装系…

ASEMI代理长电可控硅MAC97A8图片,MAC97A8大小

编辑-Z 长电可控硅MAC97A8参数&#xff1a; 型号&#xff1a;MAC97A8 VDRM/VRRM&#xff1a;600V IT(RMS)&#xff1a;1A ITSM&#xff1a;8A 栅极电流&#xff08;峰值&#xff09;&#xff1a;1A 栅极电压&#xff08;峰值&#xff09;&#xff1a;5V 栅极功率&#…

翼辉+飞腾全国产实时操作系统与全国产主板工控方案在电力产品的应用

SylixOS&#xff08;MS-RTOS&#xff09;电力产品应用简介 一、电网传统装置&#xff1a; 继电保护、测控、稳控、PMU、时间同步、故障录波等装置&#xff08;输变电&#xff09; 一般使用AMP方案&#xff0c;少数客户使用SMP方案。2019年11月使用SylixOS的国内首台100%全国…

centos8 安装mysql8

1、下载mysql8软件库 wget https://repo.mysql.com//mysql80-community-release-el8-3.noarch.rpm 2、安装软件库 rpm -ivh mysql80-community-release-el8-3.noarch.rpm 3、安装mysql yum install mysql-server 4、启动mysql systemctl start mysqld systemctl enable…

【非集中申请期】国家自然科学基金最新申请指南情况汇总

2023年国自然集中申请期函评季临近尾声。当下&#xff0c;申请人除了在日常的科研工作中也要合理、及时的关注一些非集中申请期的项目动态&#xff0c;恰如2023年各类非集中期申请项目指南正在陆续发布中&#xff0c;这些仍然是非常好的申请机会。近期的项目指南名称与申请截止…

快速上手MATLAB:科研、工程、数据分析,MATLAB入门(下)教你基础知识!分享《MATLAB初学者教程 MATLAB编程-菜鸟入门(清晰版)》

快速上手MATLAB&#xff1a;科研、工程、数据分析&#xff0c;MATLAB入门&#xff08;上&#xff09;教你基础知识&#xff01; 福利&#xff1a;文末有资料分享&#xff01;&#xff01;前言一、文件读取1. 工作空间数据读取2. 文本文件读取3. 常用的数据导入和导出函数4. 图像…

不吹不黑,安利5个网工必备工具包

大家好&#xff0c;我是老杨。 要说网工的好用工具&#xff0c;你心里肯定有不少选项。工具常用常新&#xff0c;与时俱进&#xff0c;但你的思维却不一定。 研究一个新工具&#xff0c;除了能提升你的工作效率&#xff0c;也能对你的认知有所开拓和提升。 所以&#xff0c;…

《程序员面试金典(第6版)》面试题 02.01. 移除重复节点(哈希映射,多指针暴力破解,链表)

题目描述 编写代码&#xff0c;移除未排序链表中的重复节点。保留最开始出现的节点。 题目传输门&#xff1a;面试题 02.01. 移除重复节点 示例1: 输入&#xff1a;[1, 2, 3, 3, 2, 1]输出&#xff1a;[1, 2, 3]示例2: 输入&#xff1a;[1, 1, 1, 1, 2]输出&#xff1a;[1, 2]…

Hive---拉链表设计与实现

1 数据同步问题 Hive在实际工作中主要用于构建离线数据仓库&#xff0c;定期的从各种数据源中同步采集数据到Hive中&#xff0c;经过分层转换提供数据应用。比如每天需要从MySQL中同步最新的订单信息、用户信息、店铺信息等到数据仓库中&#xff0c;进行订单分析、用户分析。 …

【C++修炼之路】定位new(项目记录)

————————————每一个不曾起舞的日子都是对生命的辜负。 C之定位new 1. 什么是定位new2. 定位new的语法3. 具体实例 1. 什么是定位new 一般的new运算符负责在heap堆中找到一个足以能够满足要求的内存块。 而定位new&#xff08;Placement new&#xff09;是C中的一…

运行100万个并发任务,不同语言各需要多少内存

作者&#xff1a;DataStax 公司&#xff08;美国的一家数据库系统开发商&#xff09;Piotr Kołaczkowski 原文见&#xff1a; https://pkolaczk.github.io/memory-consumption-of-async/ 在这篇博客文章中&#xff0c;探讨了处理大量网络连接时候的Rust、Go、Java、C#、Pyth…

企企通“码上顺”清洗工具 | 让数据更有价值,让业务更出色

数据清理工作是企业数据管理、数据治理中的最基础的工作之一&#xff0c;不仅是一项苦活、累活&#xff0c;也是一个既考验业务又检验技术的活。 物料主数据作为企业核心的数据资产&#xff0c;在智慧供应链、业财一体化等数字化建设中发挥着重要作用。在当今高速发展的商业环…

《汇编语言》- 读书笔记 - 实验2 用机器指令和汇编指令编程

《汇编语言》- 读书笔记 - 实验2 用机器指令和汇编指令编程 1. 预备知识: Debug 的使用2 .实验任务 1. 预备知识: Debug 的使用 统一完善到&#xff1a;实验 1 查看 CPU 和内存&#xff0c;用机器指令和汇编指令编程。不在这拆开写了。 2 .实验任务 使用 Debug&#xff0c;将…

功率放大器在压电驱动器中的作用及应用

功率放大器在压电驱动器中的作用是将低功率信号放大为足够大的电力信号&#xff0c;以驱动压电陶瓷材料产生相应的机械振动。 压电陶瓷材料是一种特殊的陶瓷材料&#xff0c;能够将机械能转换为电能&#xff0c;因此被广泛应用于各种类型的振动器件和传感器中。这些器件通常需要…

Combiner

概述 Conbiner在MapReduce的Shuffle阶段起作用&#xff0c;它负责局部数据的聚合&#xff0c;我们可以看到&#xff0c;对于大数据量&#xff0c;如果没有Combiner&#xff0c;将会在磁盘上写入多个文件等待ReduceTask来拉取&#xff0c;但是如果有Combiner组件&#xff0c;我们…

5 个章节、25 条规范,全方位 Get 数据集选择与创建的「百科全书」

By 超神经 内容一览&#xff1a;如果你正在学习如何创建或选择一个合适的数据集&#xff0c;那么这篇文章会给你一些实用的建议&#xff0c;帮助你在选择和创建数据集时做出明智的决策。 关键词&#xff1a;机器学习 数据集 本文首发自 HyperAI 超神经微信公众平台~ 作者 |…

星标3.5k,一款国产的轻量级开源在线项目任务管理工具

今天给大家推荐一个轻量级的开源在线项目任务管理工具&#xff1a;DooTask 图片 DooTask 提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM&#xff0c;文件管理等工具。 高效便捷的团队沟通工具 针对项目和任务建立群组&#xff0c;工作问题可…

SRP Batcher在真机上失效

1&#xff09;SRP Batcher在真机上失效 ​2&#xff09;Shader里面对同一张纹理多次采样会影响效率吗 3&#xff09;为什么纹理开启了mipmap后&#xff0c;纹理内存反而下降了 4&#xff09;TMP为什么有多次Delegate.Combine()的GC 这是第336篇UWA技术知识分享的推送&#xff0…