堆:手写一个我

727 阅读4分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

堆的定义和性质:

:就是一个用一维数组来表示一个完全二叉树的这么一个数据结构。所谓二叉树就是一种树,每一个父节点,有最多两个子节点,一般叫做左右子树

完美二叉树:是一个二叉树层数为k的时候,它的元素数量等于2k-1

image-20210308141724961

而一个完全二叉树可以理解为是一个完美二叉树缺少一部分或者不缺少一部分的二叉树,但是内容一定是从上到下,从左到右的填充,也就是缺少的部分总在右边;

image-20210308141802366

小根堆: 即根节点小于等于它的左孩子,也小于等于它的右孩子,且每个点都小于左右子节点;左孩子是左边集合的最小值,右孩子是右边的最小值

根节点是左右孩子的最小值--->推论出:根节点为堆的最小值

如何手写一个堆:

手写堆的前提是需要掌握基本的增删改查方法。 其中dwon与up的操作在后续代码中实现, 基本方法如下

size-->表示堆的大小;

  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);
    
  5. 修改任意一个元素:

    heap[k] = x;
    down(k);
    up(k);
    

堆排序:

步骤:(输出前m个的最小值)

  1. 初始化堆
  2. 建堆
  3. down操作

其中down操作的实现过程:

比较三个点的最小值,如果不符合堆的定义那么就交换、递归执行down操作

import java.util.*;
import java.io.*;

public class Main{
    static int N = 100010;
    // 定义堆
    static int[] h = new int[N];
    // 确定堆的大小
    static int size;
    
    // 当数在三个数中大时,使数往下沉
    public static void down(int u){
        // 设三个数的最小值为t;
        int t = u;
        // u*2为 u的左儿子; u*2+1 为u的右儿子;
        // 如果左儿子的下标小于堆的大小,则表示存在这个点;
        // 并且左儿子值比最小t的值小,则将t指向左儿子
        if(u*2 <= size && h[u*2] < h[t]) t = u * 2;
        // 如果右儿子的下标小于堆的大小,则表示存在这个点;
        // 并且右儿子值比最小t的值小,则将t指向右儿子
        if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1;
        // 如果最后t的最小值不是自己(u);
        // 那么交换两个下标所在的值;交换完在down一下,防止破坏堆结构;
        if(t != u){
            int temp = h[u];
            h[u] = h[t];
            h[t] = temp;
            down(t);
        }
        
    }
    
    public static void main(String[] args) throws IOException{
        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]);
        int m = Integer.parseInt(s[1]);
        
        String[] str = br.readLine().split(" ");
        // 初始化堆
        for(int i = 1; i <= n; i++){
            h[i] = Integer.parseInt(str[i-1]);
        }
        // 设置堆的大小
        size = n;
        //通过递推可得到时间复杂度:建堆-->时间复杂度为O(n)
        for(int i = n/2;i != 0;i--){
            down(i);
        }
        
        while(m-- != 0){
            // 输出最当前最小值,也就是堆顶
            bw.write(h[1]+" ");
            // 将最后的值覆盖掉堆顶--就是删掉堆顶
            h[1]  =h[size];
            // 堆大小--
            size--;
            // 在把堆顶down一下  找出最小值;
            down(1);
        }
        
        bw.flush();
        br.close();
        bw.close();
    }
    
}

实现up操作:

u/2为u的父节点,h[u] < h[u/2]-->子节点小于父节点;交换完后,up(u父节点的父节点);

首先堆是完全二叉树:(以下是编号)

  1
 2  3
4 5 6 7

2 / 2 = 1, 3 / 2 = 1.

4 / 2 = 2, 5 / 2 = 2, 6 / 2 = 3, 7 / 2 = 3

通过上面操作就能找到父节点;

public static void up(int u){
        if(u / 2 > 0 && h[u] < h[u / 2]){
            heapSwap(u, u / 2);
            up(u/2);
        }
    }

模拟堆:

模拟堆的实现核心是写出down操作、up操作,以及交换的操作。

import java.util.*;
import java.io.*;

public class Main{
    static int N = 100010;
    static int[] h = new int[N];
    static int[] ph = new int[N]; //存放第k个点的值的下标
    static int[] hp = new int[N]; //存放队中点的值是第几个插入的
    static int size;  //size 记录的是堆当前的数据多少
    
    public static void down(int u){
        int t = u;
        if(u*2 <=size && h[u*2] < h[t]) t = u*2;
        if(u*2+1 <= size && h[u*2+1] < h[t]) t = u*2+1;
        if(t != u){
            heap_swap(u,t);
            down(t);
        }
    }
    public static void up(int u){
        if(u / 2 > 0 && h[u] < h[u / 2]){
            heap_swap(u,u/2);
            up(u/2);
        }
    }
    
    public static void heap_swap(int u,int v)
    {   
        // 相对照
        // swap(h[u],h[v]);   //值交换 
        // swap(hp[u],hp[v]);  //堆中点的插入顺序(编号)交换
        // swap(ph[hp[u]],ph[hp[v]]); //对编号第h[u] h[v]的值交换
        
        swap(h,u,v);
        swap(hp, u, v);
        swap(ph, hp[u], hp[v]);
    
    }
    
    public static void swap(int[] a, int u, int v){
        int tmp = a[u];
        a[u] = a[v];
        a[v] = tmp;
    }
    
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int n = Integer.parseInt(br.readLine());
        size = 0;
        int m = 0;  
        
        while(n-- != 0){
            String[] s = br.readLine().split(" ");
            String op = s[0];
            if("I".equals(op)){
                int x = Integer.valueOf(s[1]);
                m++;
                h[++size]=x;
                // ph[m]=size;
                // hp[size]=m;
                // 建立映射关系即:第m个插入的数的编号是size;
                ph[m] = size;
                //第size编号下的是第m个插入的数;
                hp[size] = m;
                // 将插入的数向上调整
                up(size);
            }else if("PM".equals(op))    bw.write(h[1]+"\n");
            else if("DM".equals(op)){
                heap_swap(1,size);
                size--;
                down(1);
            }else if("D".equals(op)){
                int k = Integer.parseInt(s[1]);
                int u=ph[k];                //这里一定要用u=ph[k]保存第k个插入点的下标
                heap_swap(u,size);          //因为在此处heapSwap操作后ph[k]的值已经发生 
                size--;                    //如果在up,down操作中仍然使用ph[k]作为参数就会发生错误
                up(u);
                down(u);
            }else if("C".equals(op)){
                int k = Integer.parseInt(s[1]);
                int x = Integer.parseInt(s[2]);
                h[ph[k]]=x;                 //此处由于未涉及heapSwap操作且下面的up、down操作只会发生一个所以
                down(ph[k]);                //所以可直接传入ph[k]作为参数
                up(ph[k]);
            }
        }
        bw.flush();
        br.close();
        bw.close();
        
    }
    
}

为了方便阅读,我在代码中编写了注释,没有写到外面,这样方便读者理解。

结束:

如果你看到这里或者正好对你有所帮助,希望能点个关注或者推荐,感谢;

有错误的地方,欢迎在评论指出,作者看到会进行修改。