赞
踩
我们已经知道队列是一种先进先出(FIFO)的数据结构,但是有些情况下,队列不能满足我们的一些需求,我们需要根据元素的优先级来进行操作,优先级高的元素先出队列。
例如:
医院的夜间门诊系统
队列元素是病人对象
优先级是病人的病情严重情况、挂号时间
电商网站的特卖、抢购系统
当用户提交订单请求时,可以将普通用户和VIP用户的订单都放入队列里,普通用户的优先级别最低,VIP用户根据其会员等级来决定优先级,即使普通用户和VIP用户同时下单,级别高的用户可以先抢购到商品。如果在全都是普通用户的情况下,则根据用户的下单时间来出队。
在上面这些场景下,后台操作的数据具有优先级,那么普通的队列就在这里不适合,所以我们需要用到优先级队列PriorityQue。
优先级队列应该提供两个最基本的操作:一是返回最高优先级对象;二是添加新的对象。
JDK1.8中的PriorityQueue底层使用了堆的数据结构,那么我们先了解一下堆的概念。
堆通常是一个可以被看做一棵二叉树的数组对象。堆总是满足下列性质:
- 堆中某个结点的值总是不大于或不小于其父结点的值;
- 堆总是一颗完全二叉树。
将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
堆的定义如下:n个元素的序列{k1, k2, k3, …, kn}当且仅当满足下关系时,称之为堆。
若将和此序列对应的一维数组(即以一维数组作此序列的存储结构)看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。由此,若序列{k1, k2, k3, …, kn}是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
从堆的概念知道,堆是一棵完全二叉树,以层序的规则采用顺序的方式来高效存储。
大根堆示意图:
如果是非完全二叉树,则不适合使用顺序方式来存储,因为为了能够还原二叉树,数组空间里必须要存储空结点,就会导致空间利用率比较低。
如下图的非完全二叉树:
将元素存储到数组中后,可以根据二叉树的性质5对树进行还原。假设i为节点在数组中的下标,则有:
给定一个序列{ 27,15,19,18,28,34,65,49,25,37 },如何将构建成堆?
首先我们将其展现为完全二叉树的形式:
仔细观察上图可以发现,除根节点外,其左右子树已经满足堆的性质,即为小根堆,所以我们通过从根节点开始向下调整,可以将它构建成堆。
向下调整算法过程:
通过一对指针(父节点指针,下文用parent代指 和 孩子节点指针,下文用child代指)来进行调整。
调整图解过程:
实现代码:
private void shiftDown(int parent,int len){ int child = parent * 2 + 1; //左孩子结点 while(child < len){ //如果孩子结点索引等于或大于len,则不存在该孩子结点,其双亲结点为叶子结点 //比较左右孩子结点值的大小 if(child + 1 < len && elem[child] < elem[child+1]){ //(child+1 < len)判断是否有右孩子 child++; //child结点一定记录值大的那个 } //这里创建大根堆,比较孩子结点是否大于双亲结点 if(elem[child] > elem[parent]){ //大于就交换 int temp = elem[child]; elem[child] = elem[parent]; elem[parent] = temp; //因为被调整的孩子结点可能也是其他孩子的双亲结点,所以继续向下调整 parent = child; child = parent * 2 + 1; }else{ break; } } }
如果想把序列{ 27,15,19,18,28,34,65,49,25,37 } 调整为大根堆,此时根节点的左右子树不满足堆的性质,我们不能在这里通过向下调整来完成,相反,我们要使用向上调整。
调整图解过程:
实现代码:
//调整堆(将初始堆调整为大根堆或小根堆) public void createHeep(){ //从最后一个结点的双亲结点开始调整 int len = elem.length; for(int i = (len - 1 - 1) / 2; i >= 0; i--){ shiftDown(i,len); } } private void shiftDown(int parent,int len){ int child = parent * 2 + 1; //左孩子结点 while(child < len){ //如果孩子结点索引等于或大于len,则不存在该孩子结点,其双亲结点为叶子结点 //比较左右孩子结点值的大小 if(child + 1 < len && elem[child] < elem[child+1]){ //(child+1 < len)判断是否有右孩子 child++; //child结点一定记录值大的那个 } //这里创建大根堆,比较孩子结点是否大于双亲结点 if(elem[child] > elem[parent]){ //大于就交换 int temp = elem[child]; elem[child] = elem[parent]; elem[parent] = temp; //因为被调整的孩子结点可能也是其他孩子的双亲结点,所以继续向下调整 parent = child; child = parent * 2 + 1; }else{ break; } } }
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明 (时间复杂度本来看的就是 近似值,多几个节点不影响最终结果)
第1层,2^0个节点,需要向下移动h-1层。
第2层,2^1个节点,需要向下移动h-2层。
第3层,2^2个节点,需要向下移动h-3层。
第4层,2^3个节点,需要向下移动h-4层。
…
第h-1层,2^h-2个节点,需要向下移动1层。
推导公式这里直接给出课件里的哈哈!
因此:建堆的时间复杂度为O(N)。
在堆中插入新元素主要分为两个操作:
插入元素图解过程:
首先要知道我们这里操作的是优先级队列,而队列必须满足尾进头出原则,堆顶元素只可能是堆中优先级最高的,先访问的一定是堆顶的元素。
删除堆顶元素也是需要两个操作
这里的调整算法与上文 堆的创建 中堆向下调整逻辑一致,只是比较规则不同(这里是大根堆,所以当parent值小于child值时进行交换),调整的图解过程也一致,所以不再赘述。
/** * 模拟实现PriorityQueue优先级队列容器类 * @param <E> */ public class MyPriorityQueue2<E extends Comparable<E>> { //存放堆元素 private Object[] elemData; //默认初始容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; //堆元素个数 private int size; //对象比较器 Comparator<E> comparator; /** * 创建默认容量大小的队列对象,并使用元素的自然排序规则 */ public MyPriorityQueue2(){ this(DEFAULT_INITIAL_CAPACITY,null); } /** * 创建指定容量大小的队列对象,并使用元素的自然排序规则 * @param initialCapacity 指定初始容量 */ public MyPriorityQueue2(int initialCapacity){ this(initialCapacity,null); } /** * 创建默认初始容量大小的队列对象,并使用指定比较器对象 * @param comparator 指定比较器 */ public MyPriorityQueue2(Comparator<E> comparator){ this(DEFAULT_INITIAL_CAPACITY,comparator); } /** * 创建指定初始容量大小的队列对象并初始化比较器对象成员 * @param initialCapacity 指定初始容量 * @param comparator 比较器对象 */ public MyPriorityQueue2(int initialCapacity, Comparator<E> comparator){ this.elemData = new Object[initialCapacity]; this.comparator = comparator; } /** * 将指定元素入队列,然后根据比较规则来调整堆结构 * @param e 指定元素对象 * @return true */ public boolean offer(E e){ isFull(); int i = size++; elemData[i] = e; if(i == 0){ //首次入队 return true; } if(comparator == null){ shiftUpWithNaturalSorting(i,size); }else{ shiftUpWithComparatorSorting(i,size); } return true; } //使用自然排序比较规则来向上调整 @SuppressWarnings("unchecked") private void shiftUpWithNaturalSorting(int child,int len){ int parent = (child - 1) / 2; while (child > 0){ Comparable<E> parentElem = (Comparable<E>) elemData[parent]; if(parentElem.compareTo((E)elemData[child]) > 0){ E temp = (E)elemData[parent]; elemData[parent] = elemData[child]; elemData[child] = temp; child = parent; parent = (child - 1) / 2; }else{ break; } } } //使用定制排序比较规则来向上调整 @SuppressWarnings("unchecked") private void shiftUpWithComparatorSorting(int child,int len){ int parent = (child - 1) / 2; while (child > 0){ if(comparator.compare((E)elemData[parent],(E)elemData[child]) > 0){ E temp = (E)elemData[parent]; elemData[parent] = elemData[child]; elemData[child] = temp; child = parent; parent = (child - 1) / 2; }else{ break; } } } /** * 将队列中优先级最高的元素出队 * @return */ public E poll(){ if(isEmpty()){ return null; } E polled = (E)elemData[0]; elemData[0] = elemData[--size]; if(comparator == null){ shiftDownComparable(); }else{ shiftDownComparator(); } return polled; } @SuppressWarnings("unchecked") private void shiftDownComparable(){ int parent = 0; Comparable<E> parentEle = (Comparable<E>) elemData[parent]; int child = 1; while (child < size){ if(child + 1 < size && ((Comparable<E>)elemData[child]).compareTo((E)(elemData[child+1])) > 0){ child++; } if(parentEle.compareTo((E)elemData[child]) > 0){ Object temp = elemData[parent]; elemData[parent] = elemData[child]; elemData[child] = temp; parent = child; child = parent * 2 + 1; }else{ break; } } } @SuppressWarnings("unchecked") private void shiftDownComparator(){ int parent = 0; int child = 1; while (child < size){ if(comparator.compare((E)elemData[child],(E)elemData[child+1]) > 0){ child++; } if(comparator.compare((E)elemData[parent],(E)elemData[child]) > 0){ Object temp = elemData[parent]; elemData[parent] = elemData[child]; elemData[child] = temp; parent = child; child = parent * 2 + 1; }else{ break; } } } //检查队列空间是否足够,不够就扩容 private void isFull(){ int len = elemData.length; if(size == len){ elemData = Arrays.copyOf(elemData, len * 2); } } /** * @return 返回当前堆顶元素,如果堆为空则返回null */ @SuppressWarnings("unchecked") public E peek(){ if (isEmpty()){ return null; }else{ return (E)elemData[0]; } } /** * @return 队列是否为空 */ public boolean isEmpty(){ return size == 0; } /** * @return 返回队列元素个数 */ public int size(){ return size; } }
通过类继承图看到PriorityQueue是实现了Queue接口的,所以也实现了队列的基本方法。
在类结构当中有几种重载的构造方法,可以根据不同的形参列表来实例化不同属性的对象。
类当中还有许多基本的方法,这里我们主要关注以下三个。
示意性的模拟一个优先级队列使用场景,这里拿上文中的电商网站的特卖、抢购系统来作例子。
import java.util.Comparator; import java.util.PriorityQueue; public class PriorityQueueTest { //优先级队列的应用 public static void main(String[] args) { //准备一批用户对象 User u1 = new User(1001, "张三", MembershipLevel.V8); User u2 = new User(1002, "李四", MembershipLevel.V5); User u3 = new User(1003, "王五", MembershipLevel.V5); User u4 = new User(1004, "赵六", MembershipLevel.V3); User u5 = new User(1005, "孙七", MembershipLevel.V1); User u6 = new User(1006, "周八", MembershipLevel.V0); //假设某商品抢购场景,让所有用户都入队列,然后根据用户会员等级出队,先出队的就可以先抢到 PriorityQueue<User> userPriorityQueue = new PriorityQueue<>(new LevelComparator()); userPriorityQueue.offer(u6); userPriorityQueue.offer(u5); userPriorityQueue.offer(u4); userPriorityQueue.offer(u3); userPriorityQueue.offer(u2); userPriorityQueue.offer(u1); int userNumber = userPriorityQueue.size(); for(int i = 0; i < userNumber; i++){ User user = userPriorityQueue.poll(); if (user != null) { System.out.println(user.getName() + "是第" + (i+1) + "个抢到商品的。"); } } } } //用户会员等级 enum MembershipLevel{ V0,V1,V2,V3,V4,V5,V6,V7,V8; } //用户类 class User{ private int id; private String name; private MembershipLevel level; public User(int id, String name, MembershipLevel level) { this.id = id; this.name = name; this.level = level; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public MembershipLevel getLevel() { return level; } public void setLevel(MembershipLevel level) { this.level = level; } } //根据用户会员等级来比较 class LevelComparator implements Comparator<User> { @Override public int compare(User o1, User o2) { return o2.getLevel().compareTo(o1.getLevel()); } }
这个例子还是比较现实的哈哈。
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都 不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
具体的题目讲解就不放在本文讲解了,之后更新到面试题栏目中。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。