java学习笔记之优先队列实现原理

普通的队列是先进先出的数据结构,而优先队列为元素赋予优先级,具有最高优先级的元素成为队列首部。

优先队列一般基于二叉堆实现。

本文会分析java中几种常见的优先队列:PriorityQueuePriorityBlockingQueueDelayQueueDelayedWorkQueue

一、二叉堆的基本原理

(一) 什么是二叉堆?

  • 完全二叉树
  • 堆的根节点的优先级最大(即最大或最小)
  • 父节点的优先级必定大于子节点,兄弟节点的优先级不确定谁大谁小
时间复杂度
插入O(log n)
删除O(log n)
构造O(n)

(二) 堆的用途

  • 取最值

(三) 堆的基本操作

1. 插入

往堆插入元素,基本思想是从最后一个位置开始,通过上浮操作不断调整位置,直到满足父节点的优先级必定大于子节点这个条件

上浮

上浮是往二叉堆添加元素用到的操作,它其实是不断的调整k的位置为父元素的位置直到满足条件为止。

// 用数组表示堆
Object []objs = new Object[10];
/**
 * 上浮:
 * k表示堆的最后一个位置;
 * obj表示将要插入的元素。
 */
private void siftUp(int k, Object obj) {
    // 1. 判断k是否为根元素的位置0,如果是则直接赋值
    while(k>0) {
         // 2. 获取父元素的位置,parent = (k-1)/2
         int parent = (k-1) >>> 1;
         // 3. 如果父元素的优先级大于等于obj,跳出循环并插入obj
         if(objs[parent] >= obj) {
             break;
		 }
		 // 4. 如果父元素的优先级小于obj,将父元素赋值到k的位置,更改k为父元素的位置,继续循环
		 objs[k] = objs[parent];
		 k = parent;
    }
    // 5. 为obj赋值
    objs[k] = obj;
}
/**
 * 添加元素,不考虑数组扩容的情况。
 * 假设size表示当前堆包含的元素个数(注意不一定等于上面定义的10)
 */
public void add(Object obj) {
    if(size==0) {
        objs[0] = obj;
    } else {
        siftUp(size, obj);
        size++;
	}
}

2. 删除

删除指定位置的元素,其基本思想是从指定位置开始,把最后一个元素放到被删除元素的位置,通过下沉或者上浮操作,使得堆满足父元素优先级大于子元素的条件。

下沉

下沉是删除时用到的操作。它是把最后一个元素放到被删除元素的位置,然后重新调整使得堆满足条件的过程。

  • 被删除元素的位置位于最后一个元素的父元素的位置后面时,可以直接把最后一个元素插入到被删除元素的位置;然后再进行上浮操作。
  • 否则,需要执行下沉操作。
// 用数组表示堆
Object []objs = new Object[10];
/**
 * k被删除元素的位置;
 * obj堆的最后一个元素;
 *  假设size为当前堆包含元素的个数(不一定是上面定义的10)
 */
private void siftDown(int k, Object obj) {
    // 1. 找到最后一个元素的父节点的位置, (最后一个元素位置-1) / 2
    int parent = (size-1-1) >>> 1;
    // 2. 判断k是否在父节点位置之后,如果在之前则需要下沉操作
    while(k <= parent) {
    	// 3.获取k的左右子节点的位置
    	int left = k<<<2 +1;
    	int right = left+1;
    	// 4.选择左右子节点中优先级最高的一个
    	int best;
    	if (objs[left] > objs[right]) {
			best = left;
		} else {
			best = right;
		}
		// 5.判断obj和best的优先级谁高。如果obj优先级高,则跳出循环直接赋值,否则继续下沉
		if (obj >= objs[best]) {
			break;
		}
		objs[k] = objs[best];
		k = best;
    }
    // 6.赋值
    objs[k] = obj;
}

/**
 * 删除第p个元素。
 */
public void remove(int p) {
    // 1.获取最后一个元素
    Object obj = objs[size-1];
    // 2.如果p不等于最后一个元素
    if (p != size-1) {
		// 3.把最后一个元素和p进行下沉操作
    	siftDown(p, obj);
    	if(objs[p] == obj) {
			// 4. 上浮
			siftUp(p, obj);
		}
	}
	size--;
}

二、PriorityQueue

(一) PriorityQueue是什么?

  • PriorityQueue是基于二叉堆原理的优先队列,队列用动态数组实现。
  • 它是非阻塞的、非线程安全的;

PriorityQueue内部维护了几个重要属性:

类型含义
queueObject[]代表PriorityQueue的队列,存放元素
sizeint当前队列包含元素个数
comparatorComparatorPriorityQueue根据比较器对元素优先级排序
modCountint记录队列被修改的次数

(二) PriorityQueue的使用

PriorityQueue的使用非常简单,一笔带过。

public static void main(String[] args) {
   PriorityQueue<String> queue = new PriorityQueue<>();
   queue.add("hello");
   System.out.println(queue.peek());
   queue.remove("hello");
   System.out.println(queue.peek());
}

(三) PriorityQueue的实现原理

PriorityQueue的基本原理和第一部分讲的基本一致。

插入

  1. 如果当前元素个数达到数组长度,则扩容;
    1.1 如果数组长度小于64,直接扩容1倍
    1.2 否则扩容0.5倍
  2. 增加元素个数
  3. 上浮操作,找到合适的位置插入。在上浮的时候,如果comparator存在则用它确定优先级,否则用自然顺序确定优先级。
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        // 扩容
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        // 上浮
        siftUp(i, e);
    return true;
}

删除

  1. 获取最后一个元素
  2. 将待删除的位置i和最后一个元素做下沉操作
  3. 如果没有执行下沉操作,那么表示待删除的位置i是叶节点,需要通过上浮调整二叉堆
  4. 执行上浮操作
private E removeAt(int i) {
    modCount++;
    int s = --size;
    if (s == i) // removed last element
        queue[i] = null;
    else {
        E moved = (E) queue[s];
        queue[s] = null;
        siftDown(i, moved);
        if (queue[i] == moved) {
            siftUp(i, moved);
            if (queue[i] != moved)
                return moved;
        }
    }
    return null;
}

三、PriorityBlockingQueue

(一) PriorityBlockingQueue是什么?

  • PriorityQueue线程安全版本
  • 同样是基于二叉堆的原理,用动态数组实现
  • 阻塞的、线程安全的

(二) PriorityBlockingQueue的实现原理

插入

原理基本和PriorityQueue一样

  1. 加锁
  2. 是否需要扩容,扩容原理和PriorityQueue一样
  3. 上浮
  4. 解锁
public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    final ReentrantLock lock = this.lock;
    lock.lock();
    int n, cap;
    Object[] array;
    while ((n = size) >= (cap = (array = queue).length))
        tryGrow(array, cap);
    try {
        Comparator<? super E> cmp = comparator;
        if (cmp == null)
            siftUpComparable(n, e, array);
        else
            siftUpUsingComparator(n, e, array, cmp);
        size = n + 1;
        notEmpty.signal();
    } finally {
        lock.unlock();
    }
    return true;
}

删除

基本原理和PriorityQueue一致,只是使用ReentrantLock保证线程安全。

四、DelayQueue

  • 基于PriorityQueue实现的延迟队列
  • 它的删除、插入操作和PriorityQueue基本一致,主要的区别在与poll()take()等方法。PriorityQueue是只要队列首部有数据就除移,而DelayQueue还需要判断是否达到除移的时间。
  • 至于take()poll()方法的区别在于,take()会阻塞,而poll()直接返回。

五、DelayedWorkQueue

原理与DelayQueue相同,不知道JDK开发团队为何重复造一个轮子。

六、一图胜千言

添加

在这里插入图片描述

在这里插入图片描述

删除

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

七、F&Q

1、侄节点与叔节点的大小关系?
没有规定两种的大小关系,叔节点的优先级可以比侄节点大也可以小。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值