【数据结构】堆和集合笔记

  1. 自己写一个堆

首先,明确一下,为什么需要堆?

=>考虑插入,删除,查找的效率。

数组,查找,最快是二分查找O(lgN)。但查找完如果要做什么操作,比如删除,就要挪动元素了。所以合起来效率是O(lgN)+O(N)=O(N)

二叉树,看起来是O(lgN),但之前写树的时候有说过,链表是不是树?是树的退化形态,每个结点都有小于等于一个的儿子。这个时候查找的效率是O(lgN)了。之前说,查找之后万一要做什么操作,树就可能不是完全二叉树,即查找效率为O(lgN)。

能不能试图用平衡二叉树?不能,rotate非常麻烦。

=>所以尝试保持一颗完全二叉树=>给这棵树起名堆。

接下来考虑需要用什么基础的数据结构存储。

需要指针吗?

完全二叉树是每一个结点要么没有儿子,要么有两个儿子,在堆里只有最后一个有儿子的父节点可以只有左儿子。所以完全可以用数组表示。

假如链表下标从1开始,2和3是它的子节点,2/2=1,3/2=1,父节点访问也很方便。

  1. 初始化

表示这棵树需要几个数据:总容量,现在有多少元素,以及存放元素的数组。

初始化需要提供总容量。

  1. 插入元素

为确保是一个完全二叉树,插在最后。

堆需要确保一件事情,小元素在上,大元素在下。所以需要向上进行一次数据交换,寻找插入值的最终位置。

注意,这里说的是寻找,不需要真的交换,只需要挪动不符合要求的元素,找到插入值的最终位置赋值即可。

  1. 删除元素

删除一定是删最小的。其余和插入一样。

为确保是一个完全二叉树,将最后一个元素和被删除的第一个元素交换,然后向下寻找最终位置。

4. 一个数组的插入

为了保证堆的性质,插入数组后需要排序。

思考一下,哪些需要排序?

如果向下调整位置,则叶子结点不需要轮。如果向上调整位置,则根节点不需要轮。

效率为重,叶子结点最多,如果向上调整,则叶子结点需要轮的距离最远。而事实上,叶子结点又占了树结点的很大一部分。

所以我们选择向下调整。

完整代码(包括测试)

#include<iostream>
using namespace std;
class h{
private:
    int *nums;
    int capacity;
    int l;
public:
    h(){
        capacity=0;
    }
    void init(int c=1){
        capacity=c;
        l=0;
        nums=new int [c+1];
    }
    void printh(){
        for(int i=1;i<=l;i++){
            cout<<nums[i]<<" ";
        }cout<<endl;
    }
    int isfull(){
        if(capacity==l){
            return 1;
        }
        return 0;
    }
    void moveup(int k){
        int tempnum=nums[k];
        int i=k;
        for(i;tempnum<nums[i/2]&&i>1;i/=2){
            nums[i]=nums[i/2];
        }
        nums[i]=tempnum;
    }
    int insert(int n){
        if(isfull()){return 0;}
        nums[l+1]=n;
        l++;
        moveup(l);
        return 1;
    }
    int isempty(){
        if(l==0){
            return 1;
        }
        return 0;
    }
    void movedown(int k){
        int tempnum=nums[k];
        int i=k;
        while(i*2<=l){
            int child=i*2;
            if(child<l){
                if(nums[child]>nums[child+1]){
                    child++;
                }
            }
            if(nums[child]<tempnum){
                nums[i]=nums[child];
                i=child;
            }
            else{
                nums[i]=tempnum;
                return ;
            }
        }
        nums[i]=tempnum;
        return ;
    }
    int remove(){
        if(isempty()){return 0;}
        nums[1]=nums[l];
        l--;
        movedown(1);
        return 1;
    }
    void buildheap(int *a,int len,int c=0){
        if(len>c){c=len;}
        init(c);
        l=len;
        for(int i=0;i<len;i++){
            nums[i+1]=a[i];
        }
        for(int i=len/2;i>=1;i--){
            movedown(i);
        }
    }
};
int main(){
    int a[6]={10,50,60,5,30,20};
    h h1;
    h1.buildheap(a,6);
    h1.printh();
}

2. c++的堆

堆在queue中,叫priority_queue,默认是大顶堆,即树根是最大的元素,可以执行一下验证。

所以插入是push,查看堆顶元素是top(),弹出堆顶是pop()。

#include<iostream>
#include<queue>
using namespace std;
int main(){
    priority_queue<int>q1;
    int a[6]={111,222,333,11,22};
    for(int i=0;i<5;i++){
        q1.push(a[i]);
    }
    cout<<q1.top()<<endl;
    q1.pop();
    cout<<q1.top()<<endl;
}

堆额外有一种方法让其变为小顶堆,即提供一个容器,前提是这个容器支持从小到大排序,比如vector。

可以借助以下程序验证。

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
int main(){
    priority_queue<int,vector<int>,greater<int> >q1;
    int a[5]={111,222,333,11,22};
    for(int i=0;i<5;i++){
        q1.push(a[i]);
    }
    for(int i=0;i<5;i++){
        cout<<q1.top()<<endl;
        q1.pop();
    }
}

3. c++的集合

集合就是set嘛,之前刷题用了好多次了。

注意三点:

  1. set默认从小到大排序(因为底层实现是红黑树,类似AVL树)

  1. set.insert()也可以插入集合,方法详见下方实验代码

  1. 对于力扣中要求返回vector但你用set做了,只要返回{set.begin(), set.end()}即可

可以用以下代码验证set

#include<iostream>
#include<set>
using namespace std;
int main(){
    set<int>s1;
    int a1[3]={333,222,111};
    for(int i=0;i<3;i++){
        s1.insert(a1[i]);
    }
    for(auto x:s1){
        cout<<x<<" ";
    }cout<<endl;
    set<int>s2;
    s2.insert(666);
    s2.insert(555);
    s1.insert(s2.begin(),s2.end());
    for(auto x:s1){
        cout<<x<<" ";
    }cout<<endl;
}

4. 不相交集

设想情景:使用者想按照某种规则将集合中的元素分类,比如集合{a,b,c,d,e},按规则得到,{a,b}{c,d}{e}三类。我们需要实现:分类,查找。

  1. 初始化

思考需要存储那些内容?

查找一个子集,只要抓住其中一个元素其实就抓住了整个集合。所以子集里存在一个父亲元素,其它元素都指向这个父亲元素(的下标)。(可不可以用指针?也可以哎,但还是下标直观)最初的子集每个元素都是一个集合里的父亲元素。

另外用户查找的时候查找的肯定是数值,所以需要一个数组存储数值。

需要知道能放多少个数以及已经放了多少个数。

暂且先这样。

2. 插入元素

就插入嘛。假设集合的父亲元素标记为0,就是没有父亲,自己就是父亲,所以每插入一个元素,它对应的parent暂且设置为0。另外0号位置不存数据。

3. 查找元素

这里肯定不是简单找一个元素在哪里,而是找这个元素属于哪个集合,找这个集合的父亲元素。

4. 合并子集

需要合并,就把一个子集的父亲元素改为指向另一个子集的父亲元素就好了。

所以查找的时候要注意,不一定找到该元素就能立刻得到它的父亲元素,可能要一层层回溯。

所以,或许可以改进一下?不要那么多层回溯,至多两层就能找到父亲?

5. 改进
  1. 查找

在查找的时候,如果不能一次找到集合的父亲,能不能将当前元素的爷爷当成当前元素的父亲?

b. 合并

在合并的时候,可不可以大树上拴小树?判断大树还是小树,就要计算每个集合的层数,所以不妨把子集的父亲元素改为负数,负号代表是父亲元素,绝对值代表树的高度(感觉做了查找改进,这一步有一、、多余)

ok,放个改进后的代码。

#include<iostream>
#include<map>
using namespace std;
struct sortset{
    int *parent;
    int *data;
    map<int,int>m;
    int capacity;
    int size;
    sortset(int c=10){
        capacity=c;
        size=0;
        parent=new int[c+1];
        data=new int[c+1];
    }
    bool insert(int n){
        if(capacity==size){return false;}
        size++;
        data[size]=n;
        parent[size]=-1;
        m[n]=size;
        return true;
    }
    int find(int n){
        map<int,int>::iterator i=m.find(n);
        if(i==m.end()){return 0;}
        int templace=i->second;
        int cont=0;
        while(parent[templace]>0){
            int tempnum=templace;
            templace=parent[templace];
            parent[tempnum]=parent[templace];
            cont++;
        }
        parent[templace]+=cont;
        return templace;
    }
    void setunion(int a,int b){
        int ra=find(a);
        int rb=find(b);
        if(ra==0 or rb==0){//元素不存在于集合中
            return ;
        }
        if(ra==rb){//同父,不操作
            return ;
        }
        if(parent[ra]>parent[rb]){//a是小树,把a拴b上
            int tempnum=parent[ra];
            parent[ra]=rb;
            parent[rb]+=tempnum;
        }
        else{//b是小树
            int tempnum=parent[rb];
            parent[rb]=ra;
            parent[ra]+=tempnum;
        }
        
    }
    void print(){
        for(int i=1;i<=size;i++){
            cout<<i<<" ";
        }cout<<endl;
        for(int i=1;i<=size;i++){
            cout<<parent[i]<<" ";
        }cout<<endl;
        for(int i=1;i<=size;i++){
            cout<<data[i]<<" ";
        }cout<<endl;
    }
};
int main(){
    sortset s;
    s.insert(11);
    s.insert(22);
    s.insert(66);
    s.insert(-5);
    s.insert(123);
    s.setunion(11,66);
    s.setunion(22,11);
    s.setunion(11,-5);
    s.print();
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值