leetcode刷题

第一天

334 自增的三元子序列

给你一个整数数组 nums ,判断这个数组中是否存在长度为 3 的递增子序列。

如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ,使得 nums[i] < nums[j] < nums[k] ,返回 true ;否则,返回 false 。

方法一

建立两个数组,一个数组用于记录从0i的最小值,即min[i],另外一个数组表示从i到最后,这段区间内的最大值,max[i]

遍历一遍数组,当发现某个值大于min[k-1]并且小于max[k+1],那么表明存在这样的三个数

class Solution {
    public boolean increasingTriplet(int[] nums) {
        int []min = new int[nums.length];
        int []max = new int[nums.length];
        int mini = 100000;
        int maxi = -100000;
        int l = nums.length;
        for (int i = 0; i < l; i++){
            if (nums[i] < mini)
                mini = nums[i];
            min[i] = mini;
            if (nums[l - 1 - i] > maxi)
                maxi = nums[l - 1 - i];
            max[l - 1 - i] = maxi;
        }

        for (int k = 1; k < l - 1; k++)
            if (nums[k] > min[k - 1] && nums[k] < max[k + 1]) return true;
        return false;
    }
}
方法二 贪心

引入两个变量ab,前者一直记录从0开始到i的最小值,b记录着次小值,遍历一遍数组,如果没有找到比b大的,说明不存在这样的三个数,如果有,说明存在

public boolean increasingTriplet(int[] nums) {
    int a = 2147483647, b = a;
    for (int n: nums) 
        if (n <= a) a = n;
        else if (n <= b) b = n;
        else return true;
    return false;
}

第二天

747 至少是其他数字两倍的最大数

给你一个整数数组 nums ,其中总是存在 唯一的 一个最大整数 。

请你找出数组中的最大元素并检查它是否 至少是数组中每个其他数字的两倍 。如果是,则返回 最大元素的下标 ,否则返回 -1 。

方法

找到该数列中的最大数和次大数,只需要比较最大数和次大数的关系即可。

class Solution {
    public int dominantIndex(int[] nums) {
        int max = -100000;
        int submax = -100000;
        int index = 0;
        for (int i = 0; i < nums.length; i++){
            if (nums[i] > max) {
                submax = max;
                max = nums[i];
                index = i;
            }
            else if(nums[i] > submax) submax = nums[i];
        }
        return max >= 2 * submax ? index : -1;
    }
}

278 第一个出错的版本

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

方法

使用二分查找答案,如果如果mid是错误版本,那么r = mid - 1,否则l = mid + 1。当循环终止时,l就是答案。

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        long l = 1, r = n;
        while (l <= r){
            long mid = (l + r) / 2;
            //防止溢出使用以下方式实现
            //int mid = l + (r - l) / 2;
            if (isBadVersion((int)mid)){
                r = mid - 1;
            }
            else{
                l = mid + 1;
            }
        }
        return (int)l;
    }
}

35 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

方法

二分搜索

class Solution {
    public int searchInsert(int[] nums, int target) {
        int l = 0, r = nums.length - 1;
        while (l <= r){
            int mid = (l + r) / 2;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[mid] > target){
                r = mid - 1;
            }
            else{
                l = mid + 1;
            }
        }
        return l;
    }
}

第三天

977 有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

方法

找到数组中绝对值发生变化的点,把这个点记录下来,然后依照归并排序的思路,建立两个指针,第一个指针从记录点从右往左走,第二个指针从记录点从左往右走,注意短路求值和边界值的处理即可。

class Solution {
    public int[] sortedSquares(int[] nums) {
        int []res = new int[nums.length];
        int index = 0;
        //短路求值,一个条件要放在左边。否则会出现越界错误
        while(index < nums.length - 1 && Math.abs(nums[index]) >= Math.abs(nums[index + 1])){
            index++;
        }//第一个循环用于寻找到绝对值的转折点
        int j = index;//往左走的指针
        int k = index + 1;//往右走的指针
        for (int i = 0; i < nums.length; i++){
            //短路求值同理 k == nums.length 说明数列的右半部分已经处理完成 只需要将左半部分的所有剩余数据从右往左依次平方放入结果里即可,否则就比较左边的绝对值和右边的绝对值的大小 哪边的绝对值小就将哪个放入到结果里即可 
            if (k == nums.length || j >= 0 && Math.abs(nums[j]) <= nums[k] ){
                res[i] = nums[j] * nums[j];
                j--;
            }
            //同理
            else if(j < 0  || k < nums.length && Math.abs(nums[j]) > nums[k]){
                res[i] = nums[k] * nums[k];
                k++;
            }
        }
        return res;
    }
}

189 轮转数组

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

方法

使用翻转实现,先对整体进行翻转,再对两个部分分别进行翻转。

比如12345k=2,先整体翻转得到54321,再对前两位和后三位分别翻转,得到答案45123,实现复杂度为O(2*n)=O(n)

class Solution {
    public void rotate(int[] nums, int k) {
        k = k % nums.length;
        reverse(nums, 0, nums.length - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.length - 1);
    }
    
    //reverse方法 用于数组翻转 时间复杂度为O(n)
    public void reverse(int []a, int s, int e){
        int temp;
        while(s < e){
            temp = a[s];
            a[s] = a[e];
            a[e] = temp;
            s++;
            e--;
        }
    }
}

第四天

373 查找和最小的K对数字

给定两个以 升序排列 的整数数组 nums1nums2 , 以及一个整数 k

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2

请找到和最小的 k 个数对 (u1,v1), (u2,v2) … ```(uk,vk)`` 。

方法 优先队列

建立一个优先队列保存下一个需要选取的数对的下标对,初始化为(0,0),(nums1.length - 1, 0)。优先队列的比较函数以对应下标数对的和为基准,每次将队首的下标(i,j)数对取出来,然后将(i, j + 1)入队,直到取出k个为止。时间复杂度:O(n*logn)

java优先队列的使用方法:需要指定比较器,重写compare函数。

class Solution {
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        List<List<Integer>> res = new ArrayList<>();
        PriorityQueue<List<Integer>> priorityQueue = new PriorityQueue<List<Integer>>(k, new Comparator<List<Integer>>() {
          @Override
            public int compare(List<Integer> o1, List<Integer> o2) {
                int res1 = nums1[o1.get(0)] + nums2[o1.get(1)];
                int res2 = nums1[o2.get(0)] + nums2[o2.get(1)];
                return res1 - res2;
            }
        });
        for (int i = 0; i < nums2.length; i++){
            List<Integer> temp = new ArrayList<>();
            temp.add(0);
            temp.add(i);
            priorityQueue.add(temp);
        }
        int kcount = 0;
        while (kcount < k && kcount < nums1.length * nums2.length){
            List<Integer> temp = new ArrayList<>();
            temp = priorityQueue.poll();
            List<Integer> rt = new ArrayList<>();
            rt.add(nums1[temp.get(0)]);
            rt.add(nums2[temp.get(1)]);
            res.add(rt);
            if (temp.get(0) + 1 < nums1.length){
                List<Integer> a = new ArrayList<>();
                a.add(temp.get(0) + 1);
                a.add(temp.get(1));
                priorityQueue.add(a);
            }
            kcount++;
        }
        return res;
    }
}

283 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

方法一

从后往前扫数组,发现0就将0移动到最后

class Solution {
    public void moveZeroes(int[] nums) {
        int pre = nums.length - 1;
        for (int i = nums.length - 1; i >= 0; i--){
            if (nums[i] == 0){
                for (int j = i; j < pre; j++){
                    int temp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = temp;
                }
                pre--;
            } 
        }
    }
}
方法二 双指针

left指针用于记录形如0......x这样的子串的头部,right指针用于记录形如0......x子串的尾部,不断交换首尾两个元素进行调整即可。

class Solution {
    public void moveZeroes(int[] nums) {
        int leftIndex = 0;
        int rightIndex = 0;
        while (rightIndex < nums.length){
            if (nums[rightIndex] != 0){
                int temp = nums[leftIndex];
                nums[leftIndex] = nums[rightIndex];
                nums[rightIndex] = temp;
                leftIndex++;//只有当发生交换时 首部指针才后移
            }
            rightIndex++;//尾部指针不断后移 
        }
    }
}

167 两数之和

给定一个已按照非递减顺序排列 的整数数组 numbers ,请你从数组中找出两个数满足相加之和等于目标数 target

函数应该以长度为 2的整数数组的形式返回这两个数的下标值。numbers 的下标 从1开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length

你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。

方法一 二分

遍历第一个数,二分查找第二个数

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length - 1; i++){
            //int firstNum = numbers[i];
            int l = i + 1;
            int r = numbers.length - 1;
            while(l <= r){
                int mid = (l + r) / 2;
                int sum = numbers[i] + numbers[mid];
                if (sum == target){
                    int []res = {i + 1, mid + 1};
                    return res;
                }
                else if (sum > target){
                    r = mid - 1;
                }
                else{
                    l = mid + 1;
                }
            }
        }
        return new int[2];
    }
}
方法二 双指针

由于原数组是非降序的,因此定义两个指针se分别指向头部和尾部,当首尾和比目标值大,尾指针向前移动,当比目标值小,首指针向后移动。直到找到答案。

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int s = 0, e = numbers.length - 1;
        while (e > s){
            if (numbers[s] + numbers[e] > target) e--;
            else if (numbers[s] + numbers[e] < target) s++;
            else return new int[]{s + 1, e + 1};
        }
        return new int[2];
    }
}

第五天

382 链表中的随机节点

给你一个单链表,随机选择链表的一个节点,并返回相应的节点值。每个节点被选中的概率一样

实现Solution类:

**Solution(ListNode head) **使用整数数组初始化对象。
int getRandom() 从链表中随机选择一个节点并返回该节点的值。链表中所有节点被选中的概率相等。

方法一 使用辅助数组
class Solution {
    ArrayList<Integer> nums;
    Random rand;
    public Solution(ListNode head) {
        nums = new ArrayList<Integer>();
        while(head != null){
            nums.add(head.val);
            head = head.next;
        }
        rand = new Random();
    }
    
    public int getRandom() {
        int choose = rand.nextInt(nums.size());
        return nums.get(choose);
    }
}
方法二 水塘抽样模型

对于位于位置i的节点,领其被取的概率为1/i,然后遍历整个链表,如果接下来还有值被取,则将原来的答案替换。可以证明通过上述方式取数,每个节点被选取的概率都是1/n。数学证明过程如下:

令第i个节点被选中的概率为 P i P_i Pi
P i = 1 i ∗ ( 1 − 1 i + 1 ) ∗ ( 1 − 1 i + 2 ) . . . ∗ ( 1 − 1 n ) = 1 n P_i={\frac 1 i}*(1-\frac 1 {i+1})*(1-\frac 1 {i+2})...*(1-\frac 1 n)=\frac 1 n Pi=i1(1i+11)(1i+21)...(1n1)=n1

class Solution {
    ListNode head;
    Random rand;
    public Solution(ListNode head) {
        this.head = head;
        rand = new Random();
    }
    
    public int getRandom() {
        int ans = 0, i = 1;
        for (ListNode temp = head; temp != null; temp = temp.next){
            if (rand.nextInt(i) == 0) ans = temp.val;
            i++;
        }
        return ans;
    }
}

344 反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组s的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用O(1) 的额外空间解决这一问题。

方法 双指针
class Solution {
    public void reverseString(char[] s) {
        int start = 0, e = s.length - 1;
        char temp = ' ';
        while(e > start){
            temp = s[start];
            s[start] = s[e];
            s[e] = temp;
            start++;
            e--;
        }
    }
}

557 反转字符串中的单词

给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。

方法 使用StringBuilder
class Solution {
    public String reverseWords(String s) {
        String []process = s.split(" ");
        for (int i = 0; i < process.length; i++){
            StringBuilder sb = new StringBuilder(process[i]);
            process[i] = sb.reverse().toString();
        }
        return String.join(" ", process);

    }
}

第六天

876 链表中间节点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

方法 双指针

指针p1一直往后扫,指针p2每两次往后扫一次。

class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode p1 = head;
        ListNode p2 = head;
        int length = 0;
        while (p1 != null){
            p1 = p1.next;
            length++;
            if (length % 2 == 0) p2 = p2.next;
        }
        return p2;
    }
}

19 删除链表的倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

方法一

先遍历一遍整个链表,得到链表的长度。然后从头找到第length-n个节点,将其删除。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode p1 = head;
        int length = 0;
        while (p1 != null){
            length++;
            p1 = p1.next;
        }
        ListNode pre = null, p = head;
        int index = 0;
        while (index < length - n){
            pre = p;
            p = p.next;
            index++;
        }
        if (pre == null) return head.next;
        pre.next = p.next;
        return head;
    }
}
方法二

建立两个指针,让第一个指针与第二个指针之间间隔n。则第一个指针的下一个节点就是需要删除的节点。初始化时建立一个哑节点,将其赋值给第一个指针,哑节点的下一个指向head

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode p1 = new ListNode(0, head);
        ListNode p2 = head;
        for (int i = 0; i < n; i++) p2 = p2.next;
        while (p2 != null){
            p2 = p2.next;
            p1 = p1.next;
        }
        if (p1.next == head) return head.next;
        p1.next = p1.next.next;
        return head;
    }
}

1220 统计元音字母序列

给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:

字符串中的每个字符都应当是小写元音字母('a', 'e', 'i', 'o', 'u')
每个元音 'a'后面都只能跟着 'e'
每个元音'e'后面只能跟着 'a' 或者是'i'
每个元音 'i'后面 不能 再跟着另一个'i'
每个元音 'o'后面只能跟着'i'或者是 'u'
每个元音'u' 后面只能跟着'a'
由于答案可能会很大,所以请你返回 模 10^9 + 7 之后的结果。

基本方法 动态规划

首先需要定义dp数组,领dp[i][j]表示以字符i结尾的长度为j的序列的数量,依据规则,可以知道:

  • 字母a的前驱字母只能是eiu
  • 字母i的前驱字母只能是eo
  • 字母e的前驱字母只能是ai
  • 字母o的前驱字母只能是i
  • 字母u的前驱字母只能是io

将元音字母按序标记为01234。可以得到,dp数组的状态转移方程为:
KaTeX parse error: No such environment: equation at position 8: \begin{̲e̲q̲u̲a̲t̲i̲o̲n̲}̲ dp[x][i]=\left…
初始状态,dp[x][i]=1,通过状态转移方程,得到第n步的结果,然后相加即可。

时间复杂度O(n),空间复杂度O(5*n)

class Solution {
    public int countVowelPermutation(int n) {
        int[][] dp = new int[5][];
        int mod = 1000000007;
        for (int i = 0; i < 5; ++i){
            dp[i] = new int[n + 1];
            dp[i][1] = 1;
        }
        for (int i = 2; i <= n; ++i){
            dp[0][i] = ((dp[1][i - 1] % mod + dp[2][i - 1] % mod) % mod + dp[4][i - 1] % mod) % mod;
            dp[1][i] = (dp[0][i - 1] % mod + dp[2][i - 1] % mod) % mod;
            dp[2][i] = (dp[1][i - 1] % mod + dp[3][i - 1] % mod) % mod;
            dp[3][i] = dp[2][i - 1];
            dp[4][i] = (dp[2][i - 1] % mod + dp[3][i - 1] % mod) % mod;
        }
        int ans = 0;
        for (int i = 0; i < 5; ++i){
            ans  = (ans % mod + dp[i][n] % mod) % mod;
        }
        return ans;
    }
}
更优方法 矩阵快速幂

上述状态转移方程的转移过程可以通过矩阵形式表示,因此,经过n步递推,也就是相当于n个相同的转移矩阵相乘,即转移矩阵的n次幂,可以通过快速幂算法对其进行优化,使得在O(logn)的时间内完成n次矩阵相乘。

class Solution {
    public static long mod = 1000000007;
    public int countVowelPermutation(int n) {
        long[][] factor = {
                {0, 1, 1, 0, 1},
                {1, 0, 1, 0, 0},
                {0, 1, 0, 1, 0},
                {0, 0, 1, 0, 0},
                {0, 0, 1, 1, 0}
        };
        long[][] res = {
                {1, 0, 0, 0, 0},
                {0, 1, 0, 0, 0},
                {0, 0, 1, 0, 0},
                {0, 0, 0, 1, 0},
                {0, 0, 0, 0, 1}
        };
        long[][] initValue = {
                {1, 0, 0, 0, 0},
                {1, 0, 0, 0, 0},
                {1, 0, 0, 0, 0},
                {1, 0, 0, 0, 0},
                {1, 0, 0, 0, 0}
        };

        int times = n - 1;
        while (times > 0){
            if (times % 2 == 1)
                res = martixMultiple(res, factor);
            factor = martixMultiple(factor, factor);
            times /= 2;
        }
        res = martixMultiple(res, initValue);
        long ans = 0;
        for (int i = 0; i < 5; i++){
            ans = (ans % mod + res[i][0] % mod) % mod;
        }
        return (int)(ans % mod);
    }
    public long[][] martixMultiple(long[][] a, long[][] b){
        int aRows = a.length;
        long [][]res = new long[aRows][];
        int Column = a[0].length;
        for (int i = 0; i < aRows; i++){
            res[i] = new long[Column];
        }
        for (int i = 0; i < aRows; ++i){
            for (int j = 0; j < Column; ++j){
                long temp = 0;
                for (int k = 0; k < Column; ++k){
                    temp = (temp + a[i][k] * b[k][j]) % mod;
                }
                res[i][j] = temp;
            }
        }
        return res;
    }
}

第七天

539 最小时间差

给定一个 24 小时制(小时:分钟 “HH:MM”)的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。

基本方法

将所给的时间全部转化为从00:00开始计时的分钟数,然后对分钟数进行排序,依次比较相邻的时间节点的时间差,找到最小的时间差,最后比较首尾时间节点的时间差,取值小的。注意:两个时间节点的时间差有两种情况。

class Solution {
    public int findMinDifference(List<String> timePoints) {
        int[] timeLine = new int[timePoints.size()];
        for (int i = 0; i < timePoints.size(); ++i){
            String[] temp = timePoints.get(i).split(":");
            timeLine[i] = Integer.parseInt(temp[0]) * 60 + Integer.parseInt(temp[1]);
        }
        Arrays.sort(timeLine);
        int _24hTimeMin = 24 * 60;
        int differ = 10000;
        for (int i = 0; i < timeLine.length - 1; ++i){
            int temp = Math.min(Math.abs(timeLine[i] - timeLine[i + 1]), -Math.abs(timeLine[i] - timeLine[i + 1]) + _24hTimeMin);
            if (temp < differ) differ = temp;
        }
        int temp = Math.min(Math.abs(timeLine[0] - timeLine[timeLine.length - 1]), -Math.abs(timeLine[0] - timeLine[timeLine.length - 1]) + _24hTimeMin);
        differ = Math.min(differ, temp);
        return differ;
    }
}
优化方法

由于24小时内,分钟最多只有1440。因此根据抽屉原理:如果给定的数列的长度超过1440,那么说明必有两个时间节点是相同的,此时可以直接返回0

代码变动主要为加上特定条件判断,在此不再赘写。

3 无重复的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

方法 滑动窗口

定义两个指针startend。开始时startend都指向字符串的头部,end指针不断往后滑动,当出现与之前的字符有重复时,移动start指针。对于判断是否存在重复,一种更优的方法是使用一个长为128的数组,每读入一个字符,将相应字符对应的数组的值置为该字符在原字符串中的位置index

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int[] str = new int[128];
        for (int i = 0; i < 128; ++i){
            str[i] = -1;
        }
        char[] inStr = s.toCharArray();
        int start = 0, end = 0;
        int ans = 0;
        while (end < inStr.length){
            if (str[inStr[end]] != -1){
                ans = Math.max(ans, end - start);//出现重复字符 更新答案
                start = Math.max(str[inStr[end]] + 1, start);//将start指针指向出现重复的上一个字符在字符串中的位置的下一个位置 取最大值是为了确保start不会回到前面的位置去 
                //以abba为例 如果不取最大值,当读到最后一个a时 start会更新成1 即回到之前的位置去了 这并不是想要的结果
            }
            str[inStr[end]] = end;
            end++;
        }
        ans = Math.max(ans, end - start);
        return ans;
    }
}

567 字符串的排列

给你两个字符串s1s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false

换句话说,s1 的排列之一是 s2 的 子串 。

方法 滑动窗口

以第一个字符串的大小为滑动窗口的大小,统计每个字符出现的次数,然后以这样的窗口去第二个字符串中去比对,如果在窗口区间内,每个字符出现的次数一样,说明第二个字符串中包含第一个字符串的排列,直接返回true,否则,如果遍历完整个第二个字符串,都没有找到,返回false

class Solution {
    public boolean checkInclusion(String s1, String s2) {
        int[] str = new int[128];
        for (int i = 0; i < 128; ++i) str[i] = 0;
        for (int i = 0; i < s1.length(); ++i){
            str[s1.charAt(i)]++;
        }
        for (int i = 0; i < s2.length(); ++i){
            if (str[s2.charAt(i)] != 0){
                if (i + s1.length() > s2.length()) return false;
                int[] temp = str.clone();
                for (int j = 0; j < s1.length(); ++j){
                    if (temp[s2.charAt(i + j)] == 0) break;
                    temp[s2.charAt(i + j)]--;
                }
                boolean flag = true;
                for (int k : temp){
                    if (k != 0) {
                        flag = false;
                        break;
                    }
                }
                if (flag) return true;
            }
        }
        return false;
    }
}
更优化方法 比较差异值

采用第一种方法每次实际变化的字符只有首尾两个字符,但是却需要对整个区间内的元素比较一遍,这是不合理的,应该将关注点放在发生变化的两个字符身上,假设发生变化的首尾字符分别是xy,初始化时,计算一个differ值,该differ值代表着两个字符串不同的地方的数量:

  • 如果x=y,说明这一次移动没有发生变化,保持differ不变。
  • 如果x在移出前,其str[x]值是0,说明这一次移动让一个原本已经没有差异的字符移出去了,这时候differ应该增加1,当x移出后,如果str[x]值为0,说明这一次移动让一个原本存在差异的字符移出去了,这样子差异值differ应该减少1
  • 字符y同理。
  • differ变为0时,说明第二个字符串中包含第一个字符串的排列,返回true,否则返回false
class Solution {
    public boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        int[] str = new int[26];
        for (int i = 0; i < s1.length(); ++i){
            str[s1.charAt(i) - 'a']++;
            str[s2.charAt(i) - 'a']--;
        }
        int differ = 0;
        for (int i : str) if (i != 0) differ++;
        if (differ == 0) return true;
        for (int i = s1.length(); i < s2.length(); ++i){
            int x = s2.charAt(i - s1.length()) - 'a';
            int y = s2.charAt(i) - 'a';
            if (str[x] == 0) differ++;
            str[x]++;
            if (str[x] == 0) differ--;
            if (str[y] == 0) differ++;
            str[y]--;
            if (str[y] == 0) differ--;
            if (differ == 0) return true; 
        } 
        return false;
    }
}

第八天

219 存在重复元素

给你一个整数数组nums和一个整数 k,判断数组中是否存在两个 不同的索引 ij ,满足 nums[i] == nums[j]abs(i - j) <= k。如果存在,返回true ;否则,返回false

方法 HashMap

使用一个hashMap记录每个元素在原数组中出现的位置,从头往后扫一遍数组,如果发现某个元素在HashMap中已经存有一个值,那么计算出一个gap,将gapk比较,如果不超过k则返回true,否则更新HashMap,遍历一遍这个数组,如果没有返回true,那么返回false

class Solution {
    public boolean containsNearbyDuplicate(int[] nums, int k) {
        int index2 = 0;
        int gap = 10000;
        Map<Integer, Integer> map = new HashMap<>();
        while (index2 < nums.length){
            if (map.containsKey(nums[index2])){
                gap = Math.min(gap, index2 - map.get(nums[index2]));
                if (gap <= k) return true;
            }
            map.put(nums[index2], index2);
            index2++;
        }
        return false;
    }
}

733 图像渲染

有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 065535 之间。

给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。

为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。

最后返回经过上色渲染后的图像。

方法一 深度优先搜索DFS

从起始点开始,向四个方向搜索,如果满足条件,重复这个过程,直到将整个图空间搜索完成。

public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};
    public static int xlength;
    public static int ylength;
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        if (image[sr][sc] == newColor) return image; //特判
        int oldColor = image[sr][sc];
        xlength = image.length;
        ylength = image[0].length;
        deepFirstSearch(image, sr, sc, oldColor, newColor);
        return image;
    }
	//判断是否合法
    public static boolean valid(int x, int y){
        return x >= 0 && y >= 0 && x <= xlength - 1 && y <= ylength - 1;
    }
	
    public static void deepFirstSearch(int[][] data, int x, int y, int oldColor, int newColor){
        if (data[x][y]  != oldColor) return;
        data[x][y] = newColor;
        for (int i = 0; i < 4; ++i)
            if (valid(x + dx[i], y + dy[i]))
                deepFirstSearch(data, x + dx[i], y + dy[i], oldColor, newColor);
    }
方法二 广度优先搜索BFS

新建一个队列,将起始点入队,然后依次判断以起始点为中心的四个方向上的点是否满足条件,如果满足条件入队,在染色时,每次取出队头的元素进行染色,直到整个队列为空为止。

public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};
    public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
        if (image[sr][sc] == newColor) return image;
        Queue<Pair<Integer, Integer>> queue = new LinkedList<>();
        queue.add(new Pair<>(sr, sc));
        int oldColor = image[sr][sc];
        image[sr][sc] = newColor;
        while (!queue.isEmpty()){
            Pair<Integer, Integer> temp = queue.poll();
            int x = temp.getKey();
            int y = temp.getValue();
            image[x][y] = newColor;
            for (int i = 0; i < 4; ++i){
                if (x + dx[i] < image.length && x + dx[i] >= 0
                && y + dy[i] < image[0].length && y + dy[i] >= 0
                && image[x + dx[i]][y + dy[i]] == oldColor)
                queue.add(new Pair<>(x + dx[i], y + dy[i]));
            }
        }
        return image;
    }

695 岛屿的最大面积

给你一个大小为m x n的二进制矩阵grid

岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。

岛屿的面积是岛上值为1的单元格的数目。

计算并返回grid中最大的岛屿面积。如果没有岛屿,则返回面积为0

方法一 深度优先搜索DFS

遍历整个地图,从每个位置开始依次执行一遍DFS,将路径包络的位置标记为1,这样子下次就不会访问同一个位置。使用一个变量ans来统计遍历的深度,最后找到深度最深的一个答案返回即可。

时间复杂度O(N*M),即地图大小。

class Solution {
    public static int xlength;
    public static int ylength;
    public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};
    public static boolean[][] isVisited;

    public int maxAreaOfIsland(int[][] grid) {
        int ans = 0;
        xlength = grid.length;
        ylength = grid[0].length;
        isVisited = new boolean[xlength][ylength];
        for (int i = 0; i < xlength; ++i){
            for (int j = 0; j < ylength; ++j){
                ans = Math.max(ans, deepFirstSearch(grid, i, j));
            }
        }
        return ans;
    }

    public static boolean valid(int x, int y){
        return x >= 0 && x < xlength && y >= 0 && y < ylength && !isVisited[x][y];
    }

    public static int deepFirstSearch(int[][] data, int x, int y){
        if (data[x][y] == 0) return 0;
        if (isVisited[x][y]) return 0;
        isVisited[x][y] = true;
        int ans = 1;
        for (int i = 0; i < 4; ++i){
            if (valid(x + dx[i], y + dy[i]))
                ans += deepFirstSearch(data, x + dx[i], y + dy[i]);
        }
        return ans;
    }
}
方法二 BFS

注意入队时,应当立刻将对应位置标记为已访问,否则会出现重复入队

class Solution {
    public static int xlength;
    public static int ylength;
    public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};
    public static boolean[][] isVisited;

    public int maxAreaOfIsland(int[][] grid) {
        int ans = 0;
        xlength = grid.length;
        ylength = grid[0].length;
        isVisited = new boolean[xlength][ylength];
        Queue<Integer> queueX = new LinkedList<>();
        Queue<Integer> queueY = new LinkedList<>();
        for (int i = 0; i < xlength; ++i){
            for (int j = 0; j < ylength; ++j){
                if (grid[i][j] == 1 && !isVisited[i][j]){
                    queueX.add(i);
                    queueY.add(j);
                    isVisited[i][j] = true;//入队之后立刻将对应位置标记为true
                    int count = 0;
                    while (!queueX.isEmpty()){
                        int x = queueX.poll();
                        int y = queueY.poll();
                        count++;
                        for (int k = 0; k < 4; ++k){
                            if (valid(x + dx[k], y + dy[k]) && grid[x + dx[k]][y + dy[k]] == 1){
                                isVisited[x + dx[k]][y + dy[k]] = true;//入队之后立刻将对应位置标记为true
                                queueX.add(x + dx[k]);
                                queueY.add(y + dy[k]);
                            }
                        }
                    }
                    ans = Math.max(ans, count);
                }
            }
        }
        return ans;
    }

    public static boolean valid(int x, int y){
        return x >= 0 && x < xlength && y >= 0 && y < ylength && !isVisited[x][y];
    }
}

第九天

2029 石子游戏

Alice 和 Bob 再次设计了一款新的石子游戏。现有一行 n个石子,每个石子都有一个关联的数字表示它的价值。给你一个整数数组 stones ,其中 stones[i] 是第 i 个石子的价值。

Alice 和 Bob 轮流进行自己的回合,Alice 先手。每一回合,玩家需要从 stones 中移除任一石子。

如果玩家移除石子后,导致所有已移除石子 的价值总和 可以被 3整除,那么该玩家就 **输掉游戏 **。
如果不满足上一条,且移除后没有任何剩余的石子,那么 Bob 将会直接获胜(即便是在 Alice 的回合)。
假设两位玩家均采用 最佳 决策。如果 Alice 获胜,返回 true;如果 Bob 获胜,返回 false

方法

首先将所有的石子分成三类,分别是模30、模31、模32。因为考虑的是石子的总和,因此只需要分别考虑每一个石子模3以后的值即可。

首先考虑0的数量。

如果0的数量是偶数,那么不管怎么样,爱丽丝和鲍勃分别去掉0,当0被拿完之后,又回到了拿掉第一个0之前的状态。因此,如果0的数量是偶数个,那么和0的数量是0个的情况是等价的。

如果0的数量是奇数,那么0起到的其实是一个交换先后手的作用。

首先考虑0的数量为偶数的情况,在这种情况下,只剩下1和2两种类别的数字,爱丽丝和鲍勃拿数字的顺序只能是以下两个序列中的一个。

  • 第一种情况,如果爱丽丝先拿掉一个1,那么这个拿数字的序列是:11212121…
  • 第二种情况,如果爱丽丝先拿掉一个2,那么这个拿数字的数列是:22121212…

考虑第一种情况,令count[]数组统计各个数字出现的数量,那么如果爱丽丝想要赢,则序列应该是以下形式:
11...212121...22 11...212121...22 11...212121...22
即,当1被拿完之后,鲍勃没有1可以拿了,只能拿2,这样爱丽丝就赢了。对于上述形式的序列,容易知道存在以下关系:
{ c o u n t [ 2 ] > = c o u n t [ 1 ] c o u n t [ 1 ] > = 1 \left\{ \begin{aligned} count[2]&>=count[1]\\ count[1]&>=1\\ \end{aligned} \right. {count[2]count[1]>=count[1]>=1
即2的数量至少要比1的数量多,但是1的数量不能够为0,如果1的数量为0,由于爱丽丝先手,因此让爱丽丝和鲍勃轮流拿2,总和先到6时,一定是爱丽丝拿的。或者2的数量只有1或2个,爱丽丝还是会因为拿完了所有的数字而输掉。

同理,考虑第二种情况,序列应当具有以下形式:
22...121212...11 22...121212...11 22...121212...11
在这种情况下,应存在以下关系:
{ c o u n t [ 1 ] > = c o u n t [ 2 ] c o u n t [ 2 ] > = 1 \left\{ \begin{aligned} count[1]&>=count[2]\\ count[2]&>=1\\ \end{aligned} \right. {count[1]count[2]>=count[2]>=1
因此,在0的个数为偶数个的情况下,如果爱丽丝想要赢,则应当满足两个条件中的一个即可,即:
{ c o u n t [ 2 ] > = c o u n t [ 1 ] c o u n t [ 1 ] > = 1 o r { c o u n t [ 1 ] > = c o u n t [ 2 ] c o u n t [ 2 ] > = 1 \left\{ \begin{aligned} count[2]&>=count[1]\\ count[1]&>=1\\ \end{aligned} \right.\quad or \quad \left\{ \begin{aligned} count[1]&>=count[2]\\ count[2]&>=1\\ \end{aligned} \right. {count[2]count[1]>=count[1]>=1or{count[1]count[2]>=count[2]>=1
故只需要满足以下条件即可:
{ c o u n t [ 1 ] > = 1 c o u n t [ 2 ] > = 1 \left\{ \begin{aligned} count[1]&>=1\\ count[2]&>=1\\ \end{aligned} \right. {count[1]count[2]>=1>=1
再考虑0的数量不是0个的情况,在这种情况下,如果爱丽丝想要赢,那么应该抓住一个0的交换先后手的作用,如果单独考虑0个0的必胜条件的反面,应当满足以下条件:
c o u n t [ 1 ] = = 0 o r c o u n t [ 2 ] = = 0 count[1] == 0\quad or \quad count[2]==0 count[1]==0orcount[2]==0
但是这显然不太合理,因为没有考虑到换手时,已经拿掉的1或2的数量,因此,需要将换手前已拿掉的1或2的数量考虑进来。

  • 如果count[1]==0,考虑以下0、1、2的序列组合[1, 0, 3],在这种情况下,爱丽丝和鲍勃拿数字的顺序为2->0->2->2,虽然鲍勃拿走了所有的数字,但是鲍勃拿走的最后一个数字让所有的数字和成为了3的倍数,因此,鲍勃会输。如果2的数量小于3个,由于所有的2最终都会被拿走,但是总和却不是3的倍数,因此爱丽丝会输。综上,应当满足以下条件:
    c o u n t [ 2 ] > = 3 i f c o u n t [ 1 ] = = 0 count[2]>=3\quad if \quad count[1] == 0 count[2]>=3ifcount[1]==0

  • 同理,如果count[2]==0,应当满足:
    c o u n t [ 1 ] > = 3 i f c o u n t [ 2 ] = = 0 count[1]>=3\quad if \quad count[2] == 0 count[1]>=3ifcount[2]==0

再另外考虑count[1]count[2]不等于0的情况,这时候爱丽丝应当发挥好0的换手作用,即爱丽丝应当让鲍勃拿掉0之后,将全部的1或者2拿完,然后让鲍勃成为第一个拿走剩余1或2的人,这就又回到了刚刚count[1]==0时的分析,由于只剩下1或者2,那么这个数量应当大于等于3,这样爱丽丝才能够赢。

即满足以下条件:
∣ c o u n t [ 1 ] − c o u n t [ 2 ] ∣ > = 3 |count[1]-count[2]|>=3 count[1]count[2]>=3

class Solution {
    public boolean stoneGameIX(int[] stones) {
        int[] count = new int[3];
        for (int i = 0; i < stones.length; ++i){
            count[stones[i] % 3]++;
        }
        if (count[0] % 2 == 0) return count[1] >= 1 && count[2] >= 1;
        else
            if (count[1] == 0) return count[2] >= 3;
            if (count[2] == 0) return count[1] >= 3;
        return Math.abs(count[1] - count[2]) >= 3;
    }
}

617 合并二叉树

给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。

你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。

方法 同时遍历两个树

使用先序遍历的方法,每访问到一个新的节点,将其值相加,返回给结果树即可。

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) return null;
        if (root1 == null) return root2;
        if (root2 == null) return root1;
        TreeNode res = new TreeNode(root1.val + root2.val);
        res.left = mergeTrees(root1.left, root2.left);
        res.right = mergeTrees(root1.right, root2.right);
        return res;
    }
}

116 填充每一个节点的下一个右侧节点指针

给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:

struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}

填充它的每个next指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将next指针设置为NULL

初始状态下,所有next 指针都被设置为NULL

方法 连接儿子+连接邻居

遍历两遍整个二叉树,第一遍连接左右儿子,第二遍连接左右邻居。

class Solution {
    public Node connect(Node root) {
        if (root == null) return root;
        connectChildren(root);
        connectRightAndLeft(root.left, root.right);
        return root;
    }
    public Node connectChildren(Node root){
        if (root != null){
            if (root.left != null) root.left.next = root.right;
            connectChildren(root.left);
            connectChildren(root.right);
        }
        return root;
    }

    public void connectRightAndLeft(Node r1, Node r2){
        if (r1 == null) return;
        Node p1 = r1;
        Node p2 = r2;
        while (p1 != null){
            p1.next = p2;
            p1 = p1.right;
            p2 = p2.left; 
        }
        if (r1.left != null){
            connectRightAndLeft(r1.left, r1.right);
            connectRightAndLeft(r2.left, r2.right);
        }
    }
}
方法二 广度优先搜索

使用一个队列对整个树进行遍历,每次连接相邻的节点,特殊节点特殊处理即可。比如节点1、3、7为特殊节点,需要将其next指针指向null。

class Solution {
    public Node connect(Node root) {
        if (root == null) return null;
        int base = 2;
        Queue<Node> q = new LinkedList<>();
        q.offer(root);
        int index = 1;
        while (!q.isEmpty()){
            Node temp = q.poll();
            if (index == base - 1) base *= 2;
            else temp.next = q.peek();
            if (temp.left != null){
                q.offer(temp.left);
                q.offer(temp.right);
            }
            index++;
        }
        return root;
    }
}

第十天

1345 跳数游戏

给你一个整数数组 arr ,你一开始在数组的第一个元素处(下标为 0)。

每一步,你可以从下标 i 跳到下标:

i + 1 满足:i + 1 < arr.length
i - 1 满足:i - 1 >= 0
j满足:arr[i] == arr[j]i != j
请你返回到达数组最后一个元素的下标处所需的 最少操作次数 。

注意:任何时候你都不能跳到数组外面。

方法 广度优先搜索

使用BFS解决该问题,首先将开头节点放入待访问队列indexQueue里,同时使用另外一个队列stepQueue记录步数,函数的终止条件为:indexQueue的头部元素为数组的最后一个下标时,此时stepQueue队列中的头部元素的值就是到达尾部所需要的最少步数。

考虑到可能存在的重复元素重复访问的问题,因此需要一个额外的Map来记录所有相同元素的下标,另外还需要一个Visited集合用于保存已经访问的元素的位置,当且仅当某一元素不再Visited集合里面时,才能将其下一步能够达到的节点放入indexQueue队列中去。当遇到重复元素时,将重复元素的下标入队,放入带访问的indexQueue,并删去Map中该元素的key,即下标,这样重复的元素只能被访问一次。

例如:

假设与0号下标元素值相同的元素的下标是(1,2,3)

第一次访问0号元素,将0号元素下一个能够访问节点放入队列,同时弹出0号元素,队列中的元素为(1,2,3),与之对应的step队列的元素为(1,1,1),**即到达(1,2,3)中的任何一个节点,所需要的最少步数都是1,**这样就不需要再重复考虑与0号元素相同的所有元素的最小step值了,故只需要在map中,将对应值删去即可。

除此之外,依据题意,还有两个节点需要是前一个节点能够到达的,分别是当前节点的前一个节点和当前节点的后一个节点,对于这两个节点的处理,只需要注意是否已经访问过了即可。

class Solution {
        public int minJumps(int[] arr) {
        int arrayLength = arr.length;
        Map<Integer, ArrayList<Integer>> sameValue = new HashMap<>();
        for (int i = 0; i < arrayLength; ++i){
            sameValue.putIfAbsent(arr[i], new ArrayList<>());
            sameValue.get(arr[i]).add(i);
        }
        Set<Integer> Visited = new HashSet<>();
        Queue<Integer> indexQueue = new LinkedList<>();
        Queue<Integer> stepQueue = new LinkedList<>();
        indexQueue.offer(0);
        stepQueue.offer(0);
        while (!indexQueue.isEmpty() && !stepQueue.isEmpty()){
            int index = indexQueue.poll();
            if (index + 1 == arrayLength) return stepQueue.peek();
            int step = stepQueue.poll();
            Visited.add(index);
            if (sameValue.containsKey(arr[index])){
                ArrayList<Integer> tempArray = sameValue.get(arr[index]);
                for (int indexOfSameValue : tempArray){
                    if (!Visited.contains(indexOfSameValue)){
                        Visited.add(indexOfSameValue);
                        indexQueue.offer(indexOfSameValue);
                        stepQueue.offer(step + 1);
                    }
                }
            }
            sameValue.remove(arr[index]);
            if (isValidArrIndex(arrayLength, index + 1) && !Visited.contains(index + 1)){
                Visited.add(index + 1);
                indexQueue.offer(index + 1);
                stepQueue.offer(step + 1);
            }
            if (isValidArrIndex(arrayLength, index - 1) && !Visited.contains(index - 1)){
                Visited.add(index - 1);
                indexQueue.offer(index - 1);
                stepQueue.offer(step + 1);
            }
        }q
        return 0;
    }

    public static boolean isValidArrIndex(int length, int index){
        return index >= 0 && index < length;
    }
}

542 01矩阵

给定一个由01组成的矩阵mat ,请输出一个大小相同的矩阵,其中每一个格子是mat中对应位置元素到最近的 0的距离。

两个相邻元素间的距离为1

方法 广度优先搜索

开始时将所有的0节点入队,然后将所有的0节点标记为已访问,然后,队列中一个元素保存以下信息{x坐标,y坐标,深度}。而这个深度就是相应点{x,y}和最近0的距离。每深入一层,深度就加上1,也就是距离会加上1。

另:可以增加一个超级源点,让其连接所有0节点,初始状态深度为-1,这样一开始队列中就只有一个元素。

class Solution {
    public static boolean[][] isVisited;
    public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};
    public static int xLength;
    public static int yLength;
    public int[][] updateMatrix(int[][] mat) {
        Queue<int[]> queue = new LinkedList<>();
        xLength = mat.length;
        yLength = mat[0].length;
        int[][] res = new int[xLength][yLength];
        isVisited = new boolean[xLength][yLength];
        for (int i = 0; i < xLength; ++i){
            for (int j = 0; j < yLength; ++j){
                if (mat[i][j] == 0){
                    isVisited[i][j] = true;
                    res[i][j] = 0;
                    queue.offer(new int[]{i, j, 0});
                }
            }
        }
        while (!queue.isEmpty()){
            int[] temp = queue.poll();
            int x = temp[0];
            int y = temp[1];
            int step = temp[2];
            isVisited[x][y] = true;
            for (int i = 0; i < 4; ++i){
                int newX = x + dx[i];
                int newY = y + dy[i];
                if (newX >= 0 && newX < xLength
                && newY >= 0 && newY < yLength
                && !isVisited[newX][newY]){
                    res[newX][newY] = step + 1;
                    queue.offer(new int[]{newX, newY, step + 1});
                    isVisited[newX][newY] = true;
                }
            }
        }
        return res;
    }
}

994 腐烂的橘子

在给定的网格中,每个单元格可以有以下三个值之一:

  • 0代表空单元格;
  • 1 代表新鲜橘子;
  • 2 代表腐烂的橘子。

每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。

返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回-1

方法 广度优先搜索

思路和上题一致,首先将所有值为2的位置入队,然后开始广搜即可。

class Solution {
    public static boolean[][] isVisited;
    public static int xLength;
    public static int yLength;
    public static int[] dx = {0, 0, 1, -1};
    public static int[] dy = {1, -1, 0, 0};

    public int orangesRotting(int[][] grid) {
        xLength = grid.length;
        yLength = grid[0].length;
        isVisited = new boolean[xLength][yLength];
        Queue<int[]> queue = new LinkedList<>();
        for (int i = 0; i < xLength; ++i){
            for (int j = 0; j < yLength; ++j){
                if (grid[i][j] == 2){
                    isVisited[i][j] = true;
                    queue.offer(new int[]{i, j, 0});
                }
            }
        }
        int ans = 0;
        while (!queue.isEmpty()){
            int[] temp = queue.poll();
            int x = temp[0];
            int y = temp[1];
            int time = temp[2];
            isVisited[x][y] = true;
            for (int i = 0; i < 4; ++i){
                if (isValid(x + dx[i], y + dy[i]) && grid[x + dx[i]][y + dy[i]] == 1){
                    isVisited[x + dx[i]][y + dy[i]] = true;
                    grid[x + dx[i]][y + dy[i]] = 2;
                    queue.offer(new int[]{x + dx[i], y + dy[i], time + 1});
                    ans = Math.max(ans, time + 1);
                }
            }
        }

        for (int[] i : grid){
            for (int j : i){
                if (j == 1) return -1;
            }
        }
        return ans;
    }

    public static boolean isValid(int x, int y){
        return x < xLength && x >= 0
                && y < yLength && y >= 0
                && !isVisited[x][y];
    }
}

第十一天

1332 删除回文子序列

给你一个字符串s,它仅由字母 'a''b'组成。每一次删除操作都可以从s中删除一个回文 子序列

返回删除给定字符串中所有字符(字符串为空)的最小删除次数。

「子序列」定义:如果一个字符串可以通过删除原字符串某些字符而不改变原字符顺序得到,那么这个字符串就是原字符串的一个子序列。

「回文」定义:如果一个字符串向后和向前读是一致的,那么这个字符串就是一个回文。

方法

由于只有两种字符,最多需要两次,当序列是回文序列时,只需要一次。

class Solution {
    public int removePalindromeSub(String s) {
        int start = 0;
        int end = s.length() - 1;
        while (end > start){
            if (s.charAt(end) != s.charAt(start)) return 2;
            end--;
            start++;
        }
        return 1;
    }
}

21 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

方法一 直接合并

使用两个指针,分别指向两个链表的头部,比较哪个值小,则将这个更小的节点放在结果链表的下一个节点,直到两个链表遍历结束。最后特判一下哪个链表还有剩余即可。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode p1 = list1, p2 = list2;
        if (p1 == null && p2 == null) return null;
        ListNode res = new ListNode();
        ListNode ans = res;
        while (p1 != null && p2 != null){
            if (p1.val <= p2.val){
                res.next = p1;
                p1 = p1.next;
            }
            else{
                res.next = p2;
                p2 = p2.next;
            }
            res = res.next;
        }
        res.next = (p1 == null ? p2 : p1);
        return ans.next;
    }
}
方法二 递归

合并两个大链表可拆分成合并两个大链表先合并两个大链表的头节点,然后再合并剩余两个子链表。递推关系如下:
m e r g e l i s t ( l i s t 1 , l i s t 2 ) = { l i s t 1 , m e r g e l i s t ( l i s t 1. n e x t , l i s t 2 ) i f l i s t 1. v a l < = l i s t 2. v a l l i s t 2 , m e r g e l i s t ( l i s t 1 , l i s t 2. n e x t ) e l s e mergelist(list1,list2)=\left\{ \begin{aligned} &list1,mergelist(list1.next,list2) \quad if \quad list1.val <= list2.val\\ &list2,mergelist(list1,list2.next) \quad else\\ \end{aligned} \right. mergelist(list1,list2)={list1,mergelist(list1.next,list2)iflist1.val<=list2.vallist2,mergelist(list1,list2.next)else

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null) return list2;
        if (list2 == null) return list1;
        if (list1.val <= list2.val){
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }
        list2.next = mergeTwoLists(list1, list2.next);
        return list2;
    }
}

206 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

方法一

维护三个指针,prevcurrentnextNode。每次将currrent指向prev,遍历整个链表即可。

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) return null;
        ListNode prev, current, nextNode;
        prev = null;current = head;nextNode = head.next;
        while (nextNode != null){
            current.next = prev;
            prev = current;
            current = nextNode;
            nextNode = nextNode.next;
        }
        current.next = prev;
        return current;
    }
}
方法二 递归

RL(head)表示反转以head为起点的整个链表,可以把这个过程拆解成两步,第一步是将所有的节点压栈,第二步是各个节点弹出栈,同时调整next指向。

对于第一步,依据递归本身的特点,通过以下代码即可实现:

reverseList(head.next);

在到达链表尾部时,压栈过程终止,因此回溯的条件为:

if (head.next == null) return head;

head.nextnull时,此时就到达了链表的尾部。

当处理完整个链表之后,应当返回原始链表的最后一个节点作为结果的头节点。故需要保存这个最后一个节点用于结果的返回。

ListNode res = reverseList(head.next);
...
...//中间处理过程
return res;    

中间过程的处理,考虑能够使用的变量就只有一个head,要将链表反转,则head的下一个节点的next指针应当指向head,如果仅仅这样操作,会出现,即直接将head.next.next指向head,**会出现以下情况:head的下一个节点->head,head->head的下一个节点。**因此需要打断第二个链,通过令head.next=null即可。

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) return null;//特判
        if (head.next == null) return head;
        ListNode res = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return res;
    }
}

第十二天

2034 股票价格波动

给你一支股票价格的数据流。数据流中每一条记录包含一个 时间戳 和该时间点股票对应的 价格

不巧的是,由于股票市场内在的波动性,股票价格记录可能不是按时间顺序到来的。某些情况下,有的记录可能是错的。如果两个有相同时间戳的记录出现在数据流中,前一条记录视为错误记录,后出现的记录 更正 前一条错误的记录。

请你设计一个算法,实现:

更新 股票在某一时间戳的股票价格,如果有之前同一时间戳的价格,这一操作将 更正 之前的错误价格。
找到当前记录里 最新股票价格最新股票价格 定义为时间戳最晚的股票价格。
找到当前记录里股票的 最高价格
找到当前记录里股票的 最低价格
请你实现StockPrice类:

StockPrice()初始化对象,当前无股票价格记录。
void update(int timestamp, int price)在时间点timestamp更新股票价格为price
int current()返回股票 最新价格
int maximum()返回股票 最高价格
int minimum()返回股票 最低价格

方法

维护两个优先队列,一个优先队列用于保存最大值,一个优先队列用于保存最小值。另外再维护一个哈希表,哈希表用于保存所有时间段的数据和维持数据的最新状态。一个timeIndex用于记录最新的时间节点。

  • update:将数据分别放入哈希表、两个优先队列。维持timeIndex值为最大值。
  • current:从stockMap中返回timeIndex对应的价格。
  • maximum:不断查看最大优先队列的队头,比对队头的时间戳中在stockMap中的值,如果不相等说明数据过时,弹出,直到找到两个price相等时返回。
  • minium:逻辑和maxium一致。

注意:不能在update中更新两个优先队列,因为不一致的值可能在两个优先队列中的其他位置,而不在队头。

class StockPrice {
    private Queue<int[]> maxQueue;
    private Queue<int[]> minQueue;
    private int timeIndex;
    private Map<Integer, Integer> stockMap;
    public StockPrice() {
        minQueue = new PriorityQueue<>(Comparator.comparingInt(x->x[1]));
        maxQueue = new PriorityQueue<>((x, y)->y[1] - x[1]);
        timeIndex = 0;
        stockMap = new HashMap<>();
    }

    public void update(int timestamp, int price) {
        stockMap.put(timestamp, price);
        timeIndex = Math.max(timeIndex, timestamp);
        maxQueue.offer(new int[]{timestamp, price});
        minQueue.offer(new int[]{timestamp, price});
    }

    public int current() {
        return stockMap.get(timeIndex);
    }

    public int maximum() {
        while (true){
            int[] temp = maxQueue.peek();
            int price = stockMap.get(temp[0]);
            if (price != temp[1]) maxQueue.poll();
            else
                return price;
        }
    }

    public int minimum() {
        while (true){
            int[] temp = minQueue.peek();
            int price = stockMap.get(temp[0]);
            if (price != temp[1]) minQueue.poll();
            else
                return price;
        }
    }
}

77 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

方法 回溯

i个位置的值存在两种情况,取或不取。当被取的数字的长度达到k时,则将结果存到结果Array中。

回溯的基本思想是,取了第i个位置的数字,进行更深入一步的递归,当递归返回时,需要还原现场,也就是进行与递归之前的所做的操作的相反操作,让状态回到调用递归之前的状态

维护一个depth变量,该变量用于标记递归的深度,当到达一定深度时,通过相应的条件判断来返回。

class Solution {
    public static List<Integer> numToBeChosen;
    public static List<List<Integer>> res;
    public static int N;
    public List<List<Integer>> combine(int n, int k) {
        N = n;
        numToBeChosen = new ArrayList<>();
        res = new ArrayList<>();
        deepFirstSearch(1, k);
        return res;
    }

    public void deepFirstSearch(int currentNum, int k){
        //当长度到达了所需的长度时,将结果保存,同时返回
        if (numToBeChosen.size() == k){
            res.add(new ArrayList<>(numToBeChosen));
            return;
        }
        //当递归深度到底时,也需要返回
        if (currentNum == N + 1){
            return;	
        }
        //取第current个数字的情况
        numToBeChosen.add(currentNum);
        //取了该数字,进行更深入一步的递归
        deepFirstSearch(currentNum + 1, k);
        //递归返回之后 还原现场,回到没有取这个数字之前的状态
        numToBeChosen.remove(numToBeChosen.size() - 1);
        //在不取当前数字的情况下,进行更深一步递归
        deepFirstSearch(currentNum + 1, k);
    }
}

46 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

方法 回溯

全排列的过程可以看作是将n个数字放入n个格子里面的问题,一共有几种放置的方法。当一个格子已经放了数i,那么别的格子中就不能放i

因此,需要引入一个isUsed数组,该数组用于标记第i个位置的数字是否已经被用过了,全排列的最终目的是将所有格子放满不同的数字,因此,当某个数被标记成已经被访问过了,那么就需要进入更深一步的递归,直到将所有格子填满。当所有格子都被填满之后,需要将结果保存起来,然后返回。

当递归返回之后,需要将现场还原,进行其他情况的遍历。

class Solution {
    public static boolean[] isUsed;//用于标记下标为i的数字是否已经被取用过了
    public static List<List<Integer>> res;
    public static List<Integer> numsToBeChosen;//将要被取用的数字会保存在这里面

    public List<List<Integer>> permute(int[] nums) {
        isUsed = new boolean[nums.length + 1];
        numsToBeChosen = new ArrayList<>();
        res = new ArrayList<>();
        backTrace(0, nums);
        return res;
    }
    
    public static void backTrace(int depth, int[] nums){
        if (depth == nums.length){//如果已经填满了所有的格子 那么将结果保存并返回
            res.add(new ArrayList<>(numsToBeChosen));
            return;
        }
        for (int i = 0; i < nums.length; ++i) {
            //遍历整个数组 找到没有用过的数字组成最终的结果
            if (!isUsed[i]) {
                isUsed[i] = true;
                numsToBeChosen.add(nums[i]);
                backTrace(depth + 1, nums);
                //递归返回之后需要还原现场
                isUsed[i] = false;//将刚刚被用过的数字重新标记为可用的
                numsToBeChosen.remove(numsToBeChosen.size() - 1);//和add方法对应,既然之前的状态是没有add的,那么需要将add进来的数字移除掉
            }
        }
    }
}

784 字母大小写全排列

给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。

方法 回溯

与之前的组合题类似,只需要考虑第i个字母取或者不取的所有情况即可。注意递归完成之后需要还原现场。

class Solution {

    public static boolean[] isUpperCase;
    public static List<boolean[]> midRes;
    public static List<Character> charArrayToBeChosen;

    public List<String> letterCasePermutation(String s) {
        midRes = new ArrayList<>();
        charArrayToBeChosen = new ArrayList<>();
        List<String> res = new ArrayList<>();
        int n = 0;
        for (int i = 0; i < s.length(); ++i){
            if (Character.isLetter(s.charAt(i))) n++;
        }
        isUpperCase = new boolean[n];//建立和字符串中所有字母数量等长的布尔数组
        backTrace(0, isUpperCase);
        for (int i = 0; i < midRes.size(); ++i){
            StringBuilder sb = new StringBuilder();
            boolean[] temp = midRes.get(i);
            int index = 0;
            for (int j = 0; j < s.length(); ++j){
                char t = s.charAt(j);
                if (Character.isLetter(t)){
                    if (temp[index])
                        sb.append(Character.toUpperCase(t));
                    else
                        sb.append(Character.toLowerCase(t));
                    index++;
                }
                else
                    sb.append(t);
            }
            res.add(sb.toString());
        }
        return res;
    }

    public void backTrace(int depth, boolean[] isUpperCase){
        if (depth == isUpperCase.length){ //已经递归到最底层 保存结果并返回
            midRes.add(isUpperCase.clone());
            return;
        }
        //第i个字母为小写的情况
        backTrace(depth + 1, isUpperCase);
        //第i个字母为大写的情况
        isUpperCase[depth] = !isUpperCase[depth];
        backTrace(depth + 1, isUpperCase);
    }
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值