88.合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 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)
的算法解决此问题吗?
原题解析
该问题要求将两个升序排列的数组 nums1 和 nums2 合并为一个升序排列的数组。需要注意的是,合并操作需要在 nums1 数组中原地进行,且 nums1 已经预留了足够的空间来容纳合并后的结果。算法的目标是实现时间复杂度为 O(m + n),其中 m 和 n 分别是 nums1 和 nums2 中的有效元素个数。
代码
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i=m-1;
int j=n-1;
int k=m+n-1;
while(i>=0&&j>=0){
if(nums1[i]>nums2[j]){
nums1[k--]=nums1[i--];
}else{
nums1[k--]=nums2[j--];
}
}
while(j>=0){
nums1[k--]=nums2[j--];
}
}
};
使用双指针的策略从后向前填充数组,以避免覆盖 nums1 中的有效元素。具体而言,算法的目标是在两个非递减数组 nums1 和 nums2 中进行合并,使 nums1 最终保持非递减顺序,同时满足时间复杂度为 O(m + n)。
算法逻辑
这个算法的核心是双指针法,并从数组的末尾开始操作,避免了在前端插入元素时引起的数据移动问题。具体的步骤如下:
-
初始化三个指针:
-
i 指向 nums1 中最后一个有效元素的位置(即 m-1)。
-
j 指向 nums2 中最后一个元素的位置(即 n-1)。
-
k 指向 nums1 最后的位置(即 m+n-1),该位置用于存放合并后的最大值。
-
-
从数组的尾部开始,比较 nums1[i] 和 nums2[j] 的值,将较大者放入 nums1[k],然后相应地移动指针 i 或 j,以及 k。
-
当 nums2 中仍有剩余元素时(即 j >= 0),将这些元素直接拷贝到 nums1 的前面。
-
当 nums1 的有效部分处理完后,无需做额外处理,因为 nums1 中剩余的元素已经是正确的顺序。
3. 填补剩余元素:
- 若 nums1 的元素已经遍历完,但 nums2 还有剩余元素,直接将 nums2 中的元素放入 nums1 中即可。反之,则无需额外操作,因为 nums1 中剩余的元素本来就是排序好的。
数学原理
该算法基于双指针归并思想,这种思想通常用于合并两个有序数组。通过从数组末尾开始遍历,算法有效地避免了在数组前部插入元素时的移动操作,使得整体时间复杂度保持在 O(m + n)。
时间复杂度分析
-
在最坏情况下,每个数组中的每个元素都要被比较和移动一次,因此时间复杂度为 O(m + n)。
-
空间复杂度为 O(1),因为我们只使用了常数个额外的变量来记录指针的位置,且所有的操作都是在 nums1 数组内部完成的。
细节分析
-
双指针归并法: 该算法的关键是从数组末尾开始比较并将较大的元素放入 nums1 的最后位置。由于我们从后向前填充数组,避免了前向填充时的数组元素移动问题,这也是该算法高效的原因之一。
-
边界处理:
-
如果 nums2 的元素已经全部处理完毕,那么 nums1 中剩下的部分已经是有序的,无需再处理。
-
如果 nums1 中的元素处理完毕,而 nums2 仍有剩余元素,则直接将 nums2 中剩余的元素拷贝到 nums1 的前部。
-
性能分析
- 合并过程每次移动指针 i 或 j,即每个元素最多被处理一次,因此时间复杂度为 O(m + n)。这符合题目中的进阶要求。
- 空间复杂度为 O(1),因为算法并没有使用额外的数组来存储结果,而是直接在 nums1 中修改。
示例解释
示例 1:
输入: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
-
初始化指针:i = 2, j = 2, k = 5
-
比较 nums1[2] = 3 和 nums2[2] = 6,将 6 放入 nums1[5],更新指针:k = 4, j = 1
-
比较 nums1[2] = 3 和 nums2[1] = 5,将 5 放入 nums1[4],更新指针:k = 3, j = 0
-
比较 nums1[2] = 3 和 nums2[0] = 2,将 3 放入 nums1[3],更新指针:k = 2, i = 1
-
比较 nums1[1] = 2 和 nums2[0] = 2,将 2 放入 nums1[2],更新指针:k = 1, j = -1
-
将 nums1[0] 保持不变,最终 nums1 = [1,2,2,3,5,6]
示例 2:
输入: nums1 = [1], m = 1, nums2 = [], n = 0
-
因为 nums2 为空,直接返回 nums1 = [1]。
示例 3:
输入: nums1 = [0], m = 0, nums2 = [1], n = 1
-
将 nums2[0] 直接拷贝到 nums1[0],最终 nums1 = [1]。
27.移除元素https://leetcode.cn/problems/remove-element/?envType=study-plan-v2&envId=top-interview-150
27.移除元素
给你一个数组 nums
和一个值 val
,你需要 原地 移除所有数值等于 val
的元素。元素的顺序可能发生改变。然后返回 nums
中与 val
不同的元素的数量。
假设 nums
中不等于 val
的元素数量为 k
,要通过此题,您需要执行以下操作:
- 更改
nums
数组,使nums
的前k
个元素包含不等于val
的元素。nums
的其余元素和nums
的大小并不重要。 - 返回
k
。
用户评测:
评测机将使用以下代码测试您的解决方案:
int[] nums = [...]; // 输入数组 int val = ...; // 要移除的值 int[] expectedNums = [...]; // 长度正确的预期答案。 // 它以不等于 val 的值排序。 int k = removeElement(nums, val); // 调用你的实现 assert k == expectedNums.length; sort(nums, 0, k); // 排序 nums 的前 k 个元素 for (int i = 0; i < actualLength; i++) { assert nums[i] == expectedNums[i]; }
如果所有的断言都通过,你的解决方案将会 通过。
示例 1:
输入:nums = [3,2,2,3], val = 3 输出:2, nums = [2,2,_,_] 解释:你的函数函数应该返回 k = 2, 并且 nums 中的前两个元素均为 2。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
示例 2:
输入:nums = [0,1,2,2,3,0,4,2], val = 2 输出:5, nums = [0,1,4,0,3,_,_,_] 解释:你的函数应该返回 k = 5,并且 nums 中的前五个元素为 0,0,1,3,4。 注意这五个元素可以任意顺序返回。 你在返回的 k 个元素之外留下了什么并不重要(因此它们并不计入评测)。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 50
0 <= val <= 100
原题解析
这个问题要求我们从数组中原地移除指定值 val 的所有出现,并返回不等于 val 的元素个数 k,同时保证数组前 k 个元素是非 val 的有效元素,后面的元素则可以忽略。在此过程中,不要求保持数组的顺序。
代码
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int k = 0;
for (int i = 0; i < nums.size(); ++i) {
if (nums[i] != val) {
nums[k] = nums[i];
++k;
}
}
return k;
}
};
算法逻辑
该算法的核心思想是使用 双指针法,其中一个指针用于遍历整个数组,另一个指针用于在原地记录非 val 的元素。具体而言,我们使用如下步骤完成:
-
初始化一个变量 k 为 0,表示当前数组中非 val 元素的个数,也即是目标数组的索引。
-
遍历数组 nums,对每一个元素执行以下操作:
-
如果当前元素 nums[i] 不等于 val,则将该元素复制到 nums[k] 位置,并将 k 自增 1。
-
如果当前元素等于 val,则跳过该元素,不进行任何操作。
-
-
最终,返回 k,即数组中不等于 val 的元素个数。
这种算法的时间复杂度为 O(n),其中 n 是数组 nums 的长度。算法仅遍历数组一次,并在原数组上进行修改,不需要额外的空间,因此空间复杂度为 O(1)。
数学原理
该算法的数学基础依赖于计数和位置替换的思想。
-
计数:通过变量 k 来记录数组中非 val 的元素个数。每当遍历到一个不等于 val 的元素时,将其放置在索引为 k 的位置,并将 k 自增,确保 k 始终指向下一个非 val 元素应当存储的位置。
-
位置替换:对于数组中所有不等于 val 的元素,算法会在遍历的过程中不断将它们从当前位置复制到前面的空位。由于数组的顺序可以被改变,算法无需顾虑元素的相对顺序,仅需要保证前 k 个元素是最终的有效数组内容即可。
性能分析
-
时间复杂度:该算法的时间复杂度为 O(n),其中 n 是数组的长度。算法遍历数组一次,并在每次检查时根据条件进行替换,因此总体的时间复杂度是线性的。
-
空间复杂度:由于该算法在原地修改数组,没有使用额外的空间,因此其空间复杂度为 O(1)。这意味着算法在执行时仅需要使用常数空间,具有较高的空间效率。
示例解释
示例 1:
输入: nums = [3,2,2,3], val = 3
-
初始化 k = 0
-
遍历数组:
-
第一个元素 nums[0] = 3 等于 val,跳过。
-
第二个元素 nums[1] = 2 不等于 val,将其赋值给 nums[k],即 nums[0] = 2,然后 k++。
-
第三个元素 nums[2] = 2 不等于 val,将其赋值给 nums[k],即 nums[1] = 2,然后 k++。
-
第四个元素 nums[3] = 3 等于 val,跳过。
-
最终 k = 2,数组前两个元素为 [2, 2],即有效元素的个数为 2。
示例 2:
输入: nums = [0,1,2,2,3,0,4,2], val = 2
-
初始化 k = 0
-
遍历数组:
-
nums[0] = 0 不等于 val,将其赋值给 nums[k],即 nums[0] = 0,k++。
-
nums[1] = 1 不等于 val,将其赋值给 nums[k],即 nums[1] = 1,k++。
-
nums[2] = 2 等于 val,跳过。
-
nums[3] = 2 等于 val,跳过。
-
nums[4] = 3 不等于 val,将其赋值给 nums[k],即 nums[2] = 3,k++。
-
nums[5] = 0 不等于 val,将其赋值给 nums[k],即 nums[3] = 0,k++。
-
nums[6] = 4 不等于 val,将其赋值给 nums[k],即 nums[4] = 4,k++。
-
nums[7] = 2 等于 val,跳过。
-
最终 k = 5,有效元素为 [0, 1, 3, 0, 4],共 5 个元素。
26.删除有序数组中的重复项
原题解析
该问题要求从已排序的整数数组 nums 中去除重复的元素,并返回唯一元素的个数,同时在原数组上进行修改。去重后的元素应该保留其在数组中的相对顺序。
代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n=nums.size();
if(n==0){
return 0;
}
int slow=1;
for(int fast=1;fast<n;fast++){
if(nums[fast]!=nums[fast-1]){
nums[slow]=nums[fast];
++slow;
}
}
return slow;
}
};
算法逻辑
该算法采用双指针法,其中一个指针用于遍历数组(称为快指针 fast),另一个指针用于记录不重复元素的最终位置(称为慢指针 slow)。具体步骤如下:
-
初始化指针:
-
如果数组为空,直接返回 0。
-
将慢指针 slow 初始化为 1,表示当前已处理的不重复元素个数。
-
快指针 fast 用于遍历数组,从索引 1 开始,因为第一个元素默认是唯一的。
-
-
遍历数组:
-
当 nums[fast] != nums[fast - 1] 时,说明 fast 位置的元素与前一个元素不同,因此它是一个新的唯一元素,将其复制到 nums[slow] 的位置,并将 slow 指针加一。
-
如果 nums[fast] == nums[fast - 1],说明该元素是重复的,跳过。
-
-
结束条件:
-
当 fast 遍历完整个数组时,slow 指针的位置即为数组中不重复元素的个数,也是去重后数组的长度。
-
返回 slow,即唯一元素的个数。
-
数学原理
该算法的数学基础是基于排序数组的特性,重复的元素一定是相邻的,因此只需要比较相邻元素是否相等即可识别重复元素。
性能分析
-
时间复杂度:算法的时间复杂度为 O(n),其中 n 是数组的长度。因为整个算法只遍历数组一次,每个元素只进行一次比较和(必要时的)复制操作。
-
空间复杂度:该算法的空间复杂度为 O(1),即不需要使用额外的存储空间,所有操作均在原地进行。
示例解释
示例 1:
输入: nums = [1,1,2]
-
初始化:slow = 1, fast = 1
-
fast = 1,nums[1] = nums[0] = 1,相等,跳过。
-
fast = 2,nums[2] != nums[1],不相等,将 nums[2] 赋值给 nums[1],即 nums = [1, 2, 2],并将 slow 增加到 2。
-
遍历完成,返回 slow = 2。
最终,nums 中前 2 个元素是 [1, 2],即唯一元素个数为 2。
示例 2:
输入: nums = [0,0,1,1,1,2,2,3,3,4]
-
初始化:slow = 1, fast = 1
-
fast = 1,nums[1] = nums[0] = 0,相等,跳过。
-
fast = 2,nums[2] != nums[1],不相等,将 nums[2] 赋值给 nums[1],即 nums = [0, 1, 1, 1, 1, 2, 2, 3, 3, 4],并将 slow 增加到 2。
-
继续遍历,发现 nums[3] 和 nums[4] 是重复的,跳过,直到 fast = 5 时,nums[5] != nums[4],将 nums[5] 赋值给 nums[2],即 nums = [0, 1, 2, 1, 1, 2, 2, 3, 3, 4],slow 增加到 3。
-
重复以上操作,最终返回 slow = 5。
最终,nums 中前 5 个元素是 [0, 1, 2, 3, 4],即唯一元素个数为 5。
80.删除有序数组中的重复项
原题解析
这个问题要求在一个已排序的数组中,移除重复元素,使得每个元素最多出现两次。数组的修改需要在原地进行,并且不允许分配额外的空间。最终要求返回去重后数组的长度,并确保前 k 个位置存储最终结果。
代码
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int n=nums.size();
if(n<=2){
return n;
}
int slow=2;
for(int fast=2;fast<n;++fast){
if(nums[fast]!=nums[slow-2]){
nums[slow]=nums[fast];
++slow;
}
}
return slow;
}
};
算法逻辑
该算法的核心思想是利用双指针法,类似于处理数组重复元素的经典问题。具体步骤如下:
-
初始化双指针:
-
由于每个元素最多出现两次,我们可以跳过前两个元素,直接将慢指针 slow 初始化为 2,快指针 fast 初始化为 2,表示从第三个元素开始遍历。
-
slow 指针用于存放有效元素,即最多只出现两次的元素。
-
fast 指针用于遍历整个数组。
-
-
遍历数组:
-
当 nums[fast] != nums[slow - 2] 时,意味着当前 fast 指针指向的元素可以被加入到结果中,因为它不是一个过多重复的元素。
-
将 nums[fast] 赋值给 nums[slow],然后将 slow 向前移动一位以指向下一个存储位置。
-
如果 nums[fast] == nums[slow - 2],则跳过该元素,因为这意味着该元素已经出现超过两次了。
-
-
返回结果:
-
当遍历结束后,slow 指针的位置即为最终数组的长度 k,并且数组前 k 个位置存储了最终结果。
-
数学原理
这个算法的数学基础依赖于已排序数组的特性。由于数组是升序排列的,所有重复元素必然是相邻的,因此只需比较当前元素与前两个元素是否相同即可确定该元素是否可以继续保留。通过控制 slow 指针的位置,确保最多允许每个元素出现两次。
性能分析
-
时间复杂度:算法的时间复杂度为 O(n),其中 n 是数组的长度。算法只需遍历数组一次,因此时间复杂度是线性的。
-
空间复杂度:该算法的空间复杂度为 O(1),因为不需要额外的存储空间,所有操作均在原数组上进行。
示例解释
示例 1:
输入: nums = [1,1,1,2,2,3]
-
初始化:slow = 2, fast = 2
-
fast = 2 时,nums[2] = 1,与 nums[slow - 2] = nums[0] = 1 相等,跳过。
-
fast = 3 时,nums[3] = 2,与 nums[slow - 2] = nums[1] = 1 不相等,将 nums[3] 赋值给 nums[2],即 nums = [1,1,2,2,2,3],slow = 3。
-
fast = 4 时,nums[4] = 2,与 nums[slow - 2] = nums[2] = 2 不相等,将 nums[4] 赋值给 nums[3],即 nums = [1,1,2,2,2,3],slow = 4。
-
fast = 5 时,nums[5] = 3,与 nums[slow - 2] = nums[3] = 2 不相等,将 nums[5] 赋值给 nums[4],即 nums = [1,1,2,2,3,3],slow = 5。
-
最终返回 slow = 5。
示例 2:
输入: nums = [0,0,1,1,1,1,2,3,3]
-
初始化:slow = 2, fast = 2
-
fast = 2 时,nums[2] = 1,与 nums[slow - 2] = nums[0] = 0 不相等,将 nums[2] 赋值给 nums[2],slow = 3。
-
fast = 3 时,nums[3] = 1,与 nums[slow - 2] = nums[1] = 0 不相等,将 nums[3] 赋值给 nums[3],slow = 4。
-
fast = 4 和 fast = 5 时,nums[4] 和 nums[5] 与 nums[slow - 2] 相等,跳过。
-
fast = 6 时,nums[6] = 2,与 nums[slow - 2] = nums[4] = 1 不相等,将 nums[6] 赋值给 nums[4],slow = 5。
-
fast = 7 时,nums[7] = 3,与 nums[slow - 2] = nums[5] = 2 不相等,将 nums[7] 赋值给 nums[5],slow = 6。
-
fast = 8 时,nums[8] = 3,与 nums[slow - 2] = nums[6] = 3 不相等,将 nums[8] 赋值给 nums[6],slow = 7。
-
最终返回 slow = 7。
169.多数元素https://leetcode.cn/problems/majority-element/?envType=study-plan-v2&envId=top-interview-150
169.多数元素
原题解析
该问题要求在一个大小为 n 的数组中找到多数元素,即出现次数超过
次的元素。题目假设数组中总是存在多数元素。为了满足题目的进阶要求,解决方案不仅需要在线性时间内完成(即时间复杂度为 O(n)),还要使用常数空间(即空间复杂度为 O(1))。
代码
class Solution {
public:
int majorityElement(vector<int>& nums) {
int candidate=0;
int count=0;
for(int num:nums){
if(count==0){
candidate=num;
count=1;
}else if(num==candidate){
count++;
}else{
count--;
}
}
return candidate;
}
};
算法逻辑
解决该问题的核心是使用 Boyer-Moore 投票算法。该算法可以在线性时间内找到多数元素,并且只需要常数空间。具体的思想如下:
-
候选人选择:
-
我们首先设置两个变量,一个是 candidate,用于存储当前的候选多数元素,另一个是 count,用于计数当前候选元素的有效票数。
-
初始化时,candidate 设为任意值,count 设为 0。
-
-
投票过程:
-
遍历数组中的每一个元素 num:
-
如果 count 为 0,说明当前没有候选人,或前面的候选人已经失去多数优势。因此,设置当前元素 num 为新的候选人,并将 count 重置为 1。
-
如果 num 等于当前候选人 candidate,则 count 增加 1,表示该候选人的支持票数增加。
-
如果 num 不等于 candidate,则 count 减少 1,表示该候选人的支持票数减少。
-
-
在遍历结束时,candidate 中存储的就是数组的多数元素。
-
-
原理解释:
-
Boyer-Moore 投票算法的本质是通过对比和抵消,将不属于多数元素的选票“抵消”掉。由于多数元素的出现次数超过数组元素的一半,即
,即便存在其他元素的抵消,也无法将多数元素完全抵消。因此,最终剩下的候选人就是多数元素。
-
数学原理
Boyer-Moore 投票算法的数学基础依赖于以下事实:
-
多数元素的出现次数超过数组长度的一半,即多数元素的个数 x > n / 2。
-
在每次投票过程中,当当前候选人 candidate 的支持票数为 0 时,算法会更换候选人。然而,正因为多数元素的数量超过其他元素的总和,无论如何进行抵消,最终多数元素仍然会作为剩余的候选人。
该算法通过逐步抵消非多数元素,实现了在线性时间内找到多数元素的目标。
性能分析
-
时间复杂度:该算法只遍历一次数组,每次遍历的操作都是常数时间内完成的,因此时间复杂度为 O(n),其中 n 是数组的长度。
-
空间复杂度:由于算法只使用了两个额外的变量 candidate 和 count,因此空间复杂度为 O(1),符合题目的要求。
示例解释
示例 1:
输入: nums = [3,2,3]
-
初始化:candidate = 0, count = 0
-
第一个元素 num = 3,由于 count == 0,将 candidate 设置为 3,并将 count 设为 1。
-
第二个元素 num = 2,与 candidate 不同,因此 count 减少 1,count = 0。
-
第三个元素 num = 3,由于 count == 0,将 candidate 设置为 3,并将 count 设为 1。
最终返回的候选人 candidate 是 3,即多数元素为 3。
示例 2:
输入: nums = [2,2,1,1,1,2,2]
-
初始化:candidate = 0, count = 0
-
第一个元素 num = 2,由于 count == 0,将 candidate 设置为 2,并将 count 设为 1。
-
第二个元素 num = 2,与 candidate 相同,因此 count 增加到 2。
-
第三个元素 num = 1,与 candidate 不同,因此 count 减少到 1。
-
第四个元素 num = 1,与 candidate 不同,因此 count 减少到 0。
-
第五个元素 num = 1,由于 count == 0,将 candidate 设置为 1,并将 count 设为 1。
-
第六个元素 num = 2,与 candidate 不同,因此 count 减少到 0。
-
第七个元素 num = 2,由于 count == 0,将 candidate 设置为 2,并将 count 设为 1。
最终返回的候选人 candidate 是 2,即多数元素为 2。