堆 与 堆排序

Posted -YIN

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了堆 与 堆排序相关的知识,希望对你有一定的参考价值。

堆 与 堆排序

堆 是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。

概念与性质

将 n 个元素的序列 k1,k2,…,kn ,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足以下条件时称作堆

若将和此序列对应的一维数组( 即以一维数组做此序列的存储结构) 看成是 一个完全二叉树。

堆的性质

  1. 是一个完全二叉树
  2. 且树中所有非终端结点的值均不大于(或 不小于) 其左、右孩子结点的值。

大根堆:
堆顶元素为序列中 n 个元素的最值 (即完全二叉树中根节点值最大

小根堆:
堆顶元素为序列中 n 个元素的最值 (即完全二叉树中根节点值最小


堆的调整

用到了树的性质 5


详见:二叉树博客

向下调整

向下调整的前提:堆顶左右子树都同为大(小)堆,而堆顶不满足条件,

如给出一个数组,将其看作完全二叉树结构,以调整成小堆为例:将其向下调整直到满足堆的特性

int heap[] = 27,15,19,18,28,34,65,49,25,37;


  1. 从根节点开始(通过下标获取,标记为parent),比较左右子树的值,找出较小(大)的值,用child来标记。
  2. 比较child和parent的值,如果child比parent小(大),则不满足小(大)堆的规则,需要进行交换。;如果child比parent大(小),满足小(大)堆的特性不需要交换,调整结束。
  3. 交换之后,大(小)元素往下移动可能会导致子树不满足堆的特性;因此需要从child开始继续往下调整

代码:

// 向下调整
void AdjustDown(int arr[], int parent, int size)
	int child = parent * 2 + 1;   // 左孩子
	while (child < size)  // 如果进入循环表示至少有左节点(完全二叉树)
		// 1.找孩子中值较小的节点
		if (child + 1 < size && arr[child + 1] < arr[child])
			child += 1;
		// 2. 检测双亲是否满足堆特性
		if (arr[parent] > arr[child])
			Swap(&arr[parent], &arr[child]);
			// 继续向下调整,直到所有节点满足堆特性
			parent = child;
			child = parent * 2 + 1;
		
		else  return; 
	

堆的插入(向上调整)

在堆进行插入时,将新元素放入存储结构末尾,即插入堆尾(最后一个叶子节点之后),如果插入后不满足堆得特性那么需要从插入节点开始向上调整到满足堆

  1. 先设定倒数的第一个叶子节点为当前节点(通过下标获取,标记为child),找出他的父亲节点,用parent来标记。
  2. 比较parent和child的值,如果child比parent小(大),则不满足小(大)堆的规则,需要进行交换;如果child比parent大(小),满足小(大)堆的规则,不需要交换,调整结束。
  3. 交换之后,小(大)元素往上移动可能会导致子树不满足堆的特性;因此需要从parent开始继续往下调整

// 小堆的向上调整
void AdjustUp(int arr[], int size)

	int child = size - 1;
	int parent = (child - 1) / 2;
	while (child > 0)
	
		if (arr[child] < arr[parent])
			Swap(&arr[child], &arr[parent]);
			child = parent;
			parent = (child - 1) / 2;
		
		else  return; 
	

堆的创建

构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让 所有非叶子节点依次下沉。

从倒数的第一个非叶子节点的子树开始向上调整,一直调整到根节点的树,就可以调整成堆。

建堆的时间复杂度为O(N)。
其时间复杂度推导过程:

堆的删除

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法

例子:

如图中要删除72,先用堆中最后一个元素来35替换72,再将35下沉到合适位置,最后将叶子节点删除。“结点下沉”“下滤”


堆排序

是一种利用 树 形数据结构进行 选择排序 的排序算法,通过来进行选择数据。

  1. 不稳定排序。
  2. 只能用于顺序结构,不能用于链式结构 。
  3. 最坏情况下时间复杂度为 O(N*log2N)
  4. 空间复杂度为 O(1)

初始建堆所需 的比较次数较多,因此 记录数较少时不宜采用。

堆排序步骤:

  1. 建堆
      ● 升序排列建大堆
      ● 降序排列建小堆

    堆排序,为什么升序排列要建大堆,降序排列要建小堆

  2. 利用堆删除思想来进行排序

排升序序列 建大堆

void HeapSort(int arr[], int size)

	// 1. 建堆   升序--->大堆   降序--->小堆
	for (int i = (size - 2) / 2; i >= 0; --i)
		AdjustDown(arr, i, size);

	// 2. 利用堆删除的思想排序
	int end = size - 1;
	while (end > 0)
		Swap(&arr[0], &arr[end]);
		AdjustDown(arr, 0, end);   // 向下调整
		--end;
	


代码实现

typedef int HDataType;

// int(*Compare)(int left, int right);
typedef int(*COM)(int, int);

typedef struct Heap

	HDataType* array;
	int capacity;
	int size;
	COM Compare;
Heap;
// 交换
void Swap(int* a, int* b)
	*a ^= *b;
	*b ^= *a;
	*a ^= *b;


// 向下调整
void AdjustDown(Heap* hp, int parent)

	int child = parent * 2 + 1;
	int size = hp->size;
	while (child < size) 
		// 找parent小的孩子
		if (child + 1 < size && hp->Compare(hp->array[child + 1], hp->array[child]))
			child += 1;
		// 检测parent是否满足堆的特性
		if (hp->Compare(hp->array[child], hp->array[parent]))
			Swap(&hp->array[parent], &hp->array[child]);
			// 需要继续往下调整
			parent = child;
			child = parent * 2 + 1;
		
		else 	return; 
	


// 向上调整
void AdjustUp(Heap* hp)
	int child = hp->size - 1;
	int parent = (child - 1) / 2;
	
	while (child > 0)
		if (hp->Compare(hp->array[child], hp->array[parent]))
			Swap(&hp->array[child], &hp->array[parent]);
			child = parent;
			parent = (child - 1) / 2;
		
		else 	return; 
	


// 检查容量,扩容
static void CheckCapacity(Heap* hp)
	assert(hp);
	if (hp->size == hp->capacity)
		int newCapacity = hp->capacity * 2;
		// 1. 申请新空间
		int* temp = (HDataType*)malloc(sizeof(HDataType)*newCapacity);
		if (NULL == temp)
			assert(0);
			return;
		
		// 2. 拷贝元素
		for (int i = 0; i < hp->size; ++i)
			temp[i] = hp->array[i];
		// 3. 释放旧空间
		free(hp->array);
		hp->array = temp;
		hp->capacity = newCapacity;
	



// 堆的构建
void HeapCreate(Heap* hp, HDataType a[], int n, COM com)
	// 1. 初始化hp
	hp->array = (HDataType*)malloc(sizeof(HDataType)*n);
	if (NULL == hp->array)
		assert(0);
		return;
	
	hp->capacity = n;
	hp->size = 0;
	// 2. 将数组中的元素拷贝到堆结构中
	memcpy(hp->array, a, n*sizeof(HDataType));
	hp->size = n;
	hp->Compare = com;
	for (int root = (n - 2) / 2; root >= 0; root--)
		AdjustDown(hp, root);



// 堆的销毁
void HeapDestory(Heap* hp)

	assert(hp);
	if (hp->array)
	
		free(hp->array);
		hp->array = NULL;
		hp->capacity = 0;
		hp->size = 0;
	


// 堆的插入
void HeapPush(Heap* hp, HDataType x)

	// 检测容量是否足够
	CheckCapacity(hp);

	// 先将元素放入到堆中
	hp->array[hp->size] = x;
	hp->size++;

	// 将新插入的元素往上调整
	AdjustUp(hp);



// 堆的删除
void HeapPop(Heap* hp)

	if (HeapEmpty(hp))  return;
	Swap(&hp->array[0], &hp->array[hp->size - 1]);
	hp->size -= 1;
	AdjustDown(hp, 0);



// 取堆顶的数据
HDataType HeapTop(Heap* hp)

	assert(!HeapEmpty(hp));
	return hp->array[0];


// 堆的数据个数
int HeapSize(Heap* hp)

	assert(hp);
	return hp->size;



// 堆的判空
int HeapEmpty(Heap* hp)

	assert(hp);
	return 0 == hp->size;



// COM比较(建大堆、建小堆)
int Less(HDataType left, HDataType right)

	return left < right;


int Greater(HDataType left, HDataType right)

	return left > right;


TOP-K问题

TOP-K:指从大量数据(源数据)中获取最大(或最小)的K个数据。

即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下全部加载到内存中)。最佳的方式就是用堆来解决。



求解过程如下:

  1. 用数据集合中前K个元素来建堆
    求前k个最大的元素,则建小堆
    求前k个最小的元素,则建大堆
  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素,将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

时间复杂度为O(n*log₂K),空间复杂度为K

//topk问题
void PrintTopK(int* arr, int n, int k) 

	// 1. 建堆--用a中前k个元素建堆(依次一个一个插入)
	Heap hp;
	HeapCreate(&hp);
	for(int i = 0; i < k; ++i)
		HeapPush(&hp,arr[i]);

	// 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
	for (int i = k; i < n; i++)
		if (arr[i] > HeapTop(&hp))
			HeapPop(&hp);
			HeapPush(&hp, arr[i]);
		
	
	HeapPrint(&hp);
	HeapDestroy(&hp);  // 销毁堆

例题:

剑指offer40:最小的K个数

我们可以先取下标 0~k-1 的局部数组,用它来维护一个大小为K的数组,然后遍历后续的数字,进行比较后决定是否替换。这时候堆排序就派上用场了。我们可以将前K个数字建立为一个最小(大)堆,如果是要取最大的K个数,则在后续遍历中,将数字与最小堆的堆顶数字进行比较,若比它大,则进行替换,然后再重新调整为最大堆。整个过程直至所有数字遍历完为止。
时间复杂度为O(n*log₂K),空间复杂度为K。

更适合用 STL中提供的容器适配器优先级队列解决

优先级队列 priority_queue

官方手册:cplusplus.com/reference/queue/priority_queue/

优先级队列priority_queue是C++ STL库中提供的一种容器适配器,其底层容器默认为vector,又使用了堆算法将vector中元素构造成的结构;
默认情况下priority_queue是大堆

如果在priority_queue中放自定义类型的数据,用户需要在自定义类型中提供> 或者< 的重载。


TOP-K例题:
剑指offer40:最小的K个数

class Solution 
public:
    vector<int> GetLeastNumbers_Solution(vector<int> input, int k) 
        vector<int> res;
        if(k == 0 || k > input.size()) return res;
        priority_queue<int> pq;
        for(int i : input)
            if(pq.size() < k) pq.push(i);
            else
                if(i < pq.top())
                    pq.pop();
                    pq.push(i);
                
            
        
        
        while(!pq.empty())
            res.push_back(pq.top());
            pq.pop();
        
        return res;
    
;

215. 数组中的第K个最大元素

class Solution 
public:
/*
    int findKthLargest(vector<int>& nums, int k) 
        sort(nums.begin(), nums.end(), greater<int>());  // 降序排
        return nums[k-1];
    
*/
     int findKthLargest(vector<int>& nums, int k) 
         // 区间构造优先级队列
         priority_queue<int> pq(nums.begin(), nums.end());
         // 将前k-1个元素删除
         for(int i = 0; i < k-1; ++i)
            pq.pop();
        
        return pq.top();  //堆顶元素
     
;











堆的动图与部分图片来源于网络,以及《漫画算法》—程序员小灰

以上是关于堆 与 堆排序的主要内容,如果未能解决你的问题,请参考以下文章

堆排序详解

算法笔记-堆排序

堆排序

数据结构与算法堆排序总结与实现

堆排序与相关时间复杂度

左神直通BAT算法之堆排序