【Java力扣算法《代码随想录》03】第3章数组51-62题(leetcode题号704+27+26+283+844(StringBuffer类)+977+209+59+剑指29)

数组理论基础

参考《代码随想录》
在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述

51.leetcode题号704.二分查找-1种思路2种写法

704.二分查找

在这里插入图片描述

第一次错误
在这里插入图片描述

思路
二分查找是查找有序的数组,定义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种写法

27.移除元素
在这里插入图片描述
在这里插入图片描述

思路:因为元素的顺序可以改变,而且只需要原地修改数组空间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相关题)

26.删除有序数组中的重复项
在这里插入图片描述
在这里插入图片描述

思路

由于题目要求删除数组中的重复元素,因此输出数组的长度一定小于等于输入数组的长度,我们可以把输出的数组直接写在输入数组上。可以使用双指针:左指针指向当前遇到的第一个重复元素,右指针指向下一个不重复的位置。

一开始右指针比左指针多走一步,左指针指向赋好值的新元素,如果右指针指向的元素不等于自己的前一个元素,说明找到了下一个不重复值,它一定是输出数组的一个元素,则左指针加一,将右指针指向的元素复制到左指针位置,然后将右指针继续后移,

②如果右指针指向的元素等于自己的前一位,它不能在输出数组里,此时左指针不动,右指针右移一位。

整个过程保持不变的性质是:区间 [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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值