• 数据结构——堆、堆排序和优先级队列(代码为Java版本)


    目录

    1. 二叉树的顺序存储

    1.1 存储方式

    1.2 下标关系

    2. 堆(heap)

    2.1 概念

    2.2 操作 - 向下调整

    2.3 操作 - 向上调整

    2.4 操作 - 弹出堆顶元素

    2.5 操作 - 向下调整实现堆排序

    2.6 向下调整和向上调整的时间复杂度和空间复杂度对比

    3. 堆的应用 - 优先级队列

    3.1 概念

    3.2 内部原理

    3.3 操作-入队列

    3.4 操作-出队列(优先级最高)

    3.5 返回队首元素(优先级最高)

    3.6 总结

    4. java 中的优先级队列(PriorityQueue)

    4.1 PriorityQueue的常用构造方法

    4.2 PriorityQueue的扩容方法

    4.3 Top-k问题

    4.4 PriorityQueue中同一个类的对象之间的比较

    4.5 分析PriorityQueue的offer方法


    1. 二叉树的顺序存储

    1.1 存储方式

    使用数组保存二叉树结构方式即将二叉树用层序遍历方式放入数组中。一般只适合表示完全二叉树,因为非完全二叉树会有空间的浪费。这种方式的主要用法就是堆的表示。

    1.2 下标关系

    前提:根结点从0开始算起

    已知双亲(parent)的下标,则:左孩子(left)下标 = 2 * parent + 1;右孩子(right)下标 = 2 * parent + 2;
    已知孩子(不区分左右)(child)下标,则:双亲(parent)下标 = (child - 1) / 2; 

    2. 堆(heap)

    接下来的所有代码根结点都是从0开始算起!!!

    2.1 概念

    (1)堆逻辑上是一棵完全二叉树

    (2)堆物理上是保存在数组中

    (3)满足任意结点的值都大于树中结点的值,叫做大堆,或者大根堆,或者最大堆

    (4)反之,则是小堆,或者小根堆,或者最小堆

    (5)堆的基本作用是,快速找集合中的最值

    2.2 操作 - 向下调整

    前提:左右子树必须已经是一个堆,才能调整。

    说明:

    • elem 代表存储堆的数组
    • usedSize 代表数组中被视为堆数据的个数
    • child 代表孩子的下标
    • parent 代表父节点的下标

    如何建堆:

    具体的建堆代码和解析: 

    将向下调整单独封装成一个方法:

    就是找父结点下标,再找孩子的下标! 

    1. public class TestHeap {
    2. public int[] elem;
    3. public int usedSize;//已被使用的长度
    4. //做一个约定:初始化给个10个大小的int类型的数组,这里传进去的数组大小一定要一样大
    5. public TestHeap(){
    6. elem = new int[10];
    7. }
    8. //初始化数组
    9. //array数组的大小一定要为40个字节,做一个约定
    10. public void initElem(int[] array){
    11. for(int i = 0;i < array.length;i++){
    12. elem[i] = array[i];
    13. usedSize++;
    14. }
    15. }
    16. public void createHeap(){
    17. for(int parent = (usedSize - 1 -1) / 2;parent >= 0;parent--){
    18. shiftDown(parent,usedSize);
    19. }
    20. }
    21. /**
    22. *@Description:向下调整的代码
    23. *@Author:lxt
    24. *@Date:2023/10/24 15:08
    25. */
    26. private void shiftDown(int parent,int len){
    27. //找到左孩子结点的下标
    28. int child = 2 * parent + 1;
    29. while(child < len){//至少存在左孩子
    30. //比较左右孩子的大小
    31. //child + 1 < len是为了防止找右孩子的过程中越界
    32. if(child + 1 < len && elem[child] < elem[child + 1])
    33. child++;
    34. //父子结点的比较
    35. if(elem[child] > elem[parent]){
    36. int tmp = elem[parent];
    37. elem[parent] = elem[child];
    38. elem[child] = tmp;
    39. //如果有交换就要判断左子树或者右子树是不是也满足堆的要求
    40. parent = child;
    41. child = parent * 2 + 1;//找到左孩子的下标
    42. }else {
    43. break;
    44. }
    45. }
    46. }
    47. }

    2.3 操作 - 向上调整

    就是找孩子下标,再找父结点的下标!

    1. /**
    2. *@Description:向上调整的算法
    3. *@Author:lxt
    4. *@Date:2023/10/24 15:47
    5. */
    6. public void shiftUp(int child){
    7. //找到父结点
    8. int parent = (child - 1) / 2;
    9. while (child > 0){
    10. //如果孩子结点大于父结点,就交换
    11. if(elem[child] > elem[parent]){
    12. int tmp = elem[child];
    13. elem[child] = elem[parent];
    14. elem[parent] = tmp;
    15. //父子结点的标记往上走
    16. child = parent;
    17. parent = (child - 1);
    18. }else {
    19. //如果没进入上面if,证明这个不用调整,是一个堆
    20. break;
    21. }
    22. }
    23. }
    24. //向堆插入元素
    25. public void offer(int val){
    26. if(isFull()){
    27. //扩容
    28. elem = Arrays.copyOf(elem,2*elem.length);
    29. }
    30. //插到堆尾
    31. elem[usedSize++] = val;
    32. //开始向上调整
    33. shiftUp(usedSize - 1);
    34. }
    35. //判断是不是为满了
    36. public boolean isFull(){
    37. return usedSize == elem.length;
    38. }
    39. //写一个交换方法
    40. private void swap(int[] array,int i,int j){
    41. int tmp = array[i];
    42. array[i] = array[j];
    43. array[j] = tmp;
    44. }

    2.4 操作 - 弹出堆顶元素

    1. //写一个交换方法
    2. private void swap(int[] array,int i,int j){
    3. int tmp = array[i];
    4. array[i] = array[j];
    5. array[j] = tmp;
    6. }
    7. //模拟优先队列,弹出堆顶元素后,重新再进行向下调整
    8. public void pop(){
    9. //先判断是否为空
    10. if(isEmpty()){
    11. return;
    12. }
    13. //如果不会空,代表要交换堆顶和最后一个结点
    14. swap(elem,0,usedSize - 1);
    15. usedSize--;
    16. //交换完后,进行向下调整
    17. shiftDown(0,usedSize);
    18. }
    19. /**
    20. *@Description:向下调整的代码
    21. *@Author:lxt
    22. *@Date:2023/10/24 15:08
    23. */
    24. private void shiftDown(int parent,int len){
    25. //找到左孩子结点的下标
    26. int child = 2 * parent + 1;
    27. while(child < len){//至少存在左孩子
    28. //比较左右孩子的大小
    29. //child + 1 < len是为了防止找右孩子的过程中越界
    30. if(child + 1 < len && elem[child] < elem[child + 1])
    31. child++;
    32. //父子结点的比较
    33. if(elem[child] > elem[parent]){
    34. int tmp = elem[parent];
    35. elem[parent] = elem[child];
    36. elem[child] = tmp;
    37. //如果有交换就要判断左子树或者右子树是不是也满足堆的要求
    38. parent = child;
    39. child = parent * 2 + 1;//找到左孩子的下标
    40. }else {
    41. break;
    42. }
    43. }
    44. }

    2.5 操作 - 向下调整实现堆排序

    思路和弹出堆顶元素相似!!!

    1. //堆排序:不断让堆顶和最后一个结点交换,然后向下调整
    2. public void heapSort(){
    3. int end = usedSize - 1;
    4. while(end > 0){//下标为0的结点已经没有元素与之交换
    5. swap(elem,0,end);
    6. shiftDown(0,end);
    7. end--;
    8. }
    9. }
    10. /**
    11. *@Description:向下调整的代码
    12. *@Author:lxt
    13. *@Date:2023/10/24 15:08
    14. */
    15. private void shiftDown(int parent,int len){
    16. //找到左孩子结点的下标
    17. int child = 2 * parent + 1;
    18. while(child < len){//至少存在左孩子
    19. //比较左右孩子的大小
    20. //child + 1 < len是为了防止找右孩子的过程中越界
    21. if(child + 1 < len && elem[child] < elem[child + 1])
    22. child++;
    23. //父子结点的比较
    24. if(elem[child] > elem[parent]){
    25. int tmp = elem[parent];
    26. elem[parent] = elem[child];
    27. elem[child] = tmp;
    28. //如果有交换就要判断左子树或者右子树是不是也满足堆的要求
    29. parent = child;
    30. child = parent * 2 + 1;//找到左孩子的下标
    31. }else {
    32. break;
    33. }
    34. }
    35. }

    2.6 向下调整和向上调整的时间复杂度和空间复杂度对比

    这个时间复杂度是指建堆整个过程的时间复杂度,不是单指一个向下调整或者向上调整的时间复杂度!

    向下调整向上调整
    时间复杂度O(N)O(N*logN)
    空间复杂度O(1)O(1)

    (1)向下调整

    (2)向上调整

    3. 堆的应用 - 优先级队列

    3.1 概念

    在很多应用中,我们通常需要按照优先级情况对待处理对象进行处理,比如首先处理优先级最高的对象,然后处理次高的对象。最简单的一个例子就是,在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。

    在这种情况下,我们的数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)

    3.2 内部原理

    优先级队列的实现方式有很多,但最常见的是使用堆来构建。

    3.3 操作-入队列

    过程(以大堆为例):
    (1) 首先按尾插方式放入数组
    (2)比较其和其双亲的值的大小,如果双亲的值大,则满足堆的性质,插入结束(其实就是向上调整)
    (3)否则,交换其和双亲位置的值,重新进行 (2)、(3) 步骤
    (4)直到根结点
    图示:

    1. /**
    2. *@Description:向上调整的算法
    3. *@Author:lxt
    4. *@Date:2023/10/24 15:47
    5. */
    6. public void shiftUp(int child){
    7. //找到父结点
    8. int parent = (child - 1) / 2;
    9. while (child > 0){
    10. //如果孩子结点大于父结点,就交换
    11. if(elem[child] > elem[parent]){
    12. int tmp = elem[child];
    13. elem[child] = elem[parent];
    14. elem[parent] = tmp;
    15. //父子结点的标记往上走
    16. child = parent;
    17. parent = (child - 1);
    18. }else {
    19. //如果没进入上面if,证明这个不用调整,是一个堆
    20. break;
    21. }
    22. }
    23. }

    3.4 操作-出队列(优先级最高)

    为了防止破坏堆的结构,删除时并不是直接将堆顶元素删除,而是用数组的最后一个元素替换堆顶元素,然后通过向下调整方式重新调整成堆。

    3.5 返回队首元素(优先级最高)

    返回堆顶元素即可

    3.6 总结

    向上调整向下调整
    入队列×
    出队列×

    4. java 中的优先级队列(PriorityQueue)

    关于PriorityQueue的使用注意:

    (1)使用时必须导入PriorityQueue所在的包,即:

    import java.util.PriorityQueue;

     (2)PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException一次。

    (3)不能插入null对象,否则会抛出NullPointerException。

    (4)没有容量限制,可以插入任意多个元素,其内部可以自动扩容。

    (5)插入和删除元素的时间复杂度为O(logN),也就是树的高度。

    (6)PriorityQueue底层使用了堆数据结构。

    (7)PriorityQueue默认情况下是小堆——即每次获取到的元素都是最小元素。

    PriorityQueue implements Queue

    错误处理抛出异常返回特殊值
    入队列add(e)offer(e)
    出队列remove()poll()
    队首元素element()peek()

    4.1 PriorityQueue的常用构造方法

    构造器功能介绍
    PriorityQueue()创建一个空的优先级队列,默认容量是11
    PriorityQueue(int initialCapacity)

    创建一个初始容量为initialCapacity的优先级队列,注意:initialCapacity不能小于1,否则会抛出illegalArgumentException异常

    PriorityQueue(Collection c)

    用一个集合来创建优先级队列

    PriorityQueue(Comparator comparator)传入一个比较器,可以以实现同一个类不同对象的比较

    4.2 PriorityQueue的扩容方法

    在集合中找扩容方法,就去找grow方法。

    jdk1.8的源码:
     

    1. /**
    2. * Increases the capacity of the array.
    3. *
    4. * @param minCapacity the desired minimum capacity
    5. */
    6. private void grow(int minCapacity) {
    7. int oldCapacity = queue.length;
    8. // Double size if small; else grow by 50%
    9. int newCapacity = oldCapacity + ((oldCapacity < 64) ?
    10. (oldCapacity + 2) :
    11. (oldCapacity >> 1));
    12. // overflow-conscious code
    13. if (newCapacity - MAX_ARRAY_SIZE > 0)
    14. newCapacity = hugeCapacity(minCapacity);
    15. queue = Arrays.copyOf(queue, newCapacity);
    16. }
    17. private static int hugeCapacity(int minCapacity) {
    18. if (minCapacity < 0) // overflow
    19. throw new OutOfMemoryError();
    20. return (minCapacity > MAX_ARRAY_SIZE) ?
    21. Integer.MAX_VALUE :
    22. MAX_ARRAY_SIZE;
    23. }

    优先级队列的扩容说明:

    • 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
    • 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
    • 如果容量超过MAX_ARRA_SIZE,按照MAX_ARRAY_SIZE来进行扩容的

    4.3 Top-k问题

    Top-k问题:即求数据集合中前k个最大元素或者最小元素,一般情况下数据量都比较大。

    比如:专业前10名、世界500强、富豪榜、游戏中前100位活跃玩家等。

    对于Top-k问题,能想到的最简单最直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了,最佳方式就是用堆来解决,基本思路如下:

    (1)用数据集合中的前k个元素来建堆

    • 前k个最大的元素,则建小堆
    • 前k个最小的元素,则建大堆

    (2)用剩余的N-K个元素依次与堆顶元素比较,不满足则替换堆顶元素

    将剩余N-K个元素依次与堆顶元素比较完成之后,堆中剩余的k个元素就是所求的前k个最小或者最大的元素。

    代码如下所示:

    1. public static int[] maxK(int[] arr,int k){
    2. int[] ret = new int[k];
    3. if(arr == null || k == 0){
    4. return ret;
    5. }
    6. Queue minHeap = new PriorityQueue<>(k);
    7. //1.遍历数组的前k个放到堆当中。时间复杂度:K*logK
    8. for (int i = 0; i < k; i++) {
    9. minHeap.offer(arr[i]);
    10. }
    11. //2.遍历剩下的k-1个,每次和堆顶元素进行比较
    12. //堆顶元素小的时候,就出堆。时间复杂度:(N-k)*logk
    13. for (int i = k; i < arr.length; i++) {
    14. int val = minHeap.peek();
    15. if(val < arr[i]){
    16. minHeap.poll();
    17. minHeap.offer(arr[i]);
    18. }
    19. }
    20. //3.将堆内的三个元素依次交给ret数组
    21. for (int i = 0; i < k; i++) {
    22. ret[i] = minHeap.poll();
    23. }
    24. return ret;
    25. }

    4.4 PriorityQueue中同一个类的对象之间的比较

    类去实现Comparable接口或者创建一个Compartor的实现类。

    注意:

    • equals方法是比较是否相同,返回值是true或者fals。
    • compareTo方法是比较大小,返回值是正数、0或者负数。
    方法

    说明

    Object.equals因为所有类都是继承Object的,所以直接重写即可,不过只能比较相等与否
    Comparable.compareTo需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于内部顺序
    Comparator.compare需要实现一个比较器对象,对待类的侵入性弱,但对算法代码实现侵入性强

    (1)类实现Comparable接口

    1. public class Student implements Comparable{
    2. public int age;
    3. public String name;
    4. public Student() {}
    5. public Student(int age, String name) {
    6. this.age = age;
    7. this.name = name;
    8. }
    9. @Override
    10. public int compareTo(Student o) {
    11. return this.age - o.age;
    12. }
    13. }

    (2)创建一个Compartor的实现类

    1. public static void main(String[] args) {
    2. Queue m = new PriorityQueue(new Comparator() {
    3. @Override
    4. public int compare(Student o1, Student o2) {
    5. return o1.age - o2.age;
    6. }
    7. });
    8. Student lisi = new Student(12, "lisi");
    9. Student zhangsan = new Student(13, "zhangsan");
    10. m.offer(lisi);
    11. m.offer(zhangsan);
    12. System.out.println(m.poll());
    13. System.out.println(m.poll());
    14. }

    4.5 分析PriorityQueue的offer方法

    为什么说PriorityQueue默认是小根堆呢?那我们就要去看下源码了!

    1. public boolean offer(E e) {
    2. if (e == null)
    3. throw new NullPointerException();
    4. modCount++;
    5. int i = size;
    6. if (i >= queue.length)
    7. grow(i + 1);
    8. size = i + 1;
    9. if (i == 0)
    10. queue[0] = e;
    11. else
    12. siftUp(i, e);
    13. return true;
    14. }
    15. private void siftUp(int k, E x) {
    16. if (comparator != null)
    17. siftUpUsingComparator(k, x);
    18. else
    19. siftUpComparable(k, x);
    20. }
    21. private void siftUpComparable(int k, E x) {
    22. Comparablesuper E> key = (Comparablesuper E>) x;
    23. while (k > 0) {
    24. int parent = (k - 1) >>> 1;
    25. Object e = queue[parent];
    26. if (key.compareTo((E) e) >= 0)
    27. break;
    28. queue[k] = e;
    29. k = parent;
    30. }
    31. queue[k] = key;
    32. }

    由源码和上面的图片,我们可知,PriorityQueue默认就是小根堆!

    我们又该如何将PriorityQueue改变为大根堆呢?

    很简单!只要我们自己手动传入一个比较器,把比较器的内容改变和PriorityQueue默认的比较结果相反即可!

    1. public static void main(String[] args) {
    2. Queue m = new PriorityQueue<>(new Comparator() {
    3. @Override
    4. public int compare(Integer o1, Integer o2) {
    5. return o2 - o1;
    6. }
    7. });
    8. m.offer(10);
    9. m.offer(1000);
    10. System.out.println(m.poll());
    11. System.out.println(m.poll());
    12. }

  • 相关阅读:
    华为云云耀云服务器L实例评测|基于Docker环境快速部署Halo个人博客实操
    es6两个数组取交集、并集、差集、补集
    R语言dplyr包select函数筛选dataframe数据中以指定字符串结尾(end with)的数据列(变量)
    面试之—K8S、Docker面试题整理
    【解题报告】CF练一下题 | 难度CF2500左右
    数学术语之源——纤维(fiber)
    OpenText EnCase 客户案例——Banner Health 医疗机构
    Qucs初步使用指南(不是multism)
    【uvm】参数化Class中的静态属性
    计算机视觉与深度学习实战,Python工具,深度学习的视觉场景识别
  • 原文地址:https://blog.csdn.net/ANNE_fly/article/details/134027442