【C++】【排序算法】堆排序、构建堆、插入、pop堆顶【手撕最大堆】


一、堆排序的意义

构建最大堆,使其堆顶为最大元素。

二叉堆为完全二叉树。

二、堆排序的应用

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;
  }

参考

模板类、模板函数设计

快排、双路快排、三路快排

【手撕最小堆】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值