目录

​​一,题目描述​​

​​英文描述​​

​​中文描述​​

​​示例与说明​​

​​二,解题思路​​

​​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

LeetCode_Heap_剑指 Offer 40. 最小的k个数 【堆,泛型实现,自定义比较器】【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