本章主要总结与整型数组相关的题。
1.Remove Element
Question
- leetcode: Remove Element | LeetCode OJ
- lintcode: (172) Remove Element
Given an array and a value, remove all occurrences of that value in place and return the new length. The order of elements can be changed, and the elements after the new length don't matter. Example Given an array [0,4,4,0,0,2,4,4], value=4 return 4 and front four elements of the array is [0,0,0,2]
入门题,返回删除指定元素后的数组长度,使用容器操作非常简单。以 lintcode 上给出的参数为例,遍历容器内元素,若元素值与给定删除值相等,删除当前元素并往后继续遍历。
class Solution {
public:
/**
*@param A: A list of integers
*@param elem: An integer
*@return: The new length after remove
*/
int removeElement(vector &A, int elem) {
for (vector::iterator iter = A.begin(); iter < A.end(); ++iter) {
if (*iter == elem) {
iter = A.erase(iter);
--iter;
}
}
return A.size();
}
};
注意在遍历容器内元素和指定欲删除值相等时,需要先自减--iter
, 因为for
循环会对iter
自增,A.erase()
删除当前元素值并返回指向下一个元素的指针,一增一减正好平衡。如果改用while
循环,则需注意访问数组时是否越界。
由于vector每次erase的复杂度是$$O(n)$$
,我们遍历整个数组,最坏情况下,每个元素都与要删除的目标元素相等,每次都要删除元素的复杂度高达$$O(n^2)$$
观察此方法会如此低效的原因,是因为我们一次只删除一个元素,导致很多没必要的元素交换移动,如果能够将要删除的元素集中处理,则可以大幅增加效率,见题解2。
由于题中明确暗示元素的顺序可变,且新长度后的元素不用理会。我们可以使用两根指针分别往前往后遍历,头指针用于指示当前遍历的元素位置,尾指针则用于在当前元素与欲删除值相等时替换当前元素,两根指针相遇时返回尾指针索引——即删除元素后「新数组」的长度。
class Solution {
public:
int removeElement(int A[], int n, int elem) {
for (int i = 0; i < n; ++i) {
if (A[i] == elem) {
A[i] = A[n - 1];
--i;
--n;
}
}
return n;
}
};
遍历当前数组,A[i] == elem
时将数组「尾部(以 n 为长度时的尾部)」元素赋给当前遍历的元素。同时自减i
和n
,原因见题解1的分析。需要注意的是n
在遍历过程中可能会变化。
此方法只遍历一次数组,且每个循环的操作至多也不过仅是常数次,因此时间复杂度是$$O(n)$$
。
2.Zero Sum Subarray
Question
- lintcode: (138) Subarray Sum
- GeeksforGeeks: Find if there is a subarray with 0 sum - GeeksforGeeks
Given an integer array, find a subarray where the sum of numbers is zero. Your code should return the index of the first number and the index of the last number. Example Given [-3, 1, 2, -3, 4], return [0, 2] or [1, 3]. Note There is at least one subarray that it's sum equals to zero.
题目中仅要求返回一个子串(连续)中和为0的索引,而不必返回所有可能满足题意的解。最简单的想法是遍历所有子串,判断其和是否为0,使用两重循环即可搞定,最坏情况下时间复杂度为 $$O(n^2)$$
, 这种方法显然是极其低效的,极有可能会出现 TLE. 下面就不浪费篇幅贴代码了。
两重 for 循环显然是我们不希望看到的解法,那么我们再来分析下题意,题目中的对象是分析子串和,那么我们先从常见的对数组求和出发,$$f(i) = \sum _{0} ^{i} nums[i]$$
表示从数组下标 0 开始至下标 i 的和。子串和为0,也就意味着存在不同的 $$i_1$$
和 $$i_2$$
使得 $$f(i_1) - f(i_2) = 0$$
, 等价于 $$f(i_1) = f(i_2)$$
. 思路很快就明晰了,使用一 vector 保存数组中从 0 开始到索引i
的和,在将值 push 进 vector 之前先检查 vector 中是否已经存在,若存在则将相应索引加入最终结果并返回。
class Solution {
public:
/**
* @param nums: A list of integers
* @return: A list of integers includes the index of the first number
* and the index of the last number
*/
vector subarraySum(vector nums){
vector result;
int curr_sum = 0;
vector sum_i;
for (int i = 0; i != nums.size(); ++i) {
curr_sum += nums[i];
if (0 == curr_sum) {
result.push_back(0);
result.push_back(i);
return result;
}
vector::iterator iter = find(sum_i.begin(), sum_i.end(), curr_sum);
if (iter != sum_i.end()) {
result.push_back(iter - sum_i.begin() + 1);
result.push_back(i);
return result;
}
sum_i.push_back(curr_sum);
}
return result;
}
};
使用curr_sum
保存到索引i
处的累加和,sum_i
保存不同索引处的和。执行sum_i.push_back
之前先检查curr_sum
是否为0,再检查curr_sum
是否已经存在于sum_i
中。是不是觉得这种方法会比题解1好?错!时间复杂度是一样一样的!根本原因在于find
操作的时间复杂度为线性。与这种方法类似的有哈希表实现,哈希表的查找在理想情况下可认为是 $$O(1)$$
.
最坏情况下 $$O(n^2)$$
, 实测和题解1中的方法运行时间几乎一致。
终于到了祭出万能方法时候了,题解2可以认为是哈希表的雏形,而哈希表利用空间换时间的思路争取到了宝贵的时间资源 :)
class Solution {
public:
/**
* @param nums: A list of integers
* @return: A list of integers includes the index of the first number
* and the index of the last number
*/
vector subarraySum(vector nums){
vector result;
// curr_sum for the first item, index for the second item
map hash;
hash[0] = 0;
int curr_sum = 0;
for (int i = 0; i != nums.size(); ++i) {
curr_sum += nums[i];
if (hash.find(curr_sum) != hash.end()) {
result.push_back(hash[curr_sum]);
result.push_back(i);
return result;
} else {
hash[curr_sum] = i + 1;
}
}
return result;
}
};
为了将curr_sum == 0
的情况也考虑在内,初始化哈希表后即赋予 <0, 0>
. 给 hash
赋值时使用i + 1
, push_back
时则不必再加1.
由于 C++ 中的map
采用红黑树实现,故其并非真正的「哈希表」,C++ 11中引入的unordered_map
用作哈希表效率更高,实测可由1300ms 降至1000ms.
遍历求和时间复杂度为 $$O(n)$$
, 哈希表检查键值时间复杂度为 $$O(\log L)$$
, 其中 $$L$$
为哈希表长度。如果采用unordered_map
实现,最坏情况下查找的时间复杂度为线性,最好为常数级别。
除了使用哈希表,我们还可使用排序的方法找到两个子串和相等的情况。这种方法的时间复杂度主要集中在排序方法的实现。由于除了记录子串和之外还需记录索引,故引入pair
记录索引,最后排序时先按照sum
值来排序,然后再按照索引值排序。如果需要自定义排序规则可参考[^sort_pair_second].
class Solution {
public:
/**
* @param nums: A list of integers
* @return: A list of integers includes the index of the first number
* and the index of the last number
*/
vector subarraySum(vector nums){
vector result;
if (nums.empty()) {
return result;
}
const int num_size = nums.size();
vector > sum_index(num_size + 1);
for (int i = 0; i != num_size; ++i) {
sum_index[i + 1].first = sum_index[i].first + nums[i];
sum_index[i + 1].second = i + 1;
}
sort(sum_index.begin(), sum_index.end());
for (int i = 1; i < num_size + 1; ++i) {
if (sum_index[i].first == sum_index[i - 1].first) {
result.push_back(sum_index[i - 1].second);
result.push_back(sum_index[i].second - 1);
return result;
}
}
return result;
}
};
没啥好分析的,注意好边界条件即可。这里采用了链表中常用的「dummy」节点方法,pair
排序后即为我们需要的排序结果。这种排序的方法需要先求得所有子串和然后再排序,最后还需要遍历排序后的数组,效率自然是比不上哈希表。但是在某些情况下这种方法有一定优势。
遍历求子串和,时间复杂度为 $$O(n)$$
, 空间复杂度 $$O(n)$$
. 排序时间复杂度近似 $$O(n \log n)$$
, 遍历一次最坏情况下时间复杂度为 $$O(n)$$
. 总的时间复杂度可近似为 $$O(n \log n)$$
. 空间复杂度 $$O(n)$$
.
这道题的要求是找到一个即可,但是要找出所有满足要求的解呢?Stackoverflow 上有这道延伸题的讨论[^stackoverflow].
另一道扩展题来自 Google 的面试题 - Find subarray with given sum - GeeksforGeeks.
3.Subarray Sum K
Question
- GeeksforGeeks: Find subarray with given sum - GeeksforGeeks
Given an nonnegative integer array, find a subarray where the sum of numbers is k. Your code should return the index of the first number and the index of the last number. Example Given [1, 4, 20, 3, 10, 5], sum k = 33, return [2, 4].
题 Zero Sum Subarray | Data Structure and Algorithm 的升级版,这道题求子串和为 K 的索引。首先我们可以考虑使用时间复杂度相对较低的哈希表解决。前一道题的核心约束条件为 $$f(i_1) - f(i_2) = 0$$
,这道题则变为 $$f(i_1) - f(i_2) = k$$
#include
#include
#include
与 Zero Sum Subarray 题的变化之处有两个地方,第一个是判断是否存在哈希表中时需要使用hash.find(curr_sum - k)
, 最终返回结果使用result.push_back(hash[curr_sum - k]);
而不是result.push_back(hash[curr_sum]);
略,见 Zero Sum Subarray | Data Structure and Algorithm
不知道细心的你是否发现这道题的隐含条件——nonnegative integer array, 这也就意味着子串和函数 $$f(i)$$
为「单调不减」函数。单调函数在数学中可是重点研究的对象,那么如何将这种单调性引入本题中呢?不妨设 $$i_2 > i_1$$
, 题中的解等价于寻找 $$f(i_2) - f(i_1) = k$$
, 则必有 $$f(i_2) \geq k$$
.
我们首先来举个实际例子帮助分析,以整数数组 {1, 4, 20, 3, 10, 5} 为例,要求子串和为33的索引值。首先我们可以构建如下表所示的子串和 $$f(i)$$
.
$$f(i)$$ |
1 | 5 | 25 | 28 | 38 |
---|---|---|---|---|---|
$$i$$ |
0 | 1 | 2 | 3 | 4 |
要使部分子串和为33,则要求的第二个索引值必大于等于4,如果索引值再继续往后遍历,则所得的子串和必大于等于38,进而可以推断出索引0一定不是解。那现在怎么办咧?当然是把它扔掉啊!第一个索引值往后递推,直至小于33时又往后递推第二个索引值,于是乎这种技巧又可以认为是「两根指针」。
#include
#include
#include
使用for
循环, 在curr_sum > k
时使用while
递减curr_sum
, 同时递增左边索引left_index
, 最后累加curr_sum
。如果顺序不对就会出现 bug, 原因在于判断子串和是否满足条件时在递增之后(谢谢 @glbrtchen 汇报 bug)。
看似有两重循环,由于仅遍历一次数组,且索引最多挪动和数组等长的次数。故最终时间复杂度近似为 $$O(2n)$$
, 空间复杂度为 $$O(1)$$
.
4.Subarray Sum Closest
Question
- lintcode: (139) Subarray Sum Closest
Given an integer array, find a subarray with sum closest to zero. Return the indexes of the first number and last number. Example Given [-3, 1, 1, -3, 5], return [0, 2], [1, 3], [1, 1], [2, 2] or [0, 4] Challenge O(nlogn) time
题 Zero Sum Subarray | Data Structure and Algorithm 的变形题,由于要求的子串和不一定,故哈希表的方法不再适用,使用解法4 - 排序即可在 $$O(n \log n)$$
内解决。具体步骤如下:
class Solution {
public:
/**
* @param nums: A list of integers
* @return: A list of integers includes the index of the first number
* and the index of the last number
*/
vector subarraySumClosest(vector nums){
vector result;
if (nums.empty()) {
return result;
}
const int num_size = nums.size();
vector > sum_index(num_size + 1);
for (int i = 0; i < num_size; ++i) {
sum_index[i + 1].first = sum_index[i].first + nums[i];
sum_index[i + 1].second = i + 1;
}
sort(sum_index.begin(), sum_index.end());
int min_diff = INT_MAX;
int closest_index = 1;
for (int i = 1; i < num_size + 1; ++i) {
int sum_diff = abs(sum_index[i].first - sum_index[i - 1].first);
if (min_diff > sum_diff) {
min_diff = sum_diff;
closest_index = i;
}
}
int left_index = min(sum_index[closest_index - 1].second,\
sum_index[closest_index].second);
int right_index = -1 + max(sum_index[closest_index - 1].second,\
sum_index[closest_index].second);
result.push_back(left_index);
result.push_back(right_index);
return result;
}
};
为避免对单个子串和是否为最小情形的单独考虑,我们可以采取类似链表 dummy 节点的方法规避,简化代码实现。故初始化sum_index
时需要num_size + 1
个。这里为避免 vector 反复扩充空间降低运行效率,使用resize
一步到位。sum_index
即最后结果中left_index
和right_index
等边界可以结合简单例子分析确定。
$$O(n)$$
, 空间复杂度为 $$O(n+1)$$
.$$O(n \log n)$$
.$$O(n)$$
.总的时间复杂度为 $$O(n \log n)$$
, 空间复杂度为 $$O(n)$$
.
5.Recover Rotated Sorted Array
Question
- lintcode: (39) Recover Rotated Sorted Array
Given a rotated sorted array, recover it to sorted array in-place. Example [4, 5, 1, 2, 3] -> [1, 2, 3, 4, 5] Challenge In-place, O(1) extra space and O(n) time. Clarification What is rotated array: - For example, the orginal array is [1,2,3,4], The rotated array of it can be [1,2,3,4], [2,3,4,1], [3,4,1,2], [4,1,2,3]
首先可以想到逐步移位,但是这种方法显然太浪费时间,不可取。下面介绍利器『三步翻转法』,以
[4, 5, 1, 2, 3]
为例。
5
和1
4, 5
为5, 4
,后半部分1, 2, 3
翻转为3, 2, 1
。整个数组目前变为[5, 4, 3, 2, 1]
[1, 2, 3, 4, 5]
由以上3个步骤可知其核心为『翻转』的in-place实现。使用两个指针,一个指头,一个指尾,使用for循环移位交换即可。
public class Solution {
/**
* @param nums: The rotated sorted array
* @return: The recovered sorted array
*/
public void recoverRotatedSortedArray(ArrayList nums) {
if (nums == null || nums.size() <= 1) {
return;
}
int pos = 1;
while (pos < nums.size()) { // find the break point
if (nums.get(pos - 1) > nums.get(pos)) {
break;
}
pos++;
}
myRotate(nums, 0, pos - 1);
myRotate(nums, pos, nums.size() - 1);
myRotate(nums, 0, nums.size() - 1);
}
private void myRotate(ArrayList nums, int left, int right) { // in-place rotate
while (left < right) {
int temp = nums.get(left);
nums.set(left, nums.get(right));
nums.set(right, temp);
left++;
right--;
}
}
}
/**
* forked from
* http://www.jiuzhang.com/solutions/recover-rotated-sorted-array/
*/
class Solution {
private:
void reverse(vector &nums, vector::size_type start, vector::size_type end) {
for (vector::size_type i = start, j = end; i < j; ++i, --j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
public:
void recoverRotatedSortedArray(vector &nums) {
for (vector::size_type index = 0; index != nums.size() - 1; ++index) {
if (nums[index] > nums[index + 1]) {
reverse(nums, 0, index);
reverse(nums, index + 1, nums.size() - 1);
reverse(nums, 0, nums.size() - 1);
return;
}
}
}
};
首先找到分割点,随后分三步调用翻转函数。简单起见可将vector
替换为int
6.Product of Array Exclude Itself
Question
- lintcode: (50) Product of Array Exclude Itself
- GeeksforGeeks: A Product Array Puzzle - GeeksforGeeks
Given an integers array A. Define B[i] = A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1], calculate B WITHOUT divide operation. Example For A=[1, 2, 3], return [6, 3, 2].
根据题意,有 $$result[i] = left[i] \cdot right[i]$$
, 其中 $$left[i] = \prod _{j = 0} ^{i - 1} A[j]$$
, $$right[i] = \prod _{j = i + 1} ^{n - 1} A[j]$$
. 即将最后的乘积分为两部分求解,首先求得左半部分的值,然后求得右半部分的值。最后将左右两半部分乘起来即为解。
class Solution {
public:
/**
* @param A: Given an integers array A
* @return: A long long array B and B[i]= A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1]
*/
vector productExcludeItself(vector &nums) {
const int nums_size = nums.size();
vector result(nums_size, 1);
if (nums.empty() || nums_size == 1) {
return result;
}
vector left(nums_size, 1);
vector right(nums_size, 1);
for (int i = 1; i != nums_size; ++i) {
left[i] = left[i - 1] * nums[i - 1];
right[nums_size - i - 1] = right[nums_size - i] * nums[nums_size - i];
}
for (int i = 0; i != nums_size; ++i) {
result[i] = left[i] * right[i];
}
return result;
}
};
一次for
循环求出左右部分的连乘积,下标的确定可使用简单例子辅助分析。
两次for
循环,时间复杂度 $$O(n)$$
. 使用了左右两半部分辅助空间,空间复杂度 $$O(2n)$$
.
题解1中使用了左右两个辅助数组,但是仔细瞅瞅其实可以发现完全可以在最终返回结果result
基础上原地计算左右两半部分的积。
class Solution {
public:
/**
* @param A: Given an integers array A
* @return: A long long array B and B[i]= A[0] * ... * A[i-1] * A[i+1] * ... * A[n-1]
*/
vector productExcludeItself(vector &nums) {
const int nums_size = nums.size();
vector result(nums_size, 1);
// solve the left part first
for (int i = 1; i < nums_size; ++i) {
result[i] = result[i - 1] * nums[i - 1];
}
// solve the right part
long long temp = 1;
for (int i = nums_size - 1; i >= 0; --i) {
result[i] *= temp;
temp *= nums[i];
}
return result;
}
};
计算左半部分的递推式不用改,计算右半部分的乘积时由于会有左半部分值的干扰,故使用temp
保存连乘的值。注意temp
需要使用long long
, 否则会溢出。
时间复杂度同上,空间复杂度为 $$O(1)$$
.
7.Partition Array
Question
- lintcode: (31) Partition Array
Problem Statement
Given an array
nums
of integers and an intk
, partition the array (i.e move the elements in "nums") such that:
- All elements < k are moved to the left
- All elements >= k are moved to the right
Return the partitioning index, i.e the first index i nums[i] >= k.
Example
If nums =
[3,2,2,1]
andk=2
, a valid answer is1
.Note
You should do really partition in array nums instead of just counting the numbers of integers smaller than k.
If all elements in nums are smaller than k, then return nums.length
Challenge
Can you partition the array in-place and in O(n)?
容易想到的一个办法是自左向右遍历,使用right
保存大于等于 k 的索引,i
则为当前遍历元素的索引,总是保持i >= right
, 那么最后返回的right
即为所求。
class Solution {
public:
int partitionArray(vector &nums, int k) {
int right = 0;
const int size = nums.size();
for (int i = 0; i < size; ++i) {
if (nums[i] < k && i >= right) {
int temp = nums[i];
nums[i] = nums[right];
nums[right] = temp;
++right;
}
}
return right;
}
};
自左向右遍历,遇到小于 k 的元素时即和right
索引处元素交换,并自增right
指向下一个元素,这样就能保证right
之前的元素一定小于 k. 注意if
判断条件中i >= right
不能是i > right
, 否则需要对特殊情况如全小于 k 时的考虑,而且即使考虑了这一特殊情况也可能存在其他 bug. 具体是什么 bug 呢?欢迎提出你的分析意见~
遍历一次数组,时间复杂度最少为 $$O(n)$$
, 可能需要一定次数的交换。
有了解过 Quick Sort 的做这道题自然是分分钟的事,使用左右两根指针 $$left, right$$
分别代表小于、大于等于 k 的索引,左右同时开工,直至 $$left > right$$
.
class Solution {
public:
int partitionArray(vector &nums, int k) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
while (left <= right && nums[left] < k) ++left;
while (left <= right && nums[right] >= k) --right;
if (left <= right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
++left;
--right;
}
}
return left;
}
};
大循环能正常进行的条件为 $$left <= right$$
, 对于左边索引,向右搜索直到找到小于 k 的索引为止;对于右边索引,则向左搜索直到找到大于等于 k 的索引为止。注意在使用while
循环时务必进行越界检查!
找到不满足条件的索引时即交换其值,并递增left
, 递减right
. 紧接着进行下一次循环。最后返回left
即可,当nums
为空时包含在left = 0
之中,不必单独特殊考虑,所以应返回left
而不是right
.
只需要对整个数组遍历一次,时间复杂度为 $$O(n)$$
, 相比题解1,题解2对全小于 k 的数组效率较高,元素交换次数较少。
8.First Missing Positive
Question
- leetcode: First Missing Positive | LeetCode OJ
- lintcode: (189) First Missing Positive
Given an unsorted integer array, find the first missing positive integer. Example Given [1,2,0] return 3, and [3,4,-1,1] return 2. Challenge Your algorithm should run in O(n) time and uses constant space.
容易想到的方案是先排序,然后遍历求得缺的最小整数。排序算法中常用的基于比较的方法时间复杂度的理论下界为 $$O(n \log n)$$
, 不符题目要求。常见的能达到线性时间复杂度的排序算法有 基数排序,计数排序 和 桶排序。
基数排序显然不太适合这道题,计数排序对元素落在一定区间且重复值较多的情况十分有效,且需要额外的 $$O(n)$$
空间,对这道题不太合适。最后就只剩下桶排序了,桶排序通常需要按照一定规则将值放入桶中,一般需要额外的 $$O(n)$$
空间,咋看一下似乎不太适合在这道题中使用,但是若能设定一定的规则原地交换原数组的值呢?这道题的难点就在于这种规则的设定。
设想我们对给定数组使用桶排序的思想排序,第一个桶放1,第二个桶放2,如果找不到相应的数,则相应的桶的值不变(可能为负值,也可能为其他值)。
那么怎么才能做到原地排序呢?即若 $$A[i] = x$$
, 则将 x 放到它该去的地方 - $$A[x - 1] = x$$
, 同时将原来 $$A[x - 1]$$
地方的值交换给 $$A[i]$$
.
排好序后遍历桶,如果不满足 $$f[i] = i + 1$$
, 那么警察叔叔就是它了!如果都满足条件怎么办?那就返回给定数组大小再加1呗。
class Solution {
public:
/**
* @param A: a vector of integers
* @return: an integer
*/
int firstMissingPositive(vector A) {
const int size = A.size();
for (int i = 0; i < size; ++i) {
while (A[i] > 0 && A[i] <= size && \
(A[i] != i + 1) && (A[i] != A[A[i] - 1])) {
int temp = A[A[i] - 1];
A[A[i] - 1] = A[i];
A[i] = temp;
}
}
for (int i = 0; i < size; ++i) {
if (A[i] != i + 1) {
return i + 1;
}
}
return size + 1;
}
};
核心代码为那几行交换,但是要很好地处理各种边界条件则要下一番功夫了,要能正常的交换,需满足以下几个条件:
A[i]
为正数,负数和零都无法在桶中找到生存空间...A[i] \leq size
当前索引处的值不能比原数组容量大,大了的话也没用啊,肯定不是缺的第一个正数。A[i] != i + 1
, 都满足条件了还交换个毛线,交换也是自身的值。A[i] != A[A[i] - 1]
, 避免欲交换的值和自身相同,否则有重复值时会产生死循环。如果满足以上四个条件就可以愉快地交换彼此了,使用while
循环处理,此时i
并不自增,直到将所有满足条件的索引处理完。
注意交换的写法,若写成
int temp = A[i];
A[i] = A[A[i] - 1];
A[A[i] - 1] = temp;
这又是满满的 bug :( 因为在第三行中A[i]
已不再是之前的值,第二行赋值时已经改变,故源码中的写法比较安全。
最后遍历桶排序后的数组,若在数组大小范围内找到不满足条件的解,直接返回,否则就意味着原数组给的元素都是从1开始的连续正整数,返回数组大小加1即可。
「桶排序」需要遍历一次原数组,考虑到while
循环也需要一定次数的遍历,故时间复杂度至少为 $$O(n)$$
. 最后求索引值最多遍历一次排序后数组,时间复杂度最高为 $$O(n)$$
, 用到了temp
作为中间交换变量,空间复杂度为 $$O(1)$$
.
9、2 Sum
Question
- leetcode: Two Sum | LeetCode OJ
- lintcode: (56) 2 Sum
Problem Statement
Given an array of integers, find two numbers such that they add up to a specific target number.
The function
twoSum
should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are NOT zero-based.Example
numbers=
[2, 7, 11, 15]
, target=9
return
[1, 2]
Note
You may assume that each input would have exactly one solution
Challenge
Either of the following solutions are acceptable:
- O(n) Space, O(nlogn) Time
- O(n) Space, O(n) Time
找两数之和是否为target
, 如果是找数组中一个值为target
该多好啊!遍历一次就知道了,我只想说,too naive... 难道要将数组中所有元素的两两组合都求出来与target
比较吗?时间复杂度显然为 $$O(n^2)$$
, 显然不符题目要求。找一个数时直接遍历即可,那么可不可以将两个数之和转换为找一个数呢?我们先来看看两数之和为target
所对应的判断条件—— $$x_i + x_j = target$$
, 可进一步转化为 $$x_i = target - x_j$$
, 其中 $$i$$
和 $$j$$
为数组中的下标。一段神奇的数学推理就将找两数之和转化为了找一个数是否在数组中了!可见数学是多么的重要...
基本思路有了,现在就来看看怎么实现,显然我们需要额外的空间(也就是哈希表)来保存已经处理过的 $$x_j$$
(注意这里并不能先初始化哈希表,否则无法排除两个相同的元素相加为 target 的情况), 如果不满足等式条件,那么我们就往后遍历,并把之前的元素加入到哈希表中,如果target
减去当前索引后的值在哈希表中找到了,那么就将哈希表中相应的索引返回,大功告成!
class Solution:
"""
@param numbers : An array of Integer
@param target : target = numbers[index1] + numbers[index2]
@return : [index1 + 1, index2 + 1] (index1 < index2)
"""
def twoSum(self, numbers, target):
hashdict = {}
for i, item in enumerate(numbers):
if (target - item) in hashdict:
return (hashdict[target - item] + 1, i + 1)
hashdict[item] = i
return (-1, -1)
class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
vector twoSum(vector &nums, int target) {
vector result;
const int length = nums.size();
if (0 == length) {
return result;
}
// first value, second index
unordered_map hash(length);
for (int i = 0; i != length; ++i) {
if (hash.find(target - nums[i]) != hash.end()) {
result.push_back(hash[target - nums[i]]);
result.push_back(i + 1);
return result;
} else {
hash[nums[i]] = i + 1;
}
}
return result;
}
};
public class Solution {
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1 + 1, index2 + 1] (index1 < index2)
*/
public int[] twoSum(int[] numbers, int target) {
if (numbers == null || numbers.length == 0) return new int[]{0, 0};
Map hashmap = new HashMap();
int index1 = 0, index2 = 0;
for (int i = 0; i < numbers.length; i++) {
if (hashmap.containsKey(target - numbers[i])) {
index1 = hashmap.get(target - numbers[i]);
index2 = i;
return new int[]{1 + index1, 1 + index2};
} else {
hashmap.put(numbers[i], i);
}
}
return new int[]{0, 0};
}
}
unordered_map
映射值和索引。Python 中的dict
就是天然的哈希表。哈希表用了和数组等长的空间,空间复杂度为 $$O(n)$$
, 遍历一次数组,时间复杂度为 $$O(n)$$
.
但凡可以用空间换时间的做法,往往也可以使用时间换空间。另外一个容易想到的思路就是先对数组排序,然后使用两根指针分别指向首尾元素,逐步向中间靠拢,直至找到满足条件的索引为止。
class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
vector twoSum(vector &nums, int target) {
vector result;
const int length = nums.size();
if (0 == length) {
return result;
}
// first num, second is index
vector > num_index(length);
// map num value and index
for (int i = 0; i != length; ++i) {
num_index[i].first = nums[i];
num_index[i].second = i + 1;
}
sort(num_index.begin(), num_index.end());
int start = 0, end = length - 1;
while (start < end) {
if (num_index[start].first + num_index[end].first > target) {
--end;
} else if(num_index[start].first + num_index[end].first == target) {
int min_index = min(num_index[start].second, num_index[end].second);
int max_index = max(num_index[start].second, num_index[end].second);
result.push_back(min_index);
result.push_back(max_index);
return result;
} else {
++start;
}
}
return result;
}
};
length
保存数组的长度,避免反复调用nums.size()
造成性能损失。pair
组合排序前的值和索引,避免排序后找不到原有索引信息。遍历一次原数组得到pair
类型的新数组,时间复杂度为 $$O(n)$$
, 空间复杂度也为 $$O(n)$$
. 标准库中的排序方法时间复杂度近似为 $$O(n \log n)$$
, 两根指针遍历数组时间复杂度为 $$O(n)$$
.
10、3 Sum
Question
- leetcode: 3Sum | LeetCode OJ
- lintcode: (57) 3 Sum
Given an array S of n integers, are there elements a, b, c in S such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero. Example For example, given array S = {-1 0 1 2 -1 -4}, A solution set is: (-1, 0, 1) (-1, -1, 2) Note Elements in a triplet (a,b,c) must be in non-descending order. (ie, a ≤ b ≤ c) The solution set must not contain duplicate triplets.
相比之前的 2 Sum, 3 Sum 又多加了一个数,按照之前 2 Sum 的分解为『1 Sum + 1 Sum』的思路,我们同样可以将 3 Sum 分解为『1 Sum + 2 Sum』的问题,具体就是首先对原数组排序,排序后选出第一个元素,随后在剩下的元素中使用 2 Sum 的解法。
class Solution:
"""
@param numbersbers : Give an array numbersbers of n integer
@return : Find all unique triplets in the array which gives the sum of zero.
"""
def threeSum(self, numbers):
triplets = []
length = len(numbers)
if length < 3:
return triplets
numbers.sort()
for i in xrange(length):
target = 0 - numbers[i]
# 2 Sum
hashmap = {}
for j in xrange(i + 1, length):
item_j = numbers[j]
if (target - item_j) in hashmap:
triplet = [numbers[i], target - item_j, item_j]
if triplet not in triplets:
triplets.append(triplet)
else:
hashmap[item_j] = j
return triplets
由于排序后的元素已经按照大小顺序排列,且在2 Sum 中先遍历的元素较小,所以无需对列表内元素再排序。
排序时间复杂度 $$O(n \log n)$$
, 两重for
循环,时间复杂度近似为 $$O(n^2)$$
,使用哈希表(字典)实现,空间复杂度为 $$O(n)$$
.
目前这段源码为比较简易的实现,leetcode 上的运行时间为500 + ms, 还有较大的优化空间,嗯,后续再进行优化。
class Solution {
public:
vector > threeSum(vector &num)
{
vector > result;
if (num.size() < 3) return result;
int ans = 0;
sort(num.begin(), num.end());
for (int i = 0;i < num.size() - 2; ++i)
{
if (i > 0 && num[i] == num[i - 1])
continue;
int j = i + 1;
int k = num.size() - 1;
while (j < k)
{
ans = num[i] + num[j] + num[k];
if (ans == 0)
{
result.push_back({num[i], num[j], num[k]});
++j;
while (j < num.size() && num[j] == num[j - 1])
++j;
--k;
while (k >= 0 && num[k] == num[k + 1])
--k;
}
else if (ans > 0)
--k;
else
++j;
}
}
return result;
}
};
同python解法不同,没有使用hash map
S = {-1 0 1 2 -1 -4}
排序后:
S = {-4 -1 -1 0 1 2}
↑ ↑ ↑
i j k
→ ←
i每轮只走一步,j和k根据S[i]+S[j]+S[k]=ans和0的关系进行移动,且j只向后走(即S[j]只增大),k只向前走(即S[k]只减小)
如果ans>0说明S[k]过大,k向前移;如果ans<0说明S[j]过小,j向后移;ans==0即为所求。
至于如何取到所有解,看代码即可理解,不再赘述。
外循环i走了n轮,每轮j和k一共走n-i步,所以时间复杂度为$$O(n^2)$$
。 最终运行时间为52ms
11、3 Sum Closest
Question
- leetcode: 3Sum Closest | LeetCode OJ
- lintcode: (59) 3 Sum Closest
Given an array S of n integers, find three integers in S such that the sum is closest to a given number, target. Return the sum of the three integers. You may assume that each input would have exactly one solution. For example, given array S = {-1 2 1 -4}, and target = 1. The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).
和 3 Sum 的思路接近,首先对原数组排序,随后将3 Sum 的题拆解为『1 Sum + 2 Sum』的题,对于 Closest 的题使用两根指针而不是哈希表的方法较为方便。对于有序数组来说,在查找 Cloest 的值时其实是有较大的优化空间的。
class Solution:
"""
@param numbers: Give an array numbers of n integer
@param target : An integer
@return : return the sum of the three integers, the sum closest target.
"""
def threeSumClosest(self, numbers, target):
result = 2**31 - 1
length = len(numbers)
if length < 3:
return result
numbers.sort()
larger_count = 0
for i, item_i in enumerate(numbers):
start = i + 1
end = length - 1
# optimization 1 - filter the smallest sum greater then target
if start < end:
sum3_smallest = numbers[start] + numbers[start + 1] + item_i
if sum3_smallest > target:
larger_count += 1
if larger_count > 1:
return result
while (start < end):
sum3 = numbers[start] + numbers[end] + item_i
if abs(sum3 - target) < abs(result - target):
result = sum3
# optimization 2 - filter the sum3 closest to target
sum_flag = 0
if sum3 > target:
end -= 1
if sum_flag == -1:
break
sum_flag = 1
elif sum3 < target:
start += 1
if sum_flag == 1:
break
sum_flag = -1
else:
return result
return result
sys
包,保险起见就初始化了result
为还算较大的数,作为异常的返回值。item_i
后即转化为『2 Sum Cloest』问题。『2 Sum Cloest』的起始元素索引为i + 1
,之前的元素不能参与其中。target
值,若第二次大于target
果断就此罢休,后面的值肯定越来越大。sum3
和result
与target
的差值的绝对值,更新result
为较小的绝对值。target
之差的绝对值只会越来越大。对原数组排序,平均时间复杂度为 $$O(n \log n)$$
, 两重for
循环,由于有两处优化,故最坏的时间复杂度才是 $$O(n^2)$$
, 使用了result
作为临时值保存最接近target
的值,两处优化各使用了一个辅助变量,空间复杂度 $$O(1)$$
.
class Solution {
public:
int threeSumClosest(vector &num, int target)
{
if (num.size() <= 3) return accumulate(num.begin(), num.end(), 0);
sort (num.begin(), num.end());
int result = 0, n = num.size(), temp;
result = num[0] + num[1] + num[2];
for (int i = 0; i < n - 2; ++i)
{
int j = i + 1, k = n - 1;
while (j < k)
{
temp = num[i] + num[j] + num[k];
if (abs(target - result) > abs(target - temp))
result = temp;
if (result == target)
return result;
( temp > target ) ? --k : ++j;
}
}
return result;
}
};
和前面3Sum解法相似,同理使用i,j,k三个指针进行循环。
区别在于3sum中的target为0,这里新增一个变量用于比较哪组数据与target更为相近
时间复杂度同理为$$O(n^2)$$
运行时间 16ms
12、Remove Duplicates from Sorted Array
Question
- leetcode: Remove Duplicates from Sorted Array | LeetCode OJ
- lintcode: (100) Remove Duplicates from Sorted Array
Given a sorted array, remove the duplicates in place such that each element appear only once and return the new length. Do not allocate extra space for another array, you must do this in place with constant memory. For example, Given input array A = [1,1,2], Your function should return length = 2, and A is now [1,2]. Example
使用两根指针(下标),一个指针(下标)遍历数组,另一个指针(下标)只取不重复的数置于原数组中。
class Solution {
public:
/**
* @param A: a list of integers
* @return : return an integer
*/
int removeDuplicates(vector &nums) {
if (nums.size() <= 1) return nums.size();
int len = nums.size();
int newIndex = 0;
for (int i = 1; i< len; ++i) {
if (nums[i] != nums[newIndex]) {
newIndex++;
nums[newIndex] = nums[i];
}
}
return newIndex + 1;
}
};
public class Solution {
/**
* @param A: a array of integers
* @return : return an integer
*/
public int removeDuplicates(int[] nums) {
if (nums == null) return -1;
if (nums.length <= 1) return nums.length;
int newIndex = 0;
for (int i = 1; i < nums.length; i++) {
if (nums[i] != nums[newIndex]) {
newIndex++;
nums[newIndex] = nums[i];
}
}
return newIndex + 1;
}
}
注意最后需要返回的是索引值加1。
遍历一次数组,时间复杂度 $$O(n)$$
, 空间复杂度 $$O(1)$$
.
13、Remove Duplicates from Sorted Array II
Question
- leetcode: Remove Duplicates from Sorted Array II | LeetCode OJ
- lintcode: (101) Remove Duplicates from Sorted Array II
Follow up for "Remove Duplicates": What if duplicates are allowed at most twice? For example, Given sorted array A = [1,1,1,2,2,3], Your function should return length = 5, and A is now [1,1,2,2,3]. Example
在上题基础上加了限制条件元素最多可重复出现两次。因此可以在原题的基础上添加一变量跟踪元素重复出现的次数,小于指定值时执行赋值操作。但是需要注意的是重复出现次数occurence
的初始值(从1开始,而不是0)和reset的时机。这种方法比较复杂,谢谢 @meishenme 提供的简洁方法,核心思想仍然是两根指针,只不过此时新索引自增的条件是当前遍历的数组值和『新索引』或者『新索引-1』两者之一不同。
class Solution {
public:
/**
* @param A: a list of integers
* @return : return an integer
*/
int removeDuplicates(vector &nums) {
if (nums.size() <= 2) return nums.size();
int len = nums.size();
int newIndex = 1;
for (int i = 2; i < len; ++i) {
if (nums[i] != nums[newIndex] || nums[i] != nums[newIndex - 1]) {
++newIndex;
nums[newIndex] = nums[i];
}
}
return newIndex + 1;
}
};
public class Solution {
/**
* @param A: a array of integers
* @return : return an integer
*/
public int removeDuplicates(int[] nums) {
if (nums == null) return -1;
if (nums.length <= 2) return nums.length;
int newIndex = 1;
for (int i = 2; i < nums.length; i++) {
if (nums[i] != nums[newIndex] || nums[i] != nums[newIndex - 1]) {
newIndex++;
nums[newIndex] = nums[i];
}
}
return newIndex + 1;
}
}
遍历数组时 i 从2开始,newIndex 初始化为1便于分析。
时间复杂度 $$O(n)$$
, 空间复杂度 $$O(1)$$
.
14、Merge Sorted Array
Question
- leetcode: Merge Sorted Array | LeetCode OJ
- lintcode: (6) Merge Sorted Array
Given two sorted integer arrays A and B, merge B into A as one sorted array. Example A = [1, 2, 3, empty, empty], B = [4, 5] After merge, A will be filled as [1, 2, 3, 4, 5] Note You may assume that A has enough space (size that is greater or equal to m + n) to hold additional elements from B. The number of elements initialized in A and B are m and n respectively.
因为本题有 in-place 的限制,故必须从数组末尾的两个元素开始比较;否则就会产生挪动,一旦挪动就会是 $$O(n^2)$$
的。 自尾部向首部逐个比较两个数组内的元素,取较大的置于数组 A 中。由于 A 的容量较 B 大,故最后 m == 0
或者 n == 0
时仅需处理 B 中的元素,因为 A 中的元素已经在 A 中,无需处理。
class Solution:
"""
@param A: sorted integer array A which has m elements,
but size of A is m+n
@param B: sorted integer array B which has n elements
@return: void
"""
def mergeSortedArray(self, A, m, B, n):
if B is None:
return A
index = m + n - 1
while m > 0 and n > 0:
if A[m - 1] > B[n - 1]:
A[index] = A[m - 1]
m -= 1
else:
A[index] = B[n - 1]
n -= 1
index -= 1
# B has elements left
while n > 0:
A[index] = B[n - 1]
n -= 1
index -= 1
class Solution {
public:
/**
* @param A: sorted integer array A which has m elements,
* but size of A is m+n
* @param B: sorted integer array B which has n elements
* @return: void
*/
void mergeSortedArray(int A[], int m, int B[], int n) {
int index = m + n - 1;
while (m > 0 && n > 0) {
if (A[m - 1] > B[n - 1]) {
A[index] = A[m - 1];
--m;
} else {
A[index] = B[n - 1];
--n;
}
--index;
}
// B has elements left
while (n > 0) {
A[index] = B[n - 1];
--n;
--index;
}
}
};
class Solution {
/**
* @param A: sorted integer array A which has m elements,
* but size of A is m+n
* @param B: sorted integer array B which has n elements
* @return: void
*/
public void mergeSortedArray(int[] A, int m, int[] B, int n) {
if (A == null || B == null) return;
int index = m + n - 1;
while (m > 0 && n > 0) {
if (A[m - 1] > B[n - 1]) {
A[index] = A[m - 1];
m--;
} else {
A[index] = B[n - 1];
n--;
}
index--;
}
// B has elements left
while (n > 0) {
A[index] = B[n - 1];
n--;
index--;
}
}
}
第一个 while 只能用条件与。
最坏情况下需要遍历两个数组中所有元素,时间复杂度为 $$O(n)$$
. 空间复杂度 $$O(1)$$
.
15、Merge Sorted Array II
Question
- lintcode: (64) Merge Sorted Array II
Merge two given sorted integer array A and B into a new sorted integer array. Example A=[1,2,3,4] B=[2,4,5,6] return [1,2,2,3,4,4,5,6] Challenge How can you optimize your algorithm if one array is very large and the other is very small?
上题要求 in-place, 此题要求返回新数组。由于可以生成新数组,故使用常规思路按顺序遍历即可。
class Solution:
#@param A and B: sorted integer array A and B.
#@return: A new sorted integer array
def mergeSortedArray(self, A, B):
if A is None or len(A) == 0:
return B
if B is None or len(B) == 0:
return A
C = []
aLen, bLen = len(A), len(B)
i, j = 0, 0
while i < aLen and j < bLen:
if A[i] < B[j]:
C.append(A[i])
i += 1
else:
C.append(B[j])
j += 1
# A has elements left
while i < aLen:
C.append(A[i])
i += 1
# B has elements left
while j < bLen:
C.append(B[j])
j += 1
return C
class Solution {
public:
/**
* @param A and B: sorted integer array A and B.
* @return: A new sorted integer array
*/
vector mergeSortedArray(vector &A, vector &B) {
if (A.empty()) return B;
if (B.empty()) return A;
int aLen = A.size(), bLen = B.size();
vector C;
int i = 0, j = 0;
while (i < aLen && j < bLen) {
if (A[i] < B[j]) {
C.push_back(A[i]);
++i;
} else {
C.push_back(B[j]);
++j;
}
}
// A has elements left
while (i < aLen) {
C.push_back(A[i]);
++i;
}
// B has elements left
while (j < bLen) {
C.push_back(B[j]);
++j;
}
return C;
}
};
class Solution {
/**
* @param A and B: sorted integer array A and B.
* @return: A new sorted integer array
*/
public ArrayList mergeSortedArray(ArrayList A, ArrayList B) {
if (A == null || A.isEmpty()) return B;
if (B == null || B.isEmpty()) return A;
ArrayList C = new ArrayList();
int aLen = A.size(), bLen = B.size();
int i = 0, j = 0;
while (i < aLen && j < bLen) {
if (A.get(i) < B.get(j)) {
C.add(A.get(i));
i++;
} else {
C.add(B.get(j));
j++;
}
}
// A has elements left
while (i < aLen) {
C.add(A.get(i));
i++;
}
// B has elements left
while (j < bLen) {
C.add(B.get(j));
j++;
}
return C;
}
}
分三步走,后面分别单独处理剩余的元素。
遍历 A, B 数组各一次,时间复杂度 $$O(n)$$
, 空间复杂度 $$O(1)$$
.
Challenge
两个倒排列表,一个特别大,一个特别小,如何 Merge?此时应该考虑用一个二分法插入小的,即内存拷贝。
16、Median
Question
- lintcode: (80) Median
Given a unsorted array with integers, find the median of it. A median is the middle number of the array after it is sorted. If there are even numbers in the array, return the N/2-th number after sorted. Example Given [4, 5, 1, 2, 3], return 3 Given [7, 9, 4, 5], return 5 Challenge O(n) time.
寻找未排序数组的中位数,简单粗暴的方法是先排序后输出中位数索引处的数,但是基于比较的排序算法的时间复杂度为 $$O(n \log n)$$
, 不符合题目要求。线性时间复杂度的排序算法常见有计数排序、桶排序和基数排序,这三种排序方法的空间复杂度均较高,且依赖于输入数据特征(数据分布在有限的区间内),用在这里并不是比较好的解法。
由于这里仅需要找出中位数,即找出数组中前半个长度的较大的数,不需要进行完整的排序,说到这你是不是想到了快速排序了呢?快排的核心思想就是以基准为界将原数组划分为左小右大两个部分,用在这十分合适。快排的实现见 Quick Sort, 由于调用一次快排后基准元素的最终位置是知道的,故递归的终止条件即为当基准元素的位置(索引)满足中位数的条件时(左半部分长度为原数组长度一半)即返回最终结果。由于函数原型中左右最小索引并不总是原数组的最小最大,故需要引入相对位置(长度)也作为其中之一的参数。若左半部分长度偏大,则下一次递归排除右半部分,反之则排除左半部分。
class Solution:
"""
@param nums: A list of integers.
@return: An integer denotes the middle number of the array.
"""
def median(self, nums):
if not nums:
return -1
return self.helper(nums, 0, len(nums) - 1, (1 + len(nums)) / 2)
def helper(self, nums, l, u, size):
if l >= u:
return nums[u]
m = l
for i in xrange(l + 1, u + 1):
if nums[i] < nums[l]:
m += 1
nums[m], nums[i] = nums[i], nums[m]
# swap between m and l after partition, important!
nums[m], nums[l] = nums[l], nums[m]
if m - l + 1 == size:
return nums[m]
elif m - l + 1 > size:
return self.helper(nums, l, m - 1, size)
else:
return self.helper(nums, m + 1, u, size - (m - l + 1))
class Solution {
public:
/**
* @param nums: A list of integers.
* @return: An integer denotes the middle number of the array.
*/
int median(vector &nums) {
if (nums.empty()) return 0;
int len = nums.size();
return helper(nums, 0, len - 1, (len + 1) / 2);
}
private:
int helper(vector &nums, int l, int u, int size) {
// if (l >= u) return nums[u];
int m = l; // index m to track pivot
for (int i = l + 1; i <= u; ++i) {
if (nums[i] < nums[l]) {
++m;
int temp = nums[i];
nums[i] = nums[m];
nums[m] = temp;
}
}
// swap with the pivot
int temp = nums[m];
nums[m] = nums[l];
nums[l] = temp;
if (m - l + 1 == size) {
return nums[m];
} else if (m - l + 1 > size) {
return helper(nums, l, m - 1, size);
} else {
return helper(nums, m + 1, u, size - (m - l + 1));
}
}
};
public class Solution {
/**
* @param nums: A list of integers.
* @return: An integer denotes the middle number of the array.
*/
public int median(int[] nums) {
if (nums == null) return -1;
return helper(nums, 0, nums.length - 1, (nums.length + 1) / 2);
}
// l: lower, u: upper, m: median
private int helper(int[] nums, int l, int u, int size) {
if (l >= u) return nums[u];
int m = l;
for (int i = l + 1; i <= u; i++) {
if (nums[i] < nums[l]) {
m++;
int temp = nums[m];
nums[m] = nums[i];
nums[i] = temp;
}
}
// swap between array[m] and array[l]
// put pivot in the mid
int temp = nums[m];
nums[m] = nums[l];
nums[l] = temp;
if (m - l + 1 == size) {
return nums[m];
} else if (m - l + 1 > size) {
return helper(nums, l, m - 1, size);
} else {
return helper(nums, m + 1, u, size - (m - l + 1));
}
}
}
以相对距离(长度)进行理解,递归终止步的条件一直保持不变(比较左半部分的长度)。
以题目中给出的样例进行分析,size
传入的值可为(len(nums) + 1) / 2
, 终止条件为m - l + 1 == size
, 含义为基准元素到索引为l
的元素之间(左半部分)的长度(含)与(len(nums) + 1) / 2
相等。若m - l + 1 > size
, 即左半部分长度偏大,此时递归终止条件并未变化,因为l
的值在下一次递归调用时并未改变,所以仍保持为size
; 若m - l + 1 < size
, 左半部分长度偏小,下一次递归调用右半部分,由于此时左半部分的索引值已变化,故size
应改为下一次在右半部分数组中的终止条件size - (m - l + 1)
, 含义为原长度size
减去左半部分数组的长度m - l + 1
.
和快排类似,这里也有最好情况与最坏情况,平均情况下,索引m
每次都处于中央位置,即每次递归后需要遍历的数组元素个数减半,故总的时间复杂度为 $$O(n (1 + 1/2 + 1/4 + ...)) = O(2n)$$
, 最坏情况下为平方。使用了临时变量,空间复杂度为 $$O(1)$$
, 满足题目要求。
17、Partition Array by Odd and Even
Question
- lintcode: (373) Partition Array by Odd and Even
- Segregate Even and Odd numbers - GeeksforGeeks
Partition an integers array into odd number first and even number second. Example Given [1, 2, 3, 4], return [1, 3, 2, 4] Challenge Do it in-place.
将数组中的奇数和偶数分开,使用『两根指针』的方法最为自然,奇数在前,偶数在后,若不然则交换之。
public class Solution {
/**
* @param nums: an array of integers
* @return: nothing
*/
public void partitionArray(int[] nums) {
if (nums == null) return;
int left = 0, right = nums.length - 1;
while (left < right) {
// odd number
while (left < right && nums[left] % 2 != 0) {
left++;
}
// even number
while (left < right && nums[right] % 2 == 0) {
right--;
}
// swap
if (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
}
}
void partitionArray(vector &nums) {
if (nums.empty()) return;
int i=0, j=nums.size()-1;
while (i
注意处理好边界即循环时保证left < right
.
遍历一次数组,时间复杂度为 $$O(n)$$
, 使用了两根指针,空间复杂度 $$O(1)$$
.
18、Kth Largest Element
Question
- leetcode: Kth Largest Element in an Array | LeetCode OJ
- lintcode: (5) Kth Largest Element
Find K-th largest element in an array. Example In array [9,3,2,4,8], the 3rd largest element is 4. In array [1,2,3,4,5], the 1st largest element is 5, 2nd largest element is 4, 3rd largest element is 3 and etc. Note You can swap elements in the array Challenge O(n) time, O(1) extra memory.
找第 K 大数,基于比较的排序的方法时间复杂度为 $$O(n)$$
, 数组元素无区间限定,故无法使用线性排序。由于只是需要找第 K 大数,这种类型的题通常需要使用快排的思想解决。Quick Sort 总结了一些经典模板。这里比较基准值最后的位置的索引值和 K 的大小关系即可递归求解。
class Solution {
//param k : description of k
//param numbers : array of numbers
//return: description of return
public int kthLargestElement(int k, ArrayList numbers) {
if (numbers == null || numbers.isEmpty()) return -1;
int result = qSort(numbers, 0, numbers.size() - 1, k);
return result;
}
private int qSort(ArrayList nums, int l, int u, int k) {
// l should not greater than u
if (l >= u) return nums.get(u);
// index m of nums
int m = l;
for (int i = l + 1; i <= u; i++) {
if (nums.get(i) > nums.get(l)) {
m++;
Collections.swap(nums, m, i);
}
}
Collections.swap(nums, m, l);
if (m + 1 == k) {
return nums.get(m);
} else if (m + 1 > k) {
return qSort(nums, l, m - 1, k);
} else {
return qSort(nums, m + 1, u, k);
}
}
}
递归的终止条件有两个,一个是左边界的值等于右边界(实际中其实不会有 l > u), 另一个则是索引值 m + 1 == k
. 这里找的是第 K 大数,故为降序排列,for 循环中使用nums.get(i) > nums.get(l)
而不是小于号。
最坏情况下需要遍历 $$ n + n - 1 + ... + 1 = O(n^2)$$
, 平均情况下 $$n + n/2 + n/4 + ... + 1 = O(2n)=O(n)$$
. 故平均情况时间复杂度为 $$O(n)$$
. 交换数组的值时使用了额外空间,空间复杂度 $$O(1)$$
.