leetcode(七) 基本数据结构

1.两数之和(哈希表)

在这里插入图片描述

算法:
用哈希表unordered_map<int,int>存储元素下标

  • 用指针i遍历整个nums数组,对于每个nums[i],在哈希表中查找是否存在对应的target-nums[i],如果没有将i的下标存入哈希表
  • 时间复杂度是O(n)

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        int n=nums.size();

         unordered_map<int,int> hash;

         for(int i=0;i<n;i++){
             //因为数组中存在下标0,所以用count函数判断是否存在该哈希值
            if(hash.count(target-nums[i]))return {hash[target-nums[i]],i};

             hash[nums[i]]=i;
         }

         return {-1,-1};
    }
};

187.重复的DNA序列(哈希表)

在这里插入图片描述

算法

  • 遍历这个字符串,用哈希表存储每个长度为10的字符串的出现次数
  • 统计所有出现次数>=2 的字符串
class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
       unordered_map<string,int> hash;

       vector<string> res;

       for(int i=0;i+10<=s.size();i++){  //注意边界是i+10<=s.size()
           string c=s.substr(i,10);

           if(hash[c]==1)res.push_back(c);     //如果当前出现过一次,统计入res,不能写>=1,避免重复统计

           hash[c]++;
       }

       return res;
    }
};

706.设计哈希映射 **

在这里插入图片描述

  • 邻接表法
  • N取质数避免冲突
  • find操作应当返回一迭代器
  • 用数组链表模拟哈希表(邻接表法)Vector<list <pair<int,int>>>
class MyHashMap {
public:
     
     int N=10007;

     vector<list<pair<int,int>>> h;

    /** Initialize your data structure here. */
    MyHashMap() {
          h=vector<list<pair<int,int>>> (N);
    }

    list <pair<int,int>> ::iterator  find(int key){
        int t=key % N;

        for(auto it=h[t].begin();it!=h[t].end();it++)
            if(it->first==key) return it;
        
        return h[t].end();
    }
    
    /** value will always be non-negative. */
    void put(int key, int value) {
         auto it=find(key);

         int t= key % N;

         if(it==h[t].end())h[t].push_back({key,value}); //判断条件里不能写it->first 如果返回的it不存在就会报空指针错误

         else  it->second=value;
    }
    
    /** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
    int get(int key) {
         auto it=find(key);

         int t=key % N;

         if(it==h[t].end())return -1;

         else return  it->second;
    }
    
    /** Removes the mapping of the specified value key if this map contains a mapping for the key */
    void remove(int key) {
          auto it=find(key);

          int t= key % N;

          if(it!=h[t].end())h[t].erase(it);
    }
};

开放寻址法

  • 开放寻址法的基本思想是这样的:如果当前位置已经被占,则顺次查看下一个位置,直到找到一个空位置为止。
  • 对于每一个位置的状态初始化为-1,代表位置为空
  • 对于find操作,我们从哈希表的hash_key[key%N]处开始逐空查找,直到找到值为key或者位置为空(-1)为止
  • 对于remove操作,我们不能再将任意位置的值变回-1,而是赋予其他的值(本题为-2),如果赋予-1,那么就会把后面的值阻断,导致查询不到数据,这样就可能重复的插入key值

class MyHashMap {
public:
    const static int N=20011;     //必须初始化为静态变量,不然报错,不知道为啥
    
    int hash_key[N],hash_value[N];

    /** Initialize your data structure here. */
    MyHashMap() {
        memset(hash_key,-1,sizeof hash_key);   //初始化为-1
    }

    int find(int key){
        int t=key % N;

        while(hash_key[t]!=-1&&hash_key[t]!=key){
            if(++t==N)t=0;              //如果找到头,则重头开始
        }
        return t;
    }
    
    /** value will always be non-negative. */
    void put(int key, int value) {
         int t=find(key);

         hash_key[t]=key;
         
         hash_value[t]=value;
         
    }
    
    /** Returns the value to which the specified key is mapped, or -1 if this map contains no mapping for the key */
    int get(int key) {

        int t=find(key);

        if(hash_key[t]!=-1)return hash_value[t];

        else return -1;

    }
    
    /** Removes the mapping of the specified value key if this map contains a mapping for the key */
    void remove(int key) {
        int t=find(key);

        if(hash_key[t]!=-1)hash_key[t]=-2;
    }
};

652.寻找重复子树 ** (二次哈希)

在这里插入图片描述

  • 前序遍历的方式将二叉树转化成字符串,详见## Leetcode 297.二叉树的序列化和反序列化
    这样每个子树都具有唯一确定的字符串如2,4,'#',我们枚举(dfs)每一个节点,对于该节点可以构造出唯一的一个字符串,并将字符串加入Unordered_map<string ,int> 当拥有个数等于2 时,保存为一个结果
  • 每个结点仅遍历一次,unordered_map 单次操作的时间复杂度为 O(1),一共有n个节点,总复杂度为O(n)
  • 遍历结点中,可能要拷贝当前字符串到答案,拷贝(string 的相加)的时间复杂度为 O(n),故总时间复杂度为 O(n^2),相当于一个二重循环
//O(n^2)时间复杂度

class Solution {
public:
    unordered_map<string ,int>  hash; 
    
    vector<TreeNode* >res;

    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
          
          dfs(root);

          return res;
    }

    string dfs(TreeNode* root){
         if(!root)return "#";
         
         string cur;

         cur+=to_string(root->val)+',';

         cur+=dfs(root->left)+',';     //字符串的拷贝

         cur+=dfs(root->right);

         hash[cur]++;

         if(hash[cur]==2)res.push_back(root);   //个数等于2时,将该节点做根节点返回到res中

         return cur;
    }
};

优化算法

  • 由于每一次复制字符串时会占用额外的O(n)的空间,我们将每一个字符串先映射为一个整数
  • 例如"#“可以映射在哈希表unordered_map<string ,int > hash为1,而数字1在另一个哈希表unordered_map<int ,int> count映射为”#"出现的次数 ,这样每次返回一个整数对应的字符串,可以将string 的复制操作降为 O ( 1 ) O(1) O(1)
  • 实际的映射只需要将dfs出的字符串按数字顺序映射即可
class Solution {
public:
    unordered_map<string ,int>  hash;   //字符串映射为整数

    unordered_map<int,int> count;       //记录每个字符串的个数

    int cnt=0;
    
    vector<TreeNode* >res;

    vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
          hash["#"]=++cnt;          //空字符串先映射为1 
          
          dfs(root);

          return res;
    }

    string dfs(TreeNode* root){
         if(!root)return to_string(hash["#"]);       //root为空返回"#"对应的整数(转化成string)
         
         string cur;

         cur+=to_string(root->val)+','+dfs(root->left)+','+dfs(root->right); //每次相加最多只加一个字符,时间复杂度降为O(1)

         if(!hash.count(cur))hash[cur]=++cnt;   //如果当前字符串还没有出现过,先在hash中添加他的一个映射

         int t=hash[cur];

         count[t]++;                     //字符串个数++

         if(count[t]==2)res.push_back(root);  

         return to_string(hash[cur]);
    }
};


560.和为k的子数组(前缀和+哈希)

在这里插入图片描述

正确解法是用前缀和+哈希,只想到一半

  • 首先应想到统计数组中的前缀和…暴力想法是对于前缀和数组sum,我们枚举每个前缀和sum[i],
    那么如果存在答案的话,一定有j属于[0,i-1],使得sum[i]-sum[j]=k,
  • 换句话说只需要在[0,i-1]区间内找到满足条件的前缀和,且这些前缀和的值都等于sum[i]-k
  • 这样我们使用一个哈希表对前缀和的个数进行标记,每遍历一个sum[i]都在哈希表中找一下符合条件的答案即可
  • 注意这道题的写法,并没有额外的统计出前缀和,写法比较精巧
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
         unordered_map<int,int>  hash;

         int res=0;

         hash[0]=1;        //前缀和为0的数据有一个

         for(int i=0,sum=0;i<nums.size();i++){          //对于每个前缀和,求出相应的hash[sum-k]个数,顺便统计前缀和
             sum+=nums[i];

             res+=hash[sum-k];
            
             hash[sum]++;
        }

         return res;
    }
};


547.朋友圈(并查集)

在这里插入图片描述

此题考察并查集的基本操作

  • 基础的并查集能解决的一类问题是不断将两个元素所在集合合并,并随时询问两个元素是否在同一集合
  • 定义数组p[i]表示 i元素所在集合的根结点。初始时,所有元素所在集合的根结点就是自身。
  • 合并时,直接将两个集合的根结点合并,即修改 p 数组。
  • 查询时,不断通过判断 i 是否等于 f(i) 的操作,若不相等则递归判断 f(f(i)),直到 i == f(i) 为止。
    但以上做法会在一条链的情况下单次查询的时间复杂度退化至线性,故可以采用路径压缩优化,将复杂度降到近似常数。
  • 对于此题,每合并一个区域就减少一组关系(res),遍历完数组输出即可
class Solution {
public:
     vector<int> p;

     int find(int x){
         if(p[x]!=x)p[x]=find(p[x]);

         return p[x];
     }
   
    int findCircleNum(vector<vector<int>>& M) {
        int n=M.size();

        for(int i=0;i<n;i++)p.push_back(i);

        int res=n;

        for(int i=0;i<n;i++)
           for(int j=0;j<i;j++){
               if(M[i][j]==1&&find(i)!=find(j)){    //ij是朋友关系,如果不在一个连通块内,则合并该连通块,并将res-1

                    p[find(i)]=find(j); 

                    res--;
               }
           }

        return res;
    }
};

684.冗余连接(并查集)

在这里插入图片描述

同样考察并查集的基本操作
首先题目给了一个具有N个节点的树,那么一共会连接N-1条边,此时该树多连接了一条边就会构成一个环,输出多余的这条边即可
找到这条边比较简单,即如果两条边不在一个连通块内,我们将他们合并,如果发现已经联通,那么说明要添加的那一条边就是多余的边,输出他即可

class Solution {
public:
    vector<int> p;

    int find(int x){
        if(p[x]!=x)p[x]=find(p[x]);

        return p[x];
    }

    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
         int n=edges.size();

         p=vector<int> (n+1);
         
         for(int i=1;i<=n;i++)p[i]=i;        //下标从1 开始

         for(int i=0;i<n;i++){
             int u=edges[i][0],v=edges[i][1];

             if(find(u)!=find(v))p[find(u)]=find(v);     //u,v没有被连接,将它们联通

             else return {u,v};                         //如果u,v已经被联通,说明他们是多余的边
         } 

         return {-1,-1};
    }
};

692.前K个高频单词(堆)

在这里插入图片描述

  • 本题使用c++STL库函数priority_queue(优先队列/大根堆)来求解,考察优先队列的基本操作
  • top()返回堆顶元素O(1)
  • pop()弹出堆顶元素O(1)
  • push()加入元素并排序 O(logn)
  • 算法
  • 首先使用哈希表对全部单词出现次数进行统计,随后将他们加入堆,(因为默认是大根堆,所以我们将元素数值取负数,让最小的数排在堆顶)并且维持堆中元素为k个,当第堆中元素大于k的时候让堆顶元素弹出即可
class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
        typedef pair<int,string> PIS; 

        priority_queue<PIS> heap;

        unordered_map<string,int> hash; 
        
        for(auto w:words)hash[w]++;

        for(auto it:hash){
            PIS t(-it.second,it.first);

            heap.push(t);

            if(heap.size()>k)heap.pop();

        }

        vector<string> res(k);

        for(int i=k-1; i>=0; i--){      //由于取负数排列,正序输出为从小到大,我们反序输出即可

             res[i]=heap.top().second;

             heap.pop();
        }

        return res;
    }
};

295.数据流的中位数(对顶堆)

在这里插入图片描述

  • 维护动态有序序列需要使用平衡树,这里我们直接用两个二叉堆实现
  • 如图所示,我们使用一个大根堆,一个小根堆,大根堆的所有元素要小于小根堆的所有元素
  • 在动态的维护过程中,我们始终保证小根堆的元素大于等于大根堆的元素(最多多一个元素)那么当维护过程结束时,如果共有偶数个数,中位数就是两个堆的顶点,如果是奇数个数,中位数就是小根堆的顶点
  • 在插入过程中,始终应当满足
  • 如果x大于大根堆顶点,就将其插入小根堆,否则将其插入大根堆
  • 保证在维护过程中上面堆至少要大于等于下面堆的元素个数,所以每次插入元素之后,都挪动一个元素到小根堆,最后判断如果大根堆的元素过少时,再向下移动元素到大根堆
  • 向下挪动一个元素相当于小根堆少了一个而大根堆多了一个元素,如果元素个数是偶数的话,不可能出现个数不平衡的情况

在这里插入图片描述

class MedianFinder {
public:
    priority_queue<int,vector<int>,greater<int> > up;    //初始化为小根堆

    priority_queue<int > down;   //大根堆
    /** initialize your data structure here. */
    MedianFinder() {
      
    }
    
    void addNum(int num) {
         if(down.empty()||num>=down.top())up.push(num);    //大根堆为空或者插入元素大于大根堆顶,将元素插入到小根堆
 
         else{
             down.push(num);                          //否则将元素插入到大根堆,并且默认将大根堆元素向上转移一个

             up.push(down.top());

             down.pop();
         }

         if(up.size()>down.size()+1){           //如果上面小根堆元素过多,再将其转移到大根堆
             down.push(up.top());

             up.pop();
         }
    }
    
    double findMedian() {
         if(down.size()+up.size()&1)return up.top();    //奇数返回小根堆堆顶,偶数返回两个堆顶的平均数

         else return (down.top()+up.top())/2.0;
    }
};


352.将数据流变为多个不相交的区间

在这里插入图片描述

  • map维护所有区间,为此我们需要map<int,int>L可以从右端点索引到左端点map<int,int>R可以从左端点索引到右端点
  • 注意用unordered_map是不行的,因为要调用lower_bound函数找到大于等于x的第一个位置,所以无序的哈希表是满足使用要求的
  • 由此,当我们插入一个区间[x,x]的时候,我们考察x左右两边距离为1的位置,应该分成四种情况
  1. x的左右两边都存在,将三个区间合并
  2. x的左边区间存在,将左区间合并
  3. x的右边区间存在,将右区间合并
  4. x的左右两边都没有区间,将[x,x]插入

在这里插入图片描述

class SummaryRanges {
public:
    map<int,int> L,R; 
    /** Initialize your data structure here. */
    SummaryRanges() {

    }
    
    void addNum(int x) {
        if(L.size()){
            auto t=L.lower_bound(x);        //返回L中第一个大于等于x的迭代器

            if(t!=L.end()&&t->second<=x)return ;       //如果x处于已有区间内,直接返回,此处只能用t->second,不能用t.second
        }

        int left=L.count(x-1),right=R.count(x+1);    //找到x的左右两边是否有区间存在,分四种情况讨论

        if(left&&right){                         //左右都存在,合并三个区间
            R[L[x-1]]=R[x+1];
             
            L[R[x+1]]=L[x-1];

            L.erase(x-1),R.erase(x+1);          //结束后要分别删除L,R中原本对应的边界
        }
        else if(left){                         //合并左边区间
            R[L[x-1]]=x;

            L[x]=L[x-1];

            L.erase(x-1);
        }

        else if(right){                      //合并右边区间
            L[R[x+1]]=x;

            R[x]=R[x+1];

            R.erase(x+1);
        }
        else{                             //直接插入[x,x]
            L[x]=x;

            R[x]=x;
        }
    }
    
    vector<vector<int>> getIntervals() {
          vector<vector<int>>res;

          for(auto it:R)res.push_back({it.first,it.second});

          return res;
    }
};



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值