首页 > 编程语言 >LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【C++/java】【简单】

LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【C++/java】【简单】

时间:2022-12-28 19:36:22浏览次数:61  
标签:java 自定义 Offer int pos heap push data 节点


目录

​​一,题目描述​​

​​英文描述​​

​​中文描述​​

​​示例与说明​​

​​二,解题思路​​

​​1,手动实现堆——C++泛型实现​​

​​2,手动实现堆——java泛型实现​​

​​3,快速使用堆——C++​​

​​优先队列​​

​​pop_heap()、push_heap()​​

​​4,快速使用堆——java​​

​​三,AC代码​​

​​C++​​

​​Java​​

​​四,解题过程​​

​​第一博​​


一,题目描述

英文描述

English description is not available for the problem. Please switch to Chinese.

中文描述

输入整数数组 ​​arr​​​ ,找出其中最小的 ​​k​​ 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例与说明

LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【C++/java】【简单】_优先队列

LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【C++/java】【简单】_C++、Java_02

限制:

  • ​0 <= k <= arr.length <= 10000​
  • ​0 <= arr[i] <= 10000​

来源:力扣(LeetCode)
链接:​​​力扣​​ 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

二,解题思路

很明显这一题可以采用堆来实现。

通常堆的实现是基于完全二叉树的,也就是说可以创建一个数组,并利用下标定位子节点或父节点。

堆可以分为两种:大根堆(根节点最大),小根堆(根节点最小)。

堆的操作主要分两种(以大根堆为例):

  • pop:将头节点弹出,从左右节点选出较大者填补空缺,迭代完成后续操作。这一过程是自上而下完成的,可以看作sift_down;
  • push:将节点插入到数组末尾,不断和其父节点比较并替换,迭代完成后续操作。这一过程是自下而上完成的,可以看作sift_up;

说起来容易,但是要想实现功能较为完整的堆,并灵活的运用到题目中去,还需要借助泛型编程的力量。

1,手动实现堆——C++泛型实现

参考​​@AlexanderGan【堆__C++泛型实现简易的优先队列】​​,这里对算法实现的具体细节略作修改。

#include<iostream>
#include<vector>
#include<algorithm>
#include<limits.h>

using namespace std;

template<typename T>
// 小根堆,Greater可以理解为后面的数据越来越大
class Greater {
public:
// true:参数2优先级高,false:参数1优先级高。与C++中的设计保持一致
bool operator() (const T& a, const T&b) {
return a > b;
}
};

template<typename T, typename CMP>
class MyHeap {
private:
vector<T> data;

private:
// 从当前位置pos向下筛选,和子节点中优先级最高的对比,判断是否继续向下
void siftDown (int pos) {
int i = pos * 2 + 1; // 左子节点
while (i < data.size()) {
if (i + 1 < data.size() && CMP()(data[i], data[i + 1])) {
i++; // 选出优先级较高的位置
}
if (CMP()(data[pos], data[i]) == false) {
// 当前节点优先级高于任意子节点,不需要继续向下判断
break;
} else {
// 选择优先级较高的节点替换当前位置pos,继续向下判断
swap(data[i], data[pos]);
pos = i;
i = pos * 2 + 1;
}
}
}

// 从当前位置pos向上筛选,和父节点对比,判断是否继续向上
void siftUp (int pos) {
// 父节点存在,且当前节点优先级高于父节点
while ((pos - 1) / 2 >= 0 && CMP()(data[(pos - 1) / 2], data[pos])) {
swap(data[pos], data[(pos - 1) / 2]);
pos = (pos - 1) / 2;
}
}

public:
T top () {
if (data.size() > 0) return data[0];// 堆的下标从0开始
return INT_MIN; // 用最大或最小值标记非法输出
}
void push (T x) {
data.push_back(x);
siftUp(data.size() - 1);
}
void pop () {
data[0] = data[data.size() - 1]; // 把最后一个元素移到数组头部,将其覆盖
data.pop_back(); // 将数组尾部的元素弹出
siftDown(0); // 自上而下调整各个节点
}
int size() {
return data.size();
}

};

int main() {
MyHeap<int, Greater<int> > myHeap;
myHeap.push(1);
myHeap.push(3);
myHeap.push(2);
myHeap.push(6);
myHeap.push(4);
myHeap.push(5);
myHeap.push(8);
myHeap.push(7);
myHeap.push(9);

while (myHeap.size()) {
cout<<myHeap.top()<<endl;
myHeap.pop();
}
return 0;
}

2,手动实现堆——java泛型实现

C++中可以无脑使用vector来存储数据。Java一般用ArrayList或LinkedList。

  • 由于堆需要频繁的弹出数组中的元素,ArrayList作为连续数组的一种实现方式,显然不是很好的选择(连续数组删除元素后,为保持正确性,需要移动后面的元素来填补空缺);
  • LinkedList虽然可以方便的实现节点的删除,但是无法利用索引位置定位,效率也比较低下;

因此这里直接采用数组形式来实现数据存储,这样需要自己控制数组的容量并记录当前数组的元素数目。

具体实现参考​​@艾黛尔贾特【使用 Java 实现优先队列(小根堆)】​​,大佬写的很详细,还包括了扩容算法,这里为了简化代码就没加上这部分。


// 类型E或E的父类必须实现Comparable接口中的compareTo方法
public class MyHeap <E extends Comparable<? super E>> {
private static final int DEFAUT_CAPACITY = 100;// 设置数组默认大小
private int currentSize;// 表示当前堆的大小
private E[] data;// 存放数据

/**
* 构造方法,初始化数组及当前容量
*/
public MyHeap() {
data = (E[]) new Comparable[DEFAUT_CAPACITY];
currentSize = 0;
}

/**
* 交换位置i、j对应的节点
* @param i
* @param j
*/
private void swap (int i, int j) {
E tem = data[i];
data[i] = data[j];
data[j] = tem;
}

/**
* 将pos位置的节点向上调整
* @param pos
*/
private void siftUp (int pos) {
int fatIndex = (pos - 1) / 2;// 从数组下标0开始存放数据,所以计算父节点位置需要先减一
// 下标未越界且当前节点优先级高于父节点
while (fatIndex >= 0 && data[pos].compareTo(data[fatIndex]) > 0) {
swap(pos, fatIndex);
pos = fatIndex;
fatIndex = (pos - 1) / 2;
}
}

/**
* 将pos位置的节点向下调整
* @param pos
*/
private void siftDown (int pos) {
int childIndex = pos * 2 + 1;
while (childIndex < currentSize) {
// 下标未越界且右子节点优先级高于左子节点
if (childIndex + 1 < currentSize && data[childIndex + 1].compareTo(data[childIndex]) > 0) {
childIndex++;
}
// 当前节点优先级高于任意子节点,停止向下筛选
if (data[pos].compareTo(data[childIndex]) > 0) {
break;
} else {
swap(pos, childIndex);
pos = childIndex;
childIndex = pos * 2 + 1;
}
}
}

public int size() {return currentSize;}

public E top() {return currentSize > 0 ? data[0] : null;}

public void push(E e) {
if (currentSize == DEFAUT_CAPACITY) {
System.out.println("数据溢出,请重新设置默认容量");
return;
}
data[currentSize++] = e;
siftUp(currentSize - 1);
}

public void pop() {
if (currentSize == 0) {
System.out.println("暂无数据");
return;
}
data[0] = data[--currentSize];// 将最后一个元素填充到空出来的位置
siftDown(0);// 向下筛选
}
}

如何理解C++和Java中的比较器?(参考​​@蓦子骞【小根堆的建立与比较器】​​)

  • 如果comparator返回值为false,可理解为operator操作无效,a和b的顺序和形参表中一样,a依旧在前,b在后;
  • 若返回值为true, operator操作有效,交换ab位置,b在前(即更“大”);

3,快速使用堆——C++

主要有两种方法,一种是直接使用优先队列,另一种是借助pop_heap()、push_heap()实现。

优先队列

参考​​@AAMahone【C++ priority_queue的自定义比较方式】​​

优先队列的这个类型,其实有三个参数:priority_queue<class Type,class Container,class Compare>,即类型,容器和比较器,后两个参数可以缺省,这样默认的容器就是vector,比较方法是less,也就是默认大根堆(less对应大根堆!!!表示元素越来越小),可以自定义写比较方法,但此时若有比较方法参数,则容器参数不可省略!priority_queue<>的可支持的容器必须是用数组实现的容器,如vector,deque,但不能是list(推荐vector),比较方法可以写结构体重载()运算符,也可以用less,greater这些语言实现了的,但是灵活性不够,建议手写重载结构体,或者——如果不想写比较结构体的话,就将后面的两个参数缺省,直接重载类型的<运算符

priority_queue<int> Q;
for (int i = 0; i < k; ++i) {
Q.push(arr[i]);
}
for (int i = k; i < (int)arr.size(); ++i) {
if (Q.top() > arr[i]) {
Q.pop();
Q.push(arr[i]);
}
}
// -------------------------------------
struct cmp {
bool operator()(node a, node b) {
return a.val < b.val;
}
};
priority_queue<node, vector<node>, cmp> Q;

pop_heap()、push_heap()

vector<int> nums = { 4, 5, 1, 3, 2 ,8 ,7};
make_heap(nums.begin(), nums.end(),less<int>());
cout << "initial max value : " << nums.front() << endl;
// pop max value
pop_heap(nums.begin(), nums.end());
// push a new value
nums.push_back(6);
push_heap(nums.begin(), nums.end());

4,快速使用堆——java

下面的方法在leetcode上可以直接使用,但是在IDEA中会报错,不晓得是哪里的问题(jdk1.8版本太低了?)

PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
// 大根堆
public int compare(Integer num1, Integer num2) {
return num2 - num1;
}
});
queue.offer(x);// 相当于push
queue.poll();// 相当于pop
queue.peek();// 相当于top

三,AC代码

C++

class Solution {
public:

vector<int> getLeastNumbers(vector<int>& arr, int k) {
priority_queue<int, vector<int>, less<int>> heap;
vector<int> ans(k);
for (int x : arr) {
heap.push(x);
if (heap.size() > k) heap.pop();
}
for (int i = 0; i < k; i++) {
ans[i] = heap.top();
heap.pop();
}
return ans;
}
};

Java

class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>(){
public int compare (Integer num1, Integer num2) {
return num2 - num1;
}
});
for (int i = 0; i < arr.length; i++) {
heap.offer(arr[i]);
if (heap.size() > k) {
heap.poll();
}
}
int[] ans = new int[k];
for (int i = 0; i < k; i++) {
ans[i] = heap.peek();
heap.poll();
}
return ans;
}
}

四,解题过程

第一博

优先队列,没什么好说的,记录下手写首先队列的方法,以备万一

LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【C++/java】【简单】_leetcode_03

标签:java,自定义,Offer,int,pos,heap,push,data,节点
From: https://blog.51cto.com/u_15849465/5976212

相关文章

  • Web前端期末大作业--马尔代夫旅游网页设计(HTML+CSS+JavaScript+)实现
    目录​​前言介绍:​​​​网站首页:​​​​关于马尔代夫:​​​​酒店信息介绍:​​​​最新优惠政策:​​​​旅游须知模块:​​​​关于我们模块:​​​​主要源码结构:​​​......
  • java localDateTime
    #JAVA-LocalDateTime时间格式化,转换时间戳和源码分析##LocalDateTime`LocalDateTime`作为java8新加的时间类型,也是后面开发中常用的时间类型。作为时间类型,最关注的点......
  • Java学习之if---elif语句
    publicclasselif1{publicstaticvoidmain(String[]args){inttestScore=50;chargrade;if(testScore>=90){grade='A';}elseif(testScore>=80){grade=......
  • Java学习之do---while语句
    do—while1/*do-while结构如下do{循环体}while(条件表达式)特点:无条件的执行一次循环体,再来判断条件表达式的值,至少循环一次*/importjava.util.*;publicclassdh1......
  • CentOS7.2基于LAMP搭建WordPress,并自定义Logo和名称
    本次搭建LAMP+Wordpress环境如下MySQLphpWordpress_CN4.9ApacheCentOS7.2192.168.200.101、安装mariadb、php、httpd、wget2、测试php3、下载wordpress并配置4、网页......
  • Java学习之do-while-if语句实操
    //filenamedwif.java//题目要求:求100以内的素数,并输出/*由题目可知最小素数为2,其余偶数均为非素数,对于一个奇数k,使用3√k的每个整数j去除k,如果找到一个整数j能除尽k,则k......
  • Java学习之数组
    数组1//filenamesu.java数组讲解/*使用java数组一般需要经过三个步骤:①声明数组②分配空间③创建数组元素并赋值前两个步骤语法如下:数据类型[]数组名;//声明一维......
  • Java学习之字符串
    /*字符串:字符串就是一系列字符的序列。在java语言中字符串是一对双引号("")括起来的字符序列声明:字符串常量与字符常量不同,字符常量是用单引号(’)括起来的字符,而字符串......
  • .NET和JavaScript控件丨Infragistics功能简介
    使用InfragisticsUltimateUI/UX工具包简化开发,提供综合的企业级UI控件库和使用Indigo.Design的UX设计-开发协作工具-一个完整的设计到代码系统-集成原型、设计系统......
  • Java千问24:一文读懂Java语言方法的重写(覆盖、Override)
    ​很多初学Java语言的小伙伴,在学到“面向对象”这块内容的时候,都会学到的一个概念,那就是“方法的重写”。重写又叫覆盖,英文名为“Override”。虽然”重写”、”覆盖”、“O......