数据结构 -- 队列

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;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值