最长连续序列
题目
版本1 正确
public int longestConsecutive(int[] nums) {
if (nums.length == 0) {
return 0;
}
// 一个未排序的数组, 寻找最长连续序列
// 注意要求序列是连续的, 即[1, 2, 3, 4]这种而[1, 3, 4]这种就不行
// 并且构成序列的元素 不要求维持在原数组中的相对顺序 即原来是[4, 1, 2, 3]也可以得到长度为4的[1, 2, 3, 4]
// 我们可以对nums中每一个元素nums[i]作为起点, 遍历数组, 看以nums[i]为最小值, 能构成的最长连续序列为多长
// 不断更新最大值即可
// 但是采用遍历的方式, 每一个元素都要遍历一遍数组, 复杂度是O(n^2)
// 优化1
// 有没有方法不用遍历, 就可以知道递增的元素是否存在呢? 就是用哈希set, 记录下每个元素是否出现过即可
// 我们只需要知道递增的元素是否存在, 它在数组的哪个位置我们并不关心
// 优化2
// 我们是不是需要对数组中的所有nums[i]都计算一遍长度呢, 其实并不用, 例如一个元素是3, 另一个元素是2
// 那么我们只需要计算2作为起点的长度即可
HashSet<Integer> hashSet = new HashSet<>();
for (int i = 0; i < nums.length; i ++) {
hashSet.add(nums[i]);
}
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i ++) {
if (hashSet.contains(nums[i] - 1)) {
// 优化2
continue;
}
// 计算当前元素作为起点的长度
int temp = nums[i] + 1;
int tempCount = 1;
while (hashSet.contains(temp)) {
tempCount ++;
temp ++;
}
max = Math.max(max, tempCount);
}
return max;
}
正确的原因
(1) 先分析暴力解法
(2) 注意优化1和优化2对算法做出了哪些提升
黑白方格画
题目
版本1 正确
public int paintingPlan(int n, int k) {
// 在一个n * n 的方格面板内, 画任意行, 任意列, 一共有多少种画法
// 每一种画法都符合如下规则 选择i行 j列涂上黑色, 此时的 i * n + j * (n - i) = k
// 这里假设先画所有行, 那么再画列的时候, 每一列只能贡献(n - i)个黑色格子
// 同时题目要求有一个格子不同, 就认为方案不同, 因此(i, j)是(3, 5)和(5, 3)是两种方案
// 我需要枚举出所有可能的(i, j), 然后计算出所有的组合数即可
// 特殊情况
if (k == 0) {
// 一个都不需要涂
return 1;
}
if (k < n) {
// 一行都画不满, 不可能存在
return 0;
}
if (k == n) {
// 只需涂满一行
if (k == 1) {
return 1;
} else {
return 2 * n;
}
}
if (k == n * n) {
// 涂满所有格子
return 1;
}
// 普通情况, 得到所有可能的(i, j)组合
List<int []> temp = new ArrayList<>();
for (int i = 0; i <= n; i ++) {
for (int j = 0; j <= n; j ++) {
if ((i * n + j * (n - i)) == k) {
temp.add(new int[]{i, j});
}
}
}
// 对于每一种组合, 计算可能的答案数
int ans = 0;
for (int i = 0; i < temp.size(); i ++) {
int [] oneKind = temp.get(i);
int row = oneKind[0];
int col = oneKind[1];
// 从n行中选择row行有多少种可能 即C n 取 row
int rowComb = comb(n, row);
// 从n列中选择col列有多少种可能
int colComb = comb(n, col);
ans += rowComb * colComb;
}
return ans;
}
// 计算组合数, 利用递归计算, 同时用map减少重复计算
Map<int [], Integer> map = new HashMap<>();
public int comb(int n, int m) {
// 从n个数字, 挑选出m个, 有多少种组合数目 传统的计算公式是Cn取m = n! / (m! * (n - m)!)
// 这里因为存在缩小问题规模的问题, 因此n 和 m的数目都无法保证, 需要base case
if (n < m) {
// 这种情况没有组合数
return 0;
}
if (m == 0) {
// 取0个数字是一种情况
return 1;
}
// 优化1 例如C5取3 等于C5取2, 并且C5取2计算量小一些
// 因此转化成C5取2计算
if (m > n / 2) {
return comb(n, n - m);
}
// 优化2 递归计算, 降低问题规模, comb(n,m)= comb(n-1,m-1)+ comb(n-1,m)
// 解释思想,从n个球中取出m个球可以分成两种情况相加,从n个球中取出一个球,如果它属于m,还需要从n-1中取出m-1个球;如果它不属于m,则需要从n-1中取出m个球
// 防止重复计算
int [] key = new int[]{n, m};
if (map.containsKey(key)) {
return map.get(key);
}
// 从递归式子可以得出每次递归要么n < m, 要么n == m, 因此不能存在n = 0, m != 0的情况
// base case那里就不用管n < 1的情况
int ans = comb(n - 1, m - 1) + comb(n - 1, m);
map.put(key, ans);
return ans;
}
正确的原因
(1) 注意特殊的n和k
(2) 找到所有的可能数, 即不同的(i, j)
(3) 对于每种可能, 计算不同的组合数, 计算组合数的时候, 如何效率更高
在0到n-1中缺失的数字
题目
如果按照题目中认为数组元素是有序的
即当n=4的时候 可能是[0, 1, 2, 4] 可能是[0, 2, 3, 4]等 需要注意, 当n=4的时候, 多出来的那个元素一定是4, 不可能是别的数字, 不可能是[0, 1, 2, 10]这种
版本1 正确
public int missingNumber(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == mid) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 此时一定满足nums[left - 1] == left - 1
return left;
}
正确的原因
(1) 利用二分查找, 如果nums[mid] == mid, 就说明左侧的数组一定是正常的, 缺失的元素一定在右边, 如果nums[mid] != mid, 就说明缺失的元素在左侧数组. 然后跟二分一样, 缩小范围, 最后指针对应的索引就是缺失的元素
(2) 空间复杂度 o(logN) 时间复杂度o(1)
如果任务数组中的元素是无序的
当n = 4的时候, 给出的nums可能是[1, 2, 0, 4]或者[4, 1, 2, 3]等, 数组中任何元素都是无序的
版本1 求和法
public static int missingNumber(int[] nums) {
int length = nums.length;
int sum = (0 + length) * (length + 1) / 2;
for (int i = 0; i < length; i++)
sum -= nums[i];
return sum;
}
正确的原因
(1) 遍历一遍数组, 将数组所有元素求和得到sum, 然后0到n本身的和可以由等差数列计算出来, 和为B
例如[0, 2, 3, 4] 计算出来sum = 2 + 3 + 4 = 9, 然后B是[0, 1, 2, 3, 4]的和, 即B = 1 + 2 + 3 + 4 = 10.
那么缺失的元素就是B - sum = 1
(2) 空间复杂度 o(n) 时间复杂度o(1)
版本2 数组下标法
public int missingNumber(int[] nums) {
int curIndex = 0;
// 可能存在给出的nums是[0, 1, 2] 然后此时缺的元素就是3
int ans = nums.length;
while (curIndex < nums.length) {
while (nums[curIndex] == curIndex || nums[curIndex] == nums.length) {
// 如果当前元素就是nums.length, 需要记录一下位置
if (nums[curIndex] == nums.length) {
ans = curIndex;
}
curIndex ++;
// 当当前指针到达数组末尾的时候, 就可以返回结果了
if (curIndex == nums.length) {
return ans;
}
}
// 将nums[curIndex]和 nums[nums[curIndex]] 交换一次元素
int now = nums[curIndex];
int temp = nums[now];
// 如果目标位置的元素是nums.length, 需要记录下位置
if (temp == nums.length) {
ans = curIndex;
}
nums[curIndex] = temp;
nums[now] = now;
}
return ans;
}
正确的原因
(1) 因为nums中的数字是连续的, 是0到n-1的, 只会有一个数字不是, 因此如果将数组中的任何一个元素nums[i], 移动到i位置, 那么只有一个元素不在该位置上(此时n所在的位置就是缺失的元素) 或者 所有元素都在对应位置上(那么n就是缺失的元素).
例如[0, 4, 1, 2], 第一次交换完后是[0, 1, 4, 2], 第二次交换完后是[0, 1, 2, 4], 此时4所在的数组索引, 就是缺失的元素, 即返回3.
(2) 需要注意ans初始化的问题, int ans = nums.length;; 因为有可能给出的元素就是0到n-1的, 此时缺的其实是n.
(3) 一旦n元素所在的位置发生变动, 都要记录