Java 数据结构篇-实现堆的核心方法与堆的应用(实现 TOP-K 问题:最小 k 个数)

文章目录

[1.0 堆的说明](#1.0 堆的说明)

[2.0 堆的成员变量及其构造方法](#2.0 堆的成员变量及其构造方法)

[3.0 实现堆的核心方法](#3.0 实现堆的核心方法)

[3.1 实现堆的核心方法 - 获取堆顶元素 peek()](#3.1 实现堆的核心方法 - 获取堆顶元素 peek())

[3.2 实现堆的核心方法 - 下潜 down(int i)](#3.2 实现堆的核心方法 - 下潜 down(int i))

[3.3 实现堆的核心方法 - 交换元素 swap(int i,int j)](#3.3 实现堆的核心方法 - 交换元素 swap(int i,int j))

[3.4 实现堆核心方法 - 删除堆顶元素 poll()](#3.4 实现堆核心方法 - 删除堆顶元素 poll())

[3.5 实现堆的核心方法 - 替换堆顶元素 replace(int i)](#3.5 实现堆的核心方法 - 替换堆顶元素 replace(int i))

[3.6 实现堆的核心方法 - 添加元素 offer(int value)](#3.6 实现堆的核心方法 - 添加元素 offer(int value))

[3.7 实现堆的核心方法 - 建堆 heapify()](#3.7 实现堆的核心方法 - 建堆 heapify())

[3.8 实现堆的核心方法完整代码](#3.8 实现堆的核心方法完整代码)

[4.0 TOP - K 问题:最小的 K 个数](#4.0 TOP - K 问题:最小的 K 个数)

[4.1 实现最小 k 个数的思路](#4.1 实现最小 k 个数的思路)

[4.2 代码实现最小 k 个数](#4.2 代码实现最小 k 个数)


1.0 堆的说明

堆(Heap)是一种基于树的数据结构,通常用于动态分配内存空间。堆可以被看作是一棵完全二叉树,其中每个节点都满足堆的性质,即父节点的值大于或等于子节点的值(大根堆),或父节点的值小于或等于子节点的值(小根堆)。在堆中,根节点的值是最大或最小的,因此也被称为最大堆或最小堆。

堆的实现通常使用数组来存储堆中的元素,通过计算数组下标来实现节点之间的关系。堆的时间复杂度为 O(log n),其中 n 是堆中元素的数量。

堆的操作包括插入、删除和查找等。插入操作将一个新元素插入到堆中,删除操作将堆中的最大或最小元素删除,查找操作可以在堆中查找特定元素的位置。

2.0 堆的成员变量及其构造方法

主要的成员变量为:int[] arr 数组:用来存放元素的容器;int size :代表当前的元素个数。

构造方法:指定数组大小带参数的构造器、参数为数组的构造器。

代码如下:

java 复制代码
public class MyHeap {
    public int[] arr;
    public int size;

    public MyHeap(int capacity) {
        arr = new int[capacity];
    }

    public MyHeap(int[] arr) {
        this.arr = arr;
        this.size = arr.length;
        heapify();
    }

}

3.0 实现堆的核心方法

获取堆顶元素、下潜、交换元素、添加元素、替换元素、删除元素、建堆。

3.1 实现堆的核心方法 - 获取堆顶元素 peek()

用数组实现堆,在获取堆顶元之前,先需要判断该数组是否为空,若不为空,则直接返回数组索引为 0 的元素;若数组为空,则返回 0 或者抛出异常也可以。

代码如下:

java 复制代码
    //获取栈顶元素
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return arr[0];
    }

3.2 实现堆的核心方法 - 下潜 down(int i)

该方法主要用于删除栈顶元素、替换元素等核心方法。下潜的意思就是将当前的元素所在的位置不符合大顶堆或者小顶堆的规则,因此需要向下调整。找到合适的位置来存放当前的元素。

具体下潜的思路:

假设需要满足大顶堆的规则:

由以上的图来看,当前的索引为 0 处的元素 7 小于该左孩子的元素,因此当前不满足大顶堆的规则。需要将两者进行交换。

交换的结果为:

交换完之后,当前索引为 2 处的元素 7 小于该右孩子的元素,所以索引 2 与 索引 5 需要继续交换。若当前为 i 该右孩子的索引 left = 2 * i + 1;该左孩子的索引 right = 2 * i + 2 (也可以表示为 right = left + 1 )一开始默认当前的最大元素的索引 max = i ,接着来判断该左右孩子的元素是否大于当前索引 max ,若大于当前索引 max 时,需要进行交换 max = left 或者 max = right 。若不大于当前索引为 max 处的元素,则不需要交换。由于每一次都是子问题过程,所以可以利用递归来实现,当且仅当 max != i 时,说明 max 已经被交换过了,需要继续向下递出,直到 max == i 时,结束递出,开始回溯。当然,这里不需要回带任何值或者变量,即该递归函数的返回类型为 viod 。

代码如下:

java 复制代码
    //下潜
    public void down(int i) {
        int left = 2 * i + 1;
        int right = left + 1;
        int max = i;
        if (left < size && arr[left] > arr[max]) {
            max = left;
        }
        if (right < size && arr[right] > arr[max]) {
            max = right;
        }
        if (max != i) {

            //交换
            swap(i,max);

            //继续下潜
            down(max);
        }
    }

3.3 实现堆的核心方法 - 交换元素 swap(int i,int j)

交换数组索引中 i 与 j 处的元素。

代码如下:

java 复制代码
    //交换
    public void swap(int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

3.4 实现堆核心方法 - 删除堆顶元素 poll()

具体实现思路:为了更高效率的删除堆顶元素后保持原来大顶堆或者小顶堆的规则。

步骤一:先将堆顶元素与最后一个元素进行交换。即 arr[0] = arr[size - 1] 。

步骤二:将 size-- 。

步骤三:交换后的堆,可能会不满足大顶堆或者小顶堆的规则,则需要将堆顶元素进行下潜调整,找到合适的位置存放该元素。最后需要返回删除的元素。

代码如下:

java 复制代码
    //删除堆顶元素
    public int poll() {
        if (isEmpty()) {
            return 0;
        }
        int t = arr[0];
        arr[0] = arr[size - 1];
        size--;
        //下潜
        down(0);

        return t;
    }

注意:在删除堆顶元素之前,需要先判断当前的数组是否为空数组。

同理,若需要删除指定堆中的元素索引,实现思路是一样的。

代码如下:

java 复制代码
    //指定删除元素
    public int poll(int i) {
        if (i > size) {
            return 0;
        }
        int temp = arr[i];
        arr[i] = arr[size - 1];
        size--;
        //下潜
        down(i);
        return temp;
    }

先判断索引是否合法,若不合法,则返回 0 或者抛出异常也可以。

3.5 实现堆的核心方法 - 替换堆顶元素 replace(int i)

具体思路为:先判断该数组是否为空数组,若不为空数组,则直接替换堆顶元素 arr[0] = i;之后可能会不满足大顶堆或者小顶堆的规则,所以索引为 0 处需要下潜调整,找到合适的位置存放元素。

代码实现:

java 复制代码
    //替换堆顶元素
    public void replace(int i) {
        if (isEmpty()) {
            return;
        }
        arr[0] = i;
        down(0);
    }

3.6 实现堆的核心方法 - 添加元素 offer(int value)

具体实现思路:先判断当前索引为 i = size 处的双亲节点为 j = (i - 1) / 2 ,判断 arr[j] 与 value 的大小,若为大顶堆规则,则当 arr[j] > value 时,不需要继续往上走了,在 i 处存放 value 即可 arr[i] = value ;当 arr[j] <= value 时,先将 arr[j] 处的元素存放在 arr[i] 中,接着需要继续往上走, i = j ,j = (i - 1) / 2 直到 i == 0 或者 arr[j] > value 时,退出循环。在 arr[i] 处存放 value 。

代码如下:

java 复制代码
    //添加元素
    public boolean offer(int value) {
        if (isFull()) {
            return false;
        }
        int i = size;
        int j = (size - 1) / 2;
        while (i > 0 && arr[j] < value) {
            arr[i] = arr[j];
            i = j;
            j = (i - 1) / 2;
        }
        arr[i] = value;
        size++;
        return true;
    }

需要注意:添加元素前,需要先判断该数组是否满了。还有添加完之后,需要进行 size++ 。

3.7 实现堆的核心方法 - 建堆 heapify()

该方法实现的意义,若随机给出一个数组,需要实现由大顶堆或者小顶堆的结构存放元素。因此就会用到该方法。

实现思路为:需要找到最后一个非叶子节点,从后往前,将当前的元素进行下潜处理即可完成建堆。

代码如下:

java 复制代码
    //建堆
    public void heapify() {

        //先找到最后非叶子节点
        int lastNonLeafNodes = size / 2 - 1;
        for (int i = lastNonLeafNodes; i >= 0 ; i--) {
            //下潜
            down(i);
        }
    }

3.8 实现堆的核心方法完整代码

java 复制代码
public class MyHeap {
    public int[] arr;
    public int size;

    public MyHeap(int capacity) {
        arr = new int[capacity];
    }

    public MyHeap(int[] arr) {
        this.arr = arr;
        this.size = arr.length;
        heapify();
    }

    //获取栈顶元素
    public int peek() {
        if (isEmpty()) {
            return -1;
        }
        return arr[0];
    }

    //删除堆顶元素
    public int poll() {
        if (isEmpty()) {
            return 0;
        }
        int t = arr[0];
        arr[0] = arr[size - 1];
        size--;
        //下潜
        down(0);

        return t;
    }

    //指定删除元素
    public int poll(int i) {
        if (i > size) {
            return 0;
        }
        int temp = arr[i];
        arr[i] = arr[size - 1];
        size--;
        //下潜
        down(i);
        return temp;
    }

    //替换堆顶元素
    public void replace(int i) {
        if (isEmpty()) {
            return;
        }
        arr[0] = i;
        down(0);
    }


    //添加元素
    public boolean offer(int value) {
        if (isFull()) {
            return false;
        }
        int i = size;
        int j = (size - 1) / 2;
        while (i > 0 && arr[j] < value) {
            arr[i] = arr[j];
            i = j;
            j = (i - 1) / 2;
        }
        arr[i] = value;
        size++;
        return true;
    }

    //建堆
    public void heapify() {

        //先找到最后非叶子节点
        int lastNonLeafNodes = size / 2 - 1;
        for (int i = lastNonLeafNodes; i >= 0 ; i--) {
            //下潜
            down(i);
        }
    }

    //下潜
    public void down(int i) {
        int left = 2 * i + 1;
        int right = left + 1;
        int max = i;
        if (left < size && arr[left] > arr[max]) {
            max = left;
        }
        if (right < size && arr[right] > arr[max]) {
            max = right;
        }
        if (max != i) {

            //交换
            swap(i,max);

            //继续下潜
            down(max);
        }
    }

    //交换
    public void swap(int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }


    //判断是否为空数组
    public boolean isEmpty() {
        return size == 0;
    }

    //判断是否为满数组
    public boolean isFull() {
        return  size == arr.length;
    }
}

4.0 TOP - K 问题:最小的 K 个数

题目:

设计一个算法,找出数组中最小的k个数。以任意顺序返回这 k 个数均可。

示例:

复制代码
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]

提示:

0 <= len(arr) <= 100000

0 <= k <= min(100000, len(arr))

OJ 链接:

面试题 17.14. 最小K个数

4.1 实现最小 k 个数的思路

具体思路为:结合大顶堆的数据结构的特点,根节点的元素永远比孩子节点的元素大。先将给定的 arr 数组的前 k 个元素直接通过 heap.offer() 方法添加到大顶堆上,然后 arr 数组剩下的元素需要跟堆顶元素相对比,若堆顶元素大于 arr[i] 中的元素,则需要进行交换,将 arr[i] 的元素替换到堆顶,接着还不能结束,有可能替换完的元素就不符合大顶堆的规则了,因此还需要将堆顶元素下潜处理调整,找到合适的位置存放该元素;若堆顶元素不大于 arr[i] 中的元素,则不需要交换。一直将 arr 数组中的元素遍历结束,则循环停止。最后堆上存储的 k 个元素就是该数组 arr 中最小的元素了。

4.2 代码实现最小 k 个数

java 复制代码
public class Solution {
    public int[] smallestK(int[] arr, int k) {
        MaxHeap heap = new MaxHeap(k);
        for(int i = 0; i < k ; i++) {
            heap.offer(arr[i]);
        }
        for(int i = k; i < arr.length; i++) {
            if(heap.peek() > arr[i]) {
                heap.arr[0] = arr[i];
                heap.down(0);
            }
        }
        return heap.arr;
    }

}

//实现一个大顶堆
class MaxHeap {
    int[] arr;
    int size;

    public MaxHeap(int capacity) {
        arr = new int[capacity];
    }

    public MaxHeap(int[] smallestK) {
        this.arr = smallestK;
        this.size = smallestK.length;
    }

    //插入元素
    public boolean offer(int value) {
        if(isFull()) {
            return false;
        }
        int i = size;
        int j = (i - 1) / 2;
        while(i > 0 && arr[j] < value) {
            arr[i] = arr[j];
            i = j;
            j = (i - 1) / 2;
        }
        arr[i] = value;
        size++;
        return true;
    }

    //删除堆顶元素
    public int poll() {
        if(isEmpty()) {
            return 0;
        }
        int ret = arr[0];
        arr[0] = arr[size - 1];
        size--;
        down(0);
        return ret;
    }
    //下潜
    public void down(int i) {
        int left = 2 * i + 1;
        int right = left + 1;
        int max = i;
        if(left < size && arr[left] > arr[max]) {
            max = left;
        }
        if(right < size && arr[right] > arr[max]) {
            max = right;
        }
        if(max != i) {
            swap(max,i);
            down(max);
        }

    }

    //交换
    public void swap(int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    //获取堆顶元素
    public int peek() {
        if(isEmpty()) {
            return 0;
        }
        return arr[0];
    }

    //判断是否为空
    public boolean isEmpty() {
        return size == 0;
    }

    //判断是否为满
    public boolean isFull() {
        return size == arr.length;
    }

}
相关推荐
Chrikk7 分钟前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*10 分钟前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue10 分钟前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man13 分钟前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟14 分钟前
使用python向钉钉群聊发送消息
java·python·钉钉
好奇龙猫44 分钟前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
P.H. Infinity1 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天1 小时前
java的threadlocal为何内存泄漏
java
sp_fyf_20241 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
caridle1 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express