小哆啦解题记:三数之和问题的探索之旅
小哆啦开始刷力扣的第二十四天
问题背景
小哆啦最近被一个算法难题搞得脑袋都快炸啦!题目是给定一个整数数组 nums,要找出所有和为 0 且不重复的三元组 [nums[i], nums[j], nums[k]],还得满足 i != j、i != k 且 j != k。小哆啦对着代码抓耳挠腮,活像一只被困在迷宫里的小仓鼠,急得团团转。没办法,他只好向聪明绝顶的小智求助。
初次尝试:三层 for 循环
小智拍了拍小哆啦的肩膀,笑嘻嘻地说:“小哆啦,别慌!咱们先用最笨的办法,就像撒大网捕鱼一样,用三层 for 循环把数组里所有可能的三元组都捞出来,再瞅瞅它们的和是不是 0。” 小哆啦一听,眼睛顿时亮了起来,像打了鸡血似的,立刻用 TypeScript 实现了这个方法:
function threeSum(nums: number[]): number[][] {
const result: number[][] = [];
const n = nums.length;
// 对数组进行排序
nums.sort((a, b) => a - b);
for (let i = 0; i < n; i++) {
// 跳过重复的元素
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
for (let j = i + 1; j < n; j++) {
// 跳过重复的元素
if (j > i + 1 && nums[j] === nums[j - 1]) {
continue;
}
for (let k = j + 1; k < n; k++) {
// 跳过重复的元素
if (k > j + 1 && nums[k] === nums[k - 1]) {
continue;
}
if (nums[i] + nums[j] + nums[k] === 0) {
result.push([nums[i], nums[j], nums[k]]);
}
}
}
}
return result;
}
// 示例用法
const nums: number[] = [-1, 0, 1, 2, -1, -4];
console.log(threeSum(nums));
复杂度分析
小智像个小老师一样,开始给小哆啦分析起来:“你瞧哈,这个方法就像一只慢吞吞的蜗牛,用了三层嵌套的 for 循环,每个元素都得被折腾好多次,所以时间复杂度是 。而空间复杂度呢,主要取决于 sort 方法,一般是 ,但有时候可能会变成 ,就像一个调皮的孩子,说变就变。”
遇到的问题
小哆啦满心欢喜地运行了代码,结果虽然正确,可当他用一个老长老长的数组测试时,程序就像一只陷入泥潭的乌龟,运行得超级慢。小哆啦急得直跺脚,像个热锅上的蚂蚁,赶紧问小智:“这速度也太不给力了,有没有啥办法能让它跑快点呀?”
第一次优化:使用 Map
小智神秘兮兮地笑了笑,说:“咱们可以请 Map 这个小帮手来帮忙,它能让查找过程像坐火箭一样快,这样就能减少一层循环啦。你先固定一个数,然后在剩下的数里找两个数,让它们的和等于这个固定数的相反数。” 小哆啦听了,像得到了武林秘籍一样,兴奋地按照小智的思路,写出了优化后的代码:
function threeSum(nums: number[]): number[][] {
const result: number[][] = [];
const n = nums.length;
// 对数组进行排序
nums.sort((a, b) => a - b);
for (let i = 0; i < n - 2; i++) {
// 跳过重复的元素
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
const target = -nums[i];
const numMap = new Map<number, number>();
for (let j = i + 1; j < n; j++) {
const complement = target - nums[j];
if (numMap.has(complement)) {
result.push([nums[i], complement, nums[j]]);
// 跳过重复的元素
while (j + 1 < n && nums[j] === nums[j + 1]) {
j++;
}
}
numMap.set(nums[j], j);
}
}
return result;
}
// 示例用法
const nums2: number[] = [-1, 0, 1, 2, -1, -4];
console.log(threeSum(nums2));
复杂度分析
小智又开始滔滔不绝地讲解起来:“现在用了两层循环,而且 Map 的查找操作就像闪电一样快,时间复杂度是 ,所以整体时间复杂度就降到了 。不过呢,因为要给 Map 这个小帮手安排住处,空间复杂度就变成了 。”
优化效果
小哆啦再次运行代码,哇塞!程序的运行速度就像坐上了高铁,明显变快了。小哆啦高兴得一蹦三尺高,像个得到了糖果的小孩,拉着小智的手说:“哇,优化之后简直太牛啦,谢谢你,小智!你就是我的超级英雄!”
进一步优化:双指针法
小智看着小哆啦那兴奋的样子,笑着说:“小哆啦,咱们还能让程序跑得更快,试试双指针法怎么样?就像两个小伙伴在数组的两端玩接力游戏,根据三数之和和 0 的大小关系移动指针。” 小哆啦在小智的引导下,像个勇敢的小战士,写出了双指针法的代码:
function threeSum(nums: number[]): number[][] {
const result: number[][] = [];
const n = nums.length;
// 对数组进行排序
nums.sort((a, b) => a - b);
for (let i = 0; i < n - 2; i++) {
// 跳过重复的元素
if (i > 0 && nums[i] === nums[i - 1]) {
continue;
}
let left = i + 1;
let right = n - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
// 跳过重复的元素
while (left < right && nums[left] === nums[left + 1]) {
left++;
}
while (left < right && nums[right] === nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
// 示例用法
const nums3: number[] = [-1, 0, 1, 2, -1, -4];
console.log(threeSum(nums3));
复杂度分析
小智最后总结道:“排序的时间复杂度是 ,双指针遍历的时间复杂度是 ,总体时间复杂度还是 。而空间复杂度主要取决于排序算法,一般是 。这个方法就像一辆既快又省油的赛车,不仅时间复杂度没变,还不需要额外的空间给 Map 住,在空间使用上更高效呢。”
优化效果
小哆啦再次测试代码,程序就像一颗出膛的子弹,运行得飞快,而且占用的内存也更少了。小哆啦激动得眼泪都快出来了,紧紧抱住小智,说:“小智,你简直就是天才!我从中学到了好多算法优化的知识,以后我也要像你一样聪明!”
结语
经过这次和小智一起攻克算法难题的奇妙之旅,小哆啦感觉自己就像一个升级打怪成功的勇士,自信心爆棚。他深刻地明白了算法优化就像给汽车改装发动机,能让程序的性能发生翻天覆地的变化。每一次优化都是一次智慧的较量,就像在迷宫中不断寻找那条最短、最顺畅的出路。
从最初像无头苍蝇一样用三层 for 循环,到后来巧妙地借助 Map 的力量,再到最终掌握双指针法这个大杀器,小哆啦不仅学会了如何解决三数之和这个具体的问题,更领悟了算法优化的核心奥义。他知道,在未来的编程世界里,还会有无数的难题等着他去挑战,但只要有像小智这样的良师益友相伴,有不断探索和学习的热情,他就有信心战胜一切困难,成为一名顶尖的编程高手!而且呀,说不定下次遇到新的难题,小哆啦还能反过来给小智出出主意呢,嘿嘿!