一、堆
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