文章目录
204 计数质数E (筛法)
一道经典的板子题。求解有很多可以进一步优化的方法。复杂度从 n 2 n^2 n2可以降为 n ( n ) n\sqrt(n) n(n),采用埃氏筛的算法可以降为 n l g n l g n nlgnlgn nlgnlgn, 如果更进一步还可以采用线性筛的方法最快可以达到 n n n.
--------------朴素做法------------在判断的时候只需要判断小的那一半是不是因子就可。
class Solution {
public int countPrimes(int n) {
int ans = 0;
for (int i = 2; i < n; ++i) {
ans += isPrime(i) ? 1 : 0;
}
return ans;
}
public boolean isPrime(int x) {
for (int i = 2; i * i <= x; ++i) {
if (x % i == 0) {
return false;
}
}
return true;
}
}
------------埃氏筛的方法---------------
// 核心思想是两个循环,外层循环枚举数字,在内层判断,并且维护了一个数组。
// 这个数组保证了所有这个数字的倍数都不会被遍历了。
class Solution {
public int countPrimes(int n) {
int[] isPrime = new int[n];
Arrays.fill(isPrime, 1);
int ans = 0;
for (int i = 2; i < n; ++i) {
if (isPrime[i] == 1) {
ans += 1;
if ((long) i * i < n) {
for (int j = i * i; j < n; j += i) {
isPrime[j] = 0;
}
}
}
}
return ans;
}
}
------------线性筛------面试不要求---------
// 埃氏筛的冗余在于45即会被5又会被3标记
class Solution {
public int countPrimes(int n) {
List<Integer> primes = new ArrayList<Integer>();
int[] isPrime = new int[n];
Arrays.fill(isPrime, 1);
for (int i = 2; i < n; ++i) {
if (isPrime[i] == 1) {
primes.add(i);
}
for (int j = 0; j < primes.size() && i * primes.get(j) < n; ++j) {
isPrime[i * primes.get(j)] = 0;
if (i % primes.get(j) == 0) { // 如果当前是倍数,可以不必继续。
break; // 比如i是4,已经标记了8是合数。那么可以不用继续标记12 因为12会被遍历到6时候被标记。
}
}
}
return primes.size();
}
}
621. 任务调度器(偏向思路)
比较偏思路的一道题目,需要考虑下什么样子是最大的队列。两种可能,
- 数量最多的那个任务首先保证完成。然后在这个间隔内插入其他任务完成。注意可能有多个最多任务。
- 依然是保证最大的任务,但是其他的任务插满了空间,也就是说,没有空闲的位置。那么实际上需要的时间就是任务的个数
class Solution {
public int leastInterval(char[] tasks, int n) {
HashMap<Character, Integer> map = new HashMap<>();
int maxvalue = 0;
for (char t:tasks){
int cur = map.getOrDefault(t,0)+1;
map.put(t,cur);
maxvalue = Math.max(maxvalue, cur);
}
int maxnum = 0;
HashSet<Character> set = new HashSet<>();
for (char t:tasks){
if (!set.contains(t)){
if (map.get(t) == maxvalue) maxnum++;
set.add(t);
}
}
// 两种情况,首先对于最多的那个任务,是一定要完成的,也就是第一项
// 对于第二种情况,是其他的花里胡哨任务填满了最多任务等待的空间
return Math.max((n+1)*(maxvalue-1)+maxnum, tasks.length);
}
}
321. 拼接最大数 H(归并,栈)
思路:首先我们需要枚举最后的数组中,分别来自nums1和nums2的元素有多少。在确定了元素的来源之后,有一个限制是每个数组内部的排序不能改变。因此这个我们采用单调栈的方法进行维护。维护一个递减的单调栈。
在得到了两个数组的元素之后,我们需要进行合并。合并的思路就是归并排序中合并的那一套。 这个还真有毒,这个必须用特殊的比较方法。如果数字相等,就比较以后的每一位。
然后,我们用每次比较最大的数组,这里用了数组的复制。
class Solution {
public int[] maxNumber(int[] nums1, int[] nums2, int k) {
int m = nums1.length, n = nums2.length;
int[] maxSubsequence = new int[k];
int sq1_min = Math.max(0, k - n), sq1_max = Math.min(k, m);
for (int i = sq1_min; i <= sq1_max; i++) {
int[] subsequence1 = maxSubsequence(nums1, i);
int[] subsequence2 = maxSubsequence(nums2, k - i);
int[] curMaxSubsequence = merge(subsequence1, subsequence2);
if (compare(curMaxSubsequence, 0, maxSubsequence, 0) > 0) {
maxSubsequence = Arrays.copyOf(curMaxSubsequence, k);
}
}
return maxSubsequence;
}
public int[] maxSubsequence(int[] nums, int k) {
int length = nums.length;
int[] stack = new int[k];
int top = -1;
int remain = length - k;
for (int i = 0; i < length; i++) {
int num = nums[i];
while (top >= 0 && stack[top] < num && remain > 0) {
top--;
remain--;
}
if (top < k - 1) {
top++;
stack[top] = num;
} else {
remain--;
}
}
return stack;
}
public int[] merge(int[] subsequence1, int[] subsequence2) {
int x = subsequence1.length, y = subsequence2.length;
if (x == 0) {
return subsequence2;
}
if (y == 0) {
return subsequence1;
}
int mergeLength = x + y;
int[] merged = new int[mergeLength];
int index1 = 0, index2 = 0;
for (int i = 0; i < mergeLength; i++) {
if (compare(subsequence1, index1, subsequence2, index2) > 0) {
merged[i] = subsequence1[index1++];
} else {
merged[i] = subsequence2[index2++];
}
}
// 细死我了,下面这个方法不行,原因是无法比较一样的情况
// [2,0][7,0,6,5]
// int index = 0;
// while (index1<x && index2<y){
// if(subsequence1[index1]>=subsequence2[index2]){
// merged[index] = subsequence1[index1];
// index1++;
// }else{
// merged[index] = subsequence2[index2];
// index2++;
// }
// index++;
// }
// System.out.println(Arrays.toString(merged));
// while(index2<y){
// merged[index] = subsequence2[index2];
// index2++;
// index++;
// }
// while(index1<x){
// merged[index] = subsequence1[index1];
// index1++;
// index++;
// }
return merged;
}
// 这个字符串比较很好,暗藏玄机。当两个相同的时候,其实需要继续比较下一位的。直到分出胜负。
public int compare(int[] subsequence1, int index1, int[] subsequence2, int index2) {
// int index1 = 0, index2 = 0;
int x = subsequence1.length, y = subsequence2.length;
while (index1 < x && index2 < y) {
int difference = subsequence1[index1] - subsequence2[index2];
if (difference != 0) {
return difference;
}
index1++;
index2++;
}
return (x - index1) - (y - index2); // 最后的条件,谁长谁赢了
}
}
135. 分发糖果 H 左右两次遍历
这道题目有一个特点在于,是考虑了两边的情况。因此可以考虑的思路是两次遍历,分别从左右考虑。
具体来说就是,首先从左侧开始,保证如果前一个孩子的分低于当前就给当前的孩子的糖果是前一个+1;然后再从做往右遍历,保证后一个孩子的分低于当前就给当前的孩子就多给一个。
最后选择两个数组的最大值就可以就可以。
思路的核心是两边都都对排序有一定的要求,我们预处理完成之后,这个排序已经满足了。然后我们只要选择最大的一个,一定是两边都满足的。
class Solution {
public int candy(int[] ratings) {
// 思路很不错 两次遍历,因为需要考虑左右
int ret = 0;
int n = ratings.length;
int[] left = new int[n];
left[0] = 1;
for (int i = 1; i<n; i++){
if (ratings[i-1]<ratings[i]){ // 第一次的遍历,首先只考虑单侧增长
left[i] = left[i-1]+1;
}else{
left[i] = 1; // 只要满足了左侧就可以,这边先直接降低为1
}
}
int right = 1;
ret = Math.max(1,left[n-1]);
for (int i = n-2; i>=0; i--){
if (ratings[i]>ratings[i+1]){
right++;
}else{
right = 1;
}
ret += Math.max(right, left[i]);
}
return ret;
}
}
435. 无重叠区间 M (贪心思路,不必最早开始只需最早结束)
对于区间合并,常用的排序数据进行贪心的思路的思路,但是问题关键在于是对于start进行排序还是对于end进行排序呢?
答案是对于end进行排序,原因是:
假设在某一种最优的选择方法中, [ l k , r k ] [lk,rk] [lk,rk] 是最左侧区间,那么它的左侧没有其它区间,右侧有若干个不重叠的区间。设想一下,如果此时存在一个区间 [ l j , r j ] [lj,rj] [lj,rj],使得即区间 j 的右端点在区间 k 的左侧,那么我们将区间 k 替换为区间 j,其与剩余右侧被选择的区间仍然是不重叠的。
如果有多个区间的右端点都同样最小怎么办?由于我们选择的是首个区间,因此在左侧不会有其它的区间,那么左端点在何处是不重要的,我们只要任意选择一个右端点最小的区间即可.
class Solution {
public int eraseOverlapIntervals(int[][] intervals) {
// 需要思考的核心问题,为什么这里考虑的是end排序,start排序就是错误的
// 按照end进行排序,一样的情况下按照start排序
Arrays.sort(intervals, (a,b)->a[1]==b[1]?a[0]-b[0]:a[1]-b[1]);
int ans = 0;
int n = intervals.length;
int begin = -1000000;
int end = -10000000;
for (int i = 0; i<n;i++){
int[] cur = intervals[i];
if(cur[0]<end){
ans++;
}else{
end = cur[1];
}
}
return ans;
}
}
330. 按要求补齐数组 H 贪心
贪心思路:对于正整数 x,如果区间
[
1
,
x
−
1
]
[1,x−1]
[1,x−1] 内的所有数字都已经被覆盖,且 x 在数组中,则区间
[
1
,
2
x
−
1
]
[1,2x−1]
[1,2x−1]内的所有数字也都被覆盖。因此体现在代码上,我们需要维护一个当前可以覆盖到的最大的位置cur,在每次数字进来之后,我们如果在这个区间内,我们修改为cur += x
。如果当前数字不在数组内,我们需要添加一个数字cur
,然后修改边界为cur *= 2
。
class Solution {
public int minPatches(int[] nums, int n) {
// 贪心思路
int len = nums.length;
int index = 0; // 坐标
long cur = 1; // 表示当前可以覆盖区间的右端点
int ans = 0; // 需要添加的个数
while(cur<=n){
if(index<len && nums[index]<=cur){
cur += nums[index];
index++;
}else{
ans++;
cur *= 2;
}
}
return ans;
}
}
84. 柱状图中最大的矩形 H 单调栈
单调栈进行优化,之所以想到单调栈因为我们发现每次计算一个矩形和右侧全部矩形的面积和就可以。因此在弹出时候维护。关键是宽度=当前准备进栈元素的位置-弹出元素之后的栈顶index-1
class Solution {
public int largestRectangleArea(int[] heights) {
// 单调栈,维护一个最小的递增队列、
// 在弹出栈内元素的时候计算:弹出元素为左端点情况下的面积
// 高=弹出元素的值,宽=当前元素的位置-弹出元素之后的栈顶index-1
// 注意考虑开头和结尾如何计算,最后要弹空栈
Deque<int[]> stack = new LinkedList<>();
stack.offerLast(new int[]{-1, 0}); // 队列中的内容是坐标+高度
int ans = 0;
int n = heights.length;
for (int i = 0; i<n; i++){
int cur = heights[i];
while (!stack.isEmpty() && cur < stack.peekLast()[1]) {
int[] now = stack.pollLast();
ans = Math.max(ans, now[1] * (i- stack.peekLast()[0] -1));
}
stack.offerLast(new int[]{i, cur});
}
while (stack.size()!=1){
int[] now = stack.pollLast();
ans = Math.max(ans, now[1]*(n-stack.peekLast()[0]-1));
}
return ans;
}
}
85. 最大矩形 H单调栈
来自leetcode评论区的最佳方法:
class Solution {
public int maximalRectangle(char[][] matrix) {
if(matrix.length == 0 ) return 0;
int n = matrix.length;
int m = matrix[0].length;
int[] high = new int[m+2]; // 这里直接预处理了最后的循环
int ans = 0;
for(int i = 0;i<n;i++){
for (int j = 0; j<m;j++){
if (matrix[i][j] == '1'){
high[j+1]++;
}else{
high[j+1] = 0;
}
}
ans = Math.max(ans, largestRectangleArea(high));
}
return ans;
}
public int largestRectangleArea(int[] heights) {
// 因为在前面和末尾补充了0,所以判断简单了
Deque<int[]> stack = new LinkedList<>();
int ans = 0;
int n = heights.length;
for (int i = 1; i < n; i++) {
int cur = heights[i];
while (!stack.isEmpty() && cur < stack.peekLast()[1]) {
int[] now = stack.pollLast();
ans = Math.max(ans, now[1] * (i - stack.peekLast()[0] - 1));
}
stack.offerLast(new int[]{i, cur});
}
return ans;
}
}
其余打卡题目
659. 分割数组为连续子序列 贪心+哈希
首先考虑贪心的思路,如果当前数字能够接之前的后面,应该优先接上。否则每次判断能不能凑成长度为3的新的列表。
基本思路是维护两个哈希表,一个是维护当前数字的个数,另外一个是维护某个数字结尾的数组数量。后一个哈希表用于连接数字,前一个哈希表用于连接不上时候判断能不能连接三个。
**861. 翻转矩阵后的得分 **
首先有个重要的事实,矩阵反转操作的顺序并不重要。因此第一步首先反转每行第一位,保证都是1,然后再考虑每一列的问题,保证每一列1的数量最多。 对于反转问题,可以采用异或的方法实现。
376. 摆动序列 双端队列
维护一个双端队列,队列中保存了元素的大小和当前队列的属性。属性包括了上升和下降。以及初始化的时候有一个1,表示未知。然后贪心的添加最大或者最小的元素。
738. 单调递增的数字 思路
最好的思路是从后往前进行遍历。如果当前数字小于之前的数字,就前面的减一位,当前数字改为9。之所以采用从后往前的思路,关键在于涉及到了借位的问题。
最后输出时候在进行以下处理。如果某个数字是9,那么之后的数字全部都是9.
48. 旋转图像
思路:首先进行水平翻转,然后进行对角线反转。
316. 去除重复字母
采用单调栈方法,首先哈希表维护每个字符的数量。然后维护一个单调递增的单调栈,如果当前字符还有多余的就可以弹出。还需要一个集合维护当前已经在队列中的字符。