leetcode刷题(一)
41. 缺失的第一个正数
给你一个未排序的整数数组,请你找出其中没有出现的最小的正整数。
题解
要求空间复杂度O(1)。前不久刚做了一道循环排列的题,所以看见“就地”两个字,秒懂: 将数字放到对应的位置上
有个特殊的样例,
nums[i] - 1
可能会超出int
的范围
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for (int i = 0; i < n; ++i) if (nums[i] >= 0) nums[i]--;
for (int i = 0; i < n; ++i) {
int pos = nums[i];
while(pos >= 0 && pos < n && pos != nums[pos]) swap(pos, nums[pos]);
}
int ans = n + 1;
for (int i = 0; i < n; ++i) if (nums[i] != i) {
ans = i + 1;
break;
}
return ans;
}
};
287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
题解一
要求空间复杂度O(1)、时间复杂度小于O(\(n^2\))、不能更改原数组。二分枚举答案,遍历整个数组,如果小于或者等于枚举的答案的个数大于枚举的答案,说明左半段出现了重复,否则,说明右半段出现了重复。
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int l = 1, r = n;
// 时间换空间,有点儿意思
while(l < r) {
int mid = (l + r) >> 1; // 枚举答案
int cnt = 0;
for (int num: nums) if (num <= mid) cnt++;
if (cnt > mid) r = mid;
else l = mid + 1;
}
return l;
}
};
题解二
oooooorz,还可以用二进制考虑。思想就一句话:如果一个数是重复的,那么对应的二进制位中1的个数肯定比原来多,因为重复了嘛。
// beautiful code
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int ans = 0;
for (int i = 0; i < 32; ++i) {
int a = 0, b = 0;
int mask = 1 << i;
for (int j = 0; j < n; ++j) {
if (nums[j] & mask) a++;
if (j & mask) b++;
}
if (a > b) ans += mask; // ans = ans | mask; is ok!
}
return ans;
}
};
// sad code
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int n = nums.size();
int bit_cnt[40];
memset(bit_cnt, 0, sizeof(bit_cnt));
for (int num: nums) {
int t = 0;
while(num) bit_cnt[t++] += (num & 1), num >>= 1;
}
int bit_tot[40];
memset(bit_tot, 0, sizeof(bit_tot));
for (int i = 1; i < n; ++i) {
int t = 0, num = i;
while(num) bit_tot[t++] += (num & 1), num >>= 1;
}
int ans = 0;
for (int i = 0; i < 32; ++i) if (bit_tot[i] < bit_cnt[i]) ans += (1 << i);
return ans;
}
};
题解三
\(n + 1\)个\(1\)到\(n\)之间的数(包括\(1\)和\(n\))构成的数组,以下标(从0开始)和值建边构成的有向图必定含有环,因为有重复的数。其实任意一个排列(下标从1开始),按照这种方式构成的图都是由一个个环组成的。在这道题中环的入口就是重复的数,怎么求环的入口?快慢指针法,常用来判断单链表中是否有环,详细说明请看代码
神人之笔
class Solution {
public:
int findDuplicate(vector<int>& nums) {
int slow = nums[0]; // 慢指针
int fast = nums[nums[0]]; // 快指针
while(slow != fast) { // 相等,代表在环中相遇
slow = nums[slow];
fast = nums[nums[fast]];
}
求环的入口?
令环的长度为\(m\),从0走到环的入口需要\(S\)步
假设慢指针走了\(n\)步与快指针在环内相遇,那么快指针走了\(2*n\)步,且\(2 * n - n = n\),\(n \% m = 0\)。在第一次相遇的时候,慢指针在环内走了\(n - S\)步。令fast = 0
,然后走\(S\)步走到环的入口,慢指针同时移动也走了\(S\)步,算上之前,慢指针在环内共走了\(n - S + S = n\)步,恰好走到环的入口(\(n \% m = 0\)),于是快慢指针在入口相遇。
fast = 0;
while(slow != fast) { // 第二次相遇必定在入口处
fast = nums[fast];
slow = nums[slow];
}
return fast;
}
};
765. 情侣牵手
N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。
人和座位用 0 到 2N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)。
这些情侣的初始座位 row[i] 是由最初始坐在第 i 个座位上的人决定的。
题解一
很容易想到先把情侣的一人固定在当前位置上,然后交换另一个人来凑成情侣。就是不知道这样为啥能得到最优解,想不通,orz。复杂度\(O(n^2)\)
class Solution {
public:
int minSwapsCouples(vector<int>& row) {
int n = row.size();
int ans = 0;
for (int i = 0; i < n; i += 2) {
if (judeg(row[i], row[i + 1])) continue;
for (int j = i + 2; j < n; j += 2) {
if (judeg(row[j], row[j + 1])) continue;
if (judeg(row[i], row[j])) {
swap(row[i + 1], row[j]);
ans++;
break;
}
else if (judeg(row[i], row[j + 1])) {
swap(row[i + 1], row[j + 1]);
ans++;
break;
}
}
}
return ans;
}
bool judeg(int a, int b) {
if (a > b) swap(a, b);
if (b - a > 1) return false;
if (!(a & 1) && (b & 1)) return true;
return false;
}
};
153. 寻找旋转排序数组中的最小值
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
请找出其中最小的元素。
你可以假设数组中不存在重复元素。
题解
时间复杂度要求\(O(log_2(n))\),二分,然后找判断是在左区域还是右区域的条件
写惯了开区间的二分,上来就RE
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while(l < r) {
int mid = (l + r) >> 1;
if (nums[mid] <= nums[r]) r = mid;
else l = mid + 1;
}
return nums[l];
/*
整理一下就是上面的代码
for(int i = 0; i < 40; ++i) {
int mid = (l + r) >> 1;
if (nums[l] <= nums[mid]) {
if(nums[r] > nums[mid]) r = mid; // nums[r],r一定要小于n
else l = mid;
}
else r = mid; // 不要写成 l = mid,因为可能跳过了最小值
}
return min(nums[l], nums[r]);
*/
}
};
154. 寻找旋转排序数组中的最小值 II【好题】
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,1,4,5,6,7] 可能变为 [4,5,6,7,0,1,1] )。
请找出其中最小的元素。
假设存在重复的数字
题解
时间复杂度要求在\([log_2(n), n]\)之间。显然整个数组被分成了两个上升段。二分,只是需要判断下面这三种情况,边界应该怎么移动:
nums[mid] < nums[r]
,说明mid
和r
处在同一个上升段内,且不会存在比nums[mid]
更小的数,所以r = mid
nums[mid] == nums[r]
,此时无法判断答案在哪边,举例:[3,1,3,3,3]
,[3,3,3,1,3]
,都满足等式,但最小值可能出现在mid
之前也可能之后,所以只能令r = r - 1
,逐步缩小查找范围。为什么不能l++
,由样例1可知,这样更新边界可能忽略最小值。nums[mid] > nums[r]
,说明mid
和l
处在同一个上升段内,而答案在另一个上升段内,所以l = mid + 1
看了一下讨论,发现可做以下几个扩展:①找出旋转点的位置(但存在无法找到旋转点位置的情况,如
[1,1,1,1]
)。②如果是多次旋转,能不能设计较优的算法找到最小值
突然觉得leetcode上面的题很有趣啊,整道题的灵魂就是
r--
,以及考察特殊情形。妙,甚妙!总结:脑子不够灵活
while(l < r) {
int mid = (l + r) >> 1;
if (nums[mid] < nums[r]) r = mid;
else if (nums[mid] == nums[r]) r--;
else l = mid + 1;
}
return nums[l];
33. 搜索旋转排序数组
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是\(O(log_2(n))\)级别。
题解
二分,关键在于判断l
、mid
、r
的分布,分类讨论:
nums[mid] < nums[r]
说明mid
和r
在同一个上升段,l
是否与它们在同一个上升段需要讨论target < nums[mid]
,说明target
在mid
的左边target > nums[mid]
,target
可能在mid
的左边,既在第一个上升段内,也可能在mid
的右边,需要讨论
nums[mid] > nums[r]
说明mid
和l
在同在第一个上升段target < nums[mid]
,target
可能在mid
的左边,既在第一个上升段内,也可能在mid
的右边,即在第二个上升段内,需要讨论target > nums[mid]
,说明target
在mid
的右边
这分类讨论,酸爽!其实正解是先用上一题的方法,先找到旋转点,既最小值的位置\(pos\),然后分别在两个上升段内\(([0, pos - 1], [pos + 1, r])\)用二分查找。
class Solution {
public:
int search(vector<int>& nums, int target)
{
int l = 0, r = nums.size() - 1;
if (r < 0) return -1; // 坑,可能输入空数组
while(l < r){
int mid = (l + r) >> 1;
if (nums[mid] == target) return mid;
if (nums[mid] < nums[r]) {
if (target < nums[mid]) r = mid;
else {
/* think! why this code is wrong ?
if (nums[l] > target) l = mid + 1;
else r = mid;
*/
// target > nums[mid],讨论需仔细
if (nums[l] > nums[r]) {
if (nums[l] > target) l = mid + 1;
else r = mid;
}
else l = mid + 1;
}
}
else {
if (target < nums[mid]) {
if (nums[l] > target) l = mid + 1;
else r = mid;
}
else l = mid + 1;
}
}
return nums[l] == target ? l : -1;
}
};
81. 搜索旋转排序数组 II
假设按照升序排序的数组(可能有重复数字)在预先未知的某个点上进行了旋转。
( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。
编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。
题解
二分,依然是分类讨论,当有重复数字时,会导致边界无法确定更新,老办法,令r = r - 1
灵魂:
r--
,和上一道题相比,这题只能分类讨论,因为它的旋转位置可能无法确定,比如[1,1,1,1]
。越想越妙啊,如果无法确定mid处在哪个上升段,就缩小r
while(l < r){
int mid = (l + r) >> 1;
if (nums[mid] == target) return true;
if (nums[mid] < nums[r]) {
if (target < nums[mid]) r = mid;
else {
if (nums[l] >= nums[r]) { // 与上题相比,多了一个等号,因为相等也意味着l在第一个上升段
if (nums[l] > target) l = mid + 1;
else r = mid;
}
else l = mid + 1;
}
}
else if (nums[mid] == nums[r]) { // mid可能在第一个上升段,也可能在第二个上升段,所以直接令 r = r - 1
r--;
}
else {
if (target < nums[mid]) {
if (nums[l] > target) l = mid + 1;
else r = mid;
}
else l = mid + 1;
}
}