- 堆排序
- 用二叉堆实现优先队列
- treap
堆是一种数据结构。数据+关系
在具体实现中用一个数组来表示,
见图:
满足的关系为 父节点的键值大于等于他的两个子节点及其子树的最大键值(最大堆)或父节点小于等于他的两个子节点及其子树的最大键值(最小堆)
这个关系可以递归刻画。
建堆的过程
涉及的函数
1MAX-HEAPIFY :维护堆性质,时间复杂度O(logn)
我们可以理解为是下滤,即不断用该初始节点,递归的与他的子节点和子树进行比较,维护堆性质。
两种写法,递归或者递推都可以的
2 BUILD-MAX-HEAP,建堆函数,是O(n)的时间复杂度
这个建堆的过程是用下滤的方法,算法导论上证明接近线性,若用上浮,则为nlogn
具体可以看下面的代码
3 HEAPSORT 进行排序的函数,需调用上面两位函数
过程(以最大堆为例):
首先输入数组后,对每个非叶子节点进行MAX-HEAPLFY,O(n)
(建完堆后堆顶肯定是最大的元素)
然后把堆顶元素(a[0])和堆末互换,并在堆根节点进行MAX-HEAPLFY操作,再取堆顶。如此处理n-1次,即排序完毕。
全是文字,,下面这个大佬写的也很好,图画也很多qwq
https://www.cnblogs.com/0zcl/p/6737944.html
代码如下
#include <iostream>
#include <cmath>
#include <cstdio>
using namespace std;
/* 可以证明的是,堆排序的建堆过程是O(n)的(算法导论上有)。
然后我们每次执行MAX-HEAP操作,时间复杂度是logn
总共要执行n次
*/
const int maxn=1e5+7;
void maxheapify(int *a,int root,int siz){
int ori=root;
int lef=root*2+1;
int rig=root*2+2;
int aim=lef;
if(a[lef]<a[rig]&&lef<siz&&rig<siz) {
aim=rig;
}
if(a[aim]>a[root]&&aim<siz) {
swap(a[aim],a[root]);
root=aim;
}
if(root!=ori)
maxheapify(a,root,siz);
}
void heapsort(int *a,int siz){
int sizz=floor(siz/2);
for(int i=sizz;i>=0;i--){
maxheapify(a,i,siz);//对非叶子节点进行下滤处理
}//建堆的过程
for(int i=siz-1;i>=1;i--){
swap(a[i],a[0]);
maxheapify(a,0,i);
}
}
int main()
{ int num;
int a[maxn];
num=10;
while(~scanf("%d",&a[0])){
for(int i=1;i<num;i++){
scanf("%d",&a[i]);
}
heapsort(a,num);
//输出堆排序的结果
cout<<a[0];
for(int i=1;i<num;i++){
printf(" %d",a[i]);
}
cout<<endl;
}
return 0;
}
后来我又用上浮建堆的方法写了一下,但是这样建堆的时间复杂度是O(nlogn)
为什么呢
当上浮建堆的时候,只有红色根节点不用处理,但是下滤的时候,更多的绿色叶节点不用处理,所以肯定是下滤更快啦(单个处理的平均复杂度是logn,我也是大概的证明,具体的证明见算导第三版p88)
上浮建堆见下代码
#include <iostream>
#include <cstdio>
using namespace std;
/*
上滤建堆就是:
对每个节点都进行上滤操作,即判断当前节点的父节点,和当前节点是否满足堆的性质,
否则的话递归处理(也可以用for循环,这个是我看的别人的代码)
为什么万能的算法导论不用下滤呢,
因为下滤对每个节点进行处理,建堆的时间复杂度必然是nlogn,但是如果我们使用下滤建堆的话,
时间复杂度为n
这个东西是如此的好理解。
当上滤的话,只有一个节点不用处理,那个节点就是顶点。
而当下滤的话,所有叶子节点都不必处理,()
*/
void swap(int *v, int i, int j)
{
int tmp = v[i];
v[i] = v[j];
v[j] = tmp;
}
void siftup(int *v, int n)
{
int c;
for(int i = n-1; i>0 && v[i] > v[c=(i-1)/2]; i = c)
swap(v, i, c);
}
void siftdown(int *v, int n)
{
int c;
for(int i = 0; (c = 2*i+1) <= n-1; i = c)
{
if(c+1 <= n-1 && v[c] < v[c+1])
++c;
if(v[i] >= v[c])
break;
swap(v, i, c);
}
}
//堆排序
void heapsort(int *v, int n)
{
int i;
for(i = 0; i <= n-1; ++i)
siftup(v, i+1);
/*for(i = 0; i < n-1; i++)
cout << v[i] << " ";
cout << endl;
*/
for(i = n-1; i > 0; --i)
{
swap(v, 0, i);
siftdown(v, i);
}
cout<<v[0];
for(int i=1;i<10;i++){
cout<<" "<<v[i];
}
cout<<endl;
}
int main()
{ int a[11];
while(~scanf("%d",&a[0])){
for(int i=1;i<=9;i++)
scanf("%d",&a[i]);
heapsort(a,10);
}
return 0;
}
贴错了,那是别人写的,,下面才是我的。
#include <iostream>
#include <cmath>
#include <cstdio>
using namespace std;
/* 可以证明的是,堆排序的建堆过程是O(n)的(算法导论上有)。
然后我们每次执行MAX-HEAP操作,时间复杂度是logn
总共要执行n次
*/
const int maxn=1e5+7;
void maxheapify(int *a,int root,int siz){
int ori=root;
int lef=root*2+1;
int rig=root*2+2;
int aim=lef;
if(a[lef]<a[rig]&&lef<siz&&rig<siz) {
aim=rig;
}
if(a[aim]>a[root]&&aim<siz) {
swap(a[aim],a[root]);
root=aim;
}
if(root!=ori)
maxheapify(a,root,siz);
}
void sf(int *a,int root,int siz){
//for(int i = siz-1; a[i] > a[(i-1)/2] && i >=0; i /= 2)
//swap(a[i], a[i/2]);
int c;
for(int i = siz-1; i>0 && a[i] >= a[c=(i-1)/2]; i = c)
swap( a[i], a[c]);
}
void heapsort(int *a,int siz){
int sizz=floor(siz/2);
for(int i=1;i<=siz;i++)
sf(a,0,i);
/*for(int i=0;i<siz;i++)
cout<<a[i]<<" ";
cout<<endl;*/
//}//建堆的过程
for(int i=siz-1;i>=1;i--){
swap(a[i],a[0]);
maxheapify(a,0,i);
}
}
int main()
{ int num;
int a[maxn];
num=10;
while(~scanf("%d",&a[0])){
for(int i=1;i<num;i++){
scanf("%d",&a[i]);
}
heapsort(a,num);
//输出堆排序的结果
cout<<a[0];
for(int i=1;i<num;i++){
printf(" %d",a[i]);
}
cout<<endl;
}
return 0;
}
我们的下浮操作是不一样的,我是看的算导写的递归qwq
over
二叉堆实现优先队列
优先队列有多种实现方式,这个是用二叉堆实现的。左偏树,斐波那契堆,也是可以的。我以后会学习的
优先队列API:
push压入一个数,并维护堆结构
把压入的元素放到堆末尾,然后上浮(算导上先将其键值改成inf,然后又写了更改键值的函数我认为没啥用)
pop 弹出顶元素,并维护堆结构
把顶元素取到后,形成一个空洞(自己脑补把),空洞下滤过去(就是往下根据堆性质传递的意思)
top 返回顶元素,并不删除它,只是拿来用一下,这个直接返回就行
empty 判空
#include <bits/stdc++.h>
using namespace std;
/* 接下来我们就可以处理一个 用二叉堆实现的优先队列了 。
我昨天想写左偏树来着,但是昨天玩了一晚上就没有写。。。
**************************
下面简要写一下二叉堆实现优先队列的操作
*/
const int maxn=1e5;
class heappq{
private:
int a[2000];
int siz;
public:
heappq(){
siz=0;
};
//上浮和下滤都是借鉴的别人的写法,真的很好
void shiftup(int zb){
int c;
for(int i=zb;i>0&&a[c=(i-1)/2]>=a[i];){
swap(a[c],a[i]);
i=c;
}
}
void shiftdown(int zb){
for(int i=zb;i<siz;){
int nows_left=i*2+1;
int nows_right=i*2+2;
if(nows_left>=siz) break;
int loc=nows_left;
if(a[nows_right]<a[nows_left]&&nows_right<=siz-1){
loc++;
}
if(a[i]<a[loc]) break;
swap(a[loc],a[i]);
i=loc;
// cout<<i<<endl;
}
}
void push(int num){
a[siz]=num;
siz++;
shiftup(siz-1);
};
int top(){
if(!siz){
puts("the heap is null");
return -1;
}
return a[0];
};
void pop(){
swap(a[0],a[siz-1]);
/*for(int i=0;i<siz;i++){
cout<<a[i]<<" ";
}
cout<<endl;*/
siz--;
shiftdown(0);
};
bool empty(){
if(!siz) return true;
return false;
};
};
int main()
{ heappq qq;
for(int i=30;i>=1;i--)
qq.push(i);
if(!qq.empty())
puts("YES");
//qq.pop();
//cout<<qq.top()<<endl;
while(!qq.empty()){
cout<<qq.top()<<endl;
qq.pop();
}
return 0;
}
treap
treap就是树堆,因为二叉排序树退化成链实在是太不好了,所以有人想到可以在二叉排序树里在维护一个优先级,这个优先级满足堆的性质,并且通过不断的左旋和右旋来避免两个子树的长度过大。treap=heap+tree
但是treap有一个缺点,那就是优先级是随机生成的,所以有时候两个数的差别可能大于1,也就是不是那么平衡,多少感觉不太稳,但是代码量小啊,
并且因为stl里对于set的封装过度了,所以有时候可以用treap实现一些平衡树。
treap的左旋和右旋。
右旋的意思是 当当前节点 位于父节点左部,进行右旋该节点的右子树会变成父节点的左子树
左旋反着来
具体见代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <bits/stdc++.h>
using namespace std;
/* 最小堆实现优先队列有没有写,
这个写的是 treap。
treap是用二叉排序树和堆改良的 bst
内部有两种数据,一种是优先级,满足堆的结构,
另一种是数据,满足二叉树的结构,
treap只是接近于二叉排序树,但是可能又不等于二叉排序树。
*/
/* 往右边插入不用右旋,
因为treap的插入和二叉树的插入是对应的。
在维护heap的性质的情况下还得维护bst的性质。
*/
typedef int datetype;
struct node{
int key;
int priority;
node * left;
node * right;
};
class treap{
public:
void insert(node *&root,int key,int priority){
if(root==NULL){
root=(node*)new node;
root->left=NULL;
root->right=NULL;
root->priority=priority;
root->key=key;
}
else if(root->key>key){
//左枝子
insert(root->left,key,priority);
if(root->priority>priority){
rotate_right(root);
}
}
else if(root->key<key){
//右枝子
insert(root->right,key,priority);
if(root->priority<priority)
rotate_left(root);
}
}
void rotate_left(node *& nows){
node *temp=nows->right;
nows->right=temp->left;
temp->left=nows;
nows=temp;
}
void rotate_right(node *& nows){
node *temp=nows->left;
nows->left=temp->right;
temp->right=nows;
nows=temp;
}
void erase(node *&root,int key){
//删除要注意的一点就是在删除的时候避免退化。
//及时用左旋和右旋来避免这种情况。
if(root->key>key){
erase(root->left,key);
}
else if(root->key<key){
erase(root->right,key);
}
else{
if(root->left==NULL){
root=root->right;
//这种情况还包含了左右子节点
//均为null的情况
}
else if(root->right==NULL){
root=root->left;
}
else{
if(root->left->priority<root->right->priority){
rotate_right(root);
erase(root->right,key);
}
else{
rotate_left(root);
erase(root->left,key);
}
}
}
}
void inorder_middle(node *&x){
if(x==NULL)return;
inorder_middle(x->left);
printf("%d ",x->key);
inorder_middle(x->right);
}
};
int main()
{ treap x;
node *s;
s=NULL;
for(int i=0;i<10;i++){
int nums=rand();
x.insert(s,i,nums);
}
x.inorder_middle(s);
x.erase(s,3);
x.inorder_middle(s);
return 0;
}
大佬可能会发现,虽然叫堆与treap,但是treap我也只是写了一个小小的模板,重点都在堆,因为我写的时候根本没想到堆也会这样复杂。。所以耽误了进度,还有就是一玩手机就放不下了。。。
treap可以可持久化,还有实现名次树,并且平衡树总的来说我是不太会的。。改天再学习吧qwq