2020/10/15 小根堆模拟

题目1

输入一个长度为n的整数数列,从小到大输出前m小的数。
输入格式
第一行包含整数n和m。
第二行包含n个整数,表示整数数列。
输出格式
共一行,包含m个整数,表示整数数列中前m小的数。
数据范围
1≤m≤n≤105,
1≤数列中元素≤109
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3

算法说明

题目要求是对输入的数组进行排序,并输出前若干个小的数.而小根堆的作用就是能够保证根是最小的数,这样只要输出一个根再对整个堆排序,再输出根,即可.
堆的操作有两种,一种叫up,一种叫down,二者都是对一个三角(根+两个叶子节点)进行一定规律的排序,不同的是,up负责的是从叶子节点开始向上的所有;而down负责的则是从根节点向下的所有.堆是完全树.

  • 完全树的性质:一个节点x的叶子节点是2x和2x+1;一个任意节点x的父节点是x/2(也就是向下取整)
  • 关于堆在计算机的存储方式:本题使用数组对堆进行模拟,数组角标从1开始(为了防止0的子节点/父节点还是自己),创建数组h[N],角标表示的就是节点编号,值存储的就是节点的值.同时创建一个size指针,标记堆中有多少元素,
  • 关于构建堆: 有两种方式,第一种是将整个数组混乱的放进堆中,从最后一个叶子节点向前进行up操作,时间复杂度是O(nlongn);第二种是将整个数组混乱的放进堆中,再对整个堆进行重构,这种只需要从n/2节点向前down,时间复杂度小于O(n)
  • 关于堆的插入删除:插入即让size++,并为h[size]赋值,此时最后一个节点不规则,对其做up即可;对于删除某一节点,选择的方式是先覆盖,再删除的方法,以删除h[1]为例,我们让最后一个节点h[size]的值覆盖给h[1],这样堆中就有了两个h[size],然后我们将size–,也就是让堆中再次恢复只有一个h[size]的形态,此时堆内的数就不再有h[1]了,这时由于堆节点的数值有了变化,up一下即可.
  • 关于为什么构建要从n/2开始依次向前down:
  • 在这里插入图片描述
    只需要down5,4…即可,而不需要down6,因为他没有叶子节点,也就说明它本身是有序的.

源代码


import java.io.*;
//模拟堆的重点就在于up和down的函数
class Main{
    static int N=100010;
    static int[] h=new int[N];
    static int size=0;
    public static void main(String[] args)throws Exception{
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
        String[] s=br.readLine().split(" ");
        int n=Integer.parseInt(s[0]);
        size=n;
        int m=Integer.parseInt(s[1]);
        String[] s1=br.readLine().split(" ");
        for(int i=1;i<=n;i++){
            h[i]=Integer.parseInt(s1[i-1]);
        }
        //此时栈内数组已经填充完毕,但是是无序的,因此我们对整个栈进行排序,排序可以选择插入的方式(也就是对每一个插入的元素上浮)
        //但是更好的做法是将混乱的元素使用下沉来排序从n/2开始(也就是最后一个元素的父节点开始),依次往前
        //因为最后一个节点之后是没有叶子节点了,也就不用下沉了
        //本质上无论down/up都是对一个三角形的数进行排序,只不过down负责下面的所有,up负责上面的所有
        for(int i=n/2;i!=0;i--){
            down(i);
        }
        while(m-->0){
            bw.write(h[1]+" ");
            h[1]=h[size];
            size--;
            down(1);
        }
        bw.flush();
        bw.close();
        br.close();
    }
    //上浮节点为x的元素
    static void up(int x){
        while(x/2>0&&h[x/2]>h[x]) {
            int temp;
            temp=h[x/2];
            h[x/2]=h[x];
            h[x]=temp;
            x=x/2;
        }
    }
    //针对x节点,因为up只针对一个结点,但是down是会影响两个结点的,因此要用递归来实现
    static void down(int x){
        //当x节点满足条件
        int n=x;
        //目标是找到x要和谁换,所以做法满足条件先和第一个交换,那么此时备选答案变成了x(x=2n)这时
        //再用备选答案和第二个判断是否满足条件,满足条件就让x直接等于2n+1即可
        if(2*n<=size&&h[n]>h[2*n]) x=2*n;
        if(2*n+1<=size&&h[x]>h[2*n+1]) x=2*n+1; //注意这里一定是用h[x]来和h[2n+1]比较


        //为下一个x做准备
        if(x!=n) {
            int temp;
            temp=h[x];
            h[x]=h[n];
            h[n]=temp;
            down(x);
        }
    }

}

函数说明

  • up函数: 使用迭代的方法,只需要关注被迭代的节点值与其父节点的关系即可,大于则交换值,并更新迭代值为父节点
  • down函数:使用递归的方法. 由于父节点应该跟子节点中更小的那个节点值交换,再对被交换值的结点进行迭代,关键在于找到被交换的结点,我们使用的方法是比较两次,第一次比较,根据条件将更优结点存在一个变量内,作为备选答案;第二次比较,使用这个备选答案来进行比较,从而最终确定被交换的结点是哪个.
    值得一提的是,我们要确定结点更改了才能进行交换的操作,判断if(x!=n)否则会进入死循环,逻辑也不正确(明明不用修改强行交换就很不合理)
    static void down(int x){
        //n存储的是最优解,初始为x,也就是如果比较两次都不如x,那最优解就是x
        int n=x;
        //第一次比较
        if(2*x<=size&&h[x]>h[2*x]) n=2*x;
        //第二次比较
        if(2*x+1<=size&&h[n]>h[2*x+1]) n=2*x+1; //注意这里一定是用h[x]来和h[2n+1]比较


        
        if(x!=n) {
            int temp;
            temp=h[x];
            h[x]=h[n];
            h[n]=temp;
            down(n);
        }
    }

题目2

维护一个集合,初始时集合为空,支持如下几种操作:
“I x”,插入一个数x;
“PM”,输出当前集合中的最小值;
“DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
“D k”,删除第k个插入的数;
“C k x”,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。
输入格式
第一行包含整数N。
接下来N行,每行包含一个操作指令,操作指令为”I x”,”PM”,”DM”,”D k”或”C k x”中的一种。
输出格式
对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1≤N≤105
−109≤x≤109
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6

算法分析

本题其实与题目一很相似,唯一不同的,并且需要注意的是,需要有删除第i个输入的操作.因此,我们需要创建一个数组,存储第i个输入对应于哪个栈节点;在节点交换的时候,因为我们是对节点进行操作的,所以不可避免的就会使得指针指向错误,因此我们也需要对指针进行交换.也就是代码中的Ih[i]数组,输入第i个插入值,就可以得到对应栈节点啦
那么当我们交换栈节点的值时,这个指针是不是一定要交换?(因为指向1的指针应该永远指向1,交换了之后这个节点可能就变成了2了)那么好,我们要让输入i指向节点a的指针转而变成输入i指向节点b的指针,就好了,按理说也并不需要hi[]数组呀?那是因为这只是我们的原理,在实际代码实现的时候,我们对于数组的交换操作的其实是栈角标,也就是说如果没有hi[],我们很难由角标找到输入的值(也就是第几个插入i,j),而我们很需要这个输入的i,j,才能实现ih[i]ih[j]的交换,因此我们要时时维护数组来保证知道角标就能确定次序.
所以我们就明确了,其实hi[]也是为ih[]服务的~

源代码


import java.util.*;
class Main{
    static int N=100010;
    static int[] h=new int[N];
    static int size=0;
    static int count=0;
    static int[] hc=new int[N]; //从栈结点找插入次数
    static int[] ch=new int[N]; //从插入次数找栈结点
    public static void main(String[] args){
        Scanner sc=new Scanner(System.in);
        int N=sc.nextInt();
        sc.nextLine();
        while(N-->0){
            String[] s=sc.nextLine().split(" ");
            if(s[0].equals("I")){
                int x=Integer.parseInt(s[1]);
                h[++size]=x;
                count++;
                hc[size]=count;
                ch[count]=size;
                up(size);
            }
            else if(s[0].equals("PM")){
                System.out.println(h[1]);
            }
            else if(s[0].equals("DM")){
                swap(1,size);//不能直接覆盖了
                size--;
                down(1);
            }
            else if(s[0].equals("D")){
                int k=Integer.parseInt(s[1]);
                int x=ch[k]; //找到第k个插入的数是第几个结点
                swap(x,size);
                size--;
                down(x);
                up(x);
            }
            else if(s[0].equals("C")){
                int k=Integer.parseInt(s[1]);
                int x=Integer.parseInt(s[2]);
                int c=ch[k];
                h[c]=x;
                down(c);
                up(c);
            }
        }
    }
    static void swap(int x,int y){
        //交换结点的值
        int temp=h[x];
        h[x]=h[y];
        h[y]=temp;
        //交换结点所表示的输入次序
        int temp2=hc[x];
        hc[x]=hc[y];
        hc[y]=temp2;
        // 交换由输入次序找到的结点,当前x和y已经交换了由结点找到的次序(也就是是真实次序)
        ch[hc[x]]=x;
        ch[hc[y]]=y;
    }
    static void up(int x){
        //需要后继有人,且符合条件才可以换
        while(h[x]<h[x/2]&&x/2>0){
            swap(x,x/2);
            x=x/2;
        }
    }
    static void down(int x){
        int n=x;
        if(2*x<=size&&h[x]>h[2*x])  n=2*x;
        if(2*x+1<=size&&h[n]>h[2*x+1]) n=2*x+1;
        if(n!=x){
            swap(n,x);
            down(n);
        }
    }
}

代码说明

在写任意一个函数的时候,都要确定他是有意义的.
比如down要保证有儿子,并且儿子满足条件

2*x<=size&&h[x]>h[2*x]

又如up要保证有父亲(父亲不能是0),且满足条件

h[x]<h[x/2]&&x/2>0

最后,也是debug很久的一点,插入的时候, 顺序一定是先在最后一个节点插入值,处理好指针(因为指针是跟值的),最后再up
如果先up的话,这个新插入的值很可能就没有指针指向了,同时指针会指向同一个节点.(因为指针是跟着值走,一旦up完,结尾可能就变成别的值了,这样既找不到要指的结点,又发生了指针错误的问题)
结论就是先确定好指针~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值