文章目录
test88.合并两个有序数组
题目如下:
示例1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
解题思路:
这道题,基本上是简单粗暴,先将nums2添加到nums1中,然后进行排序。
Java代码:
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int a = 0;
for (int i=m;i<nums1.length;i++) {
if (a<n) {
nums1[i] = nums2[a];
a++;
}
}
Arrays.sort(nums1);
}
}
JavaScript代码:
/**
* @param {number[]} nums1
* @param {number} m
* @param {number[]} nums2
* @param {number} n
* @return {void} Do not return anything, modify nums1 in-place instead.
*/
var merge = function(nums1, m, nums2, n) {
let a = 0
for (let i = m; i < m + n; i++) {
if (a < n) {
nums1[i] = nums2[a];
a++;
}
}
nums1.sort((a, b) => a - b);
};
test11. 盛最多水的容器
题目如下:
示例1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例2:
输入:height = [1,1]
输出:1
解题思路:
最开始我想的比较直观,直接通过双重for循环将每一种可能出现的结果存到list集合中,然后对其进行排序取最大值。就在我以为这道题就这么“轻松”地解出来的时候,时间复杂度告诉我——我果然是想得太简单了,超出了时间限制,呜呜呜呜~
后来看了一些题解之后,才知道原来有个“双指针解法”。
-
通过示例中的图解可以得出:储水面积=两个指针指向的数字中较小值∗指针之间的距离。
设两个指针分别为i,j,即水槽板高度为height[i],height[j],储水有效高度为height[i],height[j]中最短的水槽板高度,指针之间的距离为(j-i);
因此可以推出公式:area = min( height[i] , height[j] ) * ( j - i );
此时我们得到的数据中有两个变量:- 储水高度:两个指针指向的数字中较小值
- 储水宽度:两个指针之间的距离
-
要使储水量面积最大,储水高度尽可能最大,储水宽度尽可能最大。但是在不知道指针指向的数字是多少的情况下,我们只能先让宽度取最大值,即取首末两端指针(作为容器边界的所有位置的范围)。那么接下来该如何移动水槽板取值呢?在首末端指针向中间移动取值的过程中,储水的宽度必然会变小,然而在同等缩小的条件下我们需要考虑到两种情况:
- 如果变换短板 ,min( height[i] , height[j] ) 可能变大,则下个水槽的储水面积可能增大 。
- 如果变换长板 ,min( height[i] , height[j] ) 不变或变小,则下个水槽的储水面积一定变小 。
所以,对应数字较小的那个指针就不可能作为容器的边界了。
不得不承认,这个解法真的很——优雅,实在是优雅。
Java代码:
class Solution {
// 超出时间限制————
// public static int maxArea(int[] height) {
// ArrayList<Integer> list = new ArrayList<Integer>();
// for (int i=0;i<height.length;i++) {
// for (int j=i;j< height.length;j++) {
// if (height[i]>height[j]) {
// list.add(height[j] * (j-i)) ;
// } else {
// list.add(height[i] * (j-i)) ;
// }
// }
// }
// Collections.sort(list);
// return list.get(list.size()-1);
// }
// 采用双指针的解法
public static int maxArea(int[] height) {
int i = 0;
int j = height.length-1;
int area = 0;
int max = 0;
while(i<j) {
area = Math.min(height[i],height[j])*(j-i);
max = Math.max(area,max);
if (height[i]<=height[j]) {
i++;
} else {
j--;
}
}
return max;
}
}
JavaScript:
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let i = 0;
let j = height.length - 1;
let area = 0;
let max = 0;
while (i < j) {
area = Math.min(height[i], height[j]) * (j - i)
max = Math.max(area, max)
if (height[i] <= height[j]) {
i++;
} else {
j--;
}
}
return max;
};
test15. 三数之和
示例1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。
示例2:
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。
示例3:
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。
解题思路:
看到这道题第一眼,我原是“不屑”的,这么简单的题,让我一下子就有了“灵感”。于是乎,直接三层for循环加if条件限制,然后通过排序进行数组去重。但是!如此“暴力”的解决方式,自然是不被允许的啊~ 毕竟,大家都是“文明人”。
最简单的题型往往要采用“优雅”的解决方式——没错,又是“双指针解法”。
题中所需要满足的条件:
- 三元组满足: i!=j,i!=k,j!=k
- nums[i] + nums[j] + nums[k] == 0
- 我们先对数组进行排序,此时的排序在接下来的步骤中有一个重要的作用。(官方题解中排序还有一个作用:用于判断该数组最小的数是否大于0,如果大于0就不用进行后续操作,直接输出为[],我的解题代码中并未写出此点)
- 然后进行遍历,取该数组的a+1位和最后一位作为指针,而第a位则作为不变项,且两指针不会重复。
- 将满足条件的三元组添加到list集合中进行排序,方便去重。
- 那么两个指针该如何移动呢?此时,我们最开始的排序操作开始发挥它的功能了——
- 当满足 nums[a] + nums[a+1] + nums[nums.length-1] == 0,我们就不需要考虑是否存在nums[a+1] ==nums[a+2]或者nums[nums.length-1]==nums[nums.length-2],因为需要数组去重,所以我们让两个指针都往中间移动。
- 当满足 nums[a] + nums[a+1] + nums[nums.length-1] > 0时,如果指向 nums[a+1] 的指针往中间移动,那么得到的结果有一种可能>0;而只有当指向 nums[nums.length-1] 的指针往中间移动,得到的结果可能会<0,==0。基于此,我们只能移动 nums[nums.length-1] 的指针。
- 同理可得,当满足 nums[a] + nums[a+1] + nums[nums.length-1] < 0时,说明得到的和太小了,我们要想让它==0,只能移动 nums[a+1] 指针,才能让它变大。
Java代码:
class Solution {
// 超出时间限制————
// public static List<List<Integer>> threeSum(int[] nums) {
// List<List<Integer>> lists = new ArrayList<>();
// for (int i = 0;i<nums.length;i++) {
// for (int j = i;j<nums.length;j++) {
// for (int k = j;k< nums.length;k++) {
// if (nums[i] + nums[j] + nums[k]==0 && i!=j && j!=k && i!=k) {
// List list = new ArrayList<Integer>();
// list.add(nums[i]);
// list.add(nums[j]);
// list.add(nums[k]);
// Collections.sort(list);
// if (!lists.contains(list)) {
// lists.add(list);
// }
// }
//
// }
// }
// }
// return lists;
// }
// 排序 + 双指针解法
public static List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> lists = new ArrayList<>();
Arrays.sort(nums);
for (int a=0;a< nums.length;a++) {
int b = a+1;
int c = nums.length-1;
while(b<c) {
if (nums[a]+nums[b]+nums[c]==0&&a!=b&&b!=c&&c!=a) {
List list = new ArrayList<Integer>();
list.add(nums[a]);
list.add(nums[b]);
list.add(nums[c]);
Collections.sort(list);
if (!lists.contains(list)) {
lists.add(list);
}
b++;
c--;
} else if (nums[a]+nums[b]+nums[c]>0) {
c--;
} else if (nums[a]+nums[b]+nums[c]<0) {
b++;
}
}
}
return lists;
}
}
JavaScript代码:
/**
* @param {number[]} nums
* @return {number[][]}
*/
var threeSum = function(nums) {
let lists = [];
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
let j = i + 1;
let k = nums.length - 1;
while (j < k) {
if (nums[i] + nums[j] + nums[k] == 0 && i != j && j != k && k != i) {
let list = [nums[i], nums[j], nums[k]];
lists.push(list);
while (j < k && nums[j] == nums[j + 1]) j++;
while (j < k && nums[k] == nums[k - 1]) k--;
j++;
k--;
} else if (nums[i] + nums[j] + nums[k] > 0) {
k--;
} else if (nums[i] + nums[j] + nums[k] < 0) {
j++;
}
}
}
return lists;
};
test16. 最接近的三数之和
示例1:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。
示例2:
输入:nums = [0,0,0], target = 1
输出:0
解题思路:
看到这道题很难不让我“会心一笑”啊,在经历了前两道“超出时间限制”的警告下,我不得不承认“优雅永不过时”。这不是摆明了要我用“双指针解法”嘛。
- 首先,我们先来对该数组进行排序。
- 取一个变量m存放和target最接近的值(对m初始化时,需要保证,target不会取到该值)
- 接着,进行遍历,取该数组的i+1位和最后一位作为指针,而第 i 位则作为不变项,且两指针不会重复。
- 采用两数相减取绝对值的方式,来判断哪一个和与target最接近。
- 重头戏来了!那么两个指针该如何移动咧——
- 当 sum>target 时,那么我们需要让sum变小,便向中间移动指向 nums[nums.length-1]的指针;
- 当 sum<target 时,那么我们需要让sum变大,便向中间移动指向 nums[i+1]的指针;
- 当 sum==target时,那么还犹豫什么呢?直接跳出遍历,输出 m 啊!
Java代码:
class Solution {
public static int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int m = 10000000;
for (int i=0;i<nums.length;i++) {
int a = i+1;
int b = nums.length-1;
while(b>a) {
int sum = nums[i]+nums[a]+nums[b];
if (sum>target) {
m = Math.abs(m-target)<Math.abs(sum-target)?m:sum;
b--;
} else if (sum<target) {
m = Math.abs(m-target)<Math.abs(sum-target)?m:sum;
a++;
} else if(sum==target) {
m=sum;
break;
}
}
}
return m;
}
}
JavaScript代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var threeSumClosest = function(nums, target) {
let m = 10000000;
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length; i++) {
let a = i + 1;
let b = nums.length - 1;
while (a < b) {
let sum = nums[i] + nums[a] + nums[b];
if (sum > target) {
m = Math.abs(m - target) < Math.abs(sum - target) ? m : sum;
b--;
} else if (sum < target) {
m = Math.abs(m - target) < Math.abs(sum - target) ? m : sum;
a++;
} else if (sum == target) {
m = sum;
break;
}
}
}
return m;
};
test4. 寻找两个正序数组的中位数
示例1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
解题思路:
咳咳,我是带着勇气做这道题的,因为它标的难度是“困难”,而且我在看题时,有被那个时间复杂度给吓到(众所不知,对于一个“算法小白”,时间和空间复杂度一直是我的“心头之患”。)
但是,我还是抱着try一try的心态尝试了一下。
我们先来看题中给的条件和要求:
- 所给的数组都是从小到大排序
- 返回这两个数组的中位数
- 首先,我们先将两个数组进行合并;
- 然后,对合并后的数组进行排序;
- 接着,我们需要考虑两种情况:
- 当得到的新数组长度是偶数时,我们需要取到最中间的两位数nums[nums.length/2-1],nums[nums.length/2],中位数取这两位和的平均值。
- 当得到的新数组长度是奇数时,我们直接取最中间的那位数作为中位数就行。
按照以上思路,在我一顿操作提交运行下,竟然“顺利”地通过了。说实话,有被“惊讶”到!
Java代码:
class Solution {
public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
int[] nums = new int[nums1.length+ nums2.length];
for (int i=0;i<nums1.length;i++) {
nums[i] = nums1[i];
}
int j = nums1.length;
for (int i=0;i<nums2.length;i++) {
nums[j++] = nums2[i];
}
double res = 0;
Arrays.sort(nums);
if (nums.length%2==0) {
int a = nums.length/2-1;
int b = nums.length/2;
res = (double) (nums[a]+nums[b])/2;
} else {
int a = nums.length/2;
res = nums[a];
}
return res;
}
}
JavaScript代码:
/**
* @param {number[]} nums1
* @param {number[]} nums2
* @return {number}
*/
var findMedianSortedArrays = function (nums1, nums2) {
let nums = [];
for (let i = 0; i < nums1.length; i++) {
nums[i] = nums1[i];
}
let j = nums1.length;
for (let i = 0; i < nums2.length; i++) {
nums[j++] = nums2[i];
}
let res = 0;
nums.sort((a, b) => a - b);
if (nums.length % 2 == 0) {
let a = parseInt(nums.length / 2 - 1);
let b = parseInt(nums.length / 2);
res = (nums[a] + nums[b]) / 2;
} else {
let a = parseInt(nums.length / 2);
res = nums[a];
}
return res;
};
test18. 四数之和
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
解题思路:
这道题和前面的 test15.三数之和 类似,唯一不同的是此题求的是 四数之和,但是解法都一样。
本题我们需要考虑到是否会数据类型范围。看下面代码注释掉的一行是我原本写的代码,执行之后发现它爆红了!!!
Java代码:
class Solution {
public static List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> lists = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0;i<nums.length-3;i++) {
for (int j = i+1;j<nums.length-2;j++) {
int k = j+1;
int l = nums.length-1;
while (k<l) {
// int sum = nums[i]+nums[j]+nums[k]+nums[l]; -- 空间复杂度
long sum =(long) nums[i]+nums[j]+nums[k]+nums[l];
if (sum>target) {
l--;
} else if (sum<target) {
k++;
} else if (sum==target&&i!=j&&j!=k&&k!=l&&l!=i) {
List list = new ArrayList<Integer>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
list.add(nums[l]);
Collections.sort(list);
if (!lists.contains(list)) {
lists.add(list);
}
l--;
k++;
}
}
}
}
return lists;
}
}
JavaScript代码:
/**
* @param {number[]} nums
* @param {number} target
* @return {number[][]}
*/
var fourSum = function (nums, target) {
let lists = []
nums.sort((a, b) => a - b);
for (let i = 0; i < nums.length - 3; i++) {
if (i > 0 && nums[i] === nums[i - 1]) continue;
for (let j = i + 1; j < nums.length - 2; j++) {
if (j > i + 1 && nums[j] === nums[j - 1]) continue;
let k = j + 1;
let l = nums.length - 1;
while (k < l) {
let sum = nums[i] + nums[j] + nums[k] + nums[l];
if (sum > target) {
l--;
} else if (sum < target) {
k++;
} else if (sum == target && i != j && j != k && k != l && l != i) {
while (k < l && nums[k] == nums[k + 1]) k++;
while (k < l && nums[l] == nums[l - 1]) l--;
lists.push([nums[i], nums[j], nums[k], nums[l]]);
l--;
k++;
}
}
}
}
return lists;
};
test31. 下一个排列
示例1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例3:
输入:nums = [1,1,5]
输出:[1,5,1]
解题思路:
我觉得这道题其实挺难的,甚至一开始没怎么理解题意,反复看了好几遍之后,才大概懂了它问的是什么——数组的排列组合其实也是有顺序的,看题目中举的例子其实说的已经很详细了。
我一开始想的解法就是——
- 找到交换数:先从这个数组对应[下标大]的开始往前判断,即从nums[nums.length-1]开始往前顺,从后往前找到第一个非递增的数,确定这个数作为交换数;
- 找跟它做交换的数,即被交换数,从我们刚刚遍历过来的数先找出所有比它大的数,从中找到与它最接近的来和它交换;
- 然后对交换数对应下标往后的所有数(即遍历过的数)进行升序排序。
事实证明,我的想法应该没问题,但用于实践好像不太行,我改了好几遍代码还是有十几条案例未通过,没有办法,我还是求助了解析。
正解思路:“双指针解法”——
我们先来想,要满足题意需要如何去做,我们需要使当前排列在此排列的基础上尽可能小的变大,那就需要将数组左边的一个较小数与数组右边的较大数交换。同时,要求这个较小数尽可能靠右,较大数尽可能小。
- 找到交换数:先通过while循环,从后往前找到第一个非递增的数;
- 找到被交换数:再次通过循环遍历,从后往前找到第一个大于交换数的数;
交换完成后,较大数右边的数需要升序排列。
所以,大家能看懂此解法如何巧妙地使用“双指针法”了嘛?
Java错误代码:
public static int[] nextPermutation(int[] nums) {
int[] numsa = new int[nums.length];
for (int i=0;i<numsa.length;i++) {
numsa[i] = -100000;
}
int flag = 0;
for (int i=nums.length-1;i>=1;i--) {
if (nums[i] <= nums[i-1]) {
flag++;
numsa[i] = nums[i];
} else if (nums[i]>nums[i-1]) {
numsa[i] = nums[i];
int renum = nextClosest(numsa,nums[i-1]);
int temp = nums[i-1];
nums[i-1] = nums[renum];
nums[renum] = temp;
nextReverse(nums,i);
break;
}
}
if (flag==nums.length-1) {
nums = nextReverse(nums,0);
}
return nums;
}
public static int nextClosest(int[] numsa,int count) {
int min = 0;
for (int i=1;i<numsa.length;i++) {
if (numsa[i]!=-100000 && numsa[i]>count) {
min = Math.abs(numsa[i]-count)<Math.abs(numsa[i-1]-count)?i:i-1;
}
}
return min;
}
public static int[] nextReverse(int[] nums,int start) {
int end = nums.length-1;
// while (nums[start]>nums[end] && start<nums.length) {
while (end>start && nums[start]>nums[end]) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start ++;
end--;
}
return nums;
}
Java正确代码:
class Solution {
public void nextPermutation(int[] nums) {
int litter = nums.length - 2;
while (litter >= 0 && nums[litter] >= nums[litter + 1]) {
litter--;
}
if (litter >= 0) {
int bigger = nums.length - 1;
while (bigger >= 0 && nums[bigger] <= nums[litter]) {
bigger--;
}
swap(litter, bigger, nums);
}
reverse(litter + 1, nums);
}
public void swap(int i, int j, int[] nums) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public void reverse(int start, int[] nums) {
int end = nums.length - 1;
while (start < end) {
swap(start, end, nums);
start++;
end--;
}
}
}
果然做算法需要细心和耐心,还需要多方面考虑。上面的代码虽然最终都运行通过了,但仔细看的话,还是不够严谨。如果你有更好的解法,请和我分享叭~
如有错误,请指正!