数组/字符串
88. 合并两个有序数组
问题: 给定两个按非递减顺序排列的整数数组 nums1 和 nums2,两个整数 m 和 n表示 nums1 和 nums2 中的元素个数。合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
思路: 直接使用双指针法从前往后合并需要新建一个临时数组来存放结果,可以从后往前遍历,将中间结果放在nums1数组中。
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int p1 = m - 1, p2 = n - 1;
int tail = m + n - 1;
int cur;
while (p1 >= 0 || p2 >= 0) {
if (p1 == -1) {
cur = nums2[p2--];
} else if (p2 == -1) {
cur = nums1[p1--];
} else if (nums1[p1] > nums2[p2]) {
cur = nums1[p1--];
} else {
cur = nums2[p2--];
}
nums1[tail--] = cur;
}
}
复杂度: 时间复杂度 O ( m + n ) O(m+n) O(m+n),空间复杂度 O ( 1 ) O(1) O(1)
27. 移除元素
问题: 给定数组 nums 和值 val,原地移除所有数值等于 val 的元素,并返回移除后数组的新长度。
思路: 双指针法,把数组后面的元素赋值到前面,覆盖掉需要删除的元素;
int removeElement(int* nums, int numsSize, int val) {
int left = 0, right = numsSize;
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[right - 1];
right--;
} else {
left++;
}
}
return left;
}
复杂度: 时间复杂度 O ( n ) O(n) O(n)(实际上只需遍历一次),空间复杂度 O ( 1 ) O(1) O(1)
26. 删除有序数组中的重复项
80. 删除有序数组中的重复项 II
一般化的问题: 给定非严格递增排列的数组
n
u
m
s
nums
nums ,原地删除重复出现的元素,使每个元素只出现
k
k
k 次 ,返回删除后数组的新长度(元素的相对顺序应该保持不变 )。
思路: 双指针法
- 快指针 f a s t fast fast指向当前正在遍历的元素,慢指针 s l o w slow slow指向删除结果中的最后一个元素;
- 删除后的数组元素满足:所有元素与比它下标小k的元素不相等,因此 f a s t fast fast指针遍历时与 s l o w slow slow指针当前位置小k的元素作比较,如果相同就直接处理下一个元素(跳过当前元素),否则就将其复制到 s l o w slow slow指针的位置作为结果的一部分,然后 s l o w slow slow指针再加一;
- 前k个元素一定不会重复,所以从第 k k k个元素开始遍历,长度小于 k k k的输入直接输出结果;
// k=2
int removeDuplicates(int* nums, int numsSize) {
if (numsSize <= 2)
return numsSize;
int fast = 2, slow = 2;
while(fast<numsSize){
if(nums[fast] != nums[slow-2]){
nums[slow] = nums[fast];
slow++;
}
fast++;
}
return slow;
}
复杂度: 时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)
关于双指针法:
原地操作数组时可以使用,一个指针指向正在操作的元素,一个指针指向结果数组的最后一个元素,整个原数组分为已被覆盖(结果数组)、已处理、未处理三部分。
169. 多数元素
问题: 给定一个大小为
n
n
n 的数组
n
u
m
s
nums
nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于
⌊
n
/
2
⌋
⌊ n/2 ⌋
⌊n/2⌋ 的元素。
直接遍历数组的每个元素,统计其出现次数,时间复杂度
O
(
n
2
)
O(n^2)
O(n2),效率太低。
思路1:随机化
由于多数元素占元素总数的超过一半,因此每次随机选择一个元素,遍历一次数组来判断其是否为多数元素,是一个具有可行性的方法。
最坏时间复杂度为 O ( ∞ ) O(\infty) O(∞),但其实平均时间复杂度为 O ( n ) O(n) O(n)。
设每次随机选择的元素为多数元素的概率为 p = 多数元素数量 n ≥ 1 2 p = \dfrac{多数元素数量}{n}\geq \dfrac{1}{2} p=n多数元素数量≥21
则随机取到的元素是多数元素的所需次数的期望是个常数:
E = ∑ i = 1 n i ⋅ 1 2 i = 2 \begin{aligned} E&=\stackrel{n}{\sum\limits_{i=1}}i\cdot\frac{1}{2^i}\\ &=2 \end{aligned} E=i=1∑ni⋅2i1=2
每次随机选择元素后需要遍历一次数组来判断该元素是否为多数元素 O ( n ) O(n) O(n),因此这种方法的时间复杂度期望为 O ( n ) O(n) O(n) 。
class Solution {
public:
int majorityElement(vector<int>& nums) {
while (true) {
int candidate = nums[rand() % nums.size()];
int count = 0;
for (int num : nums)
if (num == candidate)
++count;
if (count > nums.size() / 2)
return candidate;
}
return -1;
}
};
思路2:排序
最直接的思路:既然数组中有超过
⌊
n
/
2
⌋
⌊ n/2 ⌋
⌊n/2⌋ 的元素是多数元素,那么将数组排序后下标为
⌊
n
/
2
⌋
⌊ n/2 ⌋
⌊n/2⌋ 的元素一定是那个多数元素。
(很好理解:一块超过桌子一半宽的布不管放在桌子上什么位置都能盖住桌子的中间位置)
class Solution {
public:
int majorityElement(vector<int>& nums) {
sort(nums.begin(), nums.end());
return nums[nums.size() / 2];
}
};
C++中sort函数的用法:C++ sort()排序详解
- 时间复杂度: O ( n log n ) O(n\log n) O(nlogn),排序的时间复杂度为 O ( n log n ) O(n\log n) O(nlogn)
- 空间复杂度: O ( log n ) O(\log n) O(logn),C++的sort()函数并不是直接使用空间复杂度为 O ( n log n ) O(n\log n) O(nlogn)的快速排序,而是会视输入情况综合使用快速排序、插入排序和堆排序方法(参见sort函数在STL中的底层实现)
思路3:哈希
本题如果直接遍历求解的话,外层循环需要遍历所有元素(
O
(
n
)
O(n)
O(n)),内层循环为计算每个元素的出现次数又要遍历一次整个数组(
O
(
n
)
O(n)
O(n)),时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。外层循环不可避免,那么有没有可能在计算每个元素的出现次数上节省时间呢?这就要用到哈希表。
C++ STL中的哈希表:C++ 哈希表
class Solution {
public:
int majorityElement(vector<int>& nums) {
unordered_map<int, int> counts;
int majority = 0, cnt = 0;
for (int num: nums) {
//哈希表的项:{元素值:出现次数}
++counts[num];
//根据题目需要,在遍历时就维护最大值
if (counts[num] > cnt) {
majority = num;
cnt = counts[num];
}
}
return majority;
}
};
小技巧:在遍历时就维护最大值,不然哈希表建完了还要再遍历一遍看看哪个值最大(即哪个元素出现次数最多)
- 时间复杂度: O ( n ) O(n) O(n),将元素插入哈希表只需常数时间
- 空间复杂度: O ( n ) O(n) O(n),哈希表占用的空间与其中元素个数呈线性关系,而数组元素不超过 n − ⌊ n / 2 ⌋ n-⌊ n/2 ⌋ n−⌊n/2⌋个
思路4:分治
多数元素有一个特点:如果一个元素
n
n
n是数组
n
u
m
s
nums
nums的多数元素,那么如果将数组从某个位置断开分成两部分
l
e
f
t
left
left和
r
i
g
h
t
right
right,这个元素也必是其中至少一部分的多数元素。
用反证法思考:假如 n n n不是 l e f t left left和 r i g h t right right的多数元素,说明 n n n的数量在 l e f t left left和 r i g h t right right各自不会超过一半,那 n n n在 l e f t left left和 r i g h t right right合下来组成的 n u m s nums nums中的数量也不会超过一半,即 n n n也不是 n u m s nums nums的多数元素。
由于多数元素具有这样的特点,可以使用分治法来解决此问题:递归地将求主数组的多数元素分解为求左右子数组的多数元素,回溯时为了合并两个子数组的多数元素需要遍历两次主数组得到两个子数组的多数元素各自在主数组中的出现次数。
class Solution {
//计算元素在数组中的出现次数
int count_in_range(vector<int>& nums, int target, int lo, int hi) {
int count = 0;
for (int i = lo; i <= hi; ++i)
if (nums[i] == target)
++count;
return count;
}
int majority_element_rec(vector<int>& nums, int lo, int hi) {
if (lo == hi)
return nums[lo];
int mid = (lo + hi) / 2;
int left_majority = majority_element_rec(nums, lo, mid);
int right_majority = majority_element_rec(nums, mid + 1, hi);
if (count_in_range(nums, left_majority, lo, hi) > (hi - lo + 1) / 2)
return left_majority;
if (count_in_range(nums, right_majority, lo, hi) > (hi - lo + 1) / 2)
return right_majority;
return -1;
}
public:
int majorityElement(vector<int>& nums) {
return majority_element_rec(nums, 0, nums.size() - 1);
}
};
- 时间复杂度:
O
(
n
log
n
)
O(n\log n)
O(nlogn),分治函数会求解 2 个长度为
n
2
\dfrac{n}{2}
2n
的子问题,并做两遍长度为 n n n 的线性扫描,递推式: T ( n ) = 2 T ( 1 2 ) + 2 n T(n)=2T(\frac{1}{2})+2n T(n)=2T(21)+2n
求解得到 T ( n ) = n log n T(n) = n\log n T(n)=nlogn
分治法介绍和递推式求解参见算法导论和:分治法 ( Divide And Conquer ) 详解 - 空间复杂度: O ( log n ) O(\log n) O(logn),分治法不需要额外空间,但递归时需要递归深度大小的空间,此处递归深度为 O ( log n ) O(\log n) O(logn)