每天一道算法题,还来的及吗???(留下悔恨的泪水…)
文章目录
0518二分查找
前提:n个元素是有序的(升序)整型数组,并且数组中的元素是不重复的
要求:若找到目标元素则返回在数组中的位置(下标),若未找到就返回-1或者其他特殊值都可
难点:对区间的定义不清楚,区间的定义就是不变量,边界值模糊不清容易出错
注意:边界值、区间的确定
方法:二分法,确定分界点mid,分成两个子集
public static int search(int[] nums, int target){
//初始化边界值
int left = 0;
int right =nums.length - 1; //这里将区间确定为前闭后闭[left,right],当left=right时是有效的,所以while循环条件是<=
// 避免当 target 小于nums[0] 大于nums[nums.length - 1]时多次循环运算,减少程序不必要的运行
if (target < nums[0] || target > nums[nums.length - 1]) {
return -1;
}
while (left <= right){
int mid = left + ((right - left) / 2); //可以写成mid=(left+right)/2 ,但当数很大时left+right可能会溢出
if(target < nums[mid])//注意,我们这里用的是<而不是<=,所以target不可能等于nums[mid],下面我们就将right=mid-1;
right = mid - 1;
else if(target > nums[mid])//解释同上了
left = mid + 1;
else if(target == nums[mid])
return mid;
}
return -1;
}
0518移除元素
前提:一个数组,元素没特殊要求
要求:给定目标值,将目标值在数组中找到并且移除,原地移除,空间复杂度为1,也就是不能有额外的空间开销,最后返回移除元素之后的数组长度
难点:原地删除不能有额外的空间开销,也就是说只能在原数组上面移除,而不能new一个数组来存储移除元素后的值,这相对来说会简单的多
注意:数组中的元素不能删除只能覆盖覆盖覆盖!!!就本题来说找到重复值就应该用不是重复的值来覆盖,而不是删除哦
方法:双指针,左右指针初始位置在左端,左指针指向最后要返回的数组的元素,右指针去遍历原来数组的元素,当元素等于目标值的时候,左指针是不动的,右指针继续,所以左指针慢右指针快
重点知识:双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
public static int removeElement(int[] nums, int target){
int left = 0;
for (int right=0; right<nums.length; right++){
if (target != nums[right]){
nums[left] = nums[right];
left++;
}
}
return left;
}
双指针相关题目
26.删除排序数组中的重复项
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k 。
//该题目简单在于,数组是升序的,重复值都是相邻的
public int removeDuplicates(int[] nums) {
int left = 0;
for(int right = 1; right < nums.length; right++){
//当两个双指针对应的元素不相等的时候,当前right指针的元素赋值给left指针的下一个位置
if(nums[left] != nums[right]){
nums[++left] = nums[right];
}
}
//这里返回为left+1,是因为left是数组的下标,所以实际数组长度等于下标+1
return left+1;
}
283.移动零
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
public void moveZeroes(int[] nums) {
int left = 0;
for(int right = 0; right < nums.length; right++){
if(nums[right] != 0){
nums[left] = nums[right];
left++;//left最终的值其实就是非0元素的数量
}
}
while(nums.length - left > 0){
nums[left++] = 0;
}
}
844.比较含退格的字符串
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
public boolean backspaceCompare(String s, String t) {
String s1 = remove(s);
String t1 = remove(t);
if(s1.length() != t1.length()){
return false;
}
for(int i = 0; i < s1.length(); i++){
if(s1.toCharArray()[i] != t1.toCharArray()[i]){
return false;
}
}
return true;
}
public String remove(String s){
char[] sc = s.toCharArray();
StringBuffer sb = new StringBuffer();
for(int i = 0; i < sc.length; i++){
//不是退格键就收集结果
if(sc[i] != '#'){
sb.append(sc[i]);
}else{
//如果是退格键且不是字符串的开头位置,而且当前结果里面已经有收集字符了,那我们就要删除当前收集到的字符的最后一个字符
//相反,如果当前退格键是字符串的第一个位置,或者当前一个字符都还没有收集,那我们之间下一步
if(i != 0 && sb.length() >= 1){
sb.deleteCharAt(sb.length()-1);
}
}
}
return sb.toString();
}
//双指针做该题
public boolean backspaceCompare(String S, String T) {
int i = S.length() - 1, j = T.length() - 1;
int skipS = 0, skipT = 0;
while (i >= 0 || j >= 0) {
while (i >= 0) {
if (S.charAt(i) == '#') {
skipS++;
i--;
} else if (skipS > 0) {
skipS--;
i--;
} else {
break;
}
}
while (j >= 0) {
if (T.charAt(j) == '#') {
skipT++;
j--;
} else if (skipT > 0) {
skipT--;
j--;
} else {
break;
}
}
if (i >= 0 && j >= 0) {
if (S.charAt(i) != T.charAt(j)) {
return false;
}
} else {
if (i >= 0 || j >= 0) {
return false;
}
}
i--;
j--;
}
return true;
}
977.有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
public static int[] sortedSquares(int[] nums) {
int len = nums.length;
int[] res = new int[len];
int left = 0, right = len - 1, k = len;
while(left <= right){
if (Math.abs(nums[left]) < Math.abs(nums[right])){
res[--k] = nums[right] * nums[right];
right--;
}else{
res[--k] = nums[left] * nums[left];
left++;
}
}
return res;
}
80.删除有序数组中的重复项 II
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
public int removeDuplicates(int[] nums) {
if(nums.length <= 2){
return nums.length;
}
int left = 2, right = 2;
for (; right < nums.length; right++){
if(nums[left - 2] != nums[right]){
nums[left++] = nums[right];
}
}
return left;
}
0519有序数组的平方
前提:给定数组有序(升序),其中包括负数,
要求:求每一个元素的平方(简单),最后将求得的平方之后的数组有序(升序)排列
难点:如果用暴力解法就是先循环遍历得到平方后的值,然后再循环遍历排序,时间复杂度很高,怎么实现一边遍历求平方,一边就将排序做好?
注意:数组是升序排列,较大的值按理说应该在最右边,但是由于其中包含负数,所以最大值有可能在最左边,可以确定的是最大值一定不在数组的中间,而是在两端
易点:可以在原数组上平方,但是不能在原数组上排序,可以开辟新的数组来存排序后的结果,这就很容易了,想着也是找到一个最大值就从新建的数组的最右边依次放入,最后返回该数组即可
思想:暴力解法的思想就是先求平方,然后再排序咯,但是考虑时间复杂度这样应该不好
关键:双指针,相对于从数组中间位置去找平方后的最小值,由上述的分析易知更简单的是从两边去找最大值,所以双指针分别从两端向中间逼近,当left > right时,数组就遍历完啦
方法:双指针,左右指针分别从两端向中间移动
public static int[] sortedSquares(int[] nums) {
int len = nums.length;
int left = 0, right = len - 1;
int k = len;
int[] result = new int[k];
while (left <= right){
if (nums[left]*nums[left] > nums[right]*nums[right]){//左边平方后大于右边,左指针++
result[--k] = nums[left]*nums[left];
left++;
}else{ //左边平方后小于等于右边,右指针--
result[--k] = nums[right]*nums[right];
right--;
}
}
return result;
}
0519长度最小的子数组
前提:n个正整数数组和一个正整数target
要求:在数组中找到求和>=target的长度最小的子数组
难点:怎么找到求和为target但是长度要最短,怎么比较?都存起来?不可能啊,只能是存一个当前最小值继续遍历满足条件来,又替换存当前最小值
方法:滑动窗口(双指针),左右指针初始位置在左端,右指针跑的快,左指针跑的慢,在两个指针确定的这个窗口中满足求和>=target,然后求出子数组的长度,和前面存的长度进行比较
注意:这里是求大于等于target的长度最小子数组,不是必须等于target的哦!
重点知识:滑动窗口:所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,其实也可以理解为双指针的一种
- 对应在暴力解法中,一个for循环控制滑动窗口的起始位置,一个for循环控制滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。
public static int minSubArrayLen(int[] nums, int target){
int left=0, right=0, subLength=0, sum=0, result=Integer.MAX_VALUE;
for (; right<nums.length; right++){//右指针跑的快
sum += nums[right];
while (sum >= target){
subLength = right - left + 1;//计算子数组的长度
if (subLength < result){//比较当前子数组的长度是不是小于已经求出的子数组长度
result = subLength;
}
sum -= nums[left++];//去掉一个元素,再来判断是不是满足>=target的,如果满足子数组长度就继续减小了呀
//这里需要注意nums[left++],是先使用nums[left]的值,然后left = left-- 的
//拆成两部分写
sum -= nums[left];
left--;
}
}
return result == Integer.MAX_VALUE ? 0:result;
}
0520螺旋矩阵
前提:输入大于0小于等于20的正整数n,生成n*n的正方形矩阵
要求:元素按顺时针螺旋状遍历给正方形矩阵赋值,值是按照1,2,3,4,5,,,这样递增的
难点:找出规律、边界值的确定,坚持循环不变量原则;重点考察对代码的掌控能力。
方法:按层模拟,顺时针一圈就是一层
public static int[][] generateMatrix(int n){
int[][] matrix = new int[n][n];
int num = 1;
int left = 0, right = n - 1, up = 0, down = n-1;
while(num <= n*n){
for (int i = left; i <= right; i++){ //从左至右,列索引i++,当列索引i>right跳出循环,一行访问完成
matrix[up][i] = num++; //这里需要注意的是用i作为中间变量,因为此时的left是作为不变量的
}
up++; //从上某一行访问完毕,将up边界下移
for (int j = up; j <= down; j++){ //从上至下,行索引j++,当行索引j>down跳出循环,一列访问完成
matrix[j][right] = num++;
}
right--; //从右某一列访问完毕,将right边界左移
for (int p = right; p >= left; p--){ //从右至左,列索引p--,当列索引p<left跳出循环,一行访问完毕
matrix[down][p] = num++;
}
down--; //从下某一行访问完毕,将down边界上移
for (int q = down; q >= up; q--){ //从下至上,行索引q--,当行索引q<up跳出循环,一列访问完毕
matrix[q][left] = num++;
}
left++; //从左某一列访问完毕,将left边界右移
}
return matrix;
}
总结
数组是较为基础的数据结构,一般题目的思想都不难,重点在于我们对代码的掌控能力。
数组是存放在连续内存空间上的相同类型数据的集合,正是因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。
注意:数组的元素是不能删除的,只能覆盖!
1.二分法
循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。
2.双指针法
双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。
双指针法(快慢指针法)在数组和链表的操作中是非常常见的
3.滑动窗口
主要要理解滑动窗口如何移动 窗口起始位置,窗口结束位置,达到动态更新窗口大小的,怎么去正确的控制
滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。
4.模拟行为
不涉及到什么算法,就是单纯的模拟,十分考察对代码的掌控能力。