题目:本题出自力扣第八十八题,合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n。
示例 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 中。
提示:
nums1.length == m + n
nums2.length == n
0 <= m, n <= 200
1 <= m + n <= 200
-109 <= nums1[i], nums2[j] <= 109
进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
解法1、吃现成饭
首先上来分析一下题目,要合并两个数组,而且合并完了之后你还得返回一个数组是有序的。
然后呢,人家说了,返回的数组要存在数组1中,而且数组1的有效长度是m,数组2的有效长度是n,而且数组1总长度是m+n,其余n个是存的0占位。最终就是把数组2合并去数组1中就完了。
像我这种没技术的,就是一个循环,遍历数组2把数组2的每个元素扔到数组1中,最后用java的那个排序api混合一下就好了。吃点JDK的现成API。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// ----------java函数,内部是快速排序
// 遍历数组2
for(int i = 0;i<n;i++){
// 把数组2的元素放到数组1后面,挨个放下去到数组1中
nums1[m+i] = nums2[i];
}
// 最后调用java排序方法,直接把数组1排序就完了
Arrays.sort(nums1);
}
}
看下提交结果:
我们看到了,提交只击败了百分之22多点,这就是拉了,具体为啥呢?
我们的程序里面循环就是O(N)的复杂度,复杂度没啥毛病,那就是那个jdk的方法有问题,我们去idea里面点进去这个方法看看。
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
// 再点进去看看
太长了,其实是一个内部是快速排序的方法,其平均的时间复杂度是O(nlogn),所以这个其实就不太好。
为啥呢?因为这个东西人家两个数组其实本质是有序的,你去快排其实还是一遍一遍的走操作。没有利用到这个有序的特征,所以如果你再利用一下这个特点就能再优化一点。
那怎么优化呢?两个数组,涉及顺序,在力扣这块,我说实话,你直接就一个思路,就是双指针。我亲娘,就没有双指针不能解决的问题。
解法2、双指针走数组
既然我们分析了这个操作最后定位为双指针处理,我来说说这个具体落地。
我们先开辟一个新数组,长度是m+n。
每个数组用一个指针标记,然后指针往后走,每走一位和另一个指针的值对比一下。把小的指针的值放进新数组里面,然后这个小的指针往后走,继续比。
最后哪个走完了,就把另一个剩下的直接拼到新数组后面即可。这个新数组,最后循环再赋值给数组1返回就行了。
这样其实就是遍历了三个数组,两个指针的,一个就是最后的新数组遍历赋值给数组1的。其实就是3O(N),也就是O(N)。也就是优化了一些。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// ----------双指针,开辟新数组
// 新数组长度m + n
int newLenth = m + n;
// 开辟新数组
int[] newNum = new int[newLenth];
// 每个数组一个指针,双指针
int indexNums1=0;
int indexNums2=0;
// 遍历新数组
for(int index=0;index<newLenth;index++){
if(indexNums1 >= m){
// 数组1走到头了,把数组2后面的每个都拼到新数组后面就行了
newNum[index] = nums2[indexNums2++];
}else if(indexNums2 >= n){
// 数组2走到头了,把数组1后面的每个都拼到新数组后面就行了
newNum[index] = nums1[indexNums1++];
}else if(nums1[indexNums1] < nums2[indexNums2]){
// 如果指针1的比2的小,那就把1的那个值复制给新数组
newNum[index] = nums1[indexNums1++];
}else{
// 如果指针2的比1的小,那就把2的那个值复制给新数组
newNum[index] = nums2[indexNums2++];
}
}
// 最后循环遍历新数组,赋值给数组1
for(int i = 0;i<newLenth;i++){
nums1[i] = newNum[i];
}
// ----------双指针,原地合并
}
}
这时候就看到他击败百分百用户,突出一个优秀。
但是还有问题,我们开辟了一个新的数组空间,所以空间复杂度只能击败百分之32。那么问题就来了,我们怎么处理一下这个问题呢,我们为啥要这个新空间呢?我们看看这个数组,
两个数组里面那个长的其实后面是0占位的无效位置,这就是空间啊。我这里突然想起了sync锁的偏向的时候,把线程号和锁头做一个交换,就是这样利用原地空间,然后去存储。我们这里完全可以利用这个实现。但是呢,0是在数组1的后面。你要是要存就得把那个大的数字存上去才能做到最后的有序。
那既然处理大的数字,那就得从后面开始遍历,所以其实还是双指针遍历,但是是从后往前。
从后往前走,比较出谁大然后就往后面放,数组2的大就直接放,数组1大的就交换。
解法3、原地合并,无需新空间
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
// ----------双指针,原地合并
int newLenth = m + n;
int indexNums1 = m - 1;
int indexNums2 = n - 1;
for(int index = newLenth - 1;index >= 0;index--){
if(indexNums1 < 0 ){
// 数组1走到头了,把数组2后面的每个都拼到新数组后面就行了
nums1[index] = nums2[indexNums2--];
}else if(indexNums2 < 0 ){
// 数组2走到头了,把数组1后面的每个都拼到新数组后面就行了
nums1[index] = nums1[indexNums1--];
}else if(nums1[indexNums1] > nums2[indexNums2]){
// 如果指针1的比2的小,那就把1的那个值复制给新数组
nums1[index] = nums1[indexNums1--];
}else{
// 如果指针2的比1的小,那就把2的那个值复制给新数组
nums1[index] = nums2[indexNums2--];
}
}
}
}
看下运行结果:
总结:
我们在处理这种有序啊,两个数组啊,两个链表啊,或者是设计到前后问题,前后顺序问题,其实都可以考虑双指针遍历的问题,一前一后做对比。目前只是个理解,后面慢慢在积累中印证补充这个知识点。