小哆啦解题记:三数之和问题的探索之旅

100 阅读6分钟

小哆啦解题记:三数之和问题的探索之旅

小哆啦开始刷力扣的第二十四天

15. 三数之和 - 力扣(LeetCode)

问题背景

小哆啦最近被一个算法难题搞得脑袋都快炸啦!题目是给定一个整数数组 nums,要找出所有和为 0 且不重复的三元组 [nums[i], nums[j], nums[k]],还得满足 i != ji != 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 的力量,再到最终掌握双指针法这个大杀器,小哆啦不仅学会了如何解决三数之和这个具体的问题,更领悟了算法优化的核心奥义。他知道,在未来的编程世界里,还会有无数的难题等着他去挑战,但只要有像小智这样的良师益友相伴,有不断探索和学习的热情,他就有信心战胜一切困难,成为一名顶尖的编程高手!而且呀,说不定下次遇到新的难题,小哆啦还能反过来给小智出出主意呢,嘿嘿!