队列是一个先进先出的结构,可以用链表呀,数组来实现它,我们今天用数组实现个队列,以优先级队列方式,我们看看怎么实现,优先级队列以队列存储时规则会将即将过期的或较小的数据存储在前面,这样取出时则取头部数据即可。
优先级队列采用数组实现的完全二叉树,根据二叉树规则,在插入的时候对比,保证父节点要比子节点小就ok。
我们主要来看下入队出队的一个实现,需要定义接口,咱们来定义基本方法。
1.实现入队用add或offer,这里超出队列界限抛出异常什么的都不处理,这里add会调用offer方法
2.出队使用poll方法则是移除队列头部并返回队列头部数据,使用peek方法只得到头部数据不处理移除头部数据。
/**
* @Author df
* @Date 2022/11/28 16:48
* @Version 1.0
* java泛型中标记符含义
* E element (元素,集合中使用)
* T type (java类)
* K key (键)
* V value (值)
* N number(数值)
* ? 表示不确定的java类型
*/
public interface Queue<E> {
// 入队,超出队列界限抛出异常处理
boolean add(E e);
// 入队,超出队列界限直接返回false
boolean offer(E e);
// 将首个队列元素弹出,如果空为空
E poll();
// 查询首个队列,不移除首个队列,如果队列为空抛出异常
E peek();
}
定义实现方法 PriorityQueue,准备一下,我们需要一个数组,这个数组我们就叫queue吧,还需要一个默认的容量就叫DEFAULT_INITIAL_CAPACITY=11,还需要个长度元素,记录数组长度,就叫size把,在实例化时,就会开辟默认数组空间
public class PriorityQueue<E> implements Queue<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 用数组实现队列
transient Object[] queue;
private int size = 0;
// 默认
public PriorityQueue() {
queue = new Object[DEFAULT_INITIAL_CAPACITY];
}
}
准备工作做好就需要入队列了
1.入队
入队前我们先想想,我们怎么存储才能按顺序二叉树规则存储?
那么假如我存储一个3,假如在数组queue[0]直接插入3,我再存储个2,我直接在数组queue[1]存储2呢,我在插入个数据1,直接在queue[2]插入1,那么就形成了这样的二叉树
3
2 1
但是这不符合二叉树的规则,父节点要比任意子节点要小,现在父节点是3,它比其他子节点都要大,所以这样不对。我们要考虑在添加的时候需要对比,然后存储对应的位置就好了
如第一个元素3直接存储,第二个元素存储时则找到父节点值进行比对,3比2大则替换,将queue[0]变为2,queue[1]变为3,再存储1时则跟父节点queue[0]对比,1小于2则替换,
1
3 2
再新添加元素还是一样的方式,如果此时存储进来5对比以后比前3个位置都大,那么就存储queue[3],6就存储queue[4]而queue[1]就是它的父节点
1
3 2
5 6
你此时可能在想5和6是3的子节点,如果数据渐渐变大,怎么依次找他们自己的父节点呢,这就引申出来一个公式
父节点= (n-1)/2 5的坐标为3,(3-1)/2=1 queue[1]就是queue[3]的父亲节点
添加操作,add()调用offer(),offer方法用来判断是否超出当前队列容量进行扩容调用grow(),以及调用siftUp()用来处理子父节点判断并替换操作。
offer():先判断是否扩容,默认进来size为0,queue队列长度是11个空值,所以是不用扩容,因为要往队列存储值了,所以设置size为i+1,然后判断是不是第一次存储值,第一次存储值就直接在queue[0]直接存储,如不是,则需要进入siftUp();
siftUp():参数传当前待插入的位置i,当前元素e,进入siftUpComparable();
siftUpComparable():先把当前的元素转换为Comparable,这样就可以进行值的比对,我们最重要的点就是通过当前元素位置找到父节点位置的值,然后进行比对,子节点小于父节点,就需要替换元素。
需用到公式找父节点,(k-1)/2 ,k>0 ,假如现在k为1,那就是(1-1)/2=0,先存储3,再存储2,那么2的父节点就是3,取出父节点的值进行比对,发现当前元素大于父节点,不退出循环,直接替换queue[k=1]=3,k=0,此时while条件无法满足,执行queue[k=0]=2,这样就可以保证父节点最小
@Override
public boolean add(E e) {
return offer(e);
}
@Override
public boolean offer(E e) {
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;
}
// 扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 开辟新的容量,如果小于64就队列长度+队列长度+2也就是说11+11+2=24,大于64就需要队列长度+队列长度/2
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 判断是不是接近int值的最大值
if (newCapacity - (Integer.MAX_VALUE - 8) > 0) {
newCapacity = (minCapacity > Integer.MAX_VALUE - 8) ?
Integer.MAX_VALUE :
Integer.MAX_VALUE - 8;
}
// 开辟新的空间
queue = Arrays.copyOf(queue, newCapacity);
}
// 对比并替换存储值
private void siftUp(int k, E x) {
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
System.out.println("【入队】元素:" + key + " 当前队列:" + Arrays.toString(queue));
while (k > 0) {
//得到当前元素的父节点, >>>除2取整
int parent = (k - 1) >>> 1;
// 得到父节点的值
Object e = queue[parent];
// 当前元素大于父结点,不进行替换退出循环
if (key.compareTo((E) e) > 0) {
break;
}
// 当前元素比父节点大,则先给当前位置赋值为父结点的值
System.out.println("【入队】替换过程,父子节点位置替换,继续循环。父节点值:" + e + " 存放到位置:" + k);
queue[k] = e;
// 把父节点的小标赋值给k
k = parent;
}
queue[k] = key;
System.out.println("【入队】完成 Idx:" + k + " Val:" + key + " \r\n当前队列: \r\n" + Arrays.toString(queue));
}
main方法测试下存储
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(1);
queue.add(3);
queue.add(5);
queue.add(11);
queue.add(4);
queue.add(6);
queue.add(7);
queue.add(12);
queue.add(15);
queue.add(10);
queue.add(9);
queue.add(8);
queue.add(2);
}
控制台输出
【入队】元素:3 当前队列:[1, null, null, null, null, null, null, null, null, null, null]
【入队】完成 Idx:1 Val:3
当前队列:
[1, 3, null, null, null, null, null, null, null, null, null]
【入队】元素:5 当前队列:[1, 3, null, null, null, null, null, null, null, null, null]
【入队】完成 Idx:2 Val:5
当前队列:
[1, 3, 5, null, null, null, null, null, null, null, null]
【入队】元素:11 当前队列:[1, 3, 5, null, null, null, null, null, null, null, null]
【入队】完成 Idx:3 Val:11
当前队列:
[1, 3, 5, 11, null, null, null, null, null, null, null]
【入队】元素:4 当前队列:[1, 3, 5, 11, null, null, null, null, null, null, null]
【入队】完成 Idx:4 Val:4
当前队列:
[1, 3, 5, 11, 4, null, null, null, null, null, null]
【入队】元素:6 当前队列:[1, 3, 5, 11, 4, null, null, null, null, null, null]
【入队】完成 Idx:5 Val:6
当前队列:
[1, 3, 5, 11, 4, 6, null, null, null, null, null]
【入队】元素:7 当前队列:[1, 3, 5, 11, 4, 6, null, null, null, null, null]
【入队】完成 Idx:6 Val:7
当前队列:
[1, 3, 5, 11, 4, 6, 7, null, null, null, null]
【入队】元素:12 当前队列:[1, 3, 5, 11, 4, 6, 7, null, null, null, null]
【入队】完成 Idx:7 Val:12
当前队列:
[1, 3, 5, 11, 4, 6, 7, 12, null, null, null]
【入队】元素:15 当前队列:[1, 3, 5, 11, 4, 6, 7, 12, null, null, null]
【入队】完成 Idx:8 Val:15
当前队列:
[1, 3, 5, 11, 4, 6, 7, 12, 15, null, null]
【入队】元素:10 当前队列:[1, 3, 5, 11, 4, 6, 7, 12, 15, null, null]
【入队】完成 Idx:9 Val:10
当前队列:
[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, null]
【入队】元素:9 当前队列:[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, null]
【入队】完成 Idx:10 Val:9
当前队列:
[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, 9]
【入队】元素:8 当前队列:[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, 9, null, null, null, null, null, null, null, null, null, null, null, null, null]
【入队】完成 Idx:11 Val:8
当前队列:
[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, 9, 8, null, null, null, null, null, null, null, null, null, null, null, null]
【入队】元素:2 当前队列:[1, 3, 5, 11, 4, 6, 7, 12, 15, 10, 9, 8, null, null, null, null, null, null, null, null, null, null, null, null]
【入队】替换过程,父子节点位置替换,继续循环。父节点值:6 存放到位置:12
【入队】替换过程,父子节点位置替换,继续循环。父节点值:5 存放到位置:5
【入队】完成 Idx:2 Val:2
当前队列:
[1, 3, 2, 11, 4, 5, 7, 12, 15, 10, 9, 8, 6, null, null, null, null, null, null, null, null, null, null, null]
入队处理完了,我们处理出队的逻辑,
2.出队
出队从队列的头部,也就是queue[0]取出数据就可以了,但是还有个问题,你取出来第一个你不能让queue[0]为空呀,下次再调用poll或peek不是取不出来剩余的队列数据了吗,所以还需要操作需要找到在左子树与右子树之间最小的值,放到queue[0],并调整子树的值与队列最后一个值对比(头部弹出相当于删除一个,取最后位置删除,所以把最后一个值拿出对比,放入树中合适的位置),可以看下下图所示步骤,
假如将1弹出,则需要找出它的左子树和右子树,
左子树公式为:n*2+1 右子树:左子树的合+1;
那么找到了3和5,比对左子树的值和右子树的值3最小,所以将3替换到queue[0]位置上,然后继续找原来3(queue[1])的子节点,找到了左子树queue[3],右子树queue[4],对比4比11小,将4移动到queue[1],找到原来4(queue[4)的子节点,左子树queue[9],右子树queue[10],10和9对比9比较小,但是和最后一个值比对9比8大,需要把8移动到queue[4]的位置上,9就不动,把最后一个值置为空,就调整完毕了。还是蛮妙的把
图片来源:小傅哥-图解数据结构
代码实现
poll():因为要移除所以size就会减1操作,将队列第一个结果取出并返回,取出队列最后一个值,把最后一个值置为空,然后把0和最后一个值当目标值传给siftDown
siftDownComparable():这个方法的核心和上边是一样的,先找到节点的左右节点,比对大小,选中最小的,往上部放,再找选中的节点的子节点继续比对,直到都调整完毕。
@Override
public E poll() {
if (size == 0) {
return null;
}
int s = --size;
E result = (E) queue[0];
// 取出队列最后一个的值
E x = (E) queue[s];
// 将队列最后一个值置为空
queue[s] = null;
if (s != 0) {
siftDown(0, x);
}
return result;
}
private void siftDown(int k, E x) {
siftDownComparable(k, x);
}
/**
* 移除节点,需要把队列头部数据移除,但是我们移除完毕还需要按照规则选择新的头部节点
* 新的头部节点要满足比子节点小,怎么对比处理需要思考
* <p>
* 我们先需要以队头为首,找到它的树子节点(左右节点),找到最小的值认定为它是新的队头,此时
* 再去找当前最小值的子节点(左右节点),继续对比大小,以小的为主,当队头的子节点,
* 最后以最后一个值为目标值,找到的最小子节点跟目标值对比,大于目标值退出循环,小于目标值替换值
* 直到达到目的。最主要目的是保持树的规则不破坏,
* 由于插入时同节点大小没有比对,所以删除时,左右树节点进行比对取最小值当父节点,最终把目标值
* 放入适当的树节点下。
* 找当前的左子节点,公式 n*2+1 找右子节点 左子节点的合+1;
*
* 下行代码是对比替换的过程。
*/
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// 先找出中间节点
int half = size >>> 1;
while (k < half) {
// 找到左子节点下标,当前节点*2+1
int child = (k << 1) + 1;
Object c = queue[child];
// 找到右子节点下标,左子节点基础上+1就可
int right = child + 1;
// 左右节点的值进行比对,取最小的值赋值
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) {
c = queue[child = right];
}
// 目标值与C值比较,目标值小于C值退出循环
if (key.compareTo((E) c) <= 0) {
break;
}
queue[k] = c;
k = child;
}
queue[k] = key;
}
// 取出队列头部元素
@Override
public E peek() {
return size == 0 ? null : (E) queue[0];
}
我们测试删除两个队列查看变化:
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(1);
queue.add(3);
queue.add(5);
queue.add(11);
queue.add(4);
queue.add(6);
queue.add(7);
queue.add(12);
queue.add(15);
queue.add(10);
queue.add(9);
queue.add(8);
queue.add(2);
queue.poll();
System.out.println("【出队1】当前队列:" + Arrays.toString(queue.queue));
queue.poll();
System.out.println("【出队3】当前队列:" + Arrays.toString(queue.queue));
}
打印的结果
这个数组结构的二叉树设计还是蛮妙的,代码又很简洁,很佩服java代码的大神,需要细细品味,如果不太懂,可以打断点走流程看,相信你一定能有所收获。
完整代码:
/**
* @Author df
* @Date 2022/11/28 16:56
* @Version 1.0
* <p>
* 假如一个3进来就会存储根节点就是queue[0]的位置,在进来个2,按照二叉树性质
* 需要知道根节点并根据跟节点的值进行比对,发现2比3小,就会交换值,将2存储queue[0],将3存储到queue[1],
* 那么就形成 2
* 3
* 在存储一个5,跟父节点对比不大于根节点存储queue[2]位置,那么就形成
* 2
* 3 5
* 再存储一个6,跟父节点3比它大,存储queue[3],所以就形成了
* 2
* 3 5
* 6
* 再存储一个7,存储queue[4],所以就形成了
* 2
* 3 5
* 6 7
* 从这里你也能看到规律,找父节点先n-1除以2就可以得到自己的父节点
* 查找父节点--------------------------------------------
* 如找下标1的父节点,则利用公式 1-1/2得到0,所以下标0的值就是下标1父亲,
* 如找下标2的父节点,则利用公式 2-1/2得到0,所以下标0的值就是下标2父亲,
* 如找下标3的父节点,则利用公式 3-1/2得到1,所以下标1的值就是下标3父亲,
* 以此类推。。。都能找到自身的父亲,除以2得到的数需要取整
* 第二个规律是父节点的值小于等于任意孩子节点,同节点之间节点不予维护
* <p>
* 优先队列采用完全二叉树,在存储时就会进行数据的对比小的存储左边,大的存储右边
* 以数组形式存储
*/
public class PriorityQueue<E> implements Queue<E> {
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 用数组实现队列
transient Object[] queue;
private int size = 0;
// 默认
public PriorityQueue() {
queue = new Object[DEFAULT_INITIAL_CAPACITY];
}
//private Logger logger= LoggerFactory.getLogger(PriorityQueue.class);
@Override
public boolean add(E e) {
return offer(e);
}
// 扩容
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// 开辟新的容量,如果小于64就队列长度+队列长度+2也就是说11+11+2=24,大于64就需要队列长度+队列长度/2
int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2) : (oldCapacity >> 1));
// 判断是不是接近int值的最大值
if (newCapacity - (Integer.MAX_VALUE - 8) > 0) {
newCapacity = (minCapacity > Integer.MAX_VALUE - 8) ?
Integer.MAX_VALUE :
Integer.MAX_VALUE - 8;
}
// 开辟新的空间
queue = Arrays.copyOf(queue, newCapacity);
}
// 对比并替换存储值
private void siftUp(int k, E x) {
siftUpComparable(k, x);
}
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
System.out.println("【入队】元素:" + key + " 当前队列:" + Arrays.toString(queue));
while (k > 0) {
//得到当前元素的父节点, >>>除2取整
int parent = (k - 1) >>> 1;
// 得到父节点的值
Object e = queue[parent];
// 当前元素大于父结点,不进行替换退出循环
if (key.compareTo((E) e) > 0) {
break;
}
// 当前元素比父节点大,则先给当前位置赋值为父结点的值
System.out.println("【入队】替换过程,父子节点位置替换,继续循环。父节点值:" + e + " 存放到位置:" + k);
queue[k] = e;
// 把父节点的小标赋值给k
k = parent;
}
queue[k] = key;
System.out.println("【入队】完成 Idx:" + k + " Val:" + key + " \r\n当前队列: \r\n" + Arrays.toString(queue));
}
@Override
public boolean offer(E e) {
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;
}
@Override
public E poll() {
if (size == 0) {
return null;
}
int s = --size;
E result = (E) queue[0];
E x = (E) queue[s];
queue[s] = null;
if (s != 0) {
siftDown(0, x);
}
return result;
}
private void siftDown(int k, E x) {
siftDownComparable(k, x);
}
/**
* 移除节点,需要把队列头部数据移除,但是我们移除完毕还需要按照规则选择新的头部节点
* 新的头部节点要满足比子节点小,怎么对比处理需要思考
* <p>
* 我们先需要以队头为首,找到它的树子节点(左右节点),找到最小的值认定为它是新的队头,此时
* 再去找当前最小值的子节点(左右节点),继续对比大小,以小的为主,当队头的子节点,
* 最后以最后一个值为目标值,找到的最小子节点跟目标值对比,大于目标值退出循环,小于目标值替换值
* 直到达到目的。最主要目的是保持树的规则不破坏,
* 由于插入时同节点大小没有比对,所以删除时,左右树节点进行比对取最小值当父节点,最终把目标值
* 放入适当的树节点下。
* 找当前的左子节点,公式 n*2+1 找右子节点 左子节点的合+1;
*
* 下行代码是对比替换的过程。
*/
@SuppressWarnings("unchecked")
private void siftDownComparable(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// 先找出中间节点
int half = size >>> 1;
while (k < half) {
// 找到左子节点下标,当前节点*2+1
int child = (k << 1) + 1;
Object c = queue[child];
// 找到右子节点下标,左子节点基础上+1就可
int right = child + 1;
// 左右节点的值进行比对,取最小的值赋值
if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) {
c = queue[child = right];
}
// 目标值与C值比较,目标值小于C值退出循环
if (key.compareTo((E) c) <= 0) {
break;
}
queue[k] = c;
k = child;
}
queue[k] = key;
}
// 取出队列头部元素
@Override
public E peek() {
return size == 0 ? null : (E) queue[0];
}
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(1);
queue.add(3);
queue.add(5);
queue.add(11);
queue.add(4);
queue.add(6);
queue.add(7);
queue.add(12);
queue.add(15);
queue.add(10);
queue.add(9);
queue.add(8);
queue.add(2);
queue.poll();
System.out.println("【出队1】当前队列:" + Arrays.toString(queue.queue));
queue.poll();
System.out.println("【出队3】当前队列:" + Arrays.toString(queue.queue));
}
/**
* 1
* 3 2
* 11 4 5 7
* 12 15 10 9 8 6
*
*
*
*
*
*
*
*
*
* 2
* 3 5
* 11 4 6 7
* 12 15 10 9 8
*
*
* * 参数:0,8 去掉下标11==null
* * 1.队列长度 12/2=6
* * 2.找左子节下标 0*2+1=1 child=1 value=3
* * 3.找右子节下标 1+1=2 right=2 value=5
* * 4.左右节点对比找最小的值 3
* * 5.目标值跟3比对,目标值不小于3,开始替换
* * 6.queue[0]=3; k=1
* *
* * 7.循环到上边第2个 1*2+1=3 child=3 value=11;
* * 8.循环到上边第3个 3+1=4 child=4 value=4;
* * 9.循环到上边第4个 4 child=4 c=4
* * 10.queue[1]=4 k=4
* *
* * 11.循环到上边第7个 4*2+1=9 child=9 value=10;
* * 12.循环到上边第8个 9+1=10; child=10 value=9;
* * 13.循环到上边第9个 9 child=10 c=9
* * 14.9大于目标值退出循环
* * 15.queue[4]=8;
*
* 3
* 4 5
* 11 8 6 7
* 12 15 10 9
*
* 11/2=5
* 0+1=1 4 2 5
* 9>4 no
* queue[0]=4
* k=1
*
* 1*2+1=3,11 4,8
* child=4 c=8
* queue[1]=8;
* k=4;
*
* 4*2+1=9,10 10,9
* child=10 c=9
* 退出循环
* queue[4]=9
*
* 4
* 8 5
* 11 9 6 7
* 12 15 10
*
*
* */
}