哈希表,又称散列表,使用 O(n) 空间复杂度存储数据,通过哈希函数映射位置,从而实现近似 O(1) 时间复杂度的插入、查找、删除等操作。
C++ 中的哈希集合为 unordered_set,可以查找元素是否在集合中。如果需要同时存储键和值,则需要用 unordered_map,可以用来统计频率,记录内容等等。如果元素有穷,并且范围不大,那么可以用一个固定大小的数组来存储或统计元素。例如我们需要统计一个字符串中所有字母的出现次数,则可以用一个长度为 26 的数组来进行统计,其哈希函数即为字母在字母表的位置,这样空间复杂度就可以降低为常数。
一个简单的哈希表的实现如下。
template <typename T> class HashTable{ private: vector<list<T>> hash_table; //哈希函数 int myhash(const T & obj)const{ return hash(obj,hash_table.size()); } public: //size最好是质数 HashTable(int size=31) { hash_table.reserve(size); hase_table.resize(size); } ~HashTable() {} //查找哈希表是否存在该值 bool contains(const T & obj) { int hash_value = myhash(obj); const list<T> & slot = hash_table[hash_value]; std::list<T>::const_iterator it =slot.cbegin(); for(;it!=slot.cend() && *it != obj;++it); return it != slot.cend(); } //插入值 bool insert(const T & obj) { if(contains(obj)) { return false; } int hash_value=my_hash(obj); std::list<T>& slot = hash_table[hash_value]; slot.push_front(obj); return true;; } //删除值 bool remove(const T & obj) { list<T>& slot = hash_table[myhash(obj)]; auto it=find(slot.begin(),slot.end(),obj); if(it == slot.end()) { return false; } slot.erase(it); return true; } }; //一个简单的对整数实现的哈希函数 int hash(const int & key,const int &tableSize) { return key%tableSize; }
如果需要大小关系的维持,且插入查找并不过于频繁,则可以使用有序的 set/map 来代替
unordered_set/unordered_map。
题目一:给定一个整数数组,已知有且只有两个数的和等于给定值,求这两个数的位置。
可以利用哈希表存储遍历过的值以及它们的位置,每次遍历到位置 i 的时候,查找哈希表里是否存在 target - nums[i],若存在,则说明这两个值的和为 target。
vector<int>twoSum(vector<int>&nums,int target) { //键是数字,值是该数字在数组中的位置 unordered_map<int,int>hash; vector<int>ans; for(int i=0;i<nums.size();++i) { int num=nums[i]; auto pos = hash.find(target - num); if(pos == hash.end()) { hash[num]=i; } else{ ans.push_back(pos->second); ans.push_back(i); break; } } return ans; }
题目二:给定一个整数数组,求这和数组中的数字可以组成的最长连续数列有多长
可以把所有数字放到一个哈希表,然后不断地从哈希表中任意取一个值,并删除掉其之前之后的所有连续数字,然后更新目前的最长连续序列长度。重复这一过程,我们就可以找到所有的连续数字序列
int longestConsecutive(vector<int>&nums) { unoedered_set<int>hash; for(const int & num: nums) { hash.insert(num); } int ans=0; while(!hash.empty()) { int cur = *(hash.begin()); hash.erase(cur); int next = cur + 1; int pre = cur - 1; while(hash.count(next)) { hash.erase(next++); } while(hash.count(prev)) { hash.erase(prev--); } ans = max(ans,next - prev - 1); } return ans; }
题目三:给定一些二维坐标中的点,求同一条线上最多由多少点。
对于每个点,我们对其它点建立哈希表,统计同一斜率的点一共有多少个。这里利用的原理是,一条线可以由一个点和斜率而唯一确定。另外也要考虑斜率不存在和重复坐标的情况。
本题也利用了一个小技巧:在遍历每个点时,对于数组中位置 i 的点,我们只需要考虑 i 之后的点即可,因为 i 之前的点已经考虑过 i 了。
int maxPoints(vector<vector<int>>&points) { unordered_map<double,int>hash;//斜率-点个数 int max_count = 0, same = 1, same_y = 1; for(int i=0;i<points.size();++i) { same = 1,same_y = 1; for(int j=i+1;j<points.size();++j) { if(points[i][1] == points[j][1]) { ++same_y; if(points[i][0] == points[j][0]) { ++same;; } } else{ double dx=points[i][0] - points[j][0]; double dy=points[i]p1] - points[j][1]; ++hash[dx/dy]; } } max_count = max(max_count,same_y); for(auto item:hash) { max_count = max(max_count,same+item.second); } hash.clear(); } return max_count; }
题目一:给定一个人坐过的一些飞机的起止机场,已知这个人从 JFK 起飞,那么这个人是按什么顺序
飞的;如果存在多种可能性,返回字母序最小的那种。
先用哈希表记录起止机场,其中键是起始机场,值是一个多重集合,表示对应的终止机场。因为一个人可能坐过重复的线路,所以我们需要使用多重集合储存重复值。储存完成之后,我们可以利用栈来恢复从终点到起点飞行的顺序,再将结果逆序得到从起点到终点的顺序。
vector<string>findItinerary(vector<vector<string>>&tickets { vector<string>ans; if(tickets.empty()) { return ans; } unordered_map<string,multiset<string>>hash; for(const auto & ticket: tickets) { hash[ticket[0].insert(ticket[1]); } stack<string>s; s.push("JFK"); while(!s.empty()) { string next = s.top(); if(hash[next].empty()) { ans.push_back(next); s.pop(); } else{ s.push(*hash[next].begin()); hash[next].erase(hash[next].begin()); } } reverse(ans.begin(),ans.end()); return ans; }
一维的前缀和,二维的积分图,都是把每个位置之前的一维线段或二维矩形预先存储,方便加速计算。如果需要对前缀和或积分图的值做寻址,则要存在哈希表里;如果要对每个位置记录前缀和或积分图的值,则可以储存到一维或二维数组里,也常常伴随着动态规划。
题目一:设计一个数据结构,使得其能够快速查询给定数组中,任意两个位置间所有数字的和。
对于一维的数组,我们可以使用前缀和来解决此类问题。先建立一个与数组 nums 长度相同的新数组 psum,表示 nums 每个位置之前前所有数字的和。psum 数组可以通过 C++ 自带的partial_sum 函数建立,也可以直接遍历一遍 nums 数组,并利用状态转移方程 psum[i] = psum[i-1] + nums[i] 完成统计。如果我们需要获得位置 i 和 j 之间的数字和,只需计算 psum[j+1] - psum[i]即可。
class NumArrray { vector<int>psum; public: NumArray(vector<int>nums): psum(nums.size()+1,0){partical_sum(nums.begin(), nums.end(),psum.begin()+1);} int sumRange(int i,int j) { return psum[j+1] - psum[i]; } };
题目二:设计一个数据结构,使得其能够快速查询给定矩阵中,任意两个位置包围的长方形中所有数
字的和。
class NumMatrix{ vector<vector<int>>integral; public: NumMatrix(vector<vector<int>>matrix) { int m = matrix.size(); int n = m >0 ? matrix[0].size(): 0; integral = vector<vector<int>>(m + 1,vector<int>(n+1,0)); for(int i=1;i<=m;++i) { for(int j=1;j<=n;++j) { integral[i][j] = matrix[i-1][j-1] + integral[i-1][j] + integral[i][j-1] - integral[i-1][j-1]; } } } int sumRegion(int row1,int col1,int row2,int col2) { return integral[row2+1][cil2+1] - integral[row2+1][col1] - integral[row1][col2+1] + integral[row1][col1]; } }
题目三:给定一个数组,寻找和为 k 的连续区间个数。
本题同样是利用前缀和,不同的是这里我们使用一个哈希表 hashmap,其键是前缀和,而值是该前缀和出现的次数。在我们遍历到位置 i 时,假设当前的前缀和是 psum,那么 hashmap[psum-k],即为以当前位置结尾、满足条件的区间个数。
int subarraySum(vector<int>& nums,int k) { int count = 0,psum = 0; unordered_map<int,int>hashmap; hashmap[0]=1;//初始化很重要 for(int i:nums) { psum +=i; count += hashmap[psum-k]; ++hashmap[psum]; } return cout; }