堆与treap

  1. 堆排序
  2. 用二叉堆实现优先队列
  3. 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值