为了能够通过技术面试,“刷题”可以说是求职路上避不开的一道坎了。随着像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)的时间复杂度还是太高了一点。
不难看出,不需要在循环的内部再进行循环,因为可以通过哈希表将数组记录下来,只需要查询目标值与当前数的差是否在哈希表内就可以了。首先一遍扫描数组,将当前元素值和对应索引作为键值对加入到哈希表中,如果遇到重复元素,可以考虑更新索引或保留(选哪个都不会对最终结果有任何影响)。然后再第二遍扫描数组,查找目标值与当前数的差是否存在,同时该差对应的索引是否为当前数的索引(若是,证明使用了两次当前数,这是不被允许的)。由于哈希表相关操作都是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("无解!");
}