• Java数据结构 | 模拟实现优先级队列


    目录

    一、前言

    二、堆模拟实现优先级队列

    2.1 堆的概念

    2.2 堆的性质

    2.3 堆的存储方式

    2.4 堆的创建


    一、前言

    在前面我们学习过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话。

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

    二、堆模拟实现优先级队列

    JDK1.8中的PriorityQueue底层使用了堆的数据结构,而堆实际就是在完全二叉树的基础之上进行了一些元素的调整。

    2.1 堆的概念

    如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

    2.2 堆的性质

    • 堆中某个节点的值总是不大于或不小于其父节点的值;

    • 堆总是一棵完全二叉树。

    与二叉搜索树不同,堆的左右节点都小于根节点,而左右节点的值没有大小关系

    ce25103fc2326888dd70404427c612d4.png

    2.3 堆的存储方式

    堆是一棵完全二叉树,因此可以采用层序的规则来顺序存储

    5adc6101c3488995d1afaad490d243d0.png

    注意:对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树空间中必须要存储空节点,就会导致空间利用率比较低

    将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:

    • 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2

    • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子

    • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

    2.4 堆的创建

    • 堆的向下调整

    条件:必须左右子树已经是堆了才能调整

    对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?

    3bf6e6ac797a5d116c426480006eacb8.png

    观察这棵树可以发现,根节点的左右两边已经满足小根堆的性质,只需将根节点进行向下调整即可

    2b20ec70aaceeefd79912ac696490359.png

    调整过程:

    • 将此二叉树的根节点设置为 parent 节点 ,

    • 比较 parent 节点的孩子节点的值,将孩子节点中的较小的节点设置为 child 节点

      • 初始状态

    b84eb94d1bf77b1d4267a4f5fbfe3f09.png

    • 比较 parent 节点和 child 节点的值的大小

      • 若 parent > child , 则不满足小根堆的性质,将两者进行交换。

      • 若 parent < child , 满足小根堆的性质,不进行交换,调整结束。

    • 每次交换后,更新child 和parent的位置, parent =child,child = 2 *parent+1;

    dd1f03a187cdce295c2ea9de09fedcd1.png

    f48088e815cb6c2d27afe2c696f96900.png

    • 代码实现: 时间复杂度: O(logN) — parent固定,child 每次 x 2

    1. //   小根堆的向下调整(满足parent的左子树和右子树已经是堆了)
    2.    public void shiftDown(int parent,int len){
    3.        int child = 2*parent +1;
    4. //       必须保证右左孩子
    5.        while(child < len){
    6. //           找到左右孩子的最小值
    7.            if(child +1 < len && elem[child] > elem[child+1]){
    8.                child++;
    9.           }
    10.            if(elem[child] < elem[parent]){
    11.                int tmp = elem[child];
    12.                elem[child] = elem[parent];
    13.                elem[parent] = tmp;
    14. //               向下调整重新更新
    15.                parent =child;
    16.                child = 2 *parent+1;
    17.           }else{
    18.                break;
    19.           }
    20.       }
    21.   }

    针对向下调整的思路,我们可以进行建堆,将一个数组建造成堆,从倒数第一个非叶子节点开始,从后往前遍历数组,依次进行向下调整,就可以得到一个小根堆

    例:将以下数组[9,5,2,7,3,6,8] 建成一个小根堆

    340e0f41d2af56655d2d004eea61c600.png

    此时根节点的左右孩子两边的树均满足小根堆的特点,只需要调整以9为根的树向下调整即可,调整过程与结果如下

    2b7eb3a22345d1e66c3b9cf27c260a7c.png

    最后的结果即

    1915344803964950af67eb20fbf64d31.png

    • 代码实现

    时间复杂度:建堆的时间复杂度为 O(n) (复杂的数学计算)

    1.    public void crearHeap(){
    2. //       最后一个节点的下标为 i = usedSize -1
    3. //         (i - 1) / 2 即为最后一个非叶子节点的下标
    4.        for(int parent = (usedSize-1-1)/2; parent >= 0;parent--){
    5.           shiftDown(parent,usedSize);
    6.          //对每一个非叶子节点进行向下调整
    7.       }
    8.   }

    f8e3e904a69fc0442b98e31d41a99ceb.png

    usedsize - 即为最后一个叶子节点的下标,((usedsize -1) - 1) / 2 即为最后一个非叶子节点的下标

    • 堆的向上调整

    当我们进行元素的插入时,仍要保证这个堆是一个大根堆,则需要对堆进行向上调整

    将堆进行向上调整的步骤

    • 将插入的元素即最后一个叶子节点设置为 child ,其父亲节点设置为parent = (child-1) /2

    • 当child > parent 时 , 不满足大根堆的性质,将父亲节点的值与叶子节点的值进行交换

    • 当child

    调整完成之后重新更新parent 和 child 的位置,即 child = parent , parent = 2 * child +1

    向上调整为大根堆的代码实现如下:

    80a5c50652522895fe4388ce6b72f4cb.png

    1.  public void shiftUp(int child){
    2.        int parent = (child-1) /2;
    3.        while(child > 0){
    4.            if(elem[child] > elem[parent]){
    5.                int tmp = elem[child];
    6.                elem[child] = elem[parent];
    7.                elem[parent] = tmp;
    8.                child = parent;
    9.                parent = (child -1)/2;
    10.           }else {
    11.                break;
    12.           }
    13.       }
    14.   }
    • 堆中插入一个元素时,代码实现

    1. public void offer(int val){
    2. //如果堆为满,则对数组进行扩容
    3.        if(isFull()){
    4.            elem = Arrays.copyOf(elem,2*elem.length);
    5.       }
    6. //将插入的元素设置为堆的最后一个元素
    7.        this.elem[usedSize] = val;
    8.        usedSize++;
    9. //将堆中元素进行向上调整
    10.        shiftUp(usedSize-1);
    11.   }
    12.     public boolean isFull(){
    13.        return elem.length == usedSize;
    14.   }
    • 堆的删除(删除堆顶元素)

      • 将堆顶元素与队中堆中最后一个节点的值进行交换

      • 将堆中的元素值减少一个

      • 对堆中的元素进行向下调整

    代码实现如下:

    1. public int pop(){
    2.        if(isEmpty()){
    3.            return -1;
    4.       }
    5.        int tmp = elem[0];
    6.        elem[0] = elem[usedSize -1];
    7.        elem[usedSize -1] = tmp;
    8.        usedSize--;
    9.    //将堆中元素进行向下调整
    10.        shiftDown(0,usedSize);
    11.        return tmp;
    12.   }
    • 使用堆模拟实现优先级队列

    1. public class TestHeap {
    2.    public int[] elem;
    3.    public int usedSize;
    4.    public static int DEFAULT_SIZE = 10 ;
    5.    public TestHeap() {
    6.        this.elem = new int[DEFAULT_SIZE];
    7.   }
    8.    public void init(int[] array){
    9.        for(int i = 0; i < array.length;i++){
    10.            elem[i] = array[i];
    11.            usedSize++;
    12.       }
    13.   }
    14. //   建堆的时间复杂度为O(n)
    15.    public void crearHeap(){
    16. //       最后一个节点的下标为 i = usedSize -1
    17. //         (i - 1) / 2 即为父亲节点的下标
    18.        for(int parent = (usedSize-1-1)/2; parent >= 0;parent--){
    19.           shiftDown(parent,usedSize);
    20.       }
    21.   }
    22.    /**
    23.     *
    24.     * @param parent 每棵子树的根节点
    25.     * @param len 每棵子树的
    26.     * 时间复杂度:O(log(n))
    27.     */
    28. //   小根堆的向下调整(满足parent的左子树和右子树已经是堆了)
    29.    public void shiftDown(int parent,int len){
    30.        int child = 2*parent +1;
    31. //       必须保证右左孩子
    32.        while(child < len){
    33. //           找到左右孩子的最小值
    34.            if(child +1 < len && elem[child] > elem[child+1]){
    35.                child++;
    36.           }
    37.            if(elem[child] < elem[parent]){
    38.                int tmp = elem[child];
    39.                elem[child] = elem[parent];
    40.                elem[parent] = tmp;
    41. //               向下调整重新更新
    42.                parent =child;
    43.                child = 2 *parent+1;
    44.           }else{
    45.                break;
    46.           }
    47.       }
    48.   }
    49.    public void offer(int val){
    50.        if(isFull()){
    51.            elem = Arrays.copyOf(elem,2*elem.length);
    52.       }
    53.        this.elem[usedSize] = val;
    54.        usedSize++;
    55.        shiftUp(usedSize-1);
    56.   }
    57. //   向上调整
    58.        public void shiftUp(int child){
    59.        int parent = (child-1) /2;
    60.        while(child > 0){
    61.            if(elem[child] > elem[parent]){
    62.                int tmp = elem[child];
    63.                elem[child] = elem[parent];
    64.                elem[parent] = tmp;
    65.                child = parent;
    66.                parent = (child -1)/2;
    67.           }else {
    68.                break;
    69.           }
    70.       }
    71.   }
    72.    public boolean isFull(){
    73.        return elem.length == usedSize;
    74.   }
    75.    public boolean isEmpty(){
    76.        return usedSize == 0;
    77.   }
    78.    public int pop(){
    79.        if(isEmpty()){
    80.            return -1;
    81.       }
    82.        int tmp = elem[0];
    83.        elem[0] = elem[usedSize -1];
    84.        elem[usedSize -1] = tmp;
    85.        usedSize--;
    86.        shiftDown(0,usedSize);
    87.        return tmp;
    88.   }
    89.    public int peek(){
    90.        if(isEmpty()){
    91.            return -1;
    92.       }
    93.        return elem[0];
    94.   }
    95. }

    ced485cbb11e458d81a746890b32cf3f.gif

     

  • 相关阅读:
    【面试普通人VS高手系列】ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?
    如何招到适合自己店铺的淘宝主播
    [go学习笔记.第十八章.数据结构] 1.基本介绍,稀疏数组,队列(数组实现),链表
    字符串匹配算法(C/Java实现)
    一看就懂:这就是机器学习过程!
    React Hooks——state hooks
    Linux内核顶层Makefile的make过程说明二
    python:循环请求多个url导致链接超时的解决方案
    【SpringCloud-学习笔记】Nacos配置管理
    冒泡排序实现
  • 原文地址:https://blog.csdn.net/m0_56361048/article/details/127839213