目录
三、堆的应用 ---- 优先级队列(PriorityQueue)
🚀TopK 问题练习:LeetCode -- 查找和最小的 K 对数字
🍎方式三:利用内部类实现(简写实现 Comparator接口的方法)
一、二叉树的顺序存储
🍎存储方式
- 使用数组保存二叉树结构,方式即将二叉树用层序遍历方式放入数组中。
- 一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。
- 这种方式的主要用法就是堆的表示。
🍎下标的关系
已知双亲(parent)的下标,则:
- 左孩子(left)下标: left = 2 * parent + 1;
- 右孩子(right)下标: right = 2 * parent + 2;
已知孩子(不区分左右)(child)下标,则:
- 双亲(parent)下标:parent = (child - 1) / 2
二、堆(heap)
🍎概念
- 堆逻辑上是一棵完全二叉树
- 堆物理上是保存在数组中
- 满足任意结点的值都大于其子树中结点的值,叫做大堆,或者大根堆,或者最大堆
- 反之,则是小堆,或者小根堆,或者最小堆
- 堆的基本作用是,快速找集合中的最值
🍎堆的创建
堆是由顺序表实现的,堆的本质就是一棵完全二叉树,且每棵子树双亲结点大于(或小于)孩子结点(大于就是大根堆,小于就是小根堆),可以根据这个特点创建堆。
🍓创建堆的过程:
- 找到堆中最后一棵子树的双亲结点。对该子树进行向下调整。
- 如果需要创建大根堆,则找到该子树两个子结点值较大的结点,然后与根结点进行比较。
- 如果孩子结点比根结点大,则交换两个结点的值,然后在数组的大小范围内,对已交换结点的子树进行向下调整,直到被调整的子树下标超出数组的范围或满足大堆条件即停止调整。
- 如果需要创建小根堆,则找到该树两个子结点值较小的结点,然后根结点进行比较。
- 如果孩子结点比根结点小,则交换两结点的值,然后在数组的大小范围内,对已交换结点的子树进行向下调整调整,直到被调整的子树下标超出数组的范围或满足小堆条件即停止调整。
- 设最后一棵子树的双亲结点所对应数组下标为parent,其左子树结点为child,数组大小为len,由于该树是一棵完全二叉树,所以下标满足 child = 2 ∗ parent + 1 ; parent = ( child − 1 ) / 2
- 最后一个结点下标为 len − 1, 即最后一棵子树的父结点:parent =( (len-1) - 1 ) / 2(len-1 相当于 child )。
🍓向下调整的过程:
- 大根堆:从最后一棵子树的父节点开始向上进行比较【 parent-- 遍历每一棵子树(parent每减一次,就指向前一棵子树的父结点)。】,如果一棵子树的右结点存在且大于左结点的值,则child++指向右结点,子节点与父结点进行比较,如果子节点大于父结点,就进行交换。
- 小根堆:从根节点开始向下进行比较【 parent++ 遍每一棵子树(parent每加一次,就指向下一棵子树的父结点)。】如果一棵子树的右结点存在且小于左结点的值,则child++指向右结点,子节点与父结点进行比较,如果子节点小于父结点,就进行交换。
⭐以创建大根堆为例
🌊代码示例
public class TreeHeap {
public int[] elem;
public int usedSize;
public TreeHeap(){
this.elem = new int[10];
}
/**
* 交换结点的值
* @param array 目标数组
* @param index1 待交换结点的下标
* @param index2 待交换结点的下标
*/
public void swap(int[] array,int index1,int index2){
int tmp = array[index1];
array[index1] = array[index2];
array[index2] = tmp;
}
/**
* 向下调整
* @param parent 每棵树的根节点
* @param len 每棵树调整的结束位置
*/
public void shiftDown(int parent,int len){
int child = 2*parent+1;
while (child<len){
if(child+1<len && this.elem[child]<this.elem[child+1]){
child++;
}
if(this.elem[child]>this.elem[parent]){
swap(elem,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
/**
* 创建大根堆
* @param array 结点的值
*/
public void createTreeHeap(int[] array){
//拷贝数据
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
//从最后一棵子树的父结点开始调整
for (int parent = (usedSize-1-1)/2; parent >=0 ; parent--) {
shiftDown(parent,usedSize);
}
}
🍉时间复杂度分析
- 最坏的情况:从根一路比较到叶子,比较的次数为完全二叉树的高度
- 建堆的过程是自下而上的,堆的本质就是一棵完全二叉树,设该二叉树的高度为h,堆元素个数为n,建堆时需要对所有高度大于1的子树进行调整,最坏情况下该堆是一个满二叉树,设堆所在的层为n(n从1开始),即第n层的子树需要调整 h - n 次。
💦时间复杂度推导过程
三、堆的应用 ---- 优先级队列(PriorityQueue)
🍎概念:
- 在很多应用中,通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。
- 最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。
- 有一种数据结构提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列。
优先级队列的实现方式有很多,但最常见的是使用堆来构建。在java中,优先队列(PriorityQueue类),默认是以小根堆实现。
🍎PriorityQueue 的基本方法
🌊代码示例
public class TestDemo {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();//创建一个优先级队列
priorityQueue.offer(13);
priorityQueue.offer(3);
priorityQueue.offer(20);
System.out.println(priorityQueue); //打印优先级队列
System.out.println(priorityQueue.poll()); //弹出队头元素
System.out.println(priorityQueue.peek()); //获取队头元素
}
}
运行结果:
[3, 13, 20] //默认使用小根堆实现
3
13
🍎实现PriorityQueue 的基本方法
🛫入队
- 将待入队的元素放入堆尾(二叉树的最后一个结点)。
- 对堆尾的结点进行向上方向调整,因为入队一个元素后,堆中只有一条到新插入结点路径上不是大根堆,所以只需要对该结点进行向上调整即可。
- 向上调整:比较调整结点与父亲结点值的大小。以大根堆为例,如果子结点值比父结点的值大,则与父亲结点的值进行交换,否则就不需要调整。
- 因为交换后不知道上面的子树是否为大根堆,所以需要对交换路径上所有的结点进行向上调整,直到调整到根结点,如果在向上调整的过程中出现父亲结点比子节点的值大(该堆已满足大根堆条件),就结束调整。
- 如果是小根堆,向上调整的结点与父亲结点的进行比较,如果子结点值比父结点的值小,则与父亲结点的值进行交换,否则就不需要调整。
- 入队完成。
💦入队过程
🌊代码示例
import java.util.Arrays;
public class TreeHeap {
public int[] elem;
public int usedSize;
public TreeHeap(){
this.elem = new int[10];
}
/**
* 交换结点的值
* @param array 目标数组
* @param index1 待交换结点的下标
* @param index2 待交换结点的下标
*/
public void swap(int[] array,int index1,int index2){
int tmp = array[index1];
array[index1] = array[index2];
array[index2] = tmp;
}
/**
* 向上调整
* @param child 待调整结点的下标
*/
public void shiftUp(int child){
int parent = (child-1)/2;
while (child>0){
if(this.elem[child]>this.elem[parent]){
swap(elem,child,parent);
child = parent;
parent = (child-1)/2;
}else{
break;
}
}
}
/**
* 优先级队列 入队操作
* @param val 待入队的元素
*/
public void offer(int val){
//如果满了就进行扩容
if(isFull()){
this.elem = Arrays.copyOf(elem,elem.length*2);
}
//在最后位置插入元素
this.elem[usedSize++] = val;
//传入数组最后一个元素
shiftUp(usedSize-1);
}
/**
* 判断队列是否满了
* @return 如果满了就返回true,否则返回去false
*/
public boolean isFull(){
return this.usedSize == this.elem.length;
}
}
🛫出队
⭐每次出队列都要保证出最大的或最小的
- 将堆顶(二叉树的根节点)元素与堆尾(二叉树的最后一个结点)元素交换,保存并删除堆尾元素。
- 从根节点开始向下调整二叉树,调整为大根堆或小根堆。
- 返回之前保存的元素。
💦出队过程
🌊代码示例
public class TreeHeap {
public int[] elem;
public int usedSize;
public TreeHeap(){
this.elem = new int[10];
}
/**
* 交换结点的值
* @param array 目标数组
* @param index1 待交换结点的下标
* @param index2 待交换结点的下标
*/
public void swap(int[] array,int index1,int index2){
int tmp = array[index1];
array[index1] = array[index2];
array[index2] = tmp;
}
/**
* 向下调整 (大根堆)
* @param parent 每棵树的根节点
* @param len 每棵树调整的结束位置
*/
public void shiftDown(int parent,int len){
int child = 2*parent+1;
while (child<len){
if(child+1<len && this.elem[child]<this.elem[child+1]){
child++;
}
if(this.elem[child]>this.elem[parent]){
swap(elem,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
/**
* 优先级队列 出队操作
* @return 返回出队列结点的值
*/
public int poll(){
//如果队列为空就抛出异常
if(isEmpty()){
throw new RuntimeException("优先级队列为空!");
}
swap(elem,0,usedSize-1);
int ret = this.elem[usedSize-1]; //保存交换后的堆尾元素
usedSize--; //交换后删除
shiftDown(0,usedSize);
return ret;
}
/**
* 判断队列是否为空
* @return 如果为空就返回true,否则返回false
*/
public boolean isEmpty(){
return usedSize == 0;
}
}
🛫获取队头元素
⭐优先级队列的队头元素就是数组0下标的元素,然后0下标的元素即可。
🌊代码示例
public int peek(){
if(isEmpty()){
throw new RuntimeException("优先级队列为空!");
}
return this.elem[0];
}
/**
* 判断队列是否为空
* @return 如果为空就返回true,否则返回false
*/
public boolean isEmpty(){
return usedSize == 0;
}
四、堆排序
✨如果是升序,则使用大堆,如果是降序,则使用小堆。
升序排序的步骤:
- 将数组转换成大根堆。
- 创建一个索引end标记最后一个元素。
- 将堆顶元素与end标记的元素进行交换。
- 交换完以后,从堆顶结点开始进行向下调整,调整的结点下标小于end。
- 调整为大根堆以后,end--。
- 重复上述步骤,直到 end < 0 ,停止。
- 因为大根堆堆顶存放最大元素,所以每次将堆顶元素与end标记的元素进行交换,再调整end-1部分的堆,这样就能依次将大的元素放在后面。
- 降序排列使用小根堆也是一样的,每次使用向上调整。
💦对下面这个数组进行升序排序
💦创建成大根堆
💦排序过程
💦排序后的数组
🌊代码示例
public class TreeHeap {
public int[] elem;
public int usedSize;
public TreeHeap(){
this.elem = new int[10];
}
/**
* 交换结点的值
* @param array 目标数组
* @param index1 待交换结点的下标
* @param index2 待交换结点的下标
*/
public void swap(int[] array,int index1,int index2){
int tmp = array[index1];
array[index1] = array[index2];
array[index2] = tmp;
}
/**
* 向下调整
* @param parent 每棵树的根节点
* @param len 每棵树调整的结束位置
*/
public void shiftDown(int parent,int len){
int child = 2*parent+1;
while (child<len){
if(child+1<len && this.elem[child]<this.elem[child+1]){
child++;
}
if(this.elem[child]>this.elem[parent]){
swap(elem,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
/**
* 创建大根堆
* @param array 结点的值
*/
public void createTreeHeap(int[] array){
//拷贝数据
for (int i = 0; i < array.length; i++) {
elem[i] = array[i];
usedSize++;
}
//从最后一棵子树开始调整
for (int parent = (usedSize-1-1)/2; parent >=0 ; parent--) {
shiftDown(parent,usedSize);
}
}
/**
* 堆排序(升序)
*/
public void heapSort(){
int end = this.usedSize-1;
while(end>0){
swap(this.elem,0,end);
shiftDown(0,end);
end--;
}
}
}
五、TopK 问题
topK问题:在大量的数据中找出最大或者最小的前k个元素,例如:在100万个数据中找到最大的10个数。(这些数据是无序的)
⏳思路一:
- 对所有数据进行排序,然后找到前K个最大或最小的元素。这种方式不推荐,因为只需要前K个最值元素,没有必要对所有数据进行排序,而且对整体数据进行排序最快的时间复杂度是:O(
)。
⏳思路二:
- 使用堆。例如求前k个最大的元素,可以创建一个大根堆的优先队列,将所有数据入队,再出队K个元素,这K个元素就是前K个最大的元素。
⭐思路三: topK问题的标准解决思路
- 例如求前k个最大元素。先将数组前K个元素创建为小根堆。
✨为什么创建的是小根堆?
- 因为小根堆中堆顶元素是最小的。那么这个大小为K的小根堆堆顶元素一定是当前K个元素中最小的一个元素。然后从数组第K+1个元素开始遍历剩下的元素,如果有元素比堆顶元素大,那么这个元素可能就是topK中的一个元素(可能是前K个最大的元素之一)。此时的堆顶元素一定不是前K个最大的元素。
- 如果堆顶元素小于数组中的元素,那么出堆顶元素,入当前比堆顶大的元素,然后重新调整为小根堆。
- 遍历完数组后,这个大小为K的小根堆中的元素就是前K个最大的元素。
- 时间复杂度:O(
),当K越来越小时,时间复杂度就是O(n)。
- 同理找前K个最小的元素需要使用大根堆,思路与求前K个最大元素是一样的。
📔 总结:
- 如果求前K个最大的元素,要创建一个小根堆。
- 如果求前K个最小的元素,要创建一个大根堆。
- 求第K大的元素。需要创建一个小根堆,堆顶元素就是第K大的元素。
- 求第K小的元素。需要创建一个大根堆,堆顶元素就是第K小的元素。堆内元素就是前K个最小的元素。
🌊代码示例
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class Topk {
/**
* 前K个最小的元素(需要创建大根堆,堆的大小为K)
* @param array
* @param k
* @return
*/
public static int[] topk(int[] array,int k){
//创建一个大小为k的大根堆
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(k, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
});
//遍历数组中的元素,将元素放入创建的堆中
for(int i=0;i<array.length;i++){
if(maxHeap.size()<k){
maxHeap.offer(array[i]);
}else{
//从第k+1个元素开始,每个元素与堆顶元素进行比较
int top = maxHeap.peek();
if(top>array[i]){
maxHeap.poll();
maxHeap.offer(array[i]);
}
}
}
int[] ret = new int[k];
for (int i = 0; i < k; i++) {
ret[i] = maxHeap.poll();
}
return ret;
}
public static void main(String[] args) {
int[] array = {18,21,8,10,34,12};
int[] ret = topk(array,3);
System.out.println(Arrays.toString(ret));
}
}
运行结果:[12, 10, 8]
🚀TopK 问题练习:LeetCode -- 查找和最小的 K 对数字
📌题目描述
- 给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。
- 定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
- 请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk) 。
📋题目示例
- 示例一:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3 输出: [1,2],[1,4],[1,6] 解释: 返回序列中的前 3 对数: [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
- 示例二:
输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2 输出: [1,1],[1,1] 解释: 返回序列中的前 2 对数: [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]
- 示例三:
输入: nums1 = [1,2], nums2 = [3], k = 3 输出: [1,3],[2,3] 解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]
⏳解题思路
- 这道题其实就是topK问题,获取前k个最小的元素。这里的元素是一个数对值。
- 创建一个大小为k的大根堆,存放前k个和最小的数对。
- 给定的两个目标数组都是升序排列的,如果k小于数组的长度,那么数对中的一个数一定在数组前k个元素中。
- 遍历两个数组中的元素,遍历次数:min(k,数组长度)【k,与数组长度的最小值】,构造数对值,将前k个数对放进大根堆,并调整。
- 如果数对(两个数)的和比堆顶数对和小,则替换堆顶的元素,最终大根堆中的数对就是最小的k对数字。
🌊代码示例
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
PriorityQueue<List<Integer>> maxHeap = new PriorityQueue<>(k, new Comparator<List<Integer>>() {
@Override
public int compare(List<Integer> o1, List<Integer> o2) {
return o2.get(0)+o2.get(1) - o1.get(0) - o1.get(1);
}
});
for (int i = 0; i < Math.min(nums1.length,k); i++) {
for (int j = 0; j < Math.min(nums2.length,k); j++) {
List<Integer> list = new ArrayList<>();
list.add(nums1[i]);
list.add(nums2[j]);
if(maxHeap.size()<k){
maxHeap.offer(list);
}else{
int top = maxHeap.peek().get(0) + maxHeap.peek().get(1);
if(top>nums1[i]+nums2[j]){
maxHeap.poll();
maxHeap.offer(list);
}else{
break;
}
}
}
}
List<List<Integer>> ret = new ArrayList<>();
for (int i = 0; i < k && !maxHeap.isEmpty(); i++) {
ret.add(maxHeap.poll());
}
return ret;
}
}
六、PriorityQueue的比较方式
在了解 PriorityQueue 的比较方式前需要先了解对象的比较。
关于对象的比较在前面的文章中已经介绍过了。
文章导航:【JavaSE】----- 面向对象编程
⭐简单回顾一下对象的比较。
关于对象的比较主要有三种方式:
- 重写父类的equals 方法,用于比较两个对象是否相同:如果两个对象相同,返回 true,否则就返回 false。
- 比较大小:实现 Comparable 接口或 Comparator 接口。
PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了:Comparble 和 Comparator两种方式。
- Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接口,并覆写compareTo方法
- 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现Comparator接口并覆写compare方法。
💥补充:优先级队列在插入元素时有个要求:
- 插入的元素不能是null或者元素之间必须要能够进行比较。
✨插入元素为null的情况:
✨元素之间未指定比较规则(元素之间不能比较)的情况:
指定排序规则,对Card类的对象进行比较,通过比较的过程了解 PriorityQueue 底层的比较方式。
🍎方式一:实现 Comparable 接口
- 注意:实现 Comparable 接口需要重写接口内部的抽象方法。
🌊代码示例
import java.util.PriorityQueue;
//扑克牌
class Card implements Comparable<Card>{
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
//使用Comparable接口,需要重写compareTo方法
//按照扑克牌的数值进行比较
public int compareTo(Card o) {
return this.rank - o.rank;
}
@Override
//重写 toString 方法
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) {
//默认是小根堆
PriorityQueue<Card> priorityQueue = new PriorityQueue<>();
priorityQueue.offer(new Card(2, "♠"));
priorityQueue.offer(new Card(1, "♠"));
System.out.println(priorityQueue);
}
}
❄运行结果
💦分析比较的过程
⭐将小根堆转换为大根堆
- 将重写的 compareTo 方法的返回值交换一下位置就可以了。
✨这种方式对类的侵入性太强,一旦指定了比较规则,那么就不能轻易进行修改了。
🍎方式二:实现 Comparator 接口。
- 创建一个类,实现 Comparator 接口。通过这个类确定对象比较的规则。
🌊代码示例
//扑克牌
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
//重写 toString 方法
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
class RankComparator implements Comparator<Card> {
@Override
public int compare(Card o1, Card o2) {
return o1.rank - o2.rank;
}
}
public class Test {
public static void main(String[] args) {
Card card1 = new Card(1, "♠");
Card card2 = new Card(2, "♠");
RankComparator rankComparator = new RankComparator();
//小根堆
PriorityQueue<Card> priorityQueue = new PriorityQueue<>(rankComparator);
priorityQueue.offer(card1);
priorityQueue.offer(card2);
System.out.println(priorityQueue);
}
}
❄运行结果
💦分析比较的过程
🍎方式三:利用内部类实现(简写实现 Comparator接口的方法)
🌊代码示例
//扑克牌
class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
//重写 toString 方法
public String toString() {
return "Card{" +
"rank=" + rank +
", suit='" + suit + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) {
Card card1 = new Card(1, "♠");
Card card2 = new Card(2, "♠");
PriorityQueue<Card> priorityQueue = new PriorityQueue<>(new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
return o2.rank - o1.rank;
}
});
priorityQueue.offer(card1);
priorityQueue.offer(card2);
System.out.println(priorityQueue);
}
}
❄运行结果
✨lambda表达式,是上面方法的简写。
public class Test {
public static void main(String[] args) {
Card card1 = new Card(1, "♠");
Card card2 = new Card(2, "♠");
PriorityQueue<Card> priorityQueue = new PriorityQueue<>((x,y) -> {return y.rank-x.rank;} );
priorityQueue.offer(card1);
priorityQueue.offer(card2);
System.out.println(priorityQueue);
}
}
- 这种写法代码虽然简洁,但是可读性很差。