堆(优先队列)

一、堆

1、堆的特征

  堆是什么?是一种特殊的完全二叉树,就像下面这棵树一样:
这里写图片描述

  这棵二叉树有一个特点,就是所有父结点都比子结点要小。符合这样特点的完全二叉树我们称为最小堆。反之,如果所有父结点都比子结点要大,这样的完全二叉树称为最大堆
  我们对堆中的结点按层进行编号,该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

最大堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]  
最小堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]   

那这一特性究竟有什么用呢?

  假如有14个数分别是99、5、36、7、22、17、46、12、2、19、25、28、1和92。请找出这14个数中最小的数,请问怎么办呢?最简单的方法就是将这14个数从头到尾依次扫一遍,用一个循环就可以解决。这种方法的时间复杂度是O(14)也就是O(N)。

  现在我们需要删除其中最小的数,并增加一个新数23,再次求这14个数中最小的一个数。请问该怎么办呢?只能重新扫描所有的数,才能找到新的最小的数,这个时间复杂度也是O(N)。假如现在有14次这样的操作(删除最小的数后并添加一个新数)。那么整个时间复杂度就是O(142)即O(N2)。那有没有更好的方法呢?堆这个特殊的结构恰好能够很好地解决这个问题。

  首先我们先把这个14个数按照最小堆的要求(就是所有父结点都比子结点要小)放入一棵完全二叉树,就像下面这棵树一样:
这里写图片描述
  很显然最小的数就在堆顶,假设存储这个堆的数组叫做h的话,最小数就是h[1]。

2、堆的初始化过程

  从一个无序序列初始化为一个堆的过程就是一个反复“筛选”的过程。由完全二叉树的性质可以知,一个有n个节点的完全二叉树的最后一个非叶节点是节点[n/2],堆的初始化过程就从这个[n/2]节点开始。

  假设有一无序数组:{49,38,65,97,76,13,27,50},对其构建堆的结果如下:
这里写图片描述

  首先,未处理的数组对应的堆为图1模样。从第四个节点开始([8/2]=4),因为50 < 97,故要交换两节点,交换后还要继续对其新的左子树进行类似输出后那样的筛选。易见其左子树只有节点97,已经为最佳情况,故可以继续堆的初始化,如图2。再考虑第三个节点,因为13 < 27 < 65,即节点13为当前的最小节点,故与节点65交换,并对新的左子树进行筛选,其也为最佳情况,故可继续堆的初始化,结果如图3。然后考虑第二个节点,因为38 < 50 < 76,故已经为最优情况,不用调整。最后再考虑第一个节点,根节点。因为 13 < 38 < 49,故需要将根节点49与其右孩子节点13交换,交换后还要继续对其新的右子树进行类似输出后那样的筛选,可见右子树还需要调整,因为 27 < 49 < 65,故将节点49与节点27交换。此时已经处理完了根节点,初始化结束。最终结果如图5.

3、构建堆的demo:

  在构造堆的时候,首先需要找到最后一个节点的父节点,从这个节点开始构造最大堆;直到该节点前面所有分支节点都处理完毕,这样最大堆就构造完毕了。

//建立堆积树(从下往上调整)
void create_heap(vector<int> &a, int i, int heapsize)
{
	int largest = i;
	int temp = a[i];
    int left = 2 * i + 1;
    int right = left + 1;
    //判断当前节点和左右儿子的关系,并用largest记录值较大的结点编号
    if (left < heapsize && a[i] < a[left])
    	largest = left;
    if(right < heapsize && a[largest] < a[right])
        largest = right;
    if(largest != i)
    {
        swap(a[i], a[largest]);//如果有儿子节点大于父节点则交换
        create_heap(a, largest, heapsize);
    }
}

vector<int> vec1 = {7, 8, 9, 5, 3, 6, 1};
int length = vec1.size();
for(int i = length/2 - 1; i >= 0; --i)
{
    create_heap(a, i, length);	// 从最后一个非叶子节点向上建立最大堆
}

二、STL中关于堆的一些操作

//C++中堆的应用:make_heap, pop_heap, push_heap, sort_heap
void STL_Heap()
{
	vector<int> vec1 = {7, 8, 9, 5, 3, 6, 1};
	make_heap(vec1.begin(), vec1.end(), greater<int>());//建立最小堆
	for (const auto v : vec1)
		cout << v << " ";
	cout << endl;

	vector<int> vec2 = {7, 8, 9, 5, 3, 6, 1};
	make_heap(vec2.begin(), vec2.end(), less<int>());//建立最大堆
	vec2.push_back(0);//插入数据后重新建立堆
	push_heap(vec2.begin(), vec2.end());
	for (const auto v : vec2)
		cout << v << " ";
	cout << endl;

	vector<int> vec3 = {7, 8, 9, 5, 3, 6, 1};
	make_heap(vec3.begin(), vec3.end());
	//pop_heap()把堆顶元素放到了最后一位,然后对它前面的数字重建了堆。
	//这样一来只要再使用pop_back()把最后一位元素删除,就得到了新的堆。
	pop_heap(vec3.begin(), vec3.end());
	vec3.pop_back();
	for (const auto v : vec3)
		cout << v << " ";
	cout << endl;
}

三、STL中关于priority_queue的使用

  优先队列容器与队列一样,只能从队尾插入元素,从队首删除元素。但是它有一个特性,就是队列中最大的元素总是位于队首,所以出队时,并非按照先进先出的原则进行,而是将当前队列中最大的元素出队。这点类似于给队列里的元素进行了由大到小的顺序排序。元素的比较规则默认按元素值由大到小排序,可以重载“<”操作符来重新定义比较规则。

  优先队列具有队列的所有特性,包括基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。

头文件:

#include <queue>

基本操作:

empty():如果队列为空,则返回真
pop():删除对顶元素,删除第一个元素
push():加入一个元素
size():返回优先队列中拥有的元素个数
top():返回优先队列对顶元素,返回优先队列中有最高优先级的元素
在默认的优先队列中,优先级高的先出队。在默认的int型中先出队的为较大的数。

声明方式:

1、普通方法:

// 默认按照元素从大到小的顺序出队(最大堆)
priority_queue<int> q;
// 按照元素从小到大的顺序出队(最小堆)
priority_queue<int, vector<int>, greater<int>> q;

2、自定义优先级:

struct cmp {     
  bool operator ()(int x, int y){
     return x > y;
  }
};
// 定义方法,其中,第二个参数为容器类型。第三个参数为比较函数。
priority_queue<int, vector<int>, cmp> q; 

3、结构体声明方式:

struct node {     
  int x, y;     
  friend bool operator < (node a, node b)     {         
    return a.x > b.x;  // 结构体中,x小的优先级高     
  }
};
// 定义方法
priority_queue<node>q;

四、关于堆的总结

  像这样支持插入元素和寻找最大(小)值元素的数据结构称之为优先队列。如果使用普通队列来实现这个两个功能,那么寻找最大元素需要枚举整个队列,这样的时间复杂度比较高。如果已排序好的数组,那么插入一个元素则需要移动很多元素,时间复杂度依旧很高。而堆就是一种优先队列的实现,可以很好的解决这两种操作。

  堆的建立和增删改查:

  建立:从下往上调整。O(N)

  增:插入堆尾,从下往上冒泡。O(logN)

  删:删除堆顶元素时,堆顶元素和堆尾元素交换(同时删除堆顶元素),从上往下冒泡即可。O(logN)

  改:增+删。

  查:O(logN)

参考:https://www.i3geek.com/archives/682
http://www.cnblogs.com/chenweichu/articles/5710567.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~青萍之末~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值