入门数据结构JAVADS —— 认识 PriorityQueue 与 大小根堆

时间:2024-10-15 17:41:53

目录

前言

优先队列的概念

堆的概念

如何手搓MaxHeap类

 如何创建堆(***核心问题***)

如何插入元素

 如何弹出堆顶元素

 展示元素

 完整代码

 如何手搓MinHeap

 结尾


前言

JAVA DS 系列笔者也开了七篇了  

JAVA 数据结构_callJJ的博客-****博客

在写博客的过程中,笔者对于代码的阅读能力,调试能力,理解能力,以及写代码的能力都有增强.

因此,笔者的博客也会持续更新.

这篇博客我们主要介绍 优先队列.有一说一,这也是一个很好用的数据结构.

博客中的所有代码都在

MyJava/JavaDS2/src/priorityqueue at main · calljsh/MyJava (github.com) 中

优先队列的概念

优先队列(Priority Queue)是一种特殊的队列数据结构,它与普通队列的主要区别在于元素的出队顺序不再是按插入顺序,而是根据元素的优先级。在优先队列中,每个元素都附带有一个优先级,出队时总是优先级最高的元素最先被移除。

在 Java 中,优先队列的典型实现是通过 PriorityQueue 类实现的,默认情况下是一个小根堆,可以通过自定义比较器来改变其优先级顺序。

顺便提一嘴.在 C++ 中,优先队列可以通过 STL 的 priority_queue 类模板实现,默认情况下是一个大根堆

这里又引申出两个堆的基础类型  大小根堆.

堆的概念

堆(Heap)是一种特殊的树形数据结构,它

把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中

完全二叉树的概念定义请

入门数据结构JAVA DS——二叉树的介绍 (构建,性质,基本操作等) (1)-****博客

在这棵完全二叉树中,如果父亲结点的数值总是大于左右孩子节点的数值,且根结点的数值是最大的.

那么他就是一个大根堆.

反之,他就是一个小根堆.

 通过这样的组织数据的方式,我们就可以得到一个按照我们意愿去排序的优先队列.

 在本篇博客中,我们就以如何手搓一个大根堆为例

如何手搓MaxHeap类

首先我们要知道,堆是按完全二叉树的顺序存储方式存储 在一个一维数组中 ,所以归根结底还是要用数组去存储我们的数据.

因此我们需要实现这些方法,注意,以下是伪代码,仅供参考

/**
 * HeapInterface 接口定义了堆的基本操作。
 * 实现此接口的类需要提供对堆的基本操作,如插入、弹出元素、构建堆等。
 */
public interface HeapInterface {

    /**
     * 将一个数组初始化为堆的元素。
     *
     * @param arr 包含堆元素的数组
     */
    void initElem(int[] arr);

    /**
     * 创建堆。
     * 此方法将调整当前堆元素,使其满足堆的性质。
     */
    void createHeap();

    /**
     * 将一个值插入堆中。
     *
     * @param val 要插入的值
     */
    void push(int val);

    /**
     * 弹出堆顶元素(最大值)。
     *
     * @return 堆顶元素,若堆为空则返回 -1
     */
    int pop();

    /**
     * 显示当前堆中的所有元素。
     */
    void display();
}

我们首先创建好必要的属性,并且构造好

    public int[] elem;
    public int usedSize;

    public MinHeap()
    {
        this.elem = new int[10];
    }

 如何创建堆(***核心问题***)

为了方便创建一个堆,我们需要给我们定义的数组elem添加元素.

因此我们使用方法 void initElem

 public void initElem(int[] arr) {
        int num = arr.length;
        usedSize = num;
        elem = Arrays.copyOf(arr, num);
    }

现在我们把测试数据放进去

public class Test
{
    public static void main(String[] args)
    {
        int [] arr={27,15,19,18,28,34,65,49,25,37};
        MinHeap minHeap = new MinHeap();
        minHeap.initElem(arr);
}

 好的,现在我们的MinHeap中已经有了一组原始数据,我们现在按照堆的方式去给它们排序

这也就涉及如何去实现 void createHeap()

首先我们要了解二叉树的这么一个性质

   1. 如果下标i为0,则i表示的节点为根节点,否则i节点的双亲节点的下标为 (i - 1)/2
   2. 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
   3. 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

 通过这么几条性质.

我们就可以在一维数组中定位到完全二叉树的节点的位置

换句话说,假如完全二叉树的每个结点都存储在一维数组中,我们是可以知道它们的下标的

(重要)这样我们就可以确定,在一维数组中,那些下标的结点时双亲结点,那些下标的节点是孩子结点.

这一点非常关键,因为我们的创建算法正是基于这条性质.

请看这张图

想要达到  "在这棵完全二叉树中,如果父亲结点的数值总是大于左右孩子节点的数值,且根结点的数值是最大的." 这个效果.

我们是不是应该从倒数第一个非叶子结点开始,遍历每个双亲结点,和他们的孩子结点进行比较,如果孩子结点的值更大,就和父亲结点交换,我把这个过程叫做"交换"或者说是"向下交换",重复这个过程.直到根节点也遍历完.

从图例,我们就可以看到,一个父亲结点如果只是和自己的左右孩子结点去交换,可能还是无法满足大根堆需要的特征,因此可能需要换很多次,那么我们该设置什么样的条件去保证每个父亲结点都能被交换到合适的位置呢?

我想说的是 :

条件一:当没法换的时候,就不用继续去比较交换了,我们就需要如下性质

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

 在一次交换完成以后,再设置一个孩子结点 2*i+1,然后和节点数(或者说数组长度) 对比,如果小于结点个数,那么就不再需要比较了;

条件二:交换完成以后,新的父亲结点的值还是比左右孩子结点的值大,那肯定也不需要继续交换了.

当上述条件有一个以上被满足时,就代表,我们的父亲结点被放到了合适的位置.

总结一下,就是遍历每个父亲结点,然后进行可能重复的向下交换,把每个父亲结点放到合适的位置即可.  通过性质一找到那些是父亲结点,通过性质二,三验证是否还要进行向下交换.

 1. 如果下标i为0,则i表示的节点为根节点,否则i节点的双亲节点的下标为 (i - 1)/2
 2. 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
 3. 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

具体算法笔者已经尽力解释清楚了,现在是具体的代码

 public  void createHeap()
 // 这是创建一个大根堆
 {
     for(int parent = (usedsize-1-1)/2;parent>=0;parent--)
     // usedsizd 是大小, -1以后是下标 ,再减1除2,获得双亲结点
     //  换句话说,这个for 循环就是遍历双亲结点,不遍历孩子结点
     // 从 树的最底下开始循环,判断位置是否需要交换
     {
          siftdown(parent,usedsize);
          // 向下调整
          // 这是一次双亲结点进行交换的方法
     }
 }
 private  void  siftdown(int parent,int len)
 {
     int child = 2*parent+1;
     // 左孩子
     while(child<len)
     {
         if( child+1<len &&elem[child]<elem[child+1])
         //   有 右孩子,而且右孩子更大
         {
             child=child+1;
             // 标识给到右孩子,为下一步的交换做铺垫
             // 如果不进入这么循环,说明左子树的值更大
         }
         if(elem[parent]<elem[child])
         {
             int temp=elem[child];
             elem[child]=elem[parent];
             elem[parent]=temp;
             // 交换值
             parent = child;
             child=2*parent+1;
             // 继续检查能不能交换
         }
         else
         {
             break;
             // 无法交换了,没必要继续了,直接break;
         }
     }
 }

 问题:为什么更新孩子结点更新的是左孩子的位置

 答案:因为这是一棵树,一棵完全二叉树!

  完全二叉树按层次逐级填满节点,即先填满左侧的孩子节点。因此,我们总是先计算左孩子的下  标 2 * parent + 1

如何插入元素

对于插入元素,很显然,就是把他放在数组的末端, 然后 让插入元素的值和它的父亲结点进行比较,当大于父亲节点时,进行交换,也就是"向上交换",当满足下面两个条件的其中一个时,就不再继续去向上交换了

条件一: 没有父亲结点和它比了,说明它就是最大的值,已经被换到根节点的位置了

条件二:它没有它的父亲结点大了,也不用换了.

针对上述的算法,我们写一下代码

private  void checkup (int size)
 {
     if(elem.length==size)
     {
         elem=Arrays.copyOf(elem,elem.length*2);
     }
     else
     {
         return ;
     }
 }
    public  void push(int val)
            // 堆的插入
    {
        checkup(usedsize);
        elem[usedsize]=val;
        siftup(usedsize);
        usedsize++;
    }
    private  void siftup(int num)
    {
        int num2=(num-1)/2;
        // num2是num的双亲结点.
        while(num>0)
        {
            if(elem[num]>elem[num2])
            {
                int temp=elem[num];
                elem[num]=elem[num2];
                elem[num2]=temp;
                // 交换
                num=num2;
                num2=(num2-1)/2;
                // num变成原来的双亲结点,num2变成原来双亲结点的双亲结点
            }
            else
            {
                break;
            }
        }
    }

 如何弹出堆顶元素

这个问题也不是很难

只要把数组的第一个元素和最后一个元素交换一下,然后用一个变量temp存储堆顶元素.

那么,现在的堆顶元素,或者说根节点就是之前的叶子结点,我们再向下交换即可,

 public int pop()
   // 弹出堆顶元素
    {
        if(empty())
        {
            return -1;
        }
        else
        {
            int temp=elem[0];
            elem[0]= elem[usedsize-1];
            elem[usedsize-1]=temp;
            usedsize--;
            siftdown(0,usedsize);
            return temp;
        }
    }

    private  boolean empty()
    {
        return usedsize==0;
    }

private  void  siftdown(int parent,int len)
 {
     int child = 2*parent+1;
     // 左孩子
     while(child<len)
     {
         if( child+1<len &&elem[child]<elem[child+1])
         //   有 右孩子,而且右孩子更大
         {
             child=child+1;
             // 标识给到右孩子,为下一步的交换做铺垫
             // 如果不进入这么循环,说明左子树的值更大
         }
         if(elem[parent]<elem[child])
         {
             int temp=elem[child];
             elem[child]=elem[parent];
             elem[parent]=temp;
             // 交换值
             parent = child;
             child=2*parent+1;
             // 继续检查能不能交换
         }
         else
         {
             break;
             // 无法交换了,没必要继续了,直接break;
         }
     }
 }

 展示元素

只要遍历数组,然后打印元素即可

  public  void display()
 {
     for(int i=0;i<usedsize;i++)
     {
         System.out.print(elem[i]+" ");
     }
     System.out.println();
 }

 完整代码

完整代码如下

import java.util.Arrays;

public class Heap
    //创建一个大根堆
{
    public  int [] elem;
    public  int usedsize;
    public Heap() {
        this.elem =new int[10];
    }

    public  void intitElem (int [] arr)
    {
        int num=arr.length;
        usedsize=num;
        elem= Arrays.copyOf(arr,num);
        // 因为是完全二叉树能完全利用数组空间,所以数组存储的顺序就是层序遍历的顺序
    }

//   1. 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
//   2. 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
//   3. 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
 public  void createHeap()
 // 这是创建一个大根堆
 {
     for(int parent = (usedsize-1-1)/2;parent>=0;parent--)
     // usedsizd 是大小, -1以后是下标 ,再减1除2,获得双亲结点
     //  换句话说,这个for 循环就是遍历双亲结点,不遍历孩子结点
     // 从 树的最底下开始循环,判断位置是否需要交换
     {
          siftdown(parent,usedsize);
          // 向下调整
          // 这是一次双亲结点进行交换的方法
     }
 }
 private  void  siftdown(int parent,int len)
 {
     int child = 2*parent+1;
     // 左孩子
     while(child<len)
     {
         if( child+1<len &&elem[child]<elem[child+1])
         //   有 右孩子,而且右孩子更大
         {
             child=child+1;
             // 标识给到右孩子,为下一步的交换做铺垫
             // 如果不进入这么循环,说明左子树的值更大
         }
         if(elem[parent]<elem[child])
         {
             int temp=elem[child];
             elem[child]=elem[parent];
             elem[parent]=temp;
             // 交换值
             parent = child;
             child=2*parent+1;
             // 继续检查能不能交换
         }
         else
         {
             break;
             // 无法交换了,没必要继续了,直接break;
         }
     }
 }
 private  void checkup (int size)
 {
     if(elem.length==size)
     {
         elem=Arrays.copyOf(elem,elem.length*2);
     }
     else
     {
         return ;
     }
 }
    public  void push(int val)
            // 堆的插入
    {
        checkup(usedsize);
        elem[usedsize]=val;
        siftup(usedsize);
        usedsize++;
    }
    private  void siftup(int num)
    {
        int num2=(num-1)/2;
        // num2是num的双亲结点.
        while(num>0)
        {
            if(elem[num]>elem[num2])
            {
                int temp=elem[num];
                elem[num]=elem[num2];
                elem[num2]=temp;
                // 交换
                num=num2;
                num2=(num2-1)/2;
                // num变成原来的双亲结点,num2变成原来双亲结点的双亲结点
            }
            else
            {
                break;
            }
        }
    }
    public int pop()
   // 弹出堆顶元素
    {
        if(empty())
        {
            return -1;
        }
        else
        {
            int temp=elem[0];
            elem[0]= elem[usedsize-1];
            elem[usedsize-1]=temp;
            usedsize--;
            siftdown(0,usedsize);
            return temp;
        }
    }

    private  boolean empty()
    {
        return usedsize==0;
    }
    public  void display()
 {
     for(int i=0;i<usedsize;i++)
     {
         System.out.print(elem[i]+" ");
     }
     System.out.println();
 }
}

 如何手搓MinHeap

小根堆的创建和大根堆类似,这里仅展示代码以供参考

public class MinHeap
{
    // 创建一个小根堆
    public int[] elem;
    public int usedSize;

    public MinHeap()
    {
        this.elem = new int[10];
    }

    public void initElem(int[] arr) {
        int num = arr.length;
        usedSize = num;
        elem = Arrays.copyOf(arr, num);
    }

    public void createHeap() {
        for (int parent = (usedSize - 2) / 2; parent >= 0; parent--) {
            // 从最后一个双亲节点向下调整构造小根堆
            siftDown(parent, usedSize);
        }
    }

    private void siftDown(int parent, int len) {
        int child = 2 * parent + 1;
        while (child < len) {
            // 如果有右孩子且右孩子比左孩子小,选右孩子
            if (child + 1 < len && elem[child] > elem[child + 1]) {
                child = child + 1;
            }
            // 若父节点大于最小的子节点,则交换
            if (elem[parent] > elem[child]) {
                int temp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = temp;

                // 继续向下检查
                parent = child;
                child = 2 * parent + 1;
            } else {
                break;
            }
        }
    }

    private void checkUp(int size) {
        if (elem.length == size) {
            elem = Arrays.copyOf(elem, elem.length * 2);
        }
    }

    public void push(int val) {
        // 插入元素到堆中
        checkUp(usedSize);
        elem[usedSize] = val;
        siftUp(usedSize);
        usedSize++;
    }

    private void siftUp(int child) {
        int parent = (child - 1) / 2;
        while (child > 0) {
            // 若子节点小于父节点,则交换
            if (elem[child] < elem[parent]) {
                int temp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = temp;

                // 继续向上检查
                child = parent;
                parent = (parent - 1) / 2;
            } else {
                break;
            }
        }
    }

    public int pop() {
        // 弹出堆顶元素
        if (empty()) {
            return -1;
        } else {
            int temp = elem[0];
            elem[0] = elem[usedSize - 1];
            elem[usedSize - 1] = temp;
            usedSize--;
            siftDown(0, usedSize);
            return temp;
        }
    }

    private boolean empty() {
        return usedSize == 0;
    }

    public void display() {
        for (int i = 0; i < usedSize; i++) {
            System.out.print(elem[i] + " ");
        }
        System.out.println();
    }
}

 

 结尾

 这篇应该是数据结构系列博客耗时最长,字数最多的博客了,笔者尽力阐述相关算法思路了,感谢大家伙的支持(虽然应该也没人看).