优先队列和堆及相关问题

1. 优先队列概念

可以看出是一个有序的队列,每次出队的都是队列中最大(最小)的元素,新加的元素入队,队列中的元素依然保持有序

2. 优先队列的几种实现方式比较

在这里插入图片描述

3. 堆和二叉堆

3.1 堆的概念

  • 一棵具有特定性质的树
  • 结点的值大于等于其孩子节点的值或小于等于
  • 大于等于–>大顶堆
  • 小于等于—>小顶堆
  • 堆是一棵完全树,叶子节点只会位于第h层和第h-1层

在这里插入图片描述

3.2 二叉堆

  • 在堆的基础上,每个节点最多只能有2个子节点
  • 二叉堆是一棵完全二叉树,因此可以用数组来表示

在这里插入图片描述
在这里插入图片描述

对于一棵完全二叉树,如果使用数组来表示的话,对于第i个位置的节点而言,其父节点在第(i-1)/2位置上,其左子节点在2*i+1位置上(如果有左子节点),其右子节点在2*i+2位置上(如果有右子节点)

3.3 堆的下沉操作

以大顶堆为序,下面的代码记作根节点的位置是1,即创建的数组大小是N+1,下标0的位置空出,这种情况下:对于第i个位置的节点而言,其父节点在第i/2位置上,其左子节点在2*i位置上(如果有左子节点),其右子节点在2*i+1位置上(如果有右子节点)

如果某个根节点<子节点的值,则交换该子节点与父节点的值,重复这个过程,直到遇到一个小于等于根节点值的子节点

public void sink(int arr[],int k)
    {
        int N=arr.length;
        while(2*k<=N)
        {
            int j=2*k;//指向左子节点
            //根节点>=max(左子节点,右子节点)
            if(j<N&&arr[j]<arr[j+1])//如果右子节点存在且右子节点比左子节点大
                j++;//j指向右子节点
            if(arr[k]>=arr[j])//找到<=根节点的子节点 停止
                break;
            swap(arr, k, j);
				k=j;//k指向交换后的位置
        }
    }
    public void swap(int arr[],int i,int j)
    {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }

上面的代码实现的是堆中的下沉操作,从上到下调整堆的结构

3.4 堆的上浮操作

如果某个子节点大于根节点的值,则交换该子节点与父节点的值,重复这个过程,直到遇到一个大于等于其值的父节点

public void swim(int arr[],int k)
    {
        while(k>1&&arr[k/2]<arr[k])//a[k/2]:父节点  arr[k]:当前节点
        {
            swap(arr,k,k/2);
            k=k/2;//注意这里的k每次倍减 指向上一个父节点
        }
    }
    public void swap(int arr[],int i,int j)
    {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }

从下到上调整堆的结构

有了上浮和下沉操作之后,在堆中添加和删除元素就容易了

3.5 在堆中添加一个元素

将元素加到数组末尾,然后增加堆的大小,并将新添加的元素上浮到合适的位置即可,调用swim方法

3.6 删除堆中的最大元素

交换堆顶元素(数组第1个元素)和数组最后一个元素实现,然后将新的堆顶元素下沉(调用sink方法)到合适的位置,将堆的大小-1(大小减一相当于删除了最后1个元素,因为已经交换,所以最后一个元素就是原来的堆顶最大元素)

3.7 堆的完整代码实现

public class PriorityQueueDemo {
    int pq[];
    int size=0;
    public static void main(String[] args) {
        PriorityQueueDemo pq=new PriorityQueueDemo(5);
        pq.insert(1);
        pq.insert(2);
        pq.insert(3);
        pq.insert(4);
        pq.insert(5);
        pq.deleteMax();
        pq.print();

    }
    public PriorityQueueDemo(int N)
    {
        pq=new int[N+1];
    }
    public boolean isEmpty()
    {
        return size==0;
    }
    public int size()
    {
        return size;
    }
    public void insert(int val)
    {
        pq[++size]=val;
        swim(size);
    }
    public void deleteMax()
    {
        swap(1, size);
        pq[size]=-1;
        size--;
        sink(1);
        
    }
    public void swim(int k)
    {
        while(k>1&&pq[k/2]<pq[k])//a[k/2]:父节点  arr[k]:当前节点
        {
            swap(k,k/2);
            k=k/2;
        }
    }
    public void sink(int k)
    {
        int N=pq.length;
        while(2*k<=N)
        {
            int j=2*k;//指向左子节点
            //根节点>=max(左子节点,右子节点)
            if(j<N&&pq[j]<pq[j+1])//如果右子节点存在且右子节点比左子节点大
                j++;//j指向右子节点
            if(pq[k]>=pq[j])//找到<=根节点的子节点 停止
                break;
            swap(k, j);
						k=j;
        }
    }
    public void swap(int i,int j)
    {
        int tmp=pq[i];
        pq[i]=pq[j];
        pq[j]=tmp;
    }
    public void print()
    {
        for(int i=1;i<=size;i++)
            System.out.print(pq[i]);
        System.out.println();
    }
}

4. 堆排序

堆排序可以分为以下两个阶段:

  1. 堆的构造阶段:将原始数组元素加入到1个堆中
  2. 下沉排序阶段:按递减(递增)顺序取出所有元素并得到排序结果(每次都是取堆顶元素,因此需要下沉,添加–上浮 删除–下沉)

注意:数组元素下标是从0开始的,因此编号从0开始,所以对于编号为k的节点,其左子节点编号为2k+1,右子节点编号为2k+2, ,父节点为(k-1)/2, 堆中最右最下边的含有子节点的子树根节点的编号为N/2-1

4个步骤:

  1. 构建所有数组元素形成的堆
  2. 交换堆顶元素和当前堆的最后一个元素
  3. 堆的大小减一
  4. 调整新的大小的堆,即将堆顶元素下沉,然后重复步骤2,直到堆中只有1个元素

ps: 使用大顶堆可以得到一个升序序列, 使用小顶堆可以得到一个降序序列

import java.util.Arrays;

public class PriorityQueueDemo {
    
    public static void main(String[] args) {
        int arr[]={2,3,8,7,0,5,4,1,6,9};
        PriorityQueueDemo heap=new PriorityQueueDemo();
        heap.sort(arr);
        System.out.println(Arrays.toString(arr));


    }
    public void sort(int arr[])
    {
        int N=arr.length;
        //将数组元素构建出一个大顶堆
        //最后一个节点编号是N-1  其父节点编号为:(N-1-1)/2=N/2-1
        for(int k=N/2-1;k>=0;k--)//N/2-1指向最后一个父节点 从其开始调整
        {
            sink(arr,k,N);
        }
        //交换  下沉
        while(N>0)
        {
            swap(arr, 0, N-1);
            N--;//N--可以看出是删除堆顶最大元素 因此需要进行下沉操作
            sink(arr, 0, N);
        }

    }
   
    public void sink(int arr[],int k,int N)
    {
        
        while(2*k+1<N)
        {
            int j=2*k+1;//指向左子节点
            //根节点>=max(左子节点,右子节点)
            if(j+1<N&&arr[j]<arr[j+1])//如果右子节点存在且右子节点比左子节点大  1
                j++;//j指向右子节点
            if(arr[k]>=arr[j])//找到<=根节点的子节点 停止  2
                break;
            swap(arr, k, j);
            k=j;
        }
    }
    public void swap(int arr[],int i,int j)
    {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }
   
}

5. 堆的相关问题

5.1 在小顶堆中找到最大的元素

小顶堆中寻找最小元素简单,即堆顶元素,最大元素一定存在于叶子节点,找打第一个叶子节点开始遍历到数组尾部即可
最后一个叶子节点是最后一个节点的双亲节点的下一个节点
最后一个节点位置:N-1
最后一个节点的双亲节点位置:(N-1)/2
最后一个节点的双亲节点的下一个节点位置:(N-1)/2+1
在这里插入图片描述

5.2 在堆中删除任意元素

找到该元素,记录该元素的位置为i, 进行以下操作:

  swap(arr, i, N-1);
  N--;
  sink(arr, i, N);

5.3 在最小堆中寻找第k小的元素

删除堆顶元素k次,返回第k次删除得到的元素

5.4 动态中位数的查找

中位数定义:

  • 奇数个数据,升序之后取最中间的元素
  • 偶数个数据,升序之后取中间两个元素的平均值

动态中位数:数据是动态添加的,每次都要返回当前数据的中位数(当前是奇数个数据,下次就是偶数)

解决:使用两个堆

大顶堆:包含较小的一半的元素
小顶堆:包含较大的一半的元素
假设大顶堆中的元素大于等于小顶堆中的元素,即:
奇数个元素,大顶堆中右n/2+1个,小顶堆中有n/2个
偶数个元素,大顶堆中右n/2个,小顶堆中有n/2个

因此
奇数个元素时:中位数即大顶堆的堆顶元素
偶数个元素时,中位数即(大顶堆的堆顶元素+小顶堆的堆顶元素)/2

实现细节:
当添加第一个第二个元素时,直接依次加入大顶堆和小顶堆即可
当添加第3个元素以及后面的元素时,假设添加的元素是x, 分为以下两种情况:

大顶堆中偶数个元素,此时x应该加入大顶堆,但不能直接加入,如果x比小顶堆堆顶元素小,直接加入大顶堆;如果x比小顶堆堆顶元素大,先将x加入小顶堆中排下序,再将小顶堆堆顶元素加入大顶堆,从而保证大顶堆中是较小的一半元素

大顶堆中奇数个元素,此时x应该加入小顶堆,但不能直接加入,如果x比大顶堆堆顶元素大,直接加入小顶堆;;如果x比大顶堆堆顶元素小,先将x加入大顶堆中排下序,再将大顶堆堆顶元素加入小顶堆,从而保证小顶堆中是较大的一半元素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePanda@GPF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值