Day7 哈希表2

文章分析了四数相加和类似问题的解法,包括使用哈希表(unordered_map)构建和计数、数组和哈希表的选择、双指针搜索以及剪枝优化技巧。重点讲解了如何处理负数和重复元素,以及在不同情况下的算法优化。

454. 四数相加II

题目链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

答案链接:代码随想录

思路:

这道题相比于两数相加更加复杂,不可能O(n)进行查找。

解答:

这道题相当于O(n^2)的两数相加。用前两组数构建一个key为和、value为计数器的字典,后两组数同样这样,进行搜索。

但是,如果是一个数组,搜索不同的四元组,就会复杂很多,也不适合用哈希法。

代码:

int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
        unordered_map <int, int> umap;
        for(int i:nums1){
            for(int j:nums2){
                umap[i+j]++;
            }
        }
        int count_total = 0;
        for(int i:nums3){
            for(int j:nums4){
                count_total += umap[-(i+j)];
            }
        }
        return count_total;
    }

 

 383. 赎金信

题目链接:代码随想录

答案链接:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

思路:

这道题和之前的正反字符串非常像,基本是同一个解法。

解答:

(1)先遍历哪个数组?

我按照正常的思路,现遍历赎金信,之后用杂志进行删减。但是这样需要将杂志全部删减后,再遍历一遍字典,查看是否所有元素都<0。时间复杂度O(3n)

其实可以先构造查找字典(杂志),然后用待查找赎金信进行删减。这样,只要一个元素<0,就可以判断查找字典不全了。时间复杂度O(2n)

查找的时候,需要先构造字典,再用待查找集合进行遍历,这样可以提早出循环。

(2)选择数组还是map作为哈希表?

我利用了思维简单的map做了哈希表。但是,在key有限且紧凑的时候,用数组是更好的方法,因为数组的构造不需要红黑树,更快捷。

在这道题中,字典的所有key都是a-z区间内的,所以满足紧凑有限的特征,应该利用数组。

(3)如何遍历unordered_map?

// (1)迭代器,每个迭代器有first和second两个元素,对应key和value
for (auto it = umap.begin(); it != umap.end(); ++it) {
        std::cout << it->first << " => " << it->second << '\n';}

// (2)迭代器2,直接用简易的for循环,每个迭代器有first和second两个元素,对应key和value
for (const auto &pair : umap) {
        std::cout << pair.first << " => " << pair.second << '\n';}

// (3)结构化绑定,直接提取key和value,是最简单的方法
for (const auto &[key, value] : umap) {
        std::cout << key << " => " << value << '\n';}

代码:

(1)用map

bool canConstruct(string ransomNote, string magazine) {
        unordered_map <int,int> umap;
        for(int i=0; i<magazine.size(); i++){
            umap[magazine[i] - 'a']++;
        }
        for(int i=0; i<ransomNote.size(); i++){
            umap[ransomNote[i] - 'a']--;
            if(umap[ransomNote[i] - 'a']<0){
                return false;
            }
        }
        return true;
    }

(2)用数组

bool canConstruct(string ransomNote, string magazine) {
        int record[26] = {0};
        for(int i=0; i<magazine.size(); i++){
            record[magazine[i] - 'a']++;
        }
        for(int i=0; i<ransomNote.size(); i++){
            record[ransomNote[i] - 'a']--;
            if(record[ransomNote[i] - 'a']<0){
                return false;
            }
        }
        return true;
    }

15. 三数之和

思路:

这道题不需要返回具体的元素位置,但是不能够有重复。

解答:

因为这道题不需要返回具体的元素位置,所以可以在最开始对数组进行排序。这样可以更方便地进行去重操作。在遍历过程中,主要分为外层元素的遍历与最后两层元素的查找。在最后两层元素查找的时候,问题就简化成了两数之和,可以用哈希表补集的方法来做。

(一)前面n-2个元素的遍历

这里遍历的含义是,选定一些固定的起始元素,剩下的操作会返回以其为起始的所有不重复可行的组合。为了不重复,前n-2个元素遍历的时候,每个元素每轮前进后与之前的取值必须不同。如:1(iter1)112(iter2)23(iter3)...。这是因为,如果选择了两个相同的元素,则固定的起始元素则会相同,则有可能选择到重复的组合(可行组合为123,1(iter)123,11(iter)23重复),且不可能选择到不同的组合(可行的组合为112,1(iter)12成功,11(iter)失败)。因此,需要让元素取值有所不同。

(二)最后两个元素的查找

这里问题退化至有序的、不重复的两数之和。这里因为是一个补集的问题,因此可以使用哈希表,或者双指针。

(1)哈希表

哈希表的思路是,如果在字典中找不到待查元素,就将待查元素放入字典内。如果可以找到待查元素,就进行记录,并且剔除相应的待查元素。因为只要查找到了就进行剔除,并且序列有序,乍一看应该不会出现重复的情况。但问题出在有连续两个元素相同的情况下。

如111123,寻找相加为2的两个数。可见,最开始的两个1构成了一个解,但是34位的两个1同样构成了一个解。这其实是两数之和在有序情况下唯一可能出现重复的情况

因此,需要再加一个条件,目前的元素不能和前两个同取一个数。其实,这个条件相比于前面n-2个元素的遍历更宽,也是因为其根本上有不同的去重复逻辑。

(2)双指针

因为序列有序,并且目的是寻找两个元素相加等于特定的元素,很像优化问题,因此自然想到了双指针搜索法。其核心思想为,通过判断当前双指针指向元素与目标的差异,定向调整双指针进行搜索,最终遍历所有可能的结果。

(三)总结

查找n数之和,其实是一个比较困难的问题,因为前n-2层除了暴力搜索没有其他的办法,只有最后一层可以使用哈希表活着双指针。在暴力搜索的时候,需要注意去重的逻辑。

代码:

(1)哈希表

vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[j], c = -(a + b)
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
            if (nums[i] > 0) {
                break;
            }
            if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
                continue;
            }
            unordered_set<int> set;
            for (int j = i + 1; j < nums.size(); j++) {
                if (j > i + 2
                        && nums[j] == nums[j-1]
                        && nums[j-1] == nums[j-2]) { // 三元组元素b去重
                    continue;
                }
                int c = 0 - (nums[i] + nums[j]);
                if (set.find(c) != set.end()) {
                    result.push_back({nums[i], nums[j], c});
                    set.erase(c);// 三元组元素c去重
                } else {
                    set.insert(nums[j]);
                }
            }
        }
        return result;
    }

(2)双指针

vector<vector<int>> threeSum(vector<int>& nums) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
                return result;
            }
            // 错误去重a方法,将会漏掉-1,-1,2 这种情况
            /*
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            */
            // 正确去重a方法
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) {
                // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) right--;
                else if (nums[i] + nums[left] + nums[right] < 0) left++;
                else {
                    result.push_back(vector<int>{nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }

        }
        return result;
    }

18. 四数之和

思路:

这道题与三数之和是一个性质,在刚才的讨论中,我们其实已经将问题推广到了n数之和。此外,这道题的target由三数之和的0变成了任意值。其核心逻辑没有变,这里不再过多赘述。

感悟:

这道题详细讲一下剪枝处理。剪枝说白了就是少算一些东西,在这道题里面就是提前结束循环。结束的前提条件就是目前的元素已经不可能满足target。具体来说,就是前n-2个元素遍历的时候,判断固定的元素之和是否大于target。但是,需要注意元素如果存在负数,可能会出现问题(target=-10,[-4,-3,-2,-1]),因此需要加一个元素求和大于0的条件。

代码:

vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        for (int k = 0; k < nums.size(); k++) {
            // 剪枝处理
            if (nums[k] > target && nums[k] >= 0) {
            	break; // 这里使用break,统一通过最后的return返回
            }
            // 对nums[k]去重
            if (k > 0 && nums[k] == nums[k - 1]) {
                continue;
            }
            for (int i = k + 1; i < nums.size(); i++) {
                // 2级剪枝处理
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
                    break;
                }

                // 对nums[i]去重
                if (i > k + 1 && nums[i] == nums[i - 1]) {
                    continue;
                }
                int left = i + 1;
                int right = nums.size() - 1;
                while (right > left) {
                    // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
                    if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
                        right--;
                    // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
                    } else if ((long) nums[k] + nums[i] + nums[left] + nums[right]  < target) {
                        left++;
                    } else {
                        result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
                        // 对nums[left]和nums[right]去重
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;

                        // 找到答案时,双指针同时收缩
                        right--;
                        left++;
                    }
                }

            }
        }
        return result;
    }

### Java中哈希表的实现原理 Java中的`HashMap`是最常用的哈希表实现之一。其内部基于数组和链表(或红黑树)来存储键值对,能够以接近O(1)的时间复杂度完成插入、删除和查询操作。 #### 1. 哈希表的核心组成部分 - **哈希函数**: `HashMap`使用`hash()`方法重新计算输入键的散列码,从而决定该键值对应该存放在哪个桶(bucket)[^3]。 - **数组**: 底层是一个固定大小的数组,称为桶数组。每个桶可以存放一个节点或者链表头结点。 - **链表/红黑树**: 当发生哈希冲突时,即两个不同的键被映射到同一个桶中,则采用链地址法解决冲突。当链表长度超过一定阈值(`TREEIFY_THRESHOLD`)时,会将链表转换为红黑树以提高查找效率[^2]。 #### 2. 插入过程 在向`HashMap`中插入一个新的键值对时: 1. 首先调用键对象的`hashCode()`方法获取原始哈希值; 2. 然后通过位运算`(n - 1) & hash`将其映射至具体的桶位置(n表示当前容量),其中`&`操作确保结果落在合法范围内; 3. 如果目标桶为空,则直接创建新的节点并放置于此;否则进入下一步; 4. 若发现已有节点具有相同的键(equals返回true),则替换原有值而不新增节点; 5. 否则按照顺序追加到链表末端或插入到红黑树相应位置,并判断是否需要调整结构形式(如由链表转成红黑树)[^2]。 #### 3. 查询过程 对于给定的关键字k执行如下步骤定位对应的值v: 1. 利用同样的逻辑找到可能存在的bucket; 2. 对应slot上的数据可能是单个entry也可能是linked list甚至tree node collection; 3. 进一步比较各个candidate entry's key against k via both their hashes and actual object equality checks until match found or end reached.[^3] 以下是简单的代码示例展示如何构建以及操作一个基础版的哈希表: ```java import java.util.HashMap; public class Main { public static void main(String[] args){ HashMap<Integer,String> map=new HashMap<>(); // Insert elements into the hashmap. map.put(1,"Apple"); map.put(2,"Banana"); map.put(3,"Orange"); System.out.println("Initial Map:"+map); // Access an element from the hashmap using its key. String fruitValue=map.get(2); System.out.println("Fruit with Key '2': "+fruitValue); // Remove specific item by specifying associated key value pair inside remove function call parameter list respectively as first argument represents target record identifier while second one denotes expected mapped content therewithin so that only when they fully coincide will real deletion occur otherwise nothing happens at all here including any exception throwing whatsoever under normal circumstances unless explicitly programmed differently elsewhere beforehand somewhere somehow someway sometime someday someplace somewhat somebody something anyway anyhow anywhere anytime always already almost altogether although among amount amongst ancient another anti anything anybody anyone apart appear apply approach approve around arrange arrive art article artist artistic assume attack attempt attention attitude attract audience author authority automatic auxiliary available avoid awake aware away awful awkward alike alive alone along alongside already also alter although among amount anchor ancient anger angle angry animal annual announce annoy anonymous answer ant apartment appeal appearance apple April appropriate approval approximate arbitrary area argue argument arm army around arrow artificial artistry aspect assault associate assumption assure assist assistant association assume astonish attach attempt attend atmosphere atom atomic attire attract attribute auction audio August aunt authentic authority autumn average awaken awareness away awesome awful axis } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值