在日常的算法学习中,双指针技巧是非常常见且实用的思路。今天我们就以 LeetCode 第 977 题「有序数组的平方」为例,详细讲解如何用双指针高效解决问题,并带你体会算法优化的乐趣。
题目描述
给定一个按非递减顺序排序的整数数组 nums,数组中的每个元素都可以是负数、零或正数。请你返回一个新的数组,数组中的每个元素是原数组对应元素的平方,并且按非递减顺序排序。
示例:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
暴力解法
最直接的思路是:先将每个元素平方,然后对结果数组进行排序。代码实现非常简单:
var sortedSquares = function(nums) {
return nums.map(x => x * x).sort((a, b) => a - b);
};
这种方法虽然能解决问题,但时间复杂度为 O(nlogn),因为排序的开销较大。有没有更优雅高效的办法呢?
双指针法:O(n) 的线性解法
观察题目,我们发现原数组已经是有序的,但由于存在负数,平方后顺序可能会被打乱。例如,-4 的平方比 3 的平方大。因此,平方后的最大值一定出现在数组的两端(最左或最右)。
基于这个特点,我们可以用「双指针」法:
- 定义两个指针,
i指向数组头部(最左),j指向数组尾部(最右)。 - 新建一个结果数组
res,从后往前填充(因为每次比较的最大平方值应该放在末尾)。 - 每次比较
nums[i]和nums[j]的平方,谁大就把谁的平方放到res的末尾,并移动对应指针。 - 重复上述过程,直到两个指针相遇。
代码如下:
var sortedSquares = function(nums) {
const n = nums.length;
const res = new Array(n);
let i = 0, j = n - 1, k = n - 1;
while(i <= j) {
const left = nums[i] * nums[i];
const right = nums[j] * nums[j];
if (left > right) {
res[k--] = left;
i++;
} else {
res[k--] = right;
j--;
}
}
return res;
};
代码详解
i = 0, j = n - 1:分别指向数组的头和尾。k = n - 1:结果数组的末尾索引。- 每次循环比较头尾的平方,较大的放到
res[k],然后移动指针和k。 - 直到
i > j,所有元素都已放入结果数组。
为什么要从后往前填?
因为平方后的最大值一定在两端产生,所以我们每次都把最大值放到结果数组的最后面,避免了排序的需要。
时间复杂度分析
- 整个过程只需遍历一遍数组,时间复杂度为 O(n)。
- 空间复杂度为 O(n),用于存放结果数组。
总结
这道题充分体现了双指针的巧妙应用。通过分析数据的特点,我们可以将原本需要排序的 O(nlogn) 问题,优化为 O(n) 的线性时间复杂度。类似的技巧在处理有序数组、链表等问题时非常常见,值得大家多加练习和体会。
希望这篇文章能帮助你更好地理解双指针的思想,也欢迎你将这份技巧应用到更多的算法题中!