堆相关算法题基础-java实现

堆排序

思路

如何手写一个堆

1.插入一个数:heap[++size] = x; up(size);

2.求集合当中的最小值heap[1];

3.删除最小值:heap[1]=heap[size];size--;down(1);

4.删除任意一个元素:heap[k]=heap[size];size--;down(k);up(k);//实际上这里只会根据k和父节点大小执行down和up中的一个操作

5.修改任意一个元素:heap[k]=x;down(k);up(k);

堆是一颗完全二叉树,主要实现两个操作down(x)和up(x),down的核心思路:判断当前父节点的元素和左右子节点相比是不是最小的和最小的进行交换

创建的时候从n/2开始:n/2开始,还有一个角度可以理解,因为n是最大值,n/2是n的父节点,因为n是最大,所以n/2是最大的有子节点的父节点,所以从n/2往前遍历,就可以把整个数组遍历一遍

代码

java 复制代码
import java.util.Scanner;
public class Main{
    static int N = 100010;
    static int[] h = new int[N];
    static int size;
    //交换函数
    public static void swap(int x,int y){
        int temp = h[x];
        h[x] = h[y];
        h[y] = temp;
    }
    //堆函数核心函数,堆排序函数,传入的参数u是下标
    public static void down(int u){
        int t = u; // t用来分身变量
        //u的左分支下标小于size说明该数存在,然后如果这个数小于t,则让左下标赋值给t,即u的分身 
        if(u * 2 <= size && h[u * 2] < h[t]) t = u * 2; 
        //u的右分支下标小于size说明该数存在,然后如果这个数小于t,因为可能上面的if语句会重置t,则判断是不是小于新或旧t,
        //如果小于t的话,就让右下标赋值给t ,即u的分身。
        if(u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
        //如果u不等于t,说明左或者右下标有比较小的值赋值给t了,分身变了
        if(u != t){
            //就让u跟t这两个数交换一下位置,让小的数替代u的位置
            swap(u,t);
            //然后继续递归t下标小的位置
            down(t);
        }
    }
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        int m = scan.nextInt();
        for(int i = 1 ; i <= n ; i ++ ) h[i] = scan.nextInt(); //首先将所有数先存入数组中
        size = n;//长度为n
        //从n/2的位置开始将数组中的值插入堆中
        //堆结构是一个完全二叉树,所有有分支的数等于没有分支的数,即最后一排的数量等于上面所有的数
        //最下面一排没有分支的数不用参与插入,所以从n/2开始进行插入
        for(int i = n/2 ; i >= 0; --i ) down(i);//就是让他进行向下排序处理 

        while(m -- > 0){
            //输出前m小的m个元素
            System.out.print(h[1] + " "); //每一次输出头节点
            //因为是数组,删除第一个数复杂,删除最后一个元素比较简单
            //所以就将最后一个元素将第一个元素覆盖掉,然后删除掉最后一个元素
            h[1] = h[size--];
            //然后进行堆排序,对第一个元素进行处理
            down(1);
        }
    }
}

模拟堆

思路

类似链表第k个数

一、ph[] 和 hp[] 的含义

ph[k] = a: 第 k 个插入的数在堆中的下标是 a

hp[a] = k: 堆中下标是 a 的节点是第 k 个插入的数

二、为什么要引入 ph[] 和 hp[]?

AcWing 839. 模拟堆 和 AcWing 838. 堆排序最大的不同在于,需要记录第 k 个插入的数在堆中的位置,因而自然引出 ph[] 数组。

三、只引入 ph[] 为什么不行?

问题是交换节点 a 和 b,同时也需要交换节点 a 和 b 对应的 ph 值,即要找到 ph[i] = a 和 ph[j] = b,进行交换 swap(ph[i], ph[j]),这样就需要遍历 ph[],时间复杂度高。

注意:ph[] 的有效下标一直到当前插入的数的个数,而非堆中的节点数目cnt,因此需要遍历的点更多,并且删除节点时,也必须删除该节点对应的 ph 值,否则会存在多个 ph[?] = a 的情况。

代码

java 复制代码
/*
I x,插入一个数 x;h[++size] = x;up(size);
PM,输出当前集合中的最小值; h[1]
DM,删除当前集合中的最小值(数据保证此时的最小值唯一); h[1] = h[size--];down(1);
D k,删除第 k 个插入的数; h[k] = h[size--];down(k),up(k)
C k x,修改第 k 个插入的数,将其变为 x;h[k] = x,down(k) ,up(k);
*/
import java.util.Scanner;
public class Main{
    static int N = 100010,size,m;
    static int[] h = new int[N];
    static int[] hp = new int[N];//自身被映射数组
    static int[] ph = new int[N];//映射数组
    public static void swap(int[] a,int x,int y){
        int temp = a[x];
        a[x] = a[y];
        a[y] = temp;
    }
    public static void head_swap(int x,int y){
        //这里因为映射数组跟被映射数组是互相指向对方,如果有两个数更换位置,映射下标也要进行更换
        //ph的下标指向是按顺序插入的下标,hp所对应的值是ph的按顺序的下标,用这两个属性进行交换
        swap(ph,hp[x],hp[y]);
        //因为按照顺序插入ph到指向交换了,对应指向ph的hp也要进行交换
        swap(hp,x,y);
        //最后两个值进行交换
        swap(h,x,y);
    }
    public static void down(int x){
        int t = x;//x的分身
        //判断一下左下标是不是存在
        //判断一下左下标的值是不是比我t的值小 。那么就将左下标的值赋予t;否则不变
        if(x * 2 <= size && h[x * 2] < h[t]) t = x * 2;
        //判断一下右下标的值是不是比我t的值小。那么就将右下标的值赋予t,否则不变
        if(x *2 + 1 <= size && h[x * 2 + 1] < h[t]) t = x * 2 + 1;
        if(t != x){//如果x不等于他的分身
            head_swap(x,t);//那就进行交换顺序
            down(t);//然后一直向下进行操作
        }
    }
    public static void up(int x){
        //向上操作,判断一下根节点还不是存在
        //看一下根节点是不是比我左分支或者右分支的值大,大的话就进行交换
        while(x / 2 > 0 && h[x / 2] > h[x]){
            head_swap(x,x/2);
            x = x / 2;//相当于一直up
        }
    }
    public static void main(String[] args){
        Scanner scan = new Scanner(System.in);
        int n = scan.nextInt();
        size = 0;//size是原数组的下标
        m = 0;//m是映射数组的下标
        while(n -- > 0){
            String s = scan.next();
            if(s.equals("I")){//插入操作
                int x= scan.nextInt();
                size ++;m ++;//插入一个数两个数组的下标都加上1;
                ph[m] = size;hp[size] = m;//ph与hp数组是映射关系
                h[size] = x;//将数插入到堆中最后位置
                up(size);//然后up,往上面排序一遍
            }else if(s.equals("PM")){ //输出当前集合中的最小值
                System.out.println(h[1]);
            }else if(s.equals("DM")){//删除当前集合中的最小值
                //因为需要用到映射数组与被映射数组,因为需要找到k的位置在哪里,需要让映射的顺序,
                //因为如果用size,size是会随时改变的,不是按顺序的,因为会被up或者down顺序会被修改
                head_swap(1,size);//将最后一个数替换掉第一个最小值元素,然后数量减1,size--
                size--;
                down(1);//插入之后进行向下操作,因为可能不符合小根堆
            }else if(s.equals("D")){//删除当前集合中第k个插入得数
                int k = scan.nextInt();
                k = ph[k];//ph[k] 是一步一步插入映射的下标,不会乱序,
                head_swap(k,size);//然后将k与最后一个元素进行交换,然后长度减1,size--
                size--;
                up(k);//进行排序一遍,为了省代码量,up一遍down一遍。因为只会执行其中一个
                down(k);
            }else{
                int k = scan.nextInt();
                int x = scan.nextInt();
                k = ph[k];//ph[k] 是一步一步插入映射的下标,顺序是按照插入时候的顺序
                h[k] = x;//然后将第k为数修改为数x
                up(k);//up一遍,down一遍
                down(k);

            }
        }
    }
}
相关推荐
richxu202510012 小时前
Java开发环境搭建之 10.使用IDEA创建和管理Mysql数据库
java·ide·intellij-idea
锂享生活2 小时前
论文阅读:铁路车辆跨临界 CO₂ 空调系统模型预测控制(MPC)策略
论文阅读·算法
7澄12 小时前
Java 集合框架:List 体系与实现类深度解析
java·开发语言·vector·intellij-idea·集合·arraylist·linkedlist
行思理2 小时前
IntelliJIdea 工具新手操作技巧
java·spring·intellijidea
B站_计算机毕业设计之家2 小时前
深度学习:Yolo水果检测识别系统 深度学习算法 pyqt界面 训练集测试集 深度学习 数据库 大数据 (建议收藏)✅
数据库·人工智能·python·深度学习·算法·yolo·pyqt
mit6.8242 小时前
一些C++的学习资料备忘
开发语言·c++
骑自行车的码农2 小时前
React SSR 技术实现原理
算法·react.js
盘古开天16662 小时前
深度强化学习算法详解:从理论到实践
算法
Adellle2 小时前
Java中同步和异步的区别,以及阻塞和非阻塞的区别
java·开发语言