一、堆排序的意义
构建最大堆,使其堆顶为最大元素。
二叉堆为完全二叉树。
二、堆排序的应用
topK 类问题,通过构建K大小的堆。相较于快排思想,可以实现输入流的topK问题。
。。。
二、代码
堆中,最重要的是shft 操作。理解了shift 操作基本就没问题了。
shift up):插入时候插入到尾部,然后一直和父节点比较、交换,一直到合适的位置。
shift down):将自己最大的孩子跟自己交换,然后把自己一直向下交换到合适的位置。即将这个位置换成这个位置子树中最大的数。
其中因为是二叉堆,用数组实现的。就可以知道
从零开始存储:当前节点i的父节点是(i-1 )/2 ; 左右子节点2∗i+1,2∗i+2。
从一开始存储(同下例):父节点是i/2,左右子节点2∗i,2∗i+1。
1.构建最大堆类
应用较广,适用于TOP K类问题。对于直接排序不是很适合。
class MaxHeap {
private:
int *data;
int count;
int capacity;
void shiftUp(int k) {
while (k > 1 && data[k / 2] < data[k]) {
swap(data[k / 2], data[k]);
k /= 2;
}
}
void shiftDown(int k) {
while (2 * k <= count) {
int j = 2 * k;
if (j + 1 <= count && data[j + 1] > data[j]) j++;
if (data[k] >= data[j]) break;
swap(data[k], data[j]);
k = j;
}
}
public:
// 构造函数, 构造一个空堆, 可容纳capacity个元素
MaxHeap(int capacity) {
data = new int[capacity + 1];
count = 0;
this->capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最大堆
// 该构造堆的过程, 时间复杂度为O(n)
MaxHeap(int arr[], int n) {
data = new int[n + 1];
capacity = n;
for (int i = 0; i < n; i++)
data[i + 1] = arr[i];
count = n;
for (int i = count / 2; i >= 1; i--)
shiftDown(i);
}
~MaxHeap() {
delete[] data;
}
// 返回堆中的元素个数
int size() {
return count;
}
// 返回一个布尔值, 表示堆中是否为空
bool isEmpty() {
return count == 0;
}
// 像最大堆中插入一个新的元素 item
void insert(int item) {
data[count + 1] = item;
shiftUp(count + 1);
count++;
}
// 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
int extractMax() {
int ret = data[1];
swap(data[1], data[count]);
count--;
shiftDown(1);
return ret;
}
// 获取最大堆中的堆顶元素
int getMax() {
return data[1];
}
};
1.1 简单两种排序应用:
一种是构建一个堆,然后一个一个插入,再弹出。
一种是heapify构建,然后再弹出。(构建过程优化成了,直接从非叶子节点开始shift down)
// heapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
// 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
// 整个堆排序的整体时间复杂度为O(nlogn)
void heapSort1(int arr[], int n){
MaxHeap maxheap = MaxHeap(n);
for( int i = 0 ; i < n ; i ++ )
maxheap.insert(arr[i]);
for( int i = n-1 ; i >= 0 ; i-- )
arr[i] = maxheap.extractMax();
}
// heapSort2, 借助我们的heapify过程创建堆
// 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 实践复杂度为O(nlogn)
// 堆排序的总体时间复杂度依然是O(nlogn), 但是比上述heapSort1性能更优, 因为创建堆的性能更优
void heapSort2(int arr[], int n){
MaxHeap maxheap = MaxHeap(arr,n);
for( int i = n-1 ; i >= 0 ; i-- )
arr[i] = maxheap.extractMax();
}
1.2应用:剑指 Offer 40. 最小的k个数
这个问题的解法很多,但是自己构建堆来实现,是蛮复杂的问题。
这个解法没有应用stl 自带的优先队列来实现。可能效果还不如优先队列实现的。
但是对于应用自己构建堆来说,帮助很大
class Solution {
class MaxHeap {
private:
int *data;
int count;
int capacity;
void shiftUp(int k) {
while (k > 1 && data[k / 2] < data[k]) {
swap(data[k / 2], data[k]);
k /= 2;
}
}
void shiftDown(int k) {
while (2 * k <= count) {
int j = 2 * k;
if (j + 1 <= count && data[j + 1] > data[j]) j++;
if (data[k] >= data[j]) break;
swap(data[k], data[j]);
k = j;
}
}
public:
// 构造函数, 构造一个空堆, 可容纳capacity个元素
MaxHeap(int capacity) {
data = new int[capacity + 1];
count = 0;
this->capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最大堆
// 该构造堆的过程, 时间复杂度为O(n)
MaxHeap(int arr[], int n) {
data = new int[n + 1];
capacity = n;
for (int i = 0; i < n; i++)
data[i + 1] = arr[i];
count = n;
for (int i = count / 2; i >= 1; i--)
shiftDown(i);
}
~MaxHeap() {
delete[] data;
}
// 返回堆中的元素个数
int size() {
return count;
}
// 返回一个布尔值, 表示堆中是否为空
bool isEmpty() {
return count == 0;
}
// 像最大堆中插入一个新的元素 item
void insert(int item) {
data[count + 1] = item;
shiftUp(count + 1);
count++;
}
// 从最大堆中取出堆顶元素, 即堆中所存储的最大数据
int extractMax() {
int ret = data[1];
swap(data[1], data[count]);
count--;
shiftDown(1);
return ret;
}
// 获取最大堆中的堆顶元素
int getMax() {
return data[1];
}
};
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
if (arr.size() <= k)return arr;
if (k == 0) return {};
MaxHeap my_queue(k);
for (int i = 0; i < k; i++)
my_queue.insert(arr[i]);
for (int i = k; i < arr.size(); i++)
{
if (arr[i] < my_queue.getMax())
{
my_queue.extractMax();
my_queue.insert(arr[i]);
}
}
vector<int> result(k);
for (int i = 0; i < k; i++)
{
result[i] = my_queue.extractMax();
//my_queue.pop();
}
return result;
}
};
2.原地堆排序
常用于排序问题,TOP K 类问题也用,但是没有因为要所有数据进堆,并没有1. 建K大小的堆来说更优。
不构造新的数组,在传入数组中进行堆排序(最大堆实现的是从小到大排序)。空间复杂度O(1),时间O(nlogn)但是比之前快一点。
实现:先原地heapify构造堆,然后把最大的交换到后面,再对前面进行下一轮shiftDown。即每次将当前最大的插入到当前的最后面,最终数组会从小到大排序。
这里的shift down 是经过了插入排序的优化。
void __shiftDown(vector<int> &arr, int n, int k) {
int e = arr[k];
while (2 * k + 1 < n) {
int j = 2 * k + 1;
if (j + 1 < n && arr[j + 1] > arr[j])
j += 1;
if (e >= arr[j]) break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
// 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序
void heapSort(vector<int>& arr, int n) {
// 注意,此时我们的堆是从0开始索引的
// 从(最后一个元素的索引-1)/2开始
// 最后一个元素的索引 = n-1
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
__shiftDown(arr, n, i);
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
__shiftDown(arr, i, 0);
}
}
3.用插入排序优化shift 操作
原地堆排序的shift down 是用插入排序优化过 的。而最上面的最大堆的构建中shift 是没有优化的,可以对比发现,swap操作减少了。
下面就是优化过的shift 和没优化的对比,对堆排序有一定的优化,时间复杂度没有变化,是常数级的优化。
void shiftUp(int k){
while( k > 1 && data[k/2] < data[k] ){
swap( data[k/2], data[k] );
k /= 2;
}
}
void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[j+1] > data[j] ) j ++;
if( data[k] >= data[j] ) break;
swap( data[k] , data[j] );
k = j;
}
}
void shiftUp(int k){
int e = data[k];
while( k > 1 && data[k/2] < e ){
//swap( data[k/2], data[k] );
data[k] = data[k/2];
k /= 2;
}
data[k] = e;
}
void shiftDown(int k){
int e = data[k];
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[j+1] > data[j] ) j ++;
if( e >= data[j] ) break;
//swap( data[k] , data[j] );
data[k] = data[j];
k = j;
}
data[k] = e;
}