在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
class Solution {
public int findRepeatNumber(int[] nums) {
Set<Integer> dic = new HashSet<>();
for(int num : nums) {
if(dic.contains(num)) return num;
dic.add(num);
}
return -1;
}
}
这题没有做出来的原因是因为双for循环用时超标了,别再用for了,下一次动动脑子,别暴力了。自己最应该想到的就是set,对set的学习掌握不够,不能只是知道,更要能用这些数据结构,就像英语只会看没有用,还要会写作文,能用上。
set中没有重复元素,有contains方法,可以非常好的查找是否有增元素,一个好的封装方法。
注意set要用包装类,最好再复习一遍看看。
此外,要多用for each方法
主要是实现的具体思路 不能光有个概念 那远远不够
这道题暂且放在这里!
将一个栈当作输入栈,用于压入appendTail 传入的数据;另一个栈当作输出栈,用于deleteHead 操作。
每次 deleteHead 时,若输出栈为空则将输入栈的全部数据依次弹出并压入输出栈
使用java的同学请注意,如果你使用Stack的方式来做这道题,会造成速度较慢; 原因的话是Stack继承了Vector接口,而Vector底层是一个Object[]数组,那么就要考虑空间扩容和移位的问题了。 可以使用LinkedList来做Stack的容器,因为LinkedList实现了Deque接口,
所以Stack能做的事LinkedList都能做,其本身结构是个双向链表,扩容消耗少。
但是我的意思不是像100%代码那样直接使用一个LinkedList当做队列,那确实是快,但是不符题意。 贴上代码,这样的优化之后,效率提高了40%,超过97%。
class CQueue {
LinkedList<Integer> stack1;
LinkedList<Integer> stack2;
public CQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void appendTail(int value) {
stack1.add(value);//就是linkedlist里的addFirst(v)方法
}
public int deleteHead() {
if (stack2.isEmpty()) {
if (stack1.isEmpty()) return -1;
while (!stack1.isEmpty()) {
stack2.add(stack1.pop());
//linkedlist可以实现pop()方法 pop就是linkedlist里removeFirst方法
}
return stack2.pop();
} else return stack2.pop();
}
}
其实真的没有那么复杂,不要想复杂了,分情况分清楚就行,就是一个进队和出队。
二分查找最重要的就是区间定义不要混,决定是左闭右闭就一直使用这个就可以了!
二分查找的代码应该已经是记得很熟练了,这个是最基础的!
while(left<=right){
mid=left+(right-left)/2;(防止溢出)
if(nums[mid]==target)
return mid;
else if(nums[mid]>target)
right=mid-1;//这里容易混,千万不可以写错,分清楚
else left=mid+1;
}
return -1;
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
这道题也是困扰了我很久,主要是自己一上来就写,然后把自己绕进去了,同时卡哥的寻界没有看懂,后来看忍者算法的代码懂了,最重要的是要分别左右寻界,还是用二分法进行寻界,分别移动right或者left进行逼近。
自己最开始也是这么想的,那就是找到target之后,就看看前面的还是不是目标值,后面的是不是目标值,比较一下不就好了,但是后来绕来绕去的报错又绕昏了,看了忍者算法的代码一下子清晰了!推荐!!!
class Solution {
public int[] searchRange(int[] nums, int target) {
int leftBorder = getleftborder(nums, target);
int rightBorder = getrightborder(nums, target);
return new int[]{leftBorder,rightBorder};
}
private int getleftborder(int[] nums,int target){
//寻找左边界,先写原始二分法
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
//当找到目标值时有两种情况,1,他已经是最左边,那么返回mid,即nums[mid-1]不等于nums[mid]时就是最左边了,找到最左边界
//2.另一个情况就是它在数组最左边 mid=0,那么此时左边没有,mid-1会溢出,所以要单独拿出来判断一下!
if(mid==0||nums[mid-1]!=nums[mid])
return mid;
else right=mid-1;
//若不是最左边,那么right=mid-1,mid就会向左边移动,向左寻界!
}
else if(nums[mid]>target)
right=mid-1;
else left=mid+1;
}
return -1;//一定要写,没有找到的情况
}
private int getrightborder(int[] nums,int target){
//一样的,向右寻界
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(nums[mid]==target){
//如何判断是否在最右边,nums[mid]不等于nums[mid+1],以及mid在数组最右边了,+1就要越界了,单独拿出来讨论。
if(mid==nums.length-1||nums[mid]!=nums[mid+1])
return mid;
else left=mid+1;
//若不是在最右边,left=mid+1,mid向右移动,向右寻界!
}
else if(nums[mid]>target)
right=mid-1;
else left=mid+1;
}
return -1;
}
}
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。
示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
你不需要考虑数组中超出新长度后面的元素。
那么在这里呢就不考虑双for 时间复杂度上不符合要求
数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
学习了一个新的知识点
快慢指针
双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。
class Solution {
public int removeElement(int[] nums, int val) {
// 快慢指针
int slowIndex=0;
for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
if (nums[fastIndex] != val)
nums[slowIndex++] = nums[fastIndex];
}
return slowIndex;
}
}
LeetCode 977. 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 示例 1:
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100] 示例 2: 输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
这道题其实没有什么东西,就是对快慢指针的灵活运用,自己在写题时,不要局限对双指针法的扩展。整体来看是对快速排序的变形写法。
class Solution {
public int[] sortedSquares(int[] nums) {
//双指针法,感觉像是快速排序的变化后结果!
int left=0,right=nums.length-1;
int k=right;
int[] a=new int[nums.length];
while(left<=right){
if(nums[left]*nums[left]<nums[right]*nums[right]){
a[k--]=nums[right]*nums[right];
right--;
} else { a[k--]=nums[left]*nums[left];
left++;
}
} return a;
}
}
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0 。示例 1:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:输入:target = 4, nums = [1,4,4] 输出:1 示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
通过这道题学习了移动窗口,感觉还挺奇妙的,就是把双for循环转成了一个for循环,里面放的是终止位置,起始位置通过while来进行判断,降低了时间复杂度。while循环里的东西是这道题的核心
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left=0;//起始指针
int sum=0;//窗口里的和
int result=Integer.MAX_VALUE;//最后返回的窗口大小
for (int right = 0; right < nums.length; right++) {
//终止指针不断地向后移动
sum+=nums[right];//从0开始加的
while(sum>=target){
result=Math.min(result, right - left + 1);
// 不断更新结果 找最小的
sum=sum-nums[left];
left++;//符合条件 就要把起始指针向后移动,缩小窗口!
}
} return result == Integer.MAX_VALUE ? 0 : result;
//加一个判断进行返回,如果等于最大值说明没找到 返回0
}
}
不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。
题目详情
你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例 1:
输入:n = 3 输出:[[1,2,3],[8,9,4],[7,6,5]] 示例 2:
输入:n = 1 输出:[[1]]。
这道题其实挺难的,很考验解题能力,对代码的掌控,这道题也给了我很多启发,值得多写。
1.在写题前,面对这种有点规律的题目,要多多模拟,多走几遍,尽可能找到所有的规律,这道题你画个2、3、4之后就会发现原来中间的是要单独处理的,然后循环是分块的,逆时针一部分一部分的填进去。以及如果你再敏锐一点就会发现,旋转第二圈的时候,填的长度少了一点!
2.一入循环深似海,正如卡哥所说,循环不变量,对于这种有区间的,一定不要变来变去,像我就是一直走左闭右闭来写二分法,所以在这里,我选择左闭右开,即最后一个交给下一个循环去填入!
class Solution {
public int[][] generateMatrix(int n) {
int[][] res = new int[n][n];
int start = 0; // 每次循环的开始点(start, start)
int loop = 0; // 控制循环次数
// 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
//这个循环几圈是自己没有想到的 当时没想到这个结束循环条件
int mid = n / 2;
// 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
// 这个也是没有发现的一个规律 就是中间的是要单独处理的,n为奇数时要单独处理
//所以写代码前还是要好好想想,这个思路究竟是怎么样的!
int count = 1; // 用来给矩阵中每一个赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位 这个很重要 循环的边界是一直在减小的!没有发现这个规律
int i,j;//循环
while (loop++ < n / 2) {
//全部都是左闭右开
// 1.从左向右 i不变 j++
// 最开始写的时候放错误了,脑子里只有循环第一圈的情况,所以就j=0,j
for(j=start;j<n-offset;j++){
res[start][j]=count++;
}
// 2.从上向下 j不变 i++
for(i=start;i<n-offset;i++){
res[i][j]=count++;
}
//3.从右向左 i不变 j--
for(;j>start;j--){
res[i][j]=count++;
}
// 4.从下到上 j不变 i--
for (; i > start; i--) {
res[i][j] = count++;
}
// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
start++;
// offset 控制每一圈里每一条边遍历的长度
offset++;
}
// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 另一个没有发现的点!
if (n % 2 == 1) {
res[start][start] = count;
}
return res;
}}
在数组这里我们学习了这些方法;
1.二分法
2.二分法的变形,左右寻界
3.快慢指针
4.移动窗口 right是终止位置 不断的移动
最后一题比较考验代码的能力,全部学完之后,二刷在写的时候希望不会卡壳!