目录
数组理论基础
参考《代码随想录》
51.leetcode题号704.二分查找-1种思路2种写法
第一次错误
思路:
二分查找是查找有序的数组,定义left,right,mid来确定数组下标,
1>自己想的
①[left,right]表示target存在的范围,左右都是闭区间,即left,right有可能是target,所以会取到。mid表示所求区间范围的中间位置
②考虑边界问题,while(left <= right)
,因为left == right
时是有意义的,如一个元素的数组找这一个数
③如果nums[mid] == target
说明找到了返回找到的坐标mid。如果nums[mid]>target
,说明target在left到mid之间。则更新right=mid-1
,因为mid位置已经判断不可能是target,当前寻找范围变为[left,mid-1]
2>书上写的
考虑如果定义target在一个左闭右开的区间[left,right)
说明right不可能为target,不取这个right,代码需要改变的部分,right=nums.length,while(left<right)
因为不可能取到left==right
没有意义,target不可能在这个范围,left更新为mid+1,而right=mid
51.704代码实现
方法一:
class Solution {
public int search(int[] nums, int target) {
int l = 0;
int r = nums.length-1;
int mid = 0;
while(l <= r){
mid = l+(r-l)/2;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
r = mid-1;
}else{
l = mid+1;
}
}
return -1;
}
}
★ 52.leetcode27.移除元素-3种思路4种写法
思路:因为元素的顺序可以改变,而且只需要原地修改数组空间O(1),返回修改后数组的新长度即可。不需要考虑数组中超出新长度后面的元素。
1>自己想的从后覆盖
①所以可以每次删除值为val的元素时,每次将最后一个位置的数字对要删除的位置数字覆盖,再对数组长度减一。
②但是需要注意,这种情况下需要考虑最后一位是否也是val,如果是的话可以直接数组长度减一,等于从尾部开始删除。
③可以定义两个变量指代这两个数,end指在末尾要去覆盖val,覆盖后数组长度减一,如果自身是val则数组长度直接减一,end--
继续寻找。
star是指从头寻找要被覆盖的数val位置,被覆盖后++寻找下一个等于val的位置。
④考虑边界问题,当star与end相遇时,说明star寻找val已经到达新的尾部,如果此时等于val,则数组长度减一。则循环过程满足star <= end
2>书上写的暴力解法,用两个循环
第一个循环遍历数组元素,第二个循环更新数组,得每次都挪后面所有的元素
3>快慢指针法
书上写的双指针法也是leetcode官方的双指针法,用两个快慢指针,在一个循环内完成两个for循环的工作,优化了2>的暴力写法
4>leetcode对双指针的优化
①因为2>,3>方法都在按照顺序一个个赋值,但是题目要求顺序可以改变。4>类似1>对要覆盖的直接覆盖,无需一一赋值。
②与我自己思考的1>不同之处在于对尾部遇到val的处理,我是数组长度--
,end--
。官方是赋值后left位置不变,right--
继续给left赋新的数组尾部,直到left不为val为止。
③边界情况也不同,left==right时就停止不进入循环while(left<right),并且返回left的值就是新数组长度,why?我写的是while(star<= end),返回end+1。因为,我写的是闭区间[star,end],end = nums.length。而官方写的是左闭右开[left,right),right = nums.length
52.27代码实现
方法一:自己想的,从后覆盖
复杂度分析,时间O(N),空间O(1)。
最坏情况,没有val,两指针总共遍历数组一次
class Solution {
public int removeElement(int[] nums, int val) {
int end = nums.length-1;
int star = 0;
while(star <= end){
if(nums[end] == val){
end--;
}else{
if(nums[star] != val){
}else{
//开始覆盖操作
nums[star] = nums[end];
end--;
}
star++;
}
}
return end+1;
}
}
方法二:书上的暴力双层循环
这个运行结果比较惊讶,可能是数据小,双层循环也很快
复杂度分析,时间O(N^2),空间O(1)。
最坏情况,n-1个val,运行次数外循环遍历n次,内循环依次大概n-1次
public int removeElement_1(int[] nums, int val){
int size = nums.length;
for(int i=0; i<size; i++){
//注意,这里第一次写错了,写成了<nums.length
//为什么不能这样写,当size减小的时候,原数组末尾的val
// 无法再用后面的数组元素覆盖,而是通过新的长度size的减小直接舍弃
if(nums[i] == val){
for(int j=i+1; j<nums.length; j++){
nums[j-1] = nums[j];
}
i--;//因为i位置及之后的数字全部都向前挪了一位,挪到位置i的可能是后面的val,还没有遍历判断
size--;
}
}
return size;
}
方法三:leetcode的双指针法
复杂度分析,时间O(N),空间O(1)。
最坏情况,没有val,两指针最多都遍历数组一次
public int removeElement_1(int[] nums, int val) {
int left = 0;
int right = 0;
while(right < nums.length){
if(nums[right] == val){
//右指针遇到旧数组元素,不用加入新数组,所以left不动,right+1
}else{
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}
方法四:leetcode方法三优化,与方法一思路类似
复杂度分析,时间O(N),空间O(1)。
最坏情况,没有val,两指针总共遍历数组一次
public int removeElement_3(int[] nums, int val) {
int left = 0;
int right = nums.length;
while(left < right){
if(nums[left] == val){
nums[left] = nums[right-1];
right--;
}else{
left++;
}
}
return left;
}
★53.leetcode26.删除有序数组中的重复项-1种思路(与52.27相关题)
思路:
由于题目要求删除数组中的重复元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。可以使用双指针:左指针指向当前遇到的第一个重复元素,右指针指向下一个不重复的位置。
①一开始右指针比左指针多走一步,左指针指向赋好值的新元素,如果右指针指向的元素不等于自己的前一个元素,说明找到了下一个不重复值,它一定是输出数组的一个元素,则左指针加一,将右指针指向的元素复制到左指针位置,然后将右指针继续后移,
②如果右指针指向的元素等于自己的前一位,它不能在输出数组里,此时左指针不动,右指针右移一位。
整个过程保持不变的性质是:区间 [0,left]中的元素都不重复。当右指针遍历完输入数组以后,left+1的值就是输出数组的长度。
53.26代码实现
//虽然过了,但是有个问题就是数组为空时会返回1,可以写法上优化一下
class Solution {
public int removeDuplicates(int[] nums) {
int left = 0,right = left+1;
while(right <nums.length){
if(nums[right-1] == nums[right]){
right++;
}else{
left++;
nums[left] = nums[right];
right++;
}
}
return left+1;
}
}
思路不变,写法优化一点点。
加一个处理空数组的判断
修改先给left加一再赋值为先赋值再left加一。从闭区间变为左闭右开,[0,left)。就与官方写法完全一致了
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length == 0){
return 0;
}
int left = 1,right = 1;
while(right < nums.length){
if(nums[right-1] != nums[right]){
nums[left] = nums[right];
left++;
}
right++;
}
return left;
}
}
54.283移动零-2种思路(与52.27相关题)
283.移动零
思路:
1>暴力法
①双层循环,内层循环将0之后的元素全部往前挪一位,并将最后一位赋值为0
②外层循环遍历数组,判断哪一位是0进入内循环
2>双指针法
①可以把移动零后的前不为0的数看为新数组,将旧数组中不为0的数依次提出来赋给新数组。
②赋值过程中范围[0,left)左闭右开,因为left需要被right赋值以后才是新数组成员,且赋值后++等待right找到下一位后赋值
③全部赋值完成后将剩余位全部赋0即可。
3>官方双指针交换
与2>双指针类似,但是不是覆盖后补0,是不为0的与之前所有元素交换
54.283 代码实现
方法一:暴力双层循环
暴力循环写错了两次,以后不能省略写暴力循环
public void moveZeroes_1(int[] nums) {
//暴力循环试一下
int n = nums.length;
for(int i=0; i<n;){
if(nums[i] == 0){
for(int j=i; j<n-1; j++){
nums[j] = nums[j+1];
}
nums[n-1] = 0;
n--;
}else{
i++;
}
}
}
方法二:我的双指针覆盖后补0
跑了三遍不知道这个内存消耗咋回事这么大
class Solution {
public void moveZeroes(int[] nums) {
int left=0,right=0;
while(left < nums.length){
if(right < nums.length){
if(nums[right] != 0){
nums[left] = nums[right];
left++;
}
right++;
}else{
nums[left] = 0;
left++;
}
}
}
}
方法三:官方交换
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0;
while (right < n) {
if (nums[right] != 0) {
swap(nums, left, right);
left++;
}
right++;
}
}
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
★★55.844.比较含退格的字符串-3种写法(与52.27相关题)
844.比较含退格的字符串
思路:
1>自己想的暴力写法
分为两步,先处理字符串里出现#时,删除#的前一个字符。处理完毕后再两两比较字符串是否相等
①将字符串转为字符数组,然后依次遍历,
②当遍历到#
时,需要退格,即删除#
和他的前一个字符,用后面的其后的字符全部向前挪两位进行覆盖,并且length-2
。
③注意!!!,此处在写的时候发现一个问题例如“cd##"
,d#
需要用后一个#
覆盖,这种情况下在覆盖完成后,需要再次判断被覆盖位(#
及其前一个位置)是否为#
。如果不是,才遍历下一项。此时i在上一个检测出#的位置,即每次覆盖后,遍历的i–,倒退一格判断。
④再依次向后遍历,重复操作,内循环用来删除退格符及前一个字符,即前移两位,外循环用来遍历字符数组。特殊情况,数组只有一个字符#,字符串为空。或第一个位置为#,后续所有字符前移一位。
⑤比较字符串是否相等时,若遍历两个字符数组,每一位都相等,但长度不等也为不相等。
工具:复习String类和字符数组的互相转换
①String转化成字符数组
String.toChararray(),String类成员toCharArray函数可将其转换成字符数组。
char[]cd = c.toCharArray();
c为字符串。
②将字符数组转换成String
法1:利用String类的构造函数,直接在构造String时完成转换。
char[] data = {'a','b','c'}; String str = new String(data);
法2:调用String类的valueOf函数转换。String.valueOf(char[] ch);
2>官方解法一重构字符串
分为两步,先处理字符串里出现#时,删除#的前一个字符。处理完毕后再两两比较字符串是否相等
①简单理解就是建立一个StringBuffer类的对象ret来存储处理后的字符串,
②在这个过程中, 用char ch = String.charAt(i)
按位检索字符串的第i
位,如果不为#
则进栈,就是用ret.append(ch)
将该字符追加到ret。
③如果检索到为#,将栈顶弹出,ret.deleteCharAt(ret.length() - 1)
因为StringBuffer是一个可修改的类,内容有变化后,长度会自动变化。遍历完成后将ret转化为字符串并返回,return ret.toString()
④对两个字符串都进行上述操作后,用String.equals(String s)
比较是否相同
工具:
①StringBuffer类的append
方法和deleteCharAt
方法
StringBuffer类教程
StringBuffer类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuffer类用到的方法:
public StringBuffer append(String s)
将指定的字符串追加到此字符序列。
int length()
返回长度(字符数)。
deleteCharAt
方法是用来删除StringBuffer字符串指定字符索引的方法,其中delete(inta,intb)
方法:包含两个参数,使用时删除索引从a到b(包括a不包括b)的所有字符。”
StringBuffer.deleteCharAt(i)
删除i位置的字符,i范围[0.length-1]
String toString()
返回此序列中数据的字符串表示形式。
//StringBuffer相关用法示例
StringBuffer str = new StringBuffer();
System.out.println(str);
System.out.println(str.length());
str.append("添加第一段字符add");
System.out.println(str);
System.out.println(str.length());
str.deleteCharAt(3);
System.out.println(str);
System.out.println(str.length());
str.append("添加第二段字符add");
System.out.println(str);
System.out.println(str.length());
②String类的charAt和equals
String类的方法教程
String类用到的方法
char charAt(int index)
返回指定索引处的 char 值。
boolean equals(Object anObject)
将此字符串与指定的对象比较。
★3>官方解法二双指针
官方写法,双层循环,内层两个循环分别每次从后往前遍历s和t字符串。对skip进行操作,判断出可比较的数后跳出内层循环,进行比较。外层循环保证两个字符串里至少有一个索引>=0就要进入循环继续比较。
4>理解了官方的思路以后,我试图自己实现逻辑。 因为官方写的双层循环,所以想尝试写一个单层循环,
逻辑关系很麻烦写起来,但是主要是为了锻炼逻辑能力,所以多尝试
①每次循环,都判断当前索引走到的位置。两个索引只要有一个>=0的情况下进入循环(之所以不写&&而写||是因为,会出现一种情况,一个字符已经走完,另一个字符还有可比较的字符,说明不相等。但是&&会导致跳出循环,不对多出的字符判断不等而错误)。两个索引都从字符尾部开始遍历字符
②如果当前字符是是#
则skip++,索引--。
如果不为#但是skip此时不为0,说明当前位置需要删除,则不进行比较,skip--
意思该位删除,直接索引--
跳过
只有当遇到不为#且此时skip==0
的情况下,索引会停止,说明找到了可比较的字符,进行比较。如果相同索引都--
比较下一对。如果不同说明字符不相等返回false
★③但是这种写法碰到了一个问题,当索引有一个<0时,说明成功遍历结束且自己比较过的都与对方相等。但是另一方字符串的问题在于,此时剩余遍历的可比较的字符如果只有#则忽略,意为可以删除,如果是可比较的其他字符说明长度不等返回false。
写法上判断应该是,索引>=0的那个字符的`skip==0且不为#,说明此位不是#也不用被删除,需要参与比较,说明两字符串不相等,返回false。
错误三次:
case"ab##""c#d#"
(因为难以处理只有一个索引结束时的情况)
case"isfcow#""isfcog#w#"
(比较判断时只判断了skip==0,但是#也不能参与判断)
case"a#c""b"
判断比较不为#的时候粗心笔误
case"c#a#c""c"
笔误,又写反了t和s
55.844代码实现
方法一:自己想的暴力法,用了一个toCharArray();
class Solution {
public int deleteBackspace(char[] s){
//删除退格符
int i = 0;
int sLength = s.length;
while(i<sLength) {
if (s[i] == '#') {
if (i == 0) {
//特殊情况,只有一个退格符,字符为空。或第一个位置为退格符,后续所有字符前移一位
for (int j = i; j < sLength - 1; j++) {
s[j] = s[j + 1];
}
sLength--;//删除了一位
} else {
//通常情况,删除i位置及前一个i-1字符。后续所有字符前移两位
for (int j = i - 1; j < sLength - 2; j++) {
s[j] = s[j + 2];
}
sLength -= 2;
i--;
}
} else {
//只有遍历的当前位不是退格符时再++,遍历下一位
i++;
}
}
return sLength;
}
public boolean backspaceCompare(String s, String t) {
char[] sArr = s.toCharArray();
char[] tArr = t.toCharArray();
int sLength = s.length();
int tLength = t.length();
//操作退格符
//开始操作删除s退格符
sLength = deleteBackspace(sArr);
tLength = deleteBackspace(tArr);
//判断相等
if(sLength != tLength){
return false;
}
for(int i=0; i<sLength; i++){
if(sArr[i] != tArr[i]){
return false;
}
}
return true;
}
}
方法二:官方解法一,用StringBuffer类
//写一个build函数,接受字符串,返回处理完退格符后的字符串,
public String build(String str){
StringBuffer bu = new StringBuffer();
for(int i=0; i<str.length(); i++){
char ch = str.charAt(i);
if(ch != '#'){
bu.append(ch);
}else{
//弹出栈顶
if(bu.length()>0) {//避免"#"的情况
bu.deleteCharAt(bu.length()-1);
}
}
}
return bu.toString();
}
public boolean backspaceCompare_2(String s, String t) {
return build(s).equals(build(t));
}
方法三: 双指针从后往前分别遍历两个字符串,用一个变量记录#出现的次数,根据变量值对后续遍历到的字符进行删除或比较。双层循环
class Solution {
public boolean backspaceCompare(String s, String t) {
int pointS = s.length()-1;
int pointT = t.length()-1;
int skipS = 0;
int skipT = 0;
while(pointS>=0 || pointT>=0){//用||一个走完了,另一个没走完且有可比较的数为不相等情况。
while(pointS>=0){
if(s.charAt(pointS) == '#'){
pointS--;
skipS++;//记录要删除的个数
}else if(skipS>0){
pointS--;
skipS--;//删除该位
}else{
break;//不是#也不用被删除,参与比较
}
}
while(pointT>=0){
if(t.charAt(pointT) == '#'){
pointT--;
skipT++;//记录要删除的个数
}else if(skipT>0){
pointT--;
skipT--;//删除该位
}else{
break;//不是#也不用被删除,参与比较
}
}
if(pointS>=0 && pointT>=0){
if(t.charAt(pointT) != s.charAt(pointS)){
return false;
}
}else{
if (pointS>= 0 || pointT>= 0) {
//因为完全确定走到这里一定是两个索引走走完<0或者一个走完另一个在可比较数字
return false;
}
}
pointS--;
pointT--;
}
return true;
}
}
方法四:可能就是一些闲的发慌自己写超复杂逻辑关系,为了单层循环的双指针。错了四次,其中粗心错了两次,乡村错题本本人,逻辑关系复杂,不建议
class Solution {
public boolean backspaceCompare(String s, String t) {
//双指针指向两个字符串从后往前遍历
int skipS = 0;
int skipT = 0;//记录遍历过程中#出现的次数,代表要删除的字符个数
int pointS = s.length()-1;
int pointT = t.length()-1;//指针用来遍历数组
while(pointS>=0 || pointT >= 0){//||是防止一个已经走完了,另一个还在遍历并不匹配的字符这种不相等的情况误判为相等。
if(pointS >= 0 ){
if( s.charAt(pointS) == '#'){
skipS++;
pointS--;
}else{
if (skipS != 0) {//删除
//可以直接跳过不比较
//
pointS--;
skipS--;
}
}
}
if(pointT >= 0 ) {
if (t.charAt(pointT) == '#') {
skipT++;
pointT--;
} else {
if (skipT != 0) {//删除
//可以直接跳过不比较
pointT--;
skipT--;
}
}
}
if(pointT>=0 && pointS>=0){//说明是走到了可以比较的没有被删除的位置
if((skipT ==0 && skipS == 0) && (s.charAt(pointS) != '#' && t.charAt(pointT) != '#')){
//说明当前位置无需删除,可以进行比较
if(s.charAt(pointS) != t.charAt(pointT)){
return false;
}else{
pointT--;
pointS--;//说明此位置比较成功,比较下一对
}
}
}else{
if(pointT >= 0 ){//说明s走完了,t没有
if(t.charAt(pointT) != '#'){
if(skipT == 0){
return false;//参与比较的没被删除也不是#
}
else{//需要删除的位
pointT--;
skipT--;
}
}else{//这一位是#
pointT--;
skipT++;//记录还要删除的位数
}else if(pointS >= 0 ){//说明t走完了,s没有.两个都走完的直接跳出
if(s.charAt(pointS) != '#'){
if(skipS == 0){
return false;//参与比较的没被删除也不是#
}
else{//需要删除的位
pointS--;
skipS--;
}
}else{//这一位是#
pointS--;
skipS++;//记录还要删除的位数
}
}
}
}
return true;
}
}
56.977有序数组的平方-3种写法(与52.27相关题)
977.有序数组的平方
思路:
1> 自己想的暴力解法
①先遍历一遍数组,给每个数组都平方。
②然后冒泡排序。时间复杂度O(n^2)
官方的暴力方法类似,调用了Arrays.sort(ans)
直接排序无需自己写。
2> 自己想的双指针,从中间位置开始。
与官方的方法二相同,把正数和负数分成两个子数组,然后进行一个类似的归并排序。
①两个指针分别开始时指到最大的负数和最小的正数的位置,然后平方后比较大小,更小的就填入新数组,正指针填入后++,负指针填入后–。
②一直到其中一个走到头<0或>length-1,另一个就直接统统开平方直接填入新数组。返回新数组
时间复杂度为O(n)
★3>官方的方法三双指针,官方采用从两头开始遍历,逆序对新数组赋值。重点理解无需处理某一指针移动至边界的情况。
自己写的误区在于其实不需要判断是否剩余全是正数或全是负数,无需(nums[neg]>=0)和else if(nums[pos]<0)这两种判断情况。
因为不论原数组是否全是正数(or负数)的情况,都只需判断他的平方大小,如果更大就逆序填入新数组。因为两头数字的平方一定比中间数字的平方大。
若指针未判断的数字全是正数或全是负数,也是只有一头平方大,依然只需要比较两个指针所指位置平方的大小即可。不需要考虑左指针一定要指负数,右指针一定要指正数。
56.977代码实现
方法一:暴力
public static int[] sortedSquares(int[] nums) {
for(int i = 0; i<nums.length; i++){
nums[i] *= nums[i];
}
//直接调用排序函数
//Arrays.sort(nums);
//自己写冒泡排序
for(int i=0; i<nums.length-1; i++){
for(int j=0; j<nums.length-i-1; j++){
if(nums[j]>nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
return nums;
}
方法二:官方双指针一,从中间最小的正负数开始,正序赋值新数组。需要考虑边界
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int pos = 0;
int neg = -1;//为什么不从0开始,因为有可能是只有正数的数组
int[] ans = new int[n];
int index = 0;//遍历新数组的索引
//记录最小的负数位置
for(int i=0; i<n; i++){
if(nums[i] < 0){
neg = i;
}else{
break;//走到正数位置,说明已经找到了最小的负数,无需继续遍历
}
}
pos = neg+1;//注意pos有可能==n,访问越界。说明原数组全是负数
//neg可能依旧==-1,说明原数组全是正数,这两种情况。都直接取一个指针所指的一个方向,循环换赋值即可
while(neg>= 0 || pos<n){//至少有一个正数
if(neg<0){//原数组只有正数or赋值到此处负数已经全部赋值完
ans[index] = nums[pos]*nums[pos];//循环赋值正数
pos++;
}else if(pos == n){//原数组只有负数or赋值到此处,正数已经全部赋值完
ans[index] = nums[neg]*nums[neg];
neg--;
}else if(nums[pos]*nums[pos] > nums[neg]*nums[neg]){//能走到这说明while的两个条件都满足
ans[index] = nums[neg]*nums[neg];//负指针的平方更小
neg--;
}else{
ans[index] = nums[pos]*nums[pos];//负指针的平方更小
pos++;
}
index++;
}
return ans;
}
}
方法三:按照官方思路自己写的,与方法二类似。但不是官方的最优写法,并没有领悟到不考虑边界条件的精髓
//不简洁的方法三写法
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int pos = n-1;//尾
int neg = 0;//头
int index = n-1;//逆序赋值
int[] ans = new int[n];
while(neg<=pos){//不相遇
if(nums[neg]>=0){
//说明原数组全是正数,或neg负指针已经走到正数范围,无需再动
ans[index] = nums[pos]*nums[pos];
pos--;
}else if(nums[pos]<0){
ans[index] = nums[neg]*nums[neg];
neg++;
}else if(nums[pos]*nums[pos] > nums[neg]*nums[neg]){
ans[index] = nums[pos]*nums[pos];
pos--;
}else{
ans[index] = nums[neg]*nums[neg];
neg++;
}
index--;
}
return ans;
}
}
★方法三:官方思路官方写法,重点理解。仔细考虑不需要边界条件
class Solution {
public int[] sortedSquares(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
for(int i=0,j=n-1,index=n-1; i<=j; index--){//不相遇
if(nums[i]*nums[i] > nums[j]*nums[j]){
ans[index] = nums[i]*nums[i];
i++;
}else{
ans[index] = nums[j]*nums[j];
j--;
}
}
return ans;
}
}
//main
for(int i:num){
System.out.print(" "+i);
}
System.out.println();
num = sortedSquares_2(num);
for(int i:num){
System.out.print(" "+i);
}
System.out.println();
★★ 57.209.长度最小的子数组-3种写法(滑动窗口)
209.长度最小的子数组
思路:
1>暴力*O(n^2)
①外循环遍历数组的元素作为子数组头,内循环从子数组头开始连加之后位置,直到和>=target,记录加了几个数,即子数组的长度。
②若内循环一直加的项数== 原数组长度
,且和<target
,说明找不到子数组,返回0。
若内循环一直加的项数<原数组长度
,加到数组尾<target
。说明子数组头取从此位置往后,所加的和都不可能>=target,则return上一个找到的长度。
④外循环将当前子数组长度与之前记录的比较,若更小则替换。
官方的暴力用到工具:
Integer.MAX_VALUE
表示int数据类型的最大取值数:2 147 483 647
Integer.MIN_VALUE
表示int数据类型的最小取值数:-2 147 483 648
Math.min(ans, j - i + 1)返回两数中的较小值
★★2>官方方法二前缀和+二分查找理解了好久
重点理解二分查找函数Arrays.binarySearch
。未找到时返回的不是-1
而是-(left+1)
?问题,二分查找要找到具体的target,但是题目要找>=的,如果找不到=target二分查找返回的是什么?要怎么找到子数组的尾部
与普通的二分查找不同,此处的二分查找大于等于某个数的第一个位置的功能
如果数组[1,2,5]要找6,返回下标-4。要找0,返回下标-1。要找4,返回下标-3。(我理解的返回值意思是,找不到且target如果要在数组里,位置应该是第几个数,例如找不到的0应该插入到第一个位置,以此类推)
处理>=target的第一个位置即为**-bound-1==left(为二分查找结束时,left的位置即为>target的最小位置)**
查找库函数Arrays.binarySearch
的实现
3>滑动窗口
个人困惑:end不回退
夭折的想法
类似滑动窗口但是思路不清楚,因为实现起来感觉很费时间且时间复杂度好像还是O(n^2)(因为没有理解到end不回退,而且虽然写法是双层循环。实际两个指针都是各遍历一次数组)
可以第一次找到>=target的子数组时,记录头尾位置【left,right】,下次子数组范围【left+1,right+1】在此长度中连加,若到达right+1之前就>=target,则调整right位置。
57.209代码实现
方法一:我的暴力双层循环
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int count = 0;
int sum = 0;
int minCount = 0;
for(int i=0; i<n; i++){
sum = 0;//每轮进来重新至0
count = 0;
for(int j=i; j<n; j++){
if(sum>=target){//因为target>=1的
break;
}else{
sum += nums[j];
count++;
}
}
if(sum<target){//说明此时的子数组头一直加到原数组尾都不满足>=target
//这条判断可以不加,把mincount赋值放在sum>=target位置即可。
//加了可以适当减少运行次数
return minCount;
//如果这是空数组,返回0
//如果这是第一个子数组头,说明整个数组的和都不满足>=target,mincount==0
//如果是后续子数组头,返回之前找到的最小长度
}
if(count<minCount || minCount==0){
minCount = count;
}
}
return minCount;
}
}
★方法一:官方的暴力写法更简洁,但是超出时间限制无法通过样例
无法通过这次的样例,对官方的代码补充。外循环内部,内循环结束的位置,补充了一句判断即可在第一次外循环时,判断不可行的代码,避免O(n^2)时,n过大导致超出时间限制。
if(sum < target){//说明j走到了n+1
break;//说明此时的子数组头一直加到原数组尾都不满足>=target
}
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
if(n == 0){
return 0;
}
int count = Integer.MAX_VALUE;
//开始我认为赋值n更好,更小。而且也可以代表一个初始的最大值。去和更小的子数组长度比较
// 但是如果原数组全部累加都不满足>=target的话,
// 这种情况count也==n但是并没有找到子数组需要返回0。或者恰好找到了,又要返回n
for(int i = 0; i < n; i++){
int sum = 0;
for(int j = i; j<n; j++){
sum += nums[j];
if(sum >= target){
count = count > j-i+1? j-i+1: count;
break;//当前子数组头寻找结束
}
}
if(sum < target){//说明j走到了n+1
break;//说明此时的子数组头一直加到原数组尾都不满足>=target
}
}
return count == Integer.MAX_VALUE? 0 : count;
}
}
★★方法二:前缀和+二分查找。难理解
不自己写binarySearch
,可以调用Java库函数Arrays.binarySearch(sum,target);
//57.2前缀和+二分查找,自己实现一个二分查找
//二分查找
public static int binarySearch(int[] nums, int target){//模仿Arrays.binarySearch
//返回找到的target的坐标
int n = nums.length;
int left = 0;
int right = n-1;
int mid = (right-left)/2;
while(left<=right){//考虑边界条件<=,想到特殊情况,只有一个元素也进入
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid-1;
}else{
left = mid+1;
}
mid = left+(right-left)/2;
}
//走到这里说明没找到,返回
return -(left+1);
}
//二分+前缀和的官方写法
public static int minSubArrayLen_2(int target, int[] nums){
int n = nums.length;
int count = Integer.MAX_VALUE;//用来记录子数组长度
if(n<1){
return 0;//空数组
}
//做前缀和数组sum
int[] sum = new int[n+1];//sum[i]表示nums数组的前i个数的和[0,i-1]
for(int i=1; i<=n; i++){
sum[i] = sum[i-1]+nums[i-1];
}
for(int i=1; i<=n; i++){//外层循环遍历数组sum,确定子数组头.
int tar = target + sum[i-1];
//根据子数组头变化target,子数组头从i开始时,每一项都等于加了前i-1项
int index = binarySearch(sum,tar);//用来找子数组尾部下标
if(index<0){//sum有n+1项
//说明没找到=tar的,根据返回值还原>tar的最小位置,
// 找不到tar时,因为return -(left+1)。index可能的范围[-1,-n-2]
index = -index-1;//处理前取值范围[-1,-n-2]处理后[0,n+1]其中n+1在sum属于越界
}
if(index <= n){//找到的>=tar的下标没有越界,说明存在
//index不可能<i,因为tar=target+sum[i-1]必然tar>=sum[i-1](target=0的时候等于)
//所以index>=i
count = Math.min(count, index-(i-1));//i-1为子数组头的前项
//这个最小位置一定是<=n的,不然说明整个数组里都不存在
}
}
return count == Integer.MAX_VALUE ? 0:count;
}
★方法三:滑动窗口个人觉得类似之前的双指针
class Solution {
public int minSubArrayLen(int target, int[] nums){
int n = nums.length;
if(n == 0){
return 0;
}
int count = Integer.MAX_VALUE;
int start = 0;
int end = 0;
int sum = 0;
while(end<n){//遍历子数组头
sum += nums[end];
while(sum >= target){
count = Math.min(count,end-start+1);
sum -= nums[start];
start++;
}
end++;
}
return count == Integer.MAX_VALUE? 0 : count;
}
}
★★★58.904水果成篮(拓展)
思路:理解题意为,fruits[i] = 2说明第i颗树上是第2种水果,需要找到一个子数组里,只有两种水果的果树,因为每颗果树只能摘一个果子。所以要求这个子数组要足够长就可以摘到最多数量的两种类型水果。
自己想的错误法:
题解的这些语句完全不懂。暂时跳过
List<Integer> blockLefts = new ArrayList();
blockLefts.add(i);
search: while (true)
Set<Integer> types = new HashSet();
types.add(tree[blockLefts.get(j)]);
weight += blockLefts.get(j+1) - blockLefts.get(j);
58.904代码实现
59.76最小覆盖子串(拓展)
看不懂的语句
Map<Character, Integer> ori = new HashMap<Character, Integer>();
ori.put(c, ori.getOrDefault(c, 0) + 1);
ori.containsKey(s.charAt(r))
ori.containsKey(s.charAt(r))
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
return ansL == -1 ? "" : s.substring(ansL, ansR);
public boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}
59.76 代码实现
★60.59螺旋矩阵2-2种写法
59.螺旋矩阵2
思路:
1>我的暴力写法
①根据n为偶数和n为奇数有两种不同的写法,观察图形,偶数时,中心位置为一个2*2的小矩阵。奇数时,中心位置为n^2
。如n=3,n/2=1,循环一次,n^2
填入中心位置[n/2][n/2]
②矩阵从外到内,将一圈顺时针遍历作为循环的一次,每次将上、右、下、左边界分成四个部分依次遍历。如图,每次遍历的边界为左闭右开[min,max)
③例如,n = 4时,n/2=2循环两次,顺时针遍历两圈。第一次遍历的行列为,0行,3列,3行,0列。循环第二次时,遍历的行列为,1行,2列,2行,1列。定义一组变量存放每次循环的边界值,min,max分别代表当前顺时针的一圈,上下左右的边界值。
④第一次循环max=3,min=0。上边界,行=min,列[min,max);右边界,列=max,行[min,max);下边界行=max,列[max,min);左边界,列=min,行[max.min)。
第一次赋值时,要填入的数字为1,每次赋值后+1自增。第二次循环max–=2,min++=1,依次类推
2>官方法一,模拟
①定义一个数组directions如图,用来进入下一个方向。行变化+directions数组的第一列,即+dire[direIndex][0]
,列变化+dire数组的第二列,即+dire[direIndex][1]
。
②当即将赋值的下一个位置越界,或遇到了已经赋值过的不为0的位置时,进入下一个方向。
③即+的数据,进入dire的下一行,direIndex++,但是dire数组只有四行,代表了四个边界。direIndex需要在[0,3]循环取值,则每次direIndex++
时写为(direIndex+1)%4
,用来处理越界后,第一行开始循环取值的情况
3>官方法二,按层模拟。与我的暴力一思路写法基本相同
60.59代码实现
方法一:我的暴力解法,循环一次是完成一圈的赋值。每次将四个边分开赋值
class Solution {
public int[][] generateMatrix(int n) {
int[][] nums = new int[n][n];
//把一圈看做一次循环赋值,第一次循环的起始位置为0,0。
//需要按顺序遍历0行,3列(n-1),3行,0列。
//第二次循环遍历1行,2列,2行,1列。起始位置1,1.
//到达中心点时停止,例如n/2=2,循环两次。若为奇数,中心点自己填
if(n%2 != 0){
nums[n/2][n/2] = n*n;
}
int min = 0;
int max = n-1;//控制每次循环的边界行列值
int row = 0;//行的坐标
int col = 0;//列的坐标
int num = 1;
for(int i=0; i<n/2; i++){//以n=4为例,循环两次
//第一次循环,0行,3列(n-1),3行,0列。
//上边界
row = min;
col = min;
while(col < max){
nums[row][col++] = num++;
}
//右边界
col = max;
row = min;
while(row < max){
nums[row++][col] = num++;
}
//下边界
row = max;
col = max;
while(col > min){
nums[row][col--] = num++;
}
//左边界
col = min;
row = max;
while(row > min){
nums[row--][col] = num++;
}
max--;
min++;
}
return nums;
}
}
方法二:官方法一,模拟
int maxNum = n*n;//curNum的最大值,也是结束时的值
int curNum = 1;//遍历每个位置要赋值的值,每次赋值后++
int row = 0;//矩阵行
int column = 0;//矩阵列
int direIndex = 0;//遍历dire的行
int[][] matrix = new int[n][n];//新建螺旋矩阵
int dire[][] ={{0,1},{1,0},{0,-1},{-1,0}};//改变顺时针方向的矩阵
while(curNum <= maxNum){//maxNum作为最后一个值,是闭区间。
matrix[row][column] = curNum++;
//定义两个行列下一个位置,在真正取到之前判断是否要改变方向
int nextRow = row + dire[direIndex][0];
int nextColumn = column + dire[direIndex][1];
if(nextRow >= n || nextColumn >= n ||nextRow < 0 || nextColumn < 0 || matrix[nextRow][nextColumn] != 0){
//前四个判断是最外圈的循环越界的情况,最后一个是内圈遍历到外圈已经赋值的情况
//改变顺时针方向
direIndex = (direIndex+1)%4;
}
//遍历++,继续走
row = row + dire[direIndex][0];
column = column + dire[direIndex][1];
}
return matrix;
61.54螺旋矩阵(拓展)
★62.剑指Offer29.顺时针打印矩阵-2种写法
剑指29.顺时针打印矩阵
思路:与60题解法思路基本一致
1>使用模拟方法
①创建数组dire[][]={{0,1},{1,0},{0,-1},{-1,0}};
四个元素分别代表,行不变列+1;行+1列不变;行不变列-1;行-1列不变;
②需要得到二维数组的行列长度来确定遍历边界,数组行数为:array.length
。数组列数为:array[0].length或者array[1].length
//某一行的长度就是列数。二维数组元素个数:行数*
每行元素个数(即列数)= array.length * array[0].length
③因为此处是按顺时针输出到新的一维数组并返回,要怎么排除异己打印过的数,可以建立一个与输入的二维数组同行同列的boolean数组,用来记录该位置是否被打印,boolean类型初始值为false,遍历后赋值true。
★2>按层模拟
①根据示例输入的矩阵并不都是n*n的矩阵,不方便使用我的暴力按层模拟,因为只定义了每层的边界min,max;但可以使用官方的按层模拟,分别约束四条边的边界值,定义top,right,left,bottom。
②写好代码后发现一个问题,对于3*3
这种奇数的正方形矩阵,赋值是无法遍历到中心的那一个位置,因为每一边都是左闭右开,都取不到这个位置。
③所以,改进。重点考虑当left=right=top=bottom这种情况,还有只余一竖行(left=right)或一横行(top=bottom)的情况。怎么在遍历一次后不重复遍历进入边的遍历。
④遍历完上边和右边以后,需要判断一下,只有left<right && top<bottom
的情况,需要再遍历下边和左边。
62.剑指29代码实现
方法一:模拟,错了两次在处理空数组上
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0){
int[] array=new int[0];
return array;
}
int dire[][] = {{0,1},{1,0},{0,-1},{-1,0}};
int direIndex = 0;//遍历dire行的索引
int row = 0;//行索引
int column = 0;//列索引
int maxNum = matrix.length*matrix[0].length;//二维数组元素个数
int nums[] = new int[maxNum];//新数组及索引
boolean[][] visited = new boolean[matrix.length][matrix[0].length];//与matrix同行同列
int i = 0;
while(i < maxNum) {
nums[i] = matrix[row][column];//遍历
visited[row][column] = true;
int nextRow = row + dire[direIndex][0];
int nextColumn = column + dire[direIndex][1];
if (nextRow >= matrix.length || nextColumn >= matrix[0].length || nextRow < 0 || nextColumn < 0 || visited[nextRow][nextColumn] == true) {
//超出边界,或下一个位置已经遍历过。需要改变遍历方向
direIndex = (direIndex+1)%4;
}
row += dire[direIndex][0];
column += dire[direIndex][1];
i++;//遍历次数+1
}
return nums;
}
}
方法二:按层模拟,与n*n的矩阵不同,注意边界条件
class Solution {
public int[] spiralOrder(int[][] matrix) {
//排除空数组的情况
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return new int[0];
}
//
int top = 0;
int left = 0;
int right = matrix[0].length-1;//最大列数
int bottom = matrix.length-1;//最大行数
int row = 0;
int column = 0;
int[] nums = new int[(matrix[0].length)*(matrix.length)];
int i = 0;//nums的索引
while(left <= right && top <= bottom){
//上边界赋值
column = left;
while(column <= right){
nums[i++] = matrix[top][column++];
}
//右边界
row = top+1;
while(row <= bottom){
nums[i++] = matrix[row++][right];
}
if(left < right && top < bottom){
//下
column = right-1;
while(column > left){
nums[i++] = matrix[bottom][column--];
}
//左
row = bottom;
while(row > top){
nums[i++] = matrix[row--][left];
}
}
top++;
left++;
right--;
bottom--;
}
return nums;
}
}