刷题笔记:哈希表与字符串专题(第一遍刷搞懂每道题的解法;第二遍刷搞懂这一类题通用的思想和策略;第三遍刷思考什么样的题属于这个类别)

1.哈希表

1.1基本概念

哈希表(散列表),是根据关键字值key直接进行访问的数据结构,它通过把关键字值映射到表中一个位置(数组下标)来直接访问,以加快查找关键字值的速度。这个映射函数叫做哈希(散列)函数,存放记录的数组叫做哈希(散列)表。

给定一个表M,存在函数f(key),对任意的关键字值key,带入函数后若能得到包含该关键字的表中地址,称表M为哈希表,函数f(key)为哈希函数。

例1:给一个字符串,求每个字符出现多少次?

易忘点:ASC2码0-127,数字0的ASC2码值是48,A的ASC2码值是65,a的ASC2码值是97.

int main() {
	int char_map[128] = { 0 };
	std::string str = "asdasdasfafasd";
	for (int i = 0; i < str.length(); i++) {
		char_map[str[i]]++;
	}
	for (int i = 0; i < 128; i++) {
		if (char_map[i] > 0) {
			printf("[%c][%d]:%d\n", i, i, char_map[i]);
		}
	}
}

例2:给一个数组,里面存的是随机的正整数。使用哈希表进行排序。

哈希表的长度需要大于超过最大待排序数字。时间复杂度是O(表长+元素个数)

void hash_sort() {
	int random[10] = { 999,1,4,78,2,6,90,23,1,25 };
	int hash_map[1000] = { 0 };
	for (int i = 0; i < sizeof(random) / sizeof(random[0]); i++) {
		hash_map[random[i]]++;
	}
	//从小到大排序
	for (int i = 0; i < sizeof(hash_map) / sizeof(hash_map[0]); i++) {
		for (int j = 0; j < hash_map[i];j++) {
			printf("%d ",i);
		}
	}
}

1.2任意元素的映射

首先思考下面几个问题:

1)当遇到负数或非常大的整数时,改如何进行哈希映射呢?

2)当遇到字符串时,如何进行哈希映射呢?

3)当遇到其他无法直接映射的数据类型(浮点数、数组、对象)如何进行哈希映射呢?

解决办法就是利用哈希函数,将关键字值(大整数、字符串、浮点数等)转化为整数再对表长取余,从而关键字值被转换为哈希表的表长范围内的整数。

例3:冲突问题

首先我们写了一个整数取余哈希函数和字符串哈希函数展示,用于将大整数和字符串映射到哈希表中。

//直接对整数取余表长再返回
int int_func(int key, int table_len) {
	return key % table_len;
}
//将字符串中的字符的ASC2码相加得到整数再取余表长
int string_func(std::string key, int table_len) {
	int sum = 0;
	for (int i = 0; i < key.length(); i++) {
		sum += key[i];
	}
}

int main() {
	const int TABLE_LEN = 10;
	int hash_map[TABLE_LEN] = { 0 };
	hash_map[int_func(999999995, TABLE_LEN)]++;
	hash_map[int_func(5, TABLE_LEN)]++;
	hash_map[string_func("abc", TABLE_LEN)]++;
	hash_map[string_func("asdasd", TABLE_LEN)]++;
	for (int i = 0; i < TABLE_LEN; i++) {
		printf("hash_map[%d]=%d\n", i, hash_map[i]);
	}
	return 0;
}

但这时会发现,很可能会有多个结果映射到了同一个地方。而且不管我们如何设计哈希函数,都会出现这种发生冲突的情况,因为发生冲突的本质原因是哈希表的表长不够大。

例4:使用拉链法解决冲突

将所有哈希函数结果相同的结点连接在同一个单链表中。若选定的哈希表长度为m,则可将哈希表定义为一个长度为m的指针数组t[0..m-1],指针数组中的每个指针指向哈希函数结果相同的单链表。TABLE_LEN一般设定为质数,冲突会比其他数字少。

#pragma region 拉链法解决冲突
struct LisNode
{
	int val;
	LisNode *next;
	LisNode(int x) :val(x), next(NULL) {}
};
//整数哈希函数
int hash_func(int key, int table_len) {
	return key % table_len;
}

void insert(LisNode *hash_table[], LisNode * node, int table_len) {
	int hash_key = hash_func(node->val, table_len);
	//头插法
	node->next = hash_table[hash_key];
	hash_table[hash_key] = node;
}

bool search(LisNode *hash_table[], int value, int table_len) {
	int hash_key = hash_func(value, table_len);
	LisNode* head = hash_table[hash_key];
	while (head!=NULL)
	{
		if (head->val == value) {
			return true;
		}
		head = head->next;
	}
	return false;
}
#pragma endregion

2.例题操练

例5:最长回文串(LeetCode 409-简单

题目:已知一个只包括大小写字符的字符串,求用该字符串中的字符可以生成的最长回文字符串长度。

例子:S="abccccddaa",可生成的最长回文字符串长度为9,如dccaaaccd、adccbccda等,都是正确的。

分析:

除了中心字符外,其余只需要头部出现,尾部也出现。

字符数量为偶数时,该怎么操作?字符数量为奇数时,该怎么操作?

遇到偶数个的字符直接用,遇到奇数个的字符如果减1后是偶数就留下1个,其余全用;最后如果有剩1个的字符就随便选一个用。

算法思路:

  • 利用字符哈希方法,统计字符串中所有的字符数量;
  • 设置最长回文串偶数字符长度为max_length=0;
  • 设置是否有中心点标记flag=0;
  • 遍历每一个字符,字符数为count,若count为偶数,max_length+=count;若count为奇数,max_length+=count-1,flag=1;
  • 最终最长回文子串长度:max_length+flag;

代码展示:

class Solution {
public:
    int longestPalindrome(string s) {
        int max_length=0;
        int flag=0;
        //字符哈希
        int char_map[128]={0};
        for(int i=0;i<s.length();i++){
            char_map[s[i]]++;
        }
        for(int i=0;i<128;i++){
            if(char_map[i]%2==0){
                max_length+=char_map[i];
            }
            else{
                max_length+=char_map[i]-1;
                flag=1;
            }
        }
        return max_length+flag;
    }
};

例6:词语模式(LeetCode 290-简单

题目:已知字符串pattern与字符串str,确认str是否与pattern匹配。str与pattern匹配代表字符串str中的单词与pattern中的字符一一对应。(其中pattern中只包含小写字符,str中的单词只包含小写字符,使用空格分隔。)

例子:

输入: pattern = "abba", str = "dog cat cat dog"
输出: true
输入:pattern = "abba", str = "dog cat cat fish"
输出: false

分析:

  • 单词的个数与pattern字符串中的字符数量相同
  • 当拆解出一个单词时,若该单词已出现,则当前单词对应的pattern字符必为该单词曾经对应的pattern字符
  • 当拆解出一个单词时,若该单词未曾出现,则当前单词对应的pattern字符也必须未曾出现

算法思路:

  • 设置单词到pattern字符的映射;使用数组used[128]记录pattern字符是否使用。
  • 遍历str,按照空格拆分单词,同时对应的向前移动指向pattern字符的指针,每拆分出一个单词,判断:如果该单词从未出现在哈希表中:{如果当前的pattern字符已被使用,则返回fasle;将单词与当前指向的pattern字符做映射;标记当前指向的Pattern字符已使用}否则{如果当前单词在哈希表中的映射字符和当前指向的pattern字符不相同,则返回false}
  • 若单词个数与pattern字符个数不匹配,返回false.

代码展示:

class Solution {
public:
    bool wordPattern(string pattern, string str) {
        //单词到pattern
        std::map<string,char> word_hash;
        //是否使用
        int used[128]={0};
        //单词
        std::string word;
        //在尾部添加空格,使得遇到空格拆分单词
        str.push_back(' ');
        //pattern数组的下标
        int j=0;
        for(int i=0;i<str.length();i++){
            //遇到空格则拆分单词并开始判断
            if(str[i]==' '){
                //如果是新单词
                if(word_hash.find(word)==word_hash.end()){
                    //对应的pattern用过则false
                    if(used[pattern[j]]==1){
                        return false;
                    }
                    //没用过则标记为用过并添加到哈希表中
                    used[pattern[j]]=1;
                    word_hash[word]=pattern[j];
                }
                else{
                    //不是新单词则判断对应关系是否一致
                    if(word_hash[word]!=pattern[j]){
                        return false;
                    }
                }
                word="";
                j++;
            }
            else{
                word.push_back(str[i]);
            }
        }
        if(j!=pattern.length() ){
            return false;
        }
        return true;

    }
};

例7:同字符词语分组(LeetCode 49-中等

题目:已知一组字符串,将所有anagram(由点到字母顺序而构成的字)放到一起输出。

例子:

输入: ["eat", "tea", "tan", "ate", "nat", "bat"],
输出:
[["ate","eat","tea"],["nat","tan"],["bat"]]

分析:如何设计哈希表的key和value,就可将各个字符数相同的字符串映射到一起

算法思路1:

设置字符串到字符串向量的哈希表anagram,遍历字符串向量strs中的单词strs[i]:

  • 设置临时变量str=strs[i],对str进行排序。
  • 若str未出现在anagram中,设置str到一个空字符串向量的映射。并添加到哈希表中。

遍历哈希表anagram,将全部key对应的value push至最终结果中。

代码展示1:

class Solution {
public:
    vector<vector<string>> groupAnagrams(vector<string>& strs) {
        vector<vector<string>> result;
        //哈希表
        std::map<string,vector<string>> anagram_hash;
        for(int i=0;i<strs.size();i++){
            //先对str排序;
            string temp=strs[i];
            std::sort(temp.begin(),temp.end());
            //如果有同组元素则添加
            if(anagram_hash.find(temp)!=anagram_hash.end()){
                anagram_hash[temp].push_back(strs[i]);
            }
            else{
                //创建新的
                vector<string> new_vecstr;
                anagram_hash[temp]=new_vecstr;
                anagram_hash[temp].push_back(strs[i]);
            }
        }
        std::map<string,vector<string>>::iterator iter;
        iter=anagram_hash.begin();
        while(iter!=anagram_hash.end()){
            result.push_back(iter->second);
            iter++;
        }
        return result;
    }
};

算法思路2:

设置vector到字符串向量的哈希表anagram,遍历字符串向量strs中的单词strs[i]:

  • 统计strs[i]中的各个字符数量,存储至vec
  • 若vec未出现在anagram中,设置vec到一个空字符串向量的映射。并添加

遍历哈希表anagram至全部添加至最终结果

代码展示2:逻辑基本一样,感兴趣的可以自己编写一下

 

例8:无重复字符的最长子串(LeetCode 3-中等

问题:已知一个字符串,求用该字符串的无重复字符的最长子串的长度。

例子:

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

分析:如果用枚举的方式来计算则复杂度是O(n^2),那么优化的目标就是降低时间复杂度,避免指针不必要的回调。

算法思路1:

  • 设置一个记录字符数量的字符哈希,char_map
  • 设置一个记录当前满足条件的最长子串变量word;
  • 设置两个指针(i,begin)指向字符串第一个字符
  • 设置最长满足条件的子串的长度result
  • i指针向后逐个扫描字符串中的字符,在这个过程中,使用char_map记录字符数量,如果word中没出现过该字符{word后添加字符并比较result},否则

代码展示1:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
     //窗口头指针
     int begin=0;
     int result=0;
     std::string word="";
     //字符哈希
     int char_map[128]={0};
     for(int i=0;i<s.length();i++){
         char_map[s[i]]++;
         if(char_map[s[i]]==1){//没出现过
             word+=s[i];
             if(result<word.length()){
                 result=word.length();
             }
         }
         else{//将重复的字符s[i]去掉
             while(begin<i && char_map[s[i]]>1){
                  char_map[s[begin]]--;
                  begin++;
             }
             word="";
             for(int j=begin;j<=i;j++){
                 word+=s[j];
             }
         }
     }
     return result;
    }
};

算法思路2:

  • 设置一个字符索引的哈希表char_hash;
  • 设置首尾指针head、tail
  • 遍历str中的字符,判断是否出现过,如果出现过{判断对应索引是否<head,小于head则忽略、大于等于head时则有重复出现,通过tail和haed计算长度;更新head值为重复位置的下一个,同时更新tail}否则添加新的哈希映射。

代码展示2:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        if(s.length()<2){
            return s.length();
        }
        int result=1;
        //int是字符在s中的index
        std::map<char,int> char_hash;
        int h=0;
        int t=0;
        for(;t<s.length();t++){
            //出现过,并且在目前串内
            if(char_hash.find(s[t])!=char_hash.end() && char_hash[s[t]]>=h){
                //更新result
                if(result<(t-h)){
                    result=(t-h);
                }
                //更新head
                h=char_hash[s[t]]+1;
            }
            //最后一个要长度+1
            if(t==s.length()-1  && result<(t-h+1) ){
                    result=(t-h+1);
            }
            //添加新的,或更新旧的
            char_hash[s[t]]=t;
        }
        return result;
    }
};

例9:重复的DNA序列(LeetCode 187-中等

题目:将DNA序列看作是只包括['A','C','G','T'] 4个字符的字符串,给一个DNA字符串,找到所有长度为10的且出现超过1次的子串。

例子:

输入:s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"
输出:["AAAAACCCCC", "CCCCCAAAAA"]

算法思路1:(简单方法)因为题目要求是长度为10,与n无关,所以可以使用枚举法;这种方法虽然可以通过,但完全没挖掘到问题真正的考点

代码展示1:

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> result;
        map<string,int> hash_str;
        if(s.length()<10){
            return result;
        }
        int head=0;
        int tail=9;
        for(;tail<s.length();head++,tail++){
            string temp=s.substr(head,10);
            //判断是否出现过
            if(hash_str.find(temp)!=hash_str.end()){
                hash_str[temp]++;
            }
            else{
                hash_str[temp]=1;
            }
        }
        map<string,int>::iterator iter;
        iter=hash_str.begin();
        while(iter!=hash_str.end()){
            if(iter->second>1){
                result.push_back(iter->first);
            }
            iter++;
        }
        return result;
    }
};

算法思路2:

将A、C、G、T 4个字符分别用00,01,10,11 表示。故长度为10的DNA序列可以用20个比特位的整数所表示。

  • 设置全局整数哈希,注意长度的设置!(2^20)
  • 将DNA字符串的前10个字符利用移位运算转换为整数key;
  • 从第11个开始,按顺序遍历各个字符,遇到一个字符即将key右移2位(去掉最低位),将新字符添加到最高位
  • 遍历哈希表,输出最后结果

代码展示2:

//哈希表很大时需要全局数组,可能会栈溢出,内存爆掉
int hash_map[1048576] ={0};

string change_int2DNA(int DNA){
    static const char DNA_CHAR[]={'A','C','G','T'};
    string str;
    for(int i=0;i<10;i++){
        //把每2字节对应的字符提取出来
        str+=DNA_CHAR[DNA&3];
        DNA=DNA>>2;
    }
    return str;
}

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        vector<string> result;
        if(s.length()<10){
            return result;
        }
        //由于是全局数组,每次调用时都需要更新
        for(int i=0;i<1048576;i++){
            hash_map[i]=0;
        }
        //字符到整数的映射
        int char_map[128]={0};
        char_map['A']=0;
        char_map['C']=1;
        char_map['G']=2;
        char_map['T']=3;
        int key=0;
        //把前10个字符转成整数
        for(int i=9;i>=0;i--){
            //每存进去一个数后向左移两位
            key=(key<<2)+char_map[s[i]];
        }
        hash_map[key]=1;
        for(int i=10;i<s.length();i++){
            key=key>>2;
            key=key|(char_map[s[i]]<<18);
            hash_map[key]++;
        }
        //遍历将结果输出
        for(int i=0;i<1048576;i++){
            if(hash_map[i]>1){
                result.push_back(change_int2DNA(i));
            }
        }
        return result;
    }
};

例10:最小窗口子串(LeetCode 76-困难

题目:已知字符串S与字符串T,求在S中的最小窗口(区间),使得这个区间中包含了字符串T中所有的字符。

例子:

输入: S = "ADOBECODEBANC", T = "ABC"
输出: "BANC"

分析:这种枚举能直接解决的问题,大概率trick是在优化时间复杂度上;想办法是否能有一种O(n)的方法解决;既然有窗口,那大概率可以从设计头尾两个指针角度触发,一边遍历一边修正,这样就可实现O(n)时间复杂度的解决办法。

算法思路:

  • 设置两个字符哈希数组,map_s和map_t,map_s代表当前处理的窗口区间中的字符数量,map_t代表子串T的字符数量。
  • 设置两个指针head和tail,初始化指向字符串的第一个字符;
  • tail指针向后逐个扫描字符串中的字符,在这个过程中循环检查head指针是否可以向前移动。
  • ---如果head指向的字符在T中没出现,直接移动head
  • ---如果head指向的字符在T中出现了,但是当前区间窗口中的该字符数量足够,也移动head,并更新map_s;
  • 指针tail每向后扫描一个字符就检查是否可以更新最终的结果,也就是更新当前最小的窗口

代码展示:

class Solution {
private:
    bool is_window_ok(int map_w[],int map_t[],vector<int>& vec_t){
        //判断当前窗口是否符合条件
        for(int i=0;i<vec_t.size();i++){
            if(map_w[vec_t[i]]<map_t[vec_t[i]]){
                return false;
            }
        }
        return true;
    }
public:
    string minWindow(string s, string t) {
        const int MAX_LENGTH=128;
        //记录t中各字符个数
        int map_t[MAX_LENGTH]={0};
        //记录当前窗口中个字符个数
        int map_w[MAX_LENGTH]={0};
        //记录t字符串中有哪些字符
        vector<int> vec_t;
        //记录t中字符个数
        for(int i=0;i<t.length();i++){
            map_t[t[i]]++;
        }
        //遍历,将字符串t中出现的字符存储到vec_t中
        for(int i=0;i<MAX_LENGTH;i++){
            if(map_t[i]>0){
                vec_t.push_back(i);
            }
        }
        //窗口的头尾指针,以及最终结果
        int head=0;
        int tail=0;
        string result;
        for(;tail<s.length();tail++){
            //新字符添加到窗口
            map_w[s[tail]]++;
            //头指针不能超过尾指针
            while(head<tail){
                char head_char=s[head];
                //字符在t中但窗口储备足够就后移,并更新map_w
                if(map_w[head_char]>map_t[head_char]){
                    head++;
                    map_w[head_char]--;
                }//如果字符不在t直接后移
                else if(map_t[head_char]==0){
                    head++;
                }
                else{//如果在窗口且不能删那就跳出循环
                    break;
                }
            }
            //判断这个窗口是否可行
            if(is_window_ok(map_w,map_t,vec_t)){
                //判断当前窗口是否比目前最优结果小
                int new_len=(tail-head+1);
                if(result=="" || new_len < result.length()){
                    result=s.substr(head,new_len);
                }
            }
            
        }
        return result;

    }
};

 

 

 

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值