「LeetCode」88.合并两个有序数组

192 阅读4分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

题目描述🌍

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn ,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 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

直接合并后排序🚀

解题思路

第一种方法也是最暴力的方法,直接将 nums2[] 追加到 nums1[] 数组尾部,然后直接排序即可完成题意。

虽然思路简单易懂,编码易实现,但完全没有利用两个数组已排序这个性质,所以导致时间复杂度和空间复杂度也是稍微逊色。

代码

Java

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        for (int i = m; i < m + n; i++) {
            nums1[i] = nums2[i - m];
        }
        Arrays.sort(nums1);
    }
}

C++

class Solution {
public:
    void merge(vector<int> &nums1, int m, vector<int> &nums2, int n) {
        for (int i = m; i < m + n; ++i) {
            nums1[i] = nums2[i - m];
        }
        sort(nums1.begin(), nums1.end());
    }
};

时间复杂度:O((m+n)log(m+n))O((m+n)log(m+n)),排序序列长度为 m+nm+n,套用快速排序的时间复杂度即可。

空间复杂度:O(log(m+n))O(log(m+n)),排序序列长度为 m+nm+n,套用快速排序的空间复杂度即可。

三指针法⚡

解题思路

指针法完美借助了非递减顺序排列的特性,同时借助了额外的空间存放排序后的数组。

其中指针 i 指向 nums1[],指针 j 指向 nums2[],比较过程中通过指针 k 不断向新建的有序数组中填充元素。

代码

Java

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = 0;
        int j = 0;
        int[] sorted = new int[m + n];
        for (int k = 0; k < m + n; k++) {
            if (i == m) {
                sorted[k] = nums2[j++];
            } else if (j == n) {
                sorted[k] = nums1[i++];
            } else if (nums1[i] <= nums2[j]) {
                sorted[k] = nums1[i++];
            } else {
                sorted[k] = nums2[j++];
            }
        }
        // 将排序后的数组赋值给nums1[]
        for (int k = 0; k < m + n; k++) {
            nums1[k] = sorted[k];
        }
    }
}

C++

class Solution {
public:
    void merge(vector<int> &nums1, int m, vector<int> &nums2, int n) {
        int p1 = 0;
        int p2 = 0;
        // auxiliary space
        vector<int> sorted(m + n);
        for (int i = 0; i < m + n; ++i) {
            if (p1 == m) {
                sorted[i] = nums2[p2++];
            } else if (p2 == n) {
                sorted[i] = nums1[p1++];
            } else if (nums1[p1] <= nums2[p2]) {
                sorted[i] = nums1[p1++];
            } else {
                sorted[i] = nums2[p2++];
            }
        }
        nums1 = sorted;
    }
};

时间复杂度:O(m+n)O(m+n)

空间复杂度:O(m+n)O(m+n)

双指针法(三指针法微改进)🏆

解题思路

为什么还要做(不跳过)这个三指针,而不直接做双指针的解法呢?因为我经常忽略了第三指针其实可以用前指针表示出来!

该双指针法与如上三指针法唯一的区别就是去除了 sortedArr 有序数组的指针,而是通过 ij 间接表示。

代码

Java

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = 0;
        int j = 0;
        int[] sortedArr = new int[m + n];
        while (i < m || j < n) {
            if (i == m) {
                sortedArr[i + j] = nums2[j++];
            } else if (j == n) {
                sortedArr[i + j] = nums1[i++];
            } else if (nums1[i] <= nums2[j]) {
                sortedArr[i + j] = nums1[i++];
            } else {
                sortedArr[i + j] = nums2[j++];
            }
        }
        for (int k = 0; k < m + n; k++) {
            nums1[k] = sortedArr[k];
        }
    }
}

C++

class Solution {
public:
    void merge(vector<int> &nums1, int m, vector<int> &nums2, int n) {
        int p1 = 0;
        int p2 = 0;
        vector<int> sorted(m + n);
        while (p1 < m || p2 < n) {
            int cur;
            if (p1 == m) {
                // sorted[p1 + p2] = nums[p2++] int C++: Unsequenced modification and access to 'p2'!
                cur = nums2[p2++];
            } else if (p2 == n) {
                cur = nums1[p1++];
            } else if (nums1[p1] <= nums2[p2]) {
                cur = nums1[p1++];
            } else {
                cur = nums2[p2++];
            }
            sorted[p1 + p2 - 1] = cur;
        }
        nums1 = sorted;
    }
};

时间复杂度:O(m+n)O(m+n)

空间复杂度:O(m+n)O(m+n)

🍖可以明显地看出相对方法一,三/双指针法明显降低了时间/空间复杂度;但有办法将空间复杂度缩小至 O(1)O(1) 使其称为「原地算法」吗?

逆向双指针🥇

进阶:你可以设计实现一个时间复杂度为 O(m+n)O(m + n)原地算法(空间复杂度为 O(1)O(1))解决此问题吗?

解题思路

🔎如果 nums1[] 的长度为 m 那确实没办法实现进阶要求,但题目两个隐藏点目前只使用一个有序,而 nums1[] 长度为 m+n 仍未被充分利用。

🌅为了避免额外新建数组,我们需要将对比之后的元素放入首位/末位:

  • 首位:nums1[] 先整体后移,然后通过双指针从前往后遍历,取较小元素逐一覆盖 nums1[] 最前面的元素。
  • 末位:通过双指针从后往前逆向填充较大者,即不存在覆盖。

因为此种解法的正向双指针还需要考虑移位,比较麻烦,所以采用逆向双指针来实现。

⭐为什么 nums1[] 不会出现空位不足的情况?这里借鉴一下 LeetCode 官方的证明:

严格来说,在此遍历过程中的任意一个时刻,nums1 数组中有 mi1m-i-1 个元素被放入 nums1 的后半部,nums2 数组中有 nj1n-j-1 个元素被放入 nums1 的后半部,而在指针 j 的后面,nums1 数组有 m+ni1m+n-i-1 个位置,由于

m+ni1mi1+nj1m+n-i-1 \geq m-i-1+n-j-1

等价于

j1j \geq -1

永远成立,因此 i 后面的位置永远足够容纳被插入的元素。

其实不用证明那么麻烦,从几个维度想一想怎么都会存在空位插入(即使覆盖也都是使用过的值)

代码

Java

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1;
        int j = n - 1;
        int cur;

        while (i >= 0 || j >= 0) {
            if (i == -1) {
                cur = nums2[j--];
            } else if (j == -1) {
                cur = nums1[i--];
            } else if (nums1[i] > nums2[j]) {
                cur = nums1[i--];
            } else {
                cur = nums2[j--];
            }
            // (i + j + 1) + 1
            nums1[i + j + 1 + 1] = cur;
        }
    }
}

C++

class Solution {
public:
    void merge(vector<int> &nums1, int m, vector<int> &nums2, int n) {
        int i = m - 1;
        int j = n - 1;
        int cur;
        while (i >= 0 || j >= 0) {
            if (i == -1) {
                cur = nums2[j--];
            } else if (j == -1) {
                cur = nums1[i--];
            } else if (nums1[i] >= nums2[j]) {
                cur = nums1[i--];
            } else {
                cur = nums2[j--];
            }
            nums1[i + j + 1 + 1] = cur;
        }
    }
};

时间复杂度:O(m+n)O(m+n)

空间复杂度:O(1)O(1),无需额外空间!

最后🌅

该篇文章为 「LeetCode」 系列的 No.11 篇,在这个系列文章中:

  • 尽量给出多种解题思路
  • 提供题解的多语言代码实现
  • 记录该题涉及的知识点

👨‍💻争取做到 LeetCode 每日 1 题,所有的题解/代码均收录于 「LeetCode」 仓库,欢迎随时加入我们的刷题小分队!