算法题之哈希表

1、字母异位分组

题目

在这里插入图片描述

分析

本题可以维护一个map,保存字符串和在输出的vector中的下标,难点在于怎么去确认字符是否在这一数组中。
关键在于存在map中的string,这里我们存在里面的应该排序后的字符串,然后把传入的每个字符串先保留一个副本,然后排序了后在map中count。

复杂度

时间复杂度:O(n)
空间复杂度:O(n)

2、无重复字符的最长子串

在这里插入图片描述

分析

这题是很标准的hash题,关键在于这里的 set 要以字符为键值。
维护一个指针 left 指向不重复子串的开始位置,当 right 移动到出现重复的字符处时就更新 left 的位置和 set 中的记录,然后用 ret 记录无重复子串长度,也即当前位置减 left 然后加1。如下所示:

    int lengthOfLongestSubstring(string s) {
        int n=s.size();
        if(n==0) return 0;
        unordered_set<char> us;
        int left=0,right=0;
        int ret=0;
        while(right<n){
            if(us.count(s[right])){
                while(s[left]!=s[right]){
                    us.erase(s[left++]);
                }
                //因为这里s[left]==s[right],所以left应当再加1
                ++left;
            }
            else{
                us.insert(s[right]);
            }
            ret=max(ret,right-left+1);
            ++right;
        }
        return ret;
    }

复杂度

时间复杂度:O(n)
空间复杂度:O(n)

3、重复的DNA序列(187)

题目

在这里插入图片描述

分析

这里先记录下string的substr函数,他可以从指定位置截取指定长度的子字符串,如:

string a=s.substr(i,10);

a就是s从i位置开始(包括他自己)截取长度为10的字符串。
这里维护两个hash_set,一个记录每个子字符串,一个记录重复的字符串,然后从头开始遍历即可。
注:记录重复的字符串的hash_set是很有必要的,因为如果遇到13个a,不进行查重的话,结果会输出4个10个a的字符串。

复杂度

时间复杂度:O(n)
空间复杂度:O(n)

4、最小子覆盖串&找到字符串中的所有异位词

题目

在这里插入图片描述
在这里插入图片描述

分析

下面这个老哥针对滑动窗口相关的题,提出了如下的解法,写的相当好
https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/
他为这个类型的题写了一个模板如下:

/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
    unordered_map<char, int> need, window;
    for (char c : t) need[c]++;
    
    int left = 0, right = 0;
    int valid = 0; 
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        // 右移窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            // 左移窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

以438题为例,代码如下:

	vector<int> findAnagrams(string s, string p) {
		int n = s.size();
		if(n==0||p.empty()) return {};
		unordered_map<char, int> need, window;
		for(int i=0;i<p.size();++i) ++need[p[i]];

		int left = 0, right = 0, vaild = 0;
		vector<int> out;
		while (right < n) {
			char c = s[right];
			++right;
			if (need.count(c)) {
				++window[c];
				if (window[c] == need[c]) ++vaild;
			}

			if (right - left == p.size()) {
				char d = s[left];
				if (vaild == need.size()) out.push_back(left);
                ++left;
				if (window.count(d)) {
					if (window[d] == need[d]) --vaild;
					--window[d];
				}
			}
		}
		return out;
	}

复杂度

时间复杂度:O(n)
空间复杂度:O(n)

5、LRU缓存机制

题目

在这里插入图片描述

分析

这题算是道人气题目了,与其说是做题,倒不如说是在hash_map的基础上创造出一个新的数据结构。这里结合了hash_map的快速查找性和list的有序性。
在map中以key为键值,实值是list的迭代器,然后在list中存放一个由key和value组成的pair,如下图:
在这里插入图片描述
hashmap应当以int和list的迭代器作为值建立,如下:

unordered_map<int, list<pair<int, int>>::iterator> m;

注意:

(1)因为链表是前闭后开的,即end指向最后一个节点的下一位,即end指向最后一个节点的下一个节点。所以插入应该放在开头,因为在插入后要将迭代器存入map中,从首部插入,可直接返回begin()函数。取出操作则在结尾执行。
(2)因为删除时需要从链表尾部的节点定位到map,所以list中应该保存有key值。

复杂度

时间复杂度:O(1)
空间复杂度:O(n)

6、数组中重复的数字&缺失的第一个正数

分析

在这里插入图片描述
在这里插入图片描述

分析

这两题都使用了原地哈希的方法,即把数组视为哈希表。
1)由于数组元素的值都在指定的范围内,这个范围恰恰好与数组的下标可以一一对应。
2)看到数值,就可以知道它应该放在什么位置,这里数字nums[i] 应该放在下标为 i 的位置上,这就像是我们人为编写了哈希函数。
3)找到重复的数就是发生了哈希冲突。

数组中重复的数字
代码如下:

    int findRepeatNumber(vector<int>& nums) {
        int temp;
        for(int i=0;i<nums.size();i++){
            while (nums[i]!=i){
                if(nums[i]==nums[nums[i]]){
                    return nums[i];
                }
                swap(nums[i],nums[nums[i]]);
            }
        }
        return -1;
    }

缺失的第一个正数
这里的思路是把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 1个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
代码如下:

    int firstMissingPositive(vector<int>& nums) {
        int n=nums.size();
        for(int i=0;i<n;++i){
            while(nums[i]!=i+1){
                //下面三种情况都表示nums[i]是无效数,其中nums[i]==nums[nums[i]-1]表示出现重复
                if(nums[i]>n||nums[i]<1||nums[i]==nums[nums[i]-1]) break;
                //将nums[i]换到它应该待的位置上
                swap(nums[i],nums[nums[i]-1]);
            }
        }

        for(int j=0;j<n;++j){
            if(nums[j]!=j+1) return j+1;
        }

        return n+1;
    }

复杂度

时间复杂度:O(N)
空间复杂度:O(1)

7、前K个高频元素

题目

在这里插入图片描述

分析

这题拿到手后首先肯定要进行统计的,这里使用map来完成,

        unordered_map<int,int> um;
        for(int a:nums){
            ++um[a];
        }

统计完成后,对于TOP K问题,可以使用优先队列来处理,

        priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
        for(auto a:um){
            if(pq.size()==k){
                if(a.second>pq.top().first){
                    pq.pop();
                    pq.push(make_pair(a.second,a.first));
                }
            }
            else pq.push(make_pair(a.second,a.first));
        }

注意,这里由于要按照出现次数进行排序,所以make_pair要把map的实值作为first,键值作为second。
另外,这里priority_queue中使用的greater<pair<int,int>表示构造小顶堆,等同于使用,

struct cmp{
    bool operator()(pair<int,int> a,pair<int,int> b){
        //大的往下移
        return a.first>b.first;
    }
};

最后,将K个元素从优先队列中取出,

        vector<int> ret;
        for(int i=0;i<k;++i){
            ret.push_back(pq.top().second);
            pq.pop();
        }

复杂度

时间复杂度:O(NlgK)
空间复杂度:O(N)

8、最长连续序列

题目

在这里插入图片描述

分析

这里因为时间复杂的限制,我们没办法使用排序。
这里的思路是先用哈希表把所有的数都存下来,然后再次遍历,然后根据哈希表查找最长的连续序列。这里为了避免重复查找,关键在于每次查询都从连续序列中最小的那个数开始。代码如下:

    int longestConsecutive(vector<int>& nums) {
        int n=nums.size();
        unordered_set<int> us;
        for(int a:nums) us.insert(a);

        int maxlen=0;
        for(int a:nums){
            //这一步是关键,它避免了重复的查找。
            if(us.count(a-1)) continue;
            int len=1;
            while(us.count(a+1)){
                ++a;
                ++len;
            }
            maxlen=max(len,maxlen);
        }

        return maxlen;
    }

复杂度

时间复杂度:O(N)
空间复杂度:O(N)

9、LFU

题目

在这里插入图片描述

分析

这题的O(1)过于繁琐,所以我选择放弃。
这里采取官解提出的哈希map+红黑树set的O(lgN)的组合进行求解,先上官解代码,

struct Node {
    int cnt, time, key, value;

    Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}
    
    bool operator < (const Node& rhs) const {
        return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
    }
};

class LFUCache {
    // 缓存容量,时间戳
    int capacity, time;
    unordered_map<int, Node> key_table;
    set<Node> S;
public:
    LFUCache(int _capacity) {
        capacity = _capacity;
        time = 0;
    }
    
    int get(int key) {
        if (capacity == 0) return -1;
        auto it = key_table.find(key);
        // 如果哈希表中没有键 key,返回 -1
        if (it == key_table.end()) return -1;
        // 从哈希表中得到旧的缓存
        Node cache = it -> second;
        // 从平衡二叉树中删除旧的缓存
        S.erase(cache);
        // 将旧缓存更新
        cache.cnt += 1;
        cache.time = ++time;
        // 将新缓存重新放入哈希表和平衡二叉树中
        S.insert(cache);
        it -> second = cache;
        return cache.value;
    }
    
    void put(int key, int value) {
        if (capacity == 0) return;
        auto it = key_table.find(key);
        if (it == key_table.end()) {
            // 如果到达缓存容量上限
            if (key_table.size() == capacity) {
                // 从哈希表和平衡二叉树中删除最近最少使用的缓存
                key_table.erase(S.begin() -> key);
                S.erase(S.begin());
            }
            // 创建新的缓存
            Node cache = Node(1, ++time, key, value);
            // 将新缓存放入哈希表和平衡二叉树中
            key_table.insert(make_pair(key, cache));
            S.insert(cache);
        }
        else {
            // 这里和 get() 函数类似
            Node cache = it -> second;
            S.erase(cache);
            cache.cnt += 1;
            cache.time = ++time;
            cache.value = value;
            S.insert(cache);
            it -> second = cache;
        }
    }
};

因为同时对次数和时间都做了要求,所以这里专门构建的一个Node的结构体,以把四个元素都保存下来。
然后下面的对“<”的重载,涉及到红黑树,这里为使时间更进一步选择了红黑树而不是优先队列,同时还是因为优先队列不支持删除的操作。红黑树的顶部则应该保存在满的时候要删除的结点。根据题意首先先删除调用次数最少的元素,相同调用次数的情况下优先删除存在时间更短的那个元素。
这里设置了一个全局时间time,每次操作都将其加1,同时Node中自带一个cnt,表示到目前为止被调用的次数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值