LeetCode刷题攻略:常用数据结构(哈希表)

为了能够通过技术面试,“刷题”可以说是求职路上避不开的一道坎了。随着像LeetCode这样的刷题网站盛行,面试官也会尽量挑选一些不太热门的题目,或者领域内的题目,仅仅背题肯定是无法通过面试的。需要对大致会出现什么题目、有什么通用的解决方法有所了解,才能够对应题目快速想到最优解。

这里还是必须推荐两本学习算法与数据结构极好的书籍:《算法(第四版)》和《算法导论》。前者更强调“数据结构”的建立,实践性比较强,后者更强调数学上的精确性,分析性比较强。要学好数据结构和算法,笔者觉得这两本书都应该看看,即使是挑着合适自己的章节来看,也总能够有所收获。

这篇文章主要介绍刷题常用的数据结构,并就每一个数据结构给出一道经典例题:

哈希表

哈希表是减少时间复杂度的利器,合理的哈希表实现能够将插入、删除和查找操作都平摊到O(1)的时间复杂度,不过由于需要存储键值对,往往会增加一定的空间复杂度。当题目不太要求空间复杂度,而却对时间复杂度敏感时(这往往是主要情况),可以考虑使用哈希表。

经典例题:两数之和

题目:

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回他们的数组下标。

你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

原题链接

首先考虑一下暴力解法,只要固定数组第一个数,然后遍历后面的数,寻找是否有目标和,再固定第二个数,继续往后查找,看看有没有目标和……这个方法保证能找到答案,但由于使用了两重循环,时间复杂度是O(n ^ 2)。

代码如下:
vector<int> twoSum(vector<int>& nums, int target) 
{
    //储存答案的vector
    vector<int> answer;
    //使用双重循环遍历输入数组
    for (int i = 0; i < static_cast<int> (nums.size()); ++i)
    {
        for (int j = i + 1; j < static_cast<int> (nums.size()); ++j)
        {
            //如果元素和等于目标值
            if (nums[i] + nums[j] == target)
            {
                //找到解,返回储存对应下标的vector
                answer.push_back(i);
                answer.push_back(j);
                return answer;
            }
        }
    }
	throw invalid_argument("无解!");
}

执行一下后提交……貌似O(n ^ 2)的时间复杂度还是太高了一点。

LeetCode刷题攻略:常用数据结构(哈希表)_第1张图片
不难看出,不需要在循环的内部再进行循环,因为可以通过哈希表将数组记录下来,只需要查询目标值与当前数的差是否在哈希表内就可以了。首先一遍扫描数组,将当前元素值和对应索引作为键值对加入到哈希表中,如果遇到重复元素,可以考虑更新索引或保留(选哪个都不会对最终结果有任何影响)。然后再第二遍扫描数组,查找目标值与当前数的差是否存在,同时该差对应的索引是否为当前数的索引(若是,证明使用了两次当前数,这是不被允许的)。由于哈希表相关操作都是O(1)时间复杂度,主要复杂度在于遍历数组,即只需要O(n)时间就可以解决问题。

举例子:对于数组【6,1,3,4,3】,目标和为6。第一遍扫描后,哈希表存储的元素-索引对为6->0,1->1,3->4,4->3(注意到3在原数组中出现了两次,这里选择储存最新的索引值4)。第二次扫描数组,依次查找6-6=0,6-1=5,6-3=3是否在哈希表中存在。显然3在哈希表中存在,且索引不为当前索引2,最终找到了答案,返回索引2和4(如果我们选择储存旧的索引值2,则会在遇到最后的3时找到答案,结果不变)。

代码如下:
vector<int> twoSum(vector<int>& nums, int target) 
{
    //储存当前元素和其对应索引的哈希表
    unordered_map<int, int> map;
    //储存答案的vector
    vector<int> answer;
    //第一次遍历输入数组
    for (int i = 0; i < static_cast<int> (nums.size()); ++i)
    {
        //将当前元素和对应索引加入哈希表中
        map[nums[i]] = i;
    }
    //第二次遍历输入数组
    for (int i = 0; i < static_cast<int> (nums.size()); ++i)
    {
        //当前元素需要达成目标的差值
        int complement = target - nums[i];
        //如果找到了该差值
        if (map.find(complement) != map.end() && 
            map.find(complement)->second != i)
        {
            //找到解,返回储存对应下标的vector
            answer.push_back(map.find(complement)->second);
            answer.push_back(i);            
            return answer;
        }
    }
	throw invalid_argument("无解!");
}

虽然我们已经得到了O(n)的解法,但这个写法还不够紧凑。真的需要第一次专门往哈希表中添加数组元素,然后第二次再扫描一遍数组吗?我们能够从第二遍循环中的一个判断寻得蛛丝马迹:为了避免重复使用同一元素,需要判断哈希表中元素对应的索引不是当前元素索引。之所以要增加判断,是因为我们将元素插入哈希表和具体目标和判断分开来做导致的。如果我们在将元素插入之前就考虑哈希表中存不存在差值,就不存在重复元素问题了。

事实上,即使考虑差值存在的时候哈希表并没有存储完数组中的元素,但由于答案是成对存在的(考虑数组【2,1,4】,目标和为6,在遍历到2时哈希表中不存在4,但由于遍历到4时哈希表必然存在2,最终仍会返回正确的结果),仅考虑部分信息也不干扰我们得到正确答案!

代码如下:
vector<int> twoSum(vector<int>& nums, int target) 
{
    //储存当前元素和其所在索引的哈希表
    unordered_map<int, int> map;
    //储存答案的vector
    vector<int> answer;
    //循环遍历输入数组
    for (int i = 0; i < static_cast<int> (nums.size()); ++i)
    {
        //当前元素需要达成目标的差值
        int complement = target - nums[i];
        //如果找到了该差值
        if (map.find(complement) != map.end())
        {
            //找到解,返回储存对应下标的vector
            answer.push_back(map.find(complement)->second);
            answer.push_back(i);            
            return answer;
        }
        else
        {
            //未找到差值,将当前元素
            //和对应索引插入哈希表中
            map[nums[i]] = i;
        }
    }
    throw invalid_argument("无解!");
}

采用优化后的算法后,执行用时显然有所减少(注意到哈希表本身耗费空间,故而内存消耗变高了):
LeetCode刷题攻略:常用数据结构(哈希表)_第2张图片

你可能感兴趣的:(LeetCode刷题攻略)