注:以下代码均为c++
1. 哈希表
1.1 重复的DNA序列
什么数据结构既可以保存数据又可以计数:哈希表
vector<string> findRepeatedDnaSequences(string s) {
unordered_map<string, int> hash;
vector<string> ans;
for(int i = 0; i + 10 <= s.size(); i++){
string str = s.substr(i, 10);
hash[str]++;
if(hash[str] == 2)
ans.push_back(str);
}
return ans;
}
1.2. 设计哈希映射
我自己写的: 用vector<vector> hash;数据结构(二维数组)表示哈希表.。但是,我没有考虑到哈希表的性质:固定长度。但是下面代码也能通过。
class MyHashMap {
public:
vector<vector<int>> hash;
MyHashMap() {
}
void put(int key, int value) {
for(int i = 0; i < hash.size(); i++){
if(key == hash[i][0]){
hash[i][1] = value;
return;
}
}
vector<int> kv = {key, value};
hash.push_back(kv);
}
int get(int key) {
for(int i = 0; i < hash.size(); i++){
if(key == hash[i][0]){
return hash[i][1];
}
}
return -1;
}
void remove(int key) {
for(int i = 0; i < hash.size(); i++){
if(key == hash[i][0]){
vector<int> target = hash[i];
auto it = remove_if(hash.begin(), hash.end(), [&target](const std::vector<int>& vec) { return vec == target; });
hash.erase(it, hash.end());
}
}
}
};
哈希表的思想:将一个大的值域映射到一小的值域。
在这里我们使用拉链法。
//拉链法
class MyHashMap1 {
public:
int N = 20011;
vector<list<pair<int, int>>> h;
MyHashMap1() {
h = vector<list<pair<int, int>>>(N);
}
//查找key的位置,返回一个迭代器(拉的链)
//list是一个双向链表容器,pair<int, int>是包含两个整数的键值对。返回一个迭代器用于遍历list中的元素。
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();
}
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});
else //在链中,改值
it->second = value;
}
int get(int key) {
auto it = find(key);
int t = key % N;
if(it == h[t].end())
return -1;
else
return it->second;
}
void remove(int key) {
auto it = find(key);
int t = key % N;
if(it != h[t].end())
h[t].erase(it);
}
};
1.3 寻找重复的子树
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
int cnt = 0; //字符串的int表示,从0开始
unordered_map<string, int> hashmap;
unordered_map<int, int> count;
vector<TreeNode*> ans;
string dfs(TreeNode* root){
//递归出口
if(root == NULL)
return to_string(hashmap["#"]);
//递归
auto left = dfs(root->left);
auto right = dfs(root->right);
//完成递归后进行操作
string tree = to_string(root->val) + ',' + left + ',' + right;
if(hashmap.count(tree) == 0){
hashmap[tree] = cnt;
cnt++;
}
int t = hashmap[tree];
count[t]++;
if(count[t] == 2)
ans.push_back(root);
return to_string(t);
}
vector<TreeNode*> findDuplicateSubtrees(TreeNode* root) {
//把空字符串给一个int值
hashmap["#"] = cnt;
cnt++;
dfs(root);
return ans;
}
1.4 和为K的子数组
前缀和+哈希:
int subarraySum(vector<int>& nums, int k){
unordered_map<int, int> mp; //前缀和,次数
mp[0] = 1;
int count = 0, pre = 0; //个数,当前前缀和pre[i]
for(auto x: nums){
pre = pre + x; //求当前前缀和
if(mp.find(pre-k) != mp.end()){ //在mp中找到了pre[j-1]
count = count + mp[pre-k];
}
//注:当前前缀和一定要在找完pre[j-1]后才记录,因为要找之前的前缀和,而不包括当前前缀和。
mp[pre]++; //将前缀和记录在哈希表中
}
return count;
}
2. 并查集
并查集主要解决连通性问题。
主要功能:
1.合并两个集合
2.判断两个元素是否在同一集合中
优化:
1.路径压缩(主要)
2.按秩排序(收益较低)
路径压缩模板:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); //定义每个结点的根结点
// 并查集初始化:每个结点是单独的一个集合,其父节点是自己。
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集里寻根的过程:路径压缩
int find(int u) {
//如果我不是父结点,那么递归寻找我的父结点。
if(u != find(u)) father[u] = find(father[u]);
return father[u];
}
// 判断 u 和 v是否是同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v合并到u集合中
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
//如果两个根相同,什么都不用做。
//如果两个根不同,v的根的根 = u的根
if(u != v)
father[v] = u;
}
2.1 求连通图个数
题目:一个班级里,如果A和B是好朋友,B和C是好朋友,那么就认为A和C是好朋友,那么该班级里有多少组好朋友。
思想:
定义一个数组p用来保存每个点(同学)的祖先节点(路径压缩思想)
初始状态:每个点都是一个单独的集合,每个点的祖先节点p是它自己。
- 如果两点之间没边,不用管了。
- 如果两点之间有边,但是两点的祖先节点不是一个,那么将两点合并,集合数-1。
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] == 0) continue; //两点之间没边,跳过。
//如果i和j不是一个祖先,i的祖先的祖先=j的祖先,集合数-1
if(find(i) != find(j)){
p[find(i)] = find(j);
res--;
}
}
}
return res;
}
}
2.2 冗余连接
思路:遍历每条边,合并每条边的两个结点,如果发现其中一条边的两个结点在同一集合内,那么这条边就是多余的,返回即可。
vector<int> p;
int find(int x){
if(x != p[x])
p[x] = find(p[x]);
return p[x];
}
vector<int> findRedundantConnection(vector<vector<int>>& edges){
int n = edges.size(); //由于树多了一条边,所以结点数刚好等于边数。
//初始化:由于结点值为1~n,所以初始化到n。为什么从0开始初始化,因为只能按顺序push_back。
for(int i = 0; i <= n; i++)
p.push_back(i);
//遍历每条边
for(auto e : edges){
int i = e[0], j = e[1];
if(find(i) == find(j)) return {i, j}; //边的两个点在同一集合,返回。
p[find(i)] = find(j);
}
return {-1, -1};
}