堆的概念
概念
堆是在概念上是一个完全二叉树,在具体是物理实现上是一个数组。完全二叉树有个一优美的性质:有关双亲结点和孩子结点,由层序遍历对应于数组标号直接按的关系。说人话就是:把完全二叉树的层序遍历的一个设置为1号(不是 0 开头),我们发现一个节点编号为的 i 的左孩子标号一定是 2i ,其右孩子标号一定是 2i+1。
利用这样的性质,我们可以用来实现优先队列。(用普通的树也是可以实现的,按照邓老师的说法就是:杀鸡用牛刀)
大顶堆和小顶堆
所谓大顶堆就是,堆中最大的元素是在根的位置,任意子树的所有节点都小于等于根节点。小顶堆也是同理。
堆的操作
上浮(向上调整)
不停的和双亲节点比较。(大顶堆)把比双亲大的孩子节点换到双亲节点的位置上。一直这样比较,直到到达堆顶。
这里解释一个疑惑: 比较是从下往上比较,我们找最后一个非叶子节点。此时这个非叶子节点的左右孩子一定只有一个节点。通过完全二叉树的性质我们可以知道:叶子节点有 n/2(向上取整)。所以数组下标[1, n/2(向下取整)]就都是非叶子节点。我们就可以倒着对这些非叶子节点做处理。
void UpAdjust(int low, int high)
{
int i = high, j = i / 2;
while(j >= low)
{
if(heap[j] < heap[i])
{
swap(heap[j], heap[i]);
i = j;
j = i / 2;
}
else
break;
}
}
下沉(向下调整)
下沉和上浮在另一视角下是相反的,下沉是节点和自己的左右孩子比较。(上浮是自己和自己的双亲节点比较)
同时两者的时间复杂都是O(log n)
void DownAdjust(int low, int high)
{
int i = low, j = i * 2;
while(j <= high)
{
if(j + 1 <= high && heap[j+1] > heap[j])
j = j + 1;
if(heap[j] > heap[i])
{
swap(heap[j], heap[i]);
i = j;
j = i * 2;
}
else
break;
}
}
具体来说,这个上浮和下沉操作具体的怎么使用呢?
对于堆来说,插入 和 删除操作都是从最后一个元素开始。当我们插入元素,元素需要上浮(新的元素可能不符合规则)。当我们删除元素,元素需要下沉(新的根节点可能不符合要求)
建堆
或许你会使用反复执行insert()来建堆,这样做没有问题,只是效率还不是很理想;
O(log1 + log2 + … + logn) = O(log n!) = O(nlogn)
这里介绍一种自底向上建堆的方式:Floyd算法
其实代码非常简单。
void heapify(int n)
{
for(int i = LastInternal(n); InHeap(n,i);i--)
UpAdjust(n,i);
}
其中 InHeap(n,i) 判断优先队列是否合法(其实就是i < n && i > -1);LastInternal(n)返回了n的祖父结点。
Code(C/C++)
#include <iostream>
#include <algorithm>
using namespace std;
const int max_size = 1000;
int heap[max_size] = {0, 2, 9, 7, 4, 1, 8, 3, 6, 5};
int n = 10;
void DownAdjust(int low, int high);
void CreateHeap();
void DeleteTop();
void UpAdujust(int low, int high);
void Insert(int x);
void HeapSort();
void Print();
int main()
{
CreateHeap();
// cout<<"main"<<endl;
Print();
Insert(11);
Insert(15);
Insert(19);
Insert(12);
HeapSort();
Print();
DeleteTop();
HeapSort();
Print();
return 0;
}
void Print()
{
for(int i = 0; i < n; i++)
cout<<heap[i]<<" ";
cout<<endl;
}
void DownAdjust(int low, int high)
{
int i = low, j = i * 2;
while(j <= high)
{
if(j + 1 <= high && heap[j+1] > heap[j])
j = j + 1;
if(heap[j] > heap[i])
{
swap(heap[j], heap[i]);
i = j;
j = i * 2;
}
else
break;
}
}
void CreateHeap()
{
for(int i = n/2; i >= 1; i--)
DownAdjust(i,n);
}
void DeleteTop()
{
heap[1] = heap[n--];
DownAdjust(1,n);
}
void UpAdjust(int low, int high)
{
int i = high, j = i / 2;
while(j >= low)
{
if(heap[j] < heap[i])
{
swap(heap[j], heap[i]);
i = j;
j = i / 2;
}
else
break;
}
}
void Insert(int x)
{
heap[++n] = x;
UpAdjust(1,n);
}
void HeapSort()
{
CreateHeap();
// cout<<"HeapSort"<<endl;
for(int i = n; i > 1; i--)
{
swap(heap[i], heap[1]);
// cout<<"HeapSortFor"<<endl;
DownAdjust(1, i-1);
}
}
优先队列
概念
优先队列,理解上来说是一种队列。但是相比于普通的队列,优先队列并不是按照先进先出的条件出队,而是根据某一种属性,即在队列存在一种排序机制,使得元素的属性可以按照我们规定的一种顺序所排列。这个属性我们把他称之为优先级(实际上优先级的具体概念是程序员根据需求所赋予的)
实现
优先队列的实现是通过堆来实现的,也可以使用树来实现(不过性能来说不如堆)
具体实现按照堆的方式排序即可。可以按照不同的思路适当增加自己需要的接口。
STL对于堆和优先队列的接口
STL——heap
概念
在STL中也封装的了堆这一个数据结构。但是这个heap并没有独立成为一个头文件,而是在<algprithm>中。其主要有以下四个方法 :
make_heap();
pop_heap();
push_heap();
sort_heap();
make_heap()
make_heap():有三个参数 (1)数组(向量)的起点,(2)数组(向量)的结束(3)选择大顶堆还是小顶堆
int arr[] = {1, 3, 2, 5 ,4};
vector<int> v(arr,arr+5);
make_heap(v.begin(), v.end(),less<int>());
//默认是大顶堆,所以less<int>()不写也是可以的。
//大顶堆是 greater<int>()
make_heap(arr, arr+5, greater<int>());
push_heap()
push_heap() : 这个方法可能并不是大家想的和vector的push_back一样的类型,而是先要在数组(向量)中把元素加入和,在用和make_heap()基本一样的方式调用
make_heap(v.begin(),v.end(),less<int>());
v.push_back(12);
push_heap(v.begin(),v.end(),less<int>());
pop_heap()
pop_heap():本身并不pop元素,仅仅是做了一个交换。然后针对前n-1个元素使用了make_heap()。所以要自己手动pop元素
make_heap(v.begin(), v.end(), less<int>());
pop_heap(v.begin(), v.end(), less<int>());
v.pop_back();
sort_heap()
sort_heap:对其进行堆排序,但是这样会使堆失去堆的性质
sort_heap(arr1.begin(), arr1.end(), greater<int>());
Print(arr1);
priority_queue
在STL中<queue>中已经有大神帮我们实现的优先队列。
操作接口
top : 访问队头元素
empty : 队列判空
size :返回元素个数
push:插入元素(插入的使用会自动排序)
emplace:构造一个元素并插入队列
pop : 弹出队头
swap : 交换内容
使用
#include <iostream>
#include <queue>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
priority_queue<int, vector<int>,greater<int> > Q;
priority_queue<int, vector<int>,less<int> > q;
int array[] = {2, 1, 5, 7, 9, 0, 2};
for(int i = 0; i < 7; i++)
Q.push(array[i]);
while(!Q.empty())
{
cout<<Q.top()<<" ";
Q.pop();
}
return 0;
}
其中 greater<int> 是从小到大,less<int> 是从大到小,如果有特别的需求可以自己写cmp比较函数。
关于cmp比较函数
这里的cmp函数只有一个推荐写法(如果你不打算用默认的话)。
struct node{
int value;
int age;
};
struct cmp{
bool operator()(const node &a, const node &b)const
{
return a.value < b.value;
}
} ;
int main()
{
priority_queue<node, vector<node>, cmp> Q;
/* ....... */
return 0;
}
还有一种方式是重载友元的 < 运算符,这个办法有一个问题:没办法写两个不同的cmp。因为重载会覆盖。