草庐IT

【数据结构与算法】前缀和+哈希表算法

命由己造~ 2024-02-20 原文

文章目录

一、引入

关于前缀和和哈希这两个概念大家都不陌生,在之前的文章中也有过介绍:前缀和与差分算法详解

而哈希表最经典的一题莫过于两数之和
题目链接

题目描述:

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

只会存在一个有效答案

思路分析:
我们在遍历这个数组要做两件事:
假设现在遍历到下标为idx的位置。
1️⃣ 查看target - nums[idx]是否在哈希表中,如果在,说明这两个数加起来就是目标和,那么就找到了两个下标,一个是hash[target - nums[idx]],一个是当前位置idx。
2️⃣ 用哈希表记录两个数据,first记录当前位置的值,second记录当前位置的下标。

代码:

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> hash;
        int n = nums.size();
        for(int i = 0; i < n; i++)
        {
            if(hash.find(target - nums[i]) != hash.end())
            {
                return {hash[target - nums[i]], i};
            }
            hash[nums[i]] = i;
        }
        return {};
    }
};

二、前缀和与哈希表的结合

用一个例子来说明以下:

假设我们要寻和为5连续子数组的个数,那么只要前缀和中任意两个数的差值为5,那么就找到了子数组。

那么我们就可以直接用哈希表把前缀和的数据存储起来,first存前缀和的值,second用来标识有多少个子数组。
这里首先要注意初始化哈希表把0的位置先设置成1:hash[0] = 1,因为当我们计算前缀和为5的位置的时候,就标识了从0 ~ 5存在和为5的连续子数组。

假设目标和为k,遍历到i位置。
所以现在我们在计算前缀和的同时看看是否存在hash[k - nums[i]],这个的数值大小就代表有多少个连续的子数组和。那么为什么会存在多个呢?

因为可能数组存在负数,这样就会导致出现这种情况:

那么省略号这段区间的前缀总和就为0,所以就会存在两段子数组和为5的区间。

三、例题

3.1 和为 K 的子数组

题目链接

题目描述:

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的连续子数组的个数 。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

思路分析:
这个题如果我们只使用前缀和:先计算前缀和,然后依次遍历看是否有两个数字的差值为k。

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        // 前缀和数组
        vector<int> sums(n + 1);
        for(int i = 0; i < n; i++)
        {
            sums[i + 1] = sums[i] + nums[i];
        }
        int res = 0;
        for(int i = 0; i < n; i++)
        {
            for(int j = i; j < n; j++)
            {
                if(sums[j + 1] - sums[i] == k)
                {
                    res++;
                }
            }
        }
        return res;
    }
};

但是提交会发现运行超时。
而由于这道题只关心次数,不关注具体的解,所以我们能用哈希表来优化效率。
具体的做法在上面已经详细介绍过。

代码:

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        unordered_map<int, int> hash;
        int res = 0;
        hash[0] = 1;
        int sum = 0;
        for(int i = 0; i < n; i++)
        {
            sum += nums[i];
            res += hash[sum - k];
            hash[sum]++;
        }
        return res;
    }
};

3.2 统计「优美子数组」

题目链接

题目描述:

给你一个整数数组 nums 和一个整数 k。如果某个连续子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。
请返回这个数组中 「优美子数组」 的数目。

示例 1:

输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。

示例 2:

输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。

示例 3:

输入:nums = [2,2,2,1,2,2,1,2,2,2], k = 2
输出:16

思路分析:
这道题乍一看无从下手,但其实这道题跟上面一道题没什么区别,只要把偶数看成0,奇数看成1,就直接转化成了和为K的子数组问题了。

代码:

class Solution {
public:
    int numberOfSubarrays(vector<int>& nums, int k) {
        int n = nums.size();
        int res = 0;
        unordered_map<int, int> hash;
        hash[0] = 1;
        int sum = 0;
        for(int i = 0; i < n; i++)
        {
            // 偶数为0,奇数为1
            int ret = 0;
            if(nums[i] % 2)
            {
                ret = 1;
            }
            sum += ret;
            res += hash[sum - k];
            hash[sum]++;
        }
        return res;
    }
};

3.3 路径总和III

题目链接

题目描述:

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例 1:


输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。

示例 2:

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

方法一:
这道题可以直接用暴力遍历,每个节点都往下统计到叶子节点,看有多少个。

代码:

class Solution {
public:
    int dfs(TreeNode* root, long long targetSum)
    {
        if(root == nullptr)
        {
            return 0;
        }
        int ret = 0;
        if(targetSum - root->val == 0)
        {
            ret++;
        }
        ret += dfs(root->left, targetSum - root->val);
        ret += dfs(root->right, targetSum - root->val);
        return ret;
    }

    int pathSum(TreeNode* root, int targetSum) {
        if(root == nullptr)
        {
            return 0;
        }
        int res = dfs(root, targetSum);
        res += pathSum(root->left, targetSum);
        res += pathSum(root->right, targetSum);
        return res;
    }
};

方法二:
第二个方法当然是使用前缀和+哈希表算法。
我们边递归边求前缀和,统计的方法还是跟上面一样,这里要注意的是当回溯的时候记住要把当前的位置给去掉(没递归到当前位置的状态)。

代码:

class Solution {
public:
    unordered_map<long long, int> hash;
    int cnt;

    void dfs(TreeNode* root, long long sum, int target)
    {
        if(root == nullptr)
        {
            return;
        }
        sum += root->val;
        cnt += hash[sum - target];
        hash[sum]++;
        dfs(root->left, sum, target);
        dfs(root->right, sum, target);
        hash[sum]--;
    }

    int pathSum(TreeNode* root, int targetSum) {
        hash[0] = 1;
        cnt = 0;
        dfs(root, 0, targetSum);
        return cnt;
    }
};

四、总结

我们通过上面的问题可以总结出规律,遇到求连续的和的时候我们就应该想到用前缀和算法,而如果题目只关心次数,不关注具体的解,我们就可以使用(前缀和+哈希表)算法。



有关【数据结构与算法】前缀和+哈希表算法的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 如果指定键的值在数组中相同,如何合并哈希 - 2

    我有一个这样的哈希数组:[{:foo=>2,:date=>Sat,01Sep2014},{:foo2=>2,:date=>Sat,02Sep2014},{:foo3=>3,:date=>Sat,01Sep2014},{:foo4=>4,:date=>Sat,03Sep2014},{:foo5=>5,:date=>Sat,02Sep2014}]如果:date相同,我想合并哈希值。我对上面数组的期望是:[{:foo=>2,:foo3=>3,:date=>Sat,01Sep2014},{:foo2=>2,:foo5=>5:date=>Sat,02Sep2014},{:foo4=>4,:dat

  4. ruby - 如何在 Grape 中定义哈希数组? - 2

    我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>

  5. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  6. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  7. ruby - 在哈希的键数组中追加元素 - 2

    查看我的Ruby代码:h=Hash.new([])h[0]=:word1h[1]=h[1]输出是:Hash={0=>:word1,1=>[:word2,:word3],2=>[:word2,:word3]}我希望有Hash={0=>:word1,1=>[:word2],2=>[:word3]}为什么要附加第二个哈希元素(数组)?如何将新数组元素附加到第三个哈希元素? 最佳答案 如果您提供单个值作为Hash.new的参数(例如Hash.new([]),完全相同的对象将用作每个缺失键的默认值。这就是您所拥有的,那是你不想要的。您可以改用

  8. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  9. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  10. 区块链之加解密算法&数字证书 - 2

    目录一.加解密算法数字签名对称加密DES(DataEncryptionStandard)3DES(TripleDES)AES(AdvancedEncryptionStandard)RSA加密法DSA(DigitalSignatureAlgorithm)ECC(EllipticCurvesCryptography)非对称加密签名与加密过程非对称加密的应用对称加密与非对称加密的结合二.数字证书图解一.加解密算法加密简单而言就是通过一种算法将明文信息转换成密文信息,信息的的接收方能够通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,算法可以分为对称加密、非对称加密、对称加密和非

随机推荐