16. 最接近的三数之和 给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。 返回这三个数的和。 假定每组输入只存在恰好一个解。
方法一: 看到这道题目想到的解题思路是: 三轮循环, 把第二轮循环和第三轮循环通过另外的自定义方法实现, 在threeSumClosest中的第一轮循环中调用。 首先是, 如果数组的长度<=3, 那么直接return; 然后, 进行三大轮循环, 比较差值, 得出最小差值, 并通过map返回最小差值(key)对应的三数之和(value)。
class Solution {
// 静态属性Map; k为差值, v为三数之和
static Map<Integer, Integer> map = new HashMap<>();
public int threeSumClosest(int[] nums, int target) {
int length = nums.length;
// 给出的数组长度<=3时, 直接返回。
if (length <= 3) {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += nums[i];
}
return sum;
}
int sum = 0; // 三数之和的初始值
int firstSum = nums[0] + nums[1] + nums[2];
int minNum = Math.abs(target - firstSum); // 最小差值的初始值
for (int i = 0; i < length - 2; i++) {
minNum = Math.min(minNum, getMinNum(nums, i, firstSum, target));
// 如果数组中有三个数的和正好是target, 直接返回。
if (minNum == 0) {
break;
}
}
sum = map.get(minNum);
// map.forEach((k, v) -> System.out.println(k + ":" + v)); 辅助debug的方法
return sum;
}
public int getMinNum(int[] nums, int fromIndex, int firstNum, int target) {
int sum = firstNum; // 三数之和的初始值
int minNum = Math.abs(firstNum - target); // 最小差值的初始值
for (int i = fromIndex + 1; i < nums.length - 1; i++) {
for (int j = i + 1; j < nums.length; j++) {
sum = Integer.sum(nums[i] + nums[j], nums[fromIndex]);
minNum = Math.min(minNum, Math.abs(sum - target));
// System.out.println(nums[fromIndex] + " " + nums[i] + " " + nums[j] + " " + sum); 辅助debug的方法
map.put(Math.abs(sum - target), sum);
if (minNum == 0) {
break;
}
}
}
return minNum;
}
}
超出时间限制 总结缺点: 暴力解题, 太暴力了! 因为无序, 循环起来毫无章法, 想到可以先给数组sort排序。 -> 方法二
方法二: 思路: 先排序,过滤掉出现超过3次的; 每一轮数字的循环都要跳过相同的数字; 确定第一个数字, 再确定第二个数字, 寻找和绝对值最近的数字。 因为暴力解决超出时间限制, 所以想到先对数组进行sort排序; 但是在解题过程中没有想到实施性强的过滤三次的方法, 所以在每一大轮循环中可以过滤掉和上一个角标数值相同的角标; 确定第一个数字, 再确定第二个数字, 得出前两个数字的和, 用target-(num1+num2), 得出idealThirdNum, 也就是理想状态下第三个数的数值; 在第三个数字的循环中, 找最邻近idealThirdNum的数字: 此时是先定下来前两个数字, 用单指针一个个查找最接近idealThirdNum的值; 因为数组是已经排好序的, 如果能指向的最小的数大于差值, 那么后面的数就更不用比了; 如果在循环中遇到了第一个等于或者大于idealThirdNum的数值, 同理, 后面的数也不用比了; 如果在循环中能指向的所有数值都小于idealThirdNum, 也就是没能进入if()体, 根据isFlag标识, 在该次循环结束后将最后一个角标的值作为第三个数, 计算差值和三数之和, 存入map。 第三个数字的循环小结: 在前两个数字确定的情况下, 只往map中存入最接近的那组数据, 即 存入的要么是根据 1.首次遇到的等于或者大于idealThirdNum 所得出的k-v的, 要么就是根据 2.排序后数组中最后一个数(数组中的最大值)作为第三个数 所得出的k-v的。
class Solution {
static Map<Integer, Integer> map = new HashMap<>();
public int threeSumClosest(int[] nums, int target) {
int length = nums.length;
if (length <= 3) {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += nums[i];
}
return sum;
}
int minNum = Math.abs(nums[0] + nums[1] + nums[2] - target); // 最小差值的初始值
Arrays.sort(nums); // 对数组进行排序
for (int i = 0; i < nums.length - 2; i++) {
// 过滤掉第一轮重复的数值
if (i != 0) {
if (nums[i] == nums[i - 1]) {
continue;
}
}
int num = getMinNum(nums, target, i); // 每一轮最小差值
minNum = Math.min(minNum, num);
}
return map.get(minNum);
}
// 返回最小差值
public int getMinNum(int[] nums, int target, int fromIndex) {
//每轮最小值起始以每轮的第一组开始
int sum = nums[fromIndex] + nums[fromIndex + 1] + nums[fromIndex + 2];
int subtract = Math.abs(sum - target);//差值
int minNum = subtract;
map.put(subtract, sum);
for (int i = fromIndex + 1; i < nums.length - 1; i++) {
// 过滤掉第二轮重复的数值
if (i != fromIndex + 1) {
if (nums[i] == nums[i - 1]) {
continue;
}
}
int twoSum = nums[fromIndex] + nums[i]; // 前两个数的和
int idealThirdNum = target - twoSum; // 理想中的第三个数(差值为0)
boolean isFlag = true; //判断第三个数组有没有等于或者大于差值的
for (int j = i + 1; j < nums.length; j++) {
// 过滤掉第三轮重复的数值
if (j != i + 1) {
if (nums[j] == nums[j - 1]) {
continue;
}
}
if (nums[j] >= idealThirdNum) {
isFlag = false; // 检验该轮循环是否进入过if体内
sum = nums[fromIndex] + nums[i] + nums[j];
subtract = Math.abs(target - sum);
map.put(subtract, sum);
minNum = Math.min(minNum, subtract);
break;
/* 由于数组是排好序的, 一旦遇到第三个数大于idealThirdNum理想数的,
就可以把当前subtract和sum存入map, 跳出最内轮的循环。
如果遇到等于理想数的最好, 直接return。
*/
}
}
// 如果没有等于大于理想第三个数的, 取其中最大的数字
if (isFlag) {
sum = nums[fromIndex] + nums[i] + nums[nums.length - 1];
subtract = Math.abs(target - sum);
map.put(subtract, sum);
minNum = Math.min(minNum, subtract);
}
}
return minNum;
}
}
虽然执行通过, 但是执行用时215ms, 速度太差。 总结缺点: 1.单指针, 只是第三个数字有指针从前往后查找, 对于某一轮所指向的数值都小于idealThirdNum的情况, 效率很差。 2.此时只有第三个数字在动, 想一下如果第二个数字和第三个数字都在动, 效率会提升。如何实现? -> 方法三
方法三:
思路: 使用双指针 1.该题中对双指针的使用方法是: 第二个数字和第三个数字分别持有一个指针, 先指到各自的极端(最左与最右); 三数之和大了就右指针(第三个数)往左挪, 使sum减小; 三数之和小了就左指针(第二个数)往右挪, 使sum增大。 2.不需要map存储, 只需要在得到新的minNum时, 将minSum也同时修改为新的三数之和即可。 热评: 双指针: 先确定第一个数字的角标i, 左指针指到i+1, 右指针指到length-1, 计算三数之和与target的关系: 1.sum>target, 说明大了, 右指针往左挪 2.sum<target, 说明小了, 左指针往右挪 3.sum==target, 直接return
class Solution {
public int threeSumClosest(int[] nums, int target) {
int length = nums.length;
if (length <= 3) {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += nums[i];
}
return sum;
}
Arrays.sort(nums); // 对数组排序
int sum = nums[0] + nums[1] + nums[2];
int minNum = Math.abs(sum - target);
int minSum = sum;
for (int i = 0; i < nums.length - 2; i++) {
if (i != 0) {
if (nums[i] == nums[i - 1]) {
continue;
}
}
// 左右指针初始位置
int left = i + 1;
int right = nums.length - 1;
while (left < right) {
sum = nums[i] + nums[left] + nums[right];
if (minNum > Math.abs(sum - target)) {
minNum = Math.abs(sum - target);
minSum = sum;
}
if (sum == target) {
return sum;
} else if (sum > target) {
right--;
} else {
left++;
}
}
}
return minSum;
}
}
总结: 1.先对数组进行排序; 2.学会使用双指针。