算法笔记-力扣难题

算法笔记

求给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

 List<Integer> ans = new ArrayList<>();
        // 初始化一个数组来统计字符串 p 中每个字符的出现次数
        int[] cnt = new int[26];
        for(int i = 0; i < p.length(); i++){
            cnt[p.charAt(i) - 'a']++;
        }
        // l 和 r 分别表示滑动窗口的左右边界
        int l = 0;
        for(int r = 0; r < s.length(); r++){
            // 更新当前窗口中字符的计数数组
            cnt[s.charAt(r) - 'a']--;
            // 从左侧收缩窗口,直到当前字符的计数在限定范围内
            while(cnt[s.charAt(r) - 'a'] < 0){
                cnt[s.charAt(l) - 'a']++;
                l++;
            }
            // 检查当前窗口大小是否等于字符串 p 的大小
            if(r - l + 1 == p.length()){
                ans.add(l);
            }
        }
        return ans;
public void moveZeroes(int[] nums){
    int index = 0;
    for(int i= 0;i<nums.length;i++){
        if(nums[i]!=0){
            nums[index]=nums[i];
            index++;
        }
    }
    for(int i = index ;i<nums.length;i++){
        nums[i]=0;
    }
}

创建一个滑动窗口不断向右移动,然后维持窗口的大小比较数组元素的值,通过删除和和插入维持窗口大小然后比较里面的值

class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length == 0) {
            return new int[0];
        }

        Deque<Integer> deque = new ArrayDeque<>();
        int n = nums.length;
        int[] result = new int[n - k + 1];

        for (int i = 0; i < n; i++) {
            while (!deque.isEmpty() && deque.peek() < i - k + 1) {
                deque.poll();
            }

            while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
                deque.pollLast();
            }

            deque.offer(i);

            if(i >= k - 1) {
                result[i - k + 1] = nums[deque.peek()];
            }
        }

        return result;
    }
}


看示例 1,s 的子串 BANC 中每个字母的出现次数,都大于等于 t=ABC 中每个字母的出现次数,这就叫涵盖。

滑动窗口怎么滑
原理和 209 题一样,按照视频中的做法,我们枚举 s 子串的右端点 right(子串最后一个字母的下标),如果子串涵盖 t,就不断移动左端点 left 直到不涵盖为止。在移动过程中更新最短子串的左右端点。

具体来说:

初始化 ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中 m 是 s 的长度。
用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。
初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s 子串中每个字母的出现次数。
遍历 s,设当前枚举的子串右端点为 right,把 s[right] 的出现次数加一。
遍历 cntS 中的每个字母及其出现次数,如果出现次数都大于等于 cntT 中的字母出现次数:
如果 right−left<ansRight−ansLeft,说明我们找到了更短的子串,更新 ansLeft=left, ansRight=right。
把 s[left] 的出现次数减一。
左端点右移,即 left 加一。
重复上述三步,直到 cntS 有字母的出现次数小于 cntT 中该字母的出现次数为止。
最后,如果 ansLeft<0,说明没有找到符合要求的子串,返回空字符串,否则返回下标 ansLeft 到下标 ansRight 之间的子串。
由于本题大写字母和小写字母都有,为了方便,代码实现时可以直接创建大小为 128 的数组,保证所有 ASCII 字符都可以统计。

class Solution {
    public String minWindow(String S, String t) {
        char[] s = S.toCharArray();
        int m = s.length;
        int ansLeft = -1;
        int ansRight = m;
        int left = 0;
        int[] cntS = new int[128]; // s 子串字母的出现次数
        int[] cntT = new int[128]; // t 中字母的出现次数
        for (char c : t.toCharArray()) {
            cntT[c]++;
        }
        for (int right = 0; right < m; right++) { // 移动子串右端点
            cntS[s[right]]++; // 右端点字母移入子串
            while (isCovered(cntS, cntT)) { // 涵盖
                if (right - left < ansRight - ansLeft) { // 找到更短的子串
                    ansLeft = left; // 记录此时的左右端点
                    ansRight = right;
                }
                cntS[s[left++]]--; // 左端点字母移出子串
            }
        }
        return ansLeft < 0 ? "" : S.substring(ansLeft, ansRight + 1);
    }
利用两个for循环来判断是否涵盖 也就是s的滑动窗口里面的元素是否涵盖t里面的字符串
    private boolean isCovered(int[] cntS, int[] cntT) {
        for (int i = 'A'; i <= 'Z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        for (int i = 'a'; i <= 'z'; i++) {
            if (cntS[i] < cntT[i]) {
                return false;
            }
        }
        return true;
    }
}
解题思路就是把输入的字符串传换成char类型的字符串 用来作为滑动窗口的基础然后定义了左右端点来控制滑动窗口的大小 通过是否涵盖来移动左右端点 找到更短的字串 然后通过一个三目运算符来确定返回的字串

复杂度分析
时间复杂度:O(∣Σ∣m+n),其中 m 为 s 的长度,n 为 t 的长度,∣Σ∣ 为字符集合的大小,本题字符均为英文字母,所以 ∣Σ∣=52。注意 left 只会增加不会减少,left 每增加一次,我们就花费 O(∣Σ∣) 的时间。因为 left 至多增加 m 次,所以二重循环的时间复杂度为 O(∣Σ∣m),再算上统计 t 字母出现次数的时间 O(n),总的时间复杂度为 O(∣Σ∣m+n)。
空间复杂度:O(∣Σ∣)。如果创建了大小为 128 的数组,则 ∣Σ∣=128。

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组

是数组中的一个连续部分

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
class Solution {
    public int maxSubArray(int[] nums) {
        int res = nums[0];
        for(int i = 1; i < nums.length; i++) {
            nums[i] += Math.max(nums[i - 1], 0);
            res = Math.max(res, nums[i]);
        }
        return res;
    }
}
//利用一个for循环来得到最大值 通过max方法求res和num[i]来区分正数和负数 简直不要太聪明 通过简单的函数取最大值来确定最后的返回值 然后就是res=nums[0] 因为要和初始值比较 然后也可以写成nums[i+1] += Math.max(nums[i],0) 也就是第0个值和零比较 第一个值和第0个值比较     

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

解答

class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
        List<int[]> ans = new ArrayList<>();
        ans.add(intervals[0]);
        for (int i = 1; i < intervals.length; ++i) {
            int s = intervals[i][0], e = intervals[i][1];
            if (ans.get(ans.size() - 1)[1] < s) {
                ans.add(intervals[i]);
            } else {
                ans.get(ans.size() - 1)[1] = Math.max(ans.get(ans.size() - 1)[1], e);
            }
        }
        return ans.toArray(new int[ans.size()][]);
    }
}
//首先对于输入的数组排序 因为输入的数组可能是乱序的 排序原理就是根据二维数组的第一个值来进行排序 然后得到排序后的数组 定义一个list空数组来存放这个二维数组 注意此时存放到list数组后二维数组就变成一维数组了 然后根据list数组肯定为空数组放入第一个值 然后从第二个值开始遍历此时遍历arr.length是数组第一个值的大小 arr[0].length是第二个值的大小 然后比较第一个数组的左边值和第0个数组的右边值谁大谁小 如果第一个数组左边的值小于第零个数组的右边的值则选出第一个数组右边的值和第零个数组的左边的值合并成一个新的数组 当然这里默认第一个数组右边的值比第零个数组右边的值大 这个是需要比较出来的 反之正常加入list数组然后最后把一维数组转换成二维数组

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

示例 1:

输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
class Solution {
    public void rotate(int[] nums, int k) {

        k = k % nums.length; // 处理 k 大于数组长度的情况
        
        int[] result = new int[nums.length];
        
        // 将后面的 k 个元素复制到结果数组的前面
        System.arraycopy(nums, nums.length - k, result, 0, k);
        
        // 将前面的 nums.length - k 个元素复制到结果数组后面
        System.arraycopy(nums, 0, result, k, nums.length - k);
        
        // 将旋转后的结果复制回原始数组 nums
        System.arraycopy(result, 0, nums, 0, nums.length);
    }
}
//5个参数,
第一个参数是要被复制的数组
第二个参数是被复制的数字开始复制的下标
第三个参数是目标数组,也就是要把数据放进来的数组
第四个参数是从目标数据第几个下标开始放入数据
第五个参数表示从被复制的数组中拿几个数值放到目标数组中
比如:
数组1int[] arr = { 1, 2, 3, 4, 5 };
数组2int[] arr2 = { 5, 6,7, 8, 9 };
运行:System.arraycopy(arr, 1, arr2, 0, 3);
得到:
int[] arr2 = { 2, 3, 4, 8, 9 };
class Solution {
    public void rotate(int[] nums, int k) {
        k = k % nums.length; // 处理 k 大于数组长度的情况
        int[] arr = new int[nums.length];
        int temp = 0;
        for (int i = nums.length - k; i < nums.length; i++) {
            arr[temp++] = nums[i];
        }
        for (int j = 0; j < nums.length - k; j++) {
            arr[temp++] = nums[j];
        }
        for (int i = 0; i < nums.length; i++) {
            nums[i] = arr[i];
        }
    }
}

题目是给一个数组除去自身另外元素求积

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int len = nums.length;
        if (len == 0) return new int[0];
        int[] ans = new int[len];
        ans[0] = 1;
        int tmp = 1;
        for (int i = 1; i < len; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }
        for (int i = len - 2; i >= 0; i--) {
            tmp *= nums[i + 1];
            ans[i] *= tmp;
        }
        return ans;
    }
}

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        int mul = 1;    // 计算[0, i)的前缀积,初始为1
        for(int i = 0; i < n; i++){
            res[i] = mul;  // res[i]存放[0, i)的前缀积
            mul *= nums[i];
        }
        mul = 1;    // 计算(i, n)的后缀积,初始为1
        for(int i = n - 1; i >= 0; i--){
            res[i] *= mul;      // 将(i, n)的后缀积和前缀积相乘
            mul *= nums[i];
        }
        return res;
    }
}

class Solution {
    public int[] productExceptSelf(int[] nums) {
        int answer[]= new int[nums.length];
        int temp =0;
        for(int i = 0; i < nums.length;i++){
            int cz = 1;
            for(int j = 0;j <nums.length;j++){
                if(i!=j){
                  cz= cz*nums[j];
                  answer[temp]=cz;
                }
            }
            temp++;
        }
        return answer;
    }
}
//这段代码没有问题 也是自己完成的很有成就感 但是题目要求O(n)的时间复杂度 这题显然是要求一个for循环来求得最后的结果 我还是想不到怎么算才能求出正确的结果

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

给一个未排序的整数数组 首先肯定是要先排序 然后找出没有出现的最小的正整数 正整数而非负数 然后题目要求时间复杂度是O(1) 所以不能使用嵌套for循环 然后是没有出现的整数 还要是最小的

思路就是排序 然后找到所有的正整数 然后定义一个从1开始的循环数组 数组长度是nums.length+1 然后用if比较这两个数组的值不同的赋值给一个变量然后返回这个变量

示例 1

输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i] - 1]
                int temp = nums[nums[i] - 1];
                nums[nums[i] - 1] = nums[i];
                nums[i] = temp;
            }
        }

        // 再次遍历数组,找到第一个索引 i,使得 nums[i] != i + 1
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        // 如果所有索引都被放置了对应的数字,则缺失的数字是 n + 1
        return n + 1;
    }
}

自己完成的代码也是正确然后超时了

判断数组长度是否为1的情况

取真整数部分

创建list集合去重

比较两个数组元素的值

为了解决这个问题,并且满足时间复杂度为 O(n) 以及只使用常数级别额外空间的要求,我们可以使用原地哈希的方法。思路是将所有小于等于数组长度 n 的正数放到它们对应的索引位置上,即数字 x 放到索引 x-1 的位置上。然后,我们遍历数组,找到第一个位置 i,使得 nums[i] != i+1,那么这个位置对应的 i+1 就是最小的缺失正整数。

  • 初始化:首先,我们遍历数组,将所有非正数和大于数组长度的数字移动到数组的末尾。

  • 原地排序:然后,我们通过一个 while 循环将数组中的每个正数放在它应该在的位置。这个循环会一直执行,直到数组中的每个正数都放在了正确的位置,或者数组中的某个元素不再满足条件(即不是正数或大于数组长度)。

  • 查找缺失的数字:在所有数字都放置好之后,我们再次遍历数组,找到第一个索引 i,使得 nums[i] 不等于 i + 1。这个索引加1就是缺失的最小正数。


class Solution {
    public int firstMissingPositive(int[] nums) {
        if (nums.length == 1) {
            if (nums[0] == 0 || nums[0] == 1) {
                return nums[0] + 1;
            } else {
                return 1;
            }
        }
        Arrays.sort(nums);
        int temp = 0;
        int[] num2 = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] > 0) {
                num2[temp] = nums[i];
                temp++;
            }
        }
        List<Integer> list = new ArrayList();  
        //遍历数组往集合里存元素  
        for(int i=0;i<num2.length;i++){  
            //如果集合里面没有相同的元素才往里存  
            if(!list.contains(num2[i])){  
                list.add(num2[i]);  
            }  
        }
        int arr[] = new int[list.size()];
        arr[0]=1;
        for (int i = 1; i<list.size(); i++) {
            arr[i] = i + 1;
        }

        int res = 0;
        for (int i = 0; i <list.size(); i++) {
            if (arr[i] !=list.get(i)) {
                res = arr[i];
                break;
            }
            if(arr[list.size()-1] == list.get(list.size()-1)){
                res=arr[list.size()-1]+1;
            }
        }

        return res;
    }
}

给定一个 *m* x *n* 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法**。**

一个算法有时候会不正当地被称为原地算法,只因为它用它的输出资料会覆盖掉它的输入资料。事实上这并不足够(在快速排序案例中所展示的)或是它所必须的;输出资料的空间可能是固定的,或如果以输出为串流资料而言,也甚至是可能无法被数清楚的。另一方面来看,有时候要决定一个算法是不是原地,而数它的输出空间可能是比较可行的,像是底下的第一个的 reverse 范例;如此使得它更难去严格地定义原地算法。在理论上的应用像是log-space reduction,更是典型的总是忽略输出的空间(在这些状况,更重要的是输出为仅能写入)。

class Solution {  
    public void setZeroes(int[][] matrix) {  
        int rows = matrix.length;  
        int cols = matrix[0].length;  
  
        // 使用第一行作为列标记,第一列作为行标记  
        boolean firstRowZero = false;  
        for (int i = 0; i < rows; i++) {  
            if (matrix[i][0] == 0) {  
                firstRowZero = true;  
            }  
            for (int j = 1; j < cols; j++) {  
                if (matrix[i][j] == 0) {  
                    matrix[i][0] = 0; // 标记当前行为0  
                    matrix[0][j] = 0; // 标记当前列为0  
                }  
            }  
        }  
  
        // 根据标记,将对应的行和列置为0  
        // 注意:要从后往前遍历,避免修改当前元素时影响到后续元素的判断  
        for (int i = rows - 1; i >= 0; i--) {  
            for (int j = cols - 1; j > 0; j--) {  
                if (matrix[i][0] == 0 || matrix[0][j] == 0) {  
                    matrix[i][j] = 0;  
                }  
            }  
            // 如果第一行被标记为0,则整行都需要置为0  
            if (firstRowZero) {  
                matrix[i][0] = 0;  
            }  
        }  
    }  
}

我自己写的有问题 遍历会影响下面的数组 应该记录下来i和j的值然后拿出来在遍历赋值

class Solution {
    public void setZeroes(int[][] matrix) {
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (matrix[i][j] == 0) {
                    for (int k = 0; k < matrix[0].length; k++) {
                        matrix[i][k] = 0;
                    }
                    for (int l = 0; l < matrix.length; l++) {
                        matrix[l][j] = 0;
                    }
                }
            }
        }
    }
}

下面是改正后的代码

未必要把行列一块存到一个数组中
    可以存在两个数组中这样就可以方便遍历
  
class Solution {  
    public void setZeroes(int[][] matrix) {  
        List<Integer> zeroRows = new ArrayList<>();  
        List<Integer> zeroCols = new ArrayList<>();  
  
        // 找出所有需要置为0的行和列  
        for (int i = 0; i < matrix.length; i++) {  
            for (int j = 0; j < matrix[0].length; j++) {  
                if (matrix[i][j] == 0) {  
                    if (!zeroRows.contains(i)) {  
                        zeroRows.add(i);  
                    }  
                    if (!zeroCols.contains(j)) {  
                        zeroCols.add(j);  
                    }  
                }  
            }  
        }  
  
        // 根据标记将对应的行和列置为0  
        for (int row : zeroRows) {  
            for (int j = 0; j < matrix[0].length; j++) {  
                matrix[row][j] = 0;  
            }  
        }  
  
        for (int col : zeroCols) {  
            for (int i = 0; i < matrix.length; i++) {  
                matrix[i][col] = 0;  
            }  
        }  
    }  
}

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        if (matrix.length == 0)
            return new ArrayList<Integer>();
        int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
        Integer[] res = new Integer[(r + 1) * (b + 1)];
        while (true) {
            for (int i = l; i <= r; i++) res[x++] = matrix[t][i]; // left to right
            if (++t > b) break;
            for (int i = t; i <= b; i++) res[x++] = matrix[i][r]; // top to bottom
            if (l > --r) break;
            for (int i = r; i >= l; i--) res[x++] = matrix[b][i]; // right to left
            if (t > --b) break;
            for (int i = b; i >= t; i--) res[x++] = matrix[i][l]; // bottom to top
            if (++l > r) break;
        }
        return Arrays.asList(res);
    }
}
//这个思路没得说 一眼就能看懂 当长度为零返回空数组 然后定义了上下左右四个边 还有数组元素递增的x 定义一个新的数组用来存放二维数组遍历的结果 然后通过一个while循环来遍历整个二维数组 通过类似于四条边不断的往内部收缩来遍历 只需要注意break是跳出当前for循环而不是整个while循环 这道题就迎刃而解了非常简单 我的思路是类似的但是没有想到while一直循环中嵌套for循环 很厉害 也是学会了 下次遇到这种就知道怎么做了 起码不会没有思路是这个道理没错了 然后需要知道的是不知道循环次数的需要使用while循环 而不是一遇到遍历就想着for循环 if语句和switch case的区别 在哪中情况下去使用脑子里面要有这个概念 分支结构 循环 判断 

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在** 原地** 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

public class Solution {
    public void rotate(int[][] matrix) {
        int n = matrix.length;

        // 先转置矩阵
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
        }

        // 再按中线翻转(顺时针旋转90度)
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n / 2; j++) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[i][n - 1 - j];
                matrix[i][n - 1 - j] = temp;
            }
        }
    }
}
//利用了一个临时变量完成数组对应元素的调换 也就是除了左上到由下这条线上的数组元素没有变之外其他元素都按照这条线调换了位置,这是顺时针旋转了九十°如果是逆时针旋转九十°的话就是取x轴的中线 反之顺时针就是取y轴的中线转置,如果是旋转180°的话也就是上面的元素到下面去下面的元素到上面去 意思就是上面的操作进行两次即可

编写一个高效的算法来搜索 *m* x *n* 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。
class Solution {
    public boolean searchMatrix(int[][] matrix, int target) {
        int i = matrix.length - 1, j = 0;
        while(i >= 0 && j < matrix[0].length)
        {
            if(matrix[i][j] > target) i--;
            else if(matrix[i][j] < target) j++;
            else return true;
        }
        return false;
    }
}
//这题显然是通过嵌套循环很容易就能做出来 但是题目中所给的那个意思就是不让用嵌套for循环来做 毕竟数组从上到下从左到右都是有顺序的 所以要用上面这个思路来写也可以让i等于左上角的元素然后判断递增嘛 这题很简单 试了一下发现不能递增要一个递减一个递增 不然负数的当两个条件都不满足就会返回true但是实际是flase

考虑构建两个节点指针 A , B 分别指向两链表头节点 headA , headB ,做如下操作:

指针 A 先遍历完链表 headA ,再开始遍历链表 headB ,当走到 node 时,共走步数为:
a+(b−c)
指针 B 先遍历完链表 headB ,再开始遍历链表 headA ,当走到 node 时,共走步数为:
b+(a−c)
如下式所示,此时指针 A , B 重合,并有两种情况:

a+(b−c)=b+(a−c)
若两链表 有 公共尾部 (即 c>0 ) :指针 A , B 同时指向「第一个公共节点」node 。
若两链表 无 公共尾部 (即 c=0 ) :指针 A , B 同时指向 null 。

作者:Krahets
链接:https://leetcode.cn/problems/intersection-of-two-linked-lists/solutions/12624/intersection-of-two-linked-lists-shuang-zhi-zhen-l/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    if (headA == null || headB == null) return null;
    ListNode pA = headA, pB = headB;
    while (pA != pB) {
        pA = pA == null ? headB : pA.next;
        pB = pB == null ? headA : pB.next;
    }
    return pB;
}

}
确实牛逼,核心在于 A 链表 + B 链表,与 B 链表 + A 链表必然是相同的长度,所以一定会同时遍历到结尾。而这样的两个叠加链表同时遍历到有相同节点的时候,一定一边是 A 链表一边是 B 链表(因为题目保证没有环)。而相交节点开始到结尾的节点都相同,所以第一个相同的节点就是 A 链表和 B 链表的交点。
    
    
    

class Solution {
    public boolean isPalindrome(ListNode head) {
        List<Integer> list = new ArrayList<>();
        while (head != null) {
            list.add(head.val);
            head = head.next;
        }
        int size = list.size();
        for (int i = 0; i < size / 2; i++) {
            if ((int) list.get(i) != list.get(size - 1 - i)) {
                return false;
            }
        }
        return true;
    }
}
这题属于回文链表 主要是节点数组没有求长度的方法除非从头到尾遍历整个数组 然后才能求个数组长度 像上题这样是把整个节点数组放在一个list数组中然后通过size()方法来获取节点数组的长度 最后一个时间复杂度为n空间复杂度为1的方法

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

如果链表存在环,则双指针一定会相遇。因为每走 1 轮,fast 与 slow 的间距 +1,fast 一定会追上 slow

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while (true) {
            if (fast == null || fast.next == null) return null;
            fast = fast.next.next;
            slow = slow.next;
            if (fast == slow) break;
        }
        fast = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}
//解题思路:
这类链表题目一般都是使用双指针法解决的,例如寻找距离尾部第 K 个节点、寻找环入口、寻找公共尾部入口等。

在本题的求解过程中,双指针会产生两次“相遇”。

双指针的第一次相遇:
设两指针 fast,slow 指向链表头部 head 。
令 fast 每轮走 2 步,slow 每轮走 1 步。
执行以上两步后,可能出现两种结果:

第一种结果: fast 指针走过链表末端,说明链表无环,此时直接返回 null。

如果链表存在环,则双指针一定会相遇。因为每走 1 轮,fast 与 slow 的间距 +1,fast 一定会追上 slow 。

第二种结果: 当fast == slow时, 两指针在环中第一次相遇。下面分析此时 fast 与 slow 走过的步数关系:

设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点), 链表环 有 b 个节点(这里需要注意,a 和 b 是未知数,例如图解上链表 a=4 , b=5);设两指针分别走了 f,s 步,则有:

fast 走的步数是 slow 步数的 2 倍,即 f=2s;(解析: fast 每轮走 2 步)
fast 比 slow 多走了 n 个环的长度,即 f=s+nb;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )。
将以上两式相减得到 f=2nb,s=nb,即 fast 和 slow 指针分别走了 2n,n 个环的周长。

接下来该怎么做呢?

如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:k=a+nb ,即先走 a 步到入口节点,之后每绕 1 圈环( b 步)都会再次到入口节点。而目前 slow 指针走了 nb 步。因此,我们只要想办法让 slow 再走 a 步停下来,就可以到环的入口。

但是我们不知道 a 的值,该怎么办?依然是使用双指针法。考虑构建一个指针,此指针需要有以下性质:此指针和 slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头节点head。

双指针第二次相遇:
令 fast 重新指向链表头部节点。此时 f=0,s=nb 。
slow 和 fast 同时每轮向前走 1 步。
当 fast 指针走到 f=a 步时,slow 指针走到 s=a+nb 步。此时两指针重合,并同时指向链表环入口,返回 slow 指向的节点即可。

作者:Krahets
链接:https://leetcode.cn/problems/linked-list-cycle-ii/solutions/12616/linked-list-cycle-ii-kuai-man-zhi-zhen-shuang-zhi-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dum = new ListNode(0), cur = dum;
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                cur.next = list1;
                list1 = list1.next;
            }
            else {
                cur.next = list2;
                list2 = list2.next;
            }
            cur = cur.next;
        }
        cur.next = list1 != null ? list1 : list2;
        return dum.next;
    }
}
//如果不是升序只需要给链表元素排序即可 思路就是比较两个链表各元素的大小 然后把比较的元素放到新的链表中时间复杂度是n 

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

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {    
        ListNode pre = new ListNode(0);
        pre.next = head;
        ListNode start = pre, end = pre;
        while(n != 0) {
            start = start.next;
            n--;
        }
        while(start.next != null) {
            start = start.next;
            end = end.next;
        }
        end.next = end.next.next;
        return pre.next;
    }
}

//这题思路巧妙在于它是正向遍历链表但是依旧实现了倒数第n个节点 解题思路就是start先走n然后走完全部路程 end等start走完n开始和start一起走它走的距离是链表长度减去n的长度也就是倒数第几个位置

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。


class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode pre = new ListNode(0);
        ListNode cur = pre;
        int carry = 0;
        while(l1 != null || l2 != null) {
            int x = l1 == null ? 0 : l1.val;
            int y = l2 == null ? 0 : l2.val;
            int sum = x + y + carry;
            
            carry = sum / 10;
            sum = sum % 10;
            cur.next = new ListNode(sum);

            cur = cur.next;
            if(l1 != null)
                l1 = l1.next;
            if(l2 != null)
                l2 = l2.next;
        }
        if(carry == 1) {
            cur.next = new ListNode(carry);
        }
        return pre.next;
    }
}

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

不需要交换位置只需要改变指针的指向就可以了 这是个很大的误区

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        if (head == null || head.next == null){
            return head;
        }
        //定义一个假的节点。
        ListNode dummy=new ListNode(0);
        //假节点的next指向head。
        // dummy->1->2->3->4->5
        dummy.next=head;
        //初始化pre和end都指向dummy。pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
        ListNode pre=dummy;
        ListNode end=dummy;

        while(end.next!=null){
            //循环k次,找到需要翻转的链表的结尾,这里每次循环要判断end是否等于空,因为如果为空,end.next会报空指针异常。
            //dummy->1->2->3->4->5 若k为2,循环2次,end指向2
            for(int i=0;i<k&&end != null;i++){
                end=end.next;
            }
            //如果end==null,即需要翻转的链表的节点数小于k,不执行翻转。
            if(end==null){
                break;
            }
            //先记录下end.next,方便后面链接链表
            ListNode next=end.next;
            //然后断开链表
            end.next=null;
            //记录下要翻转链表的头节点
            ListNode start=pre.next;
            //翻转链表,pre.next指向翻转后的链表。1->2 变成2->1。 dummy->2->1
            pre.next=reverse(start);
            //翻转后头节点变到最后。通过.next把断开的链表重新链接。
            start.next=next;
            //将pre换成下次要翻转的链表的头结点的上一个节点。即start
            pre=start;
            //翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start
            end=start;
        }
        return dummy.next;


    }
    //链表翻转
    // 例子:   head: 1->2->3->4
    public ListNode reverse(ListNode head) {
         //单链表为空或只有一个节点,直接返回原单链表
        if (head == null || head.next == null){
            return head;
        }
        //前一个节点指针
        ListNode preNode = null;
        //当前节点指针
        ListNode curNode = head;
        //下一个节点指针
        ListNode nextNode = null;
        while (curNode != null){
            nextNode = curNode.next;//nextNode 指向下一个节点,保存当前节点后面的链表。
            curNode.next=preNode;//将当前节点next域指向前一个节点   null<-1<-2<-3<-4
            preNode = curNode;//preNode 指针向后移动。preNode指向当前节点。
            curNode = nextNode;//curNode指针向后移动。下一个节点变成当前节点
        }
        return preNode;

    }


}
//这段代码是一个经典的单链表反转函数。让我详细解释一下这段代码,同时提供一个简单的例子。

代码中定义了一个 `reverse` 函数,它接收一个参数 `head`,这个参数是一个指向链表头部的节点的指针。函数返回的是反转后的链表的头节点。

首先,在函数内部定义了两个辅助节点 `pre` 和 `curr`,它们分别代表前一个节点和当前节点。开始时,`pre` 被初始化为 `null`,`curr` 被初始化为传入函数的 `head`。

然后,进入一个 `while` 循环,条件是 `curr` 不为 `null`。在循环内部,首先保存下一个节点的指针,即 `next = curr.next`,因为在改变 `curr.next` 指向之后会失去对下一个节点的引用。

《接着,将 `curr` 的 `next` 指针指向 `pre`,实现反转操作:`curr.next = pre`》。然后更新 `pre` 和 `curr`,将 `pre` 移动到 `curr` 的位置,`curr` 移动到 `next` 的位置。

循环继续直到 `curr` 为 `null`,即遍历完整个链表,此时 `pre` 指向的是原链表的最后一个节点,而 `curr` 已经为 `null`。最后返回 `pre`,即反转后链表的头节点。

让我们用一个简单的例子来说明:

假设初始链表为:1 -> 2 -> 3 -> 4 -> null

经过反转操作后,链表变为:4 -> 3 -> 2 -> 1 -> null

可以通过将这段代码应用于一个简单的链表数据结构,例如:

```java
class ListNode {
    int val;
    ListNode next;
    
    public ListNode(int val) {
        this.val = val;
    }
}

ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);

ListNode reversedHead = reverse(head);
// 然后可以遍历打印 reversedHead 来验证链表是否被成功反转

在单链表中,我们常常需要涉及节点之间的连接关系,这涉及到赋值和指向的概念。让我更详细地解释一下:

  1. 赋值(Assignment):赋值操作将一个变量或对象的值传递给另一个变量或对象。在单链表中,当我们操作节点的属性或字段时,通常是在进行赋值。例如,将一个节点的 next 指针指向另一个节点,就是赋值操作。在代码中,curr.next = pre 就是一个赋值操作,它将 curr 节点的 next 指针指向 pre 节点。

  2. 指向(Pointing):指向表示一个变量或对象引用到内存中的另一个位置。在单链表中,节点之间的连接关系通常就是通过指向来实现的。例如,当一个节点的 next 指针指向另一个节点时,我们说该节点指向了另一个节点。在代码中,curr = next 就是一个指向操作,它将 curr 指针指向了 next 节点所指向的节点。

在反转链表的代码中,我们同时涉及赋值和指向操作,让我用一个简单的例子来说明这两种操作:

假设我们有两个节点 node1node2,它们分别表示链表中的两个节点。而 node1next 指针指向 node2

  • 赋值操作:如果我们执行 node1.next = null,这意味着将 node1next 指针赋值为 null,即断开了原来 node1node2 之间的连接关系。

  • 指向操作:如果我们执行 node1 = node2,这意味着将 node1 指针指向了 node2 所指向的节点,此时 node1node2 指向同一个节点,它们之间存在着连接关系。

在单链表的上下文中,当我们说 curr.next = pre 时,这既是赋值操作又暗含了指向关系。让我详细解释一下:

  1. 赋值操作:在这种情况下,curr.next = pre 是赋值操作,因为它实际上将 pre 赋值给了 curr.next。这意味着 currnext 指针现在存储着指向 pre 的引用,即它保存了 pre 的地址信息。

  2. 指向关系:在单链表中,节点之间的连接关系是通过指针的指向来实现的。因此,当我们说 curr.next = pre 时,除了进行赋值外,也隐含了一个指向关系,即现在 currnext 指针指向了 pre。这意味着 currpre 之间建立了连接,通过这个关系,链表在遍历时可以沿着这个方向继续访问节点。

所以,在这个上下文中,curr.next = pre 可以同时理解为赋值操作和建立指向关系。赋值操作将地址信息存储在指针中,而指向关系则描述了节点之间的连接方式。

当在单链表的反转过程中,将 null 赋值给 curr.next 有着重要的意义,这其实是为了在反转过程的最后一个步骤中正确设置新的链表头部。让我解释一下:

在单链表的反转中,我们需要调整每个节点的指针方向,使得原链表中的尾节点成为新链表的头节点。具体来说,反转过程中的最后一个节点的 next 指针应该指向 null,以确保新链表的最后一个节点指向 null,形成一个正确的单链表。

当反转过程结束时,pre 指向的是原链表的最后一个节点,即反转后链表的头节点。在最后一次循环中,当 curr 指向原链表的最后一个节点时,next 就是 null,这时将 curr.next 赋值为 pre 即将 null 赋给了它,意味着将原链表的最后一个节点指向 null,这样就构成了新链表的尾节点指向 null 的关系,完成了链表的反转。

因此,赋值 nullcurr.next 在反转链表的最后一步是至关重要的,它标志着新链表的尾节点的正确设置,使得整个链表的结构正确,不会形成循环引用。

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

在解决「合并K个排序链表」这个问题之前,我们先来看一个更简单的问题:如何合并两个有序链表?假设链表 a 和 b 的长度都是 n,如何在 O(n) 的时间代价以及 O(1) 的空间代价完成合并? 这个问题在面试中常常出现,为了达到空间代价是 O(1),我们的宗旨是「原地调整链表元素的 next 指针完成合并」。以下是合并的步骤和注意事项,对这个问题比较熟悉的读者可以跳过这一部分。此部分建议结合代码阅读。

首先我们需要一个变量 head 来保存合并之后链表的头部,你可以把 head 设置为一个虚拟的头(也就是 head 的 val 属性不保存任何值),这是为了方便代码的书写,在整个链表合并完之后,返回它的下一位置即可。
我们需要一个指针 tail 来记录下一个插入位置的前一个位置,以及两个指针 aPtr 和 bPtr 来记录 a 和 b 未合并部分的第一位。注意这里的描述,tail 不是下一个插入的位置,aPtr 和 bPtr 所指向的元素处于「待合并」的状态,也就是说它们还没有合并入最终的链表。 当然你也可以给他们赋予其他的定义,但是定义不同实现就会不同。
当 aPtr 和 bPtr 都不为空的时候,取 val 属性较小的合并;如果 aPtr 为空,则把整个 bPtr 以及后面的元素全部合并;bPtr 为空时同理。
在合并的时候,应该先调整 tail 的 next 属性,再后移 tail 和 *Ptr(aPtr 或者 bPtr)。那么这里 tail 和 *Ptr 是否存在先后顺序呢?它们谁先动谁后动都是一样的,不会改变任何元素的 next 指针。
代码
Java
public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null || b == null) {
return a != null ? a : b;
}
ListNode head = new ListNode(0);
ListNode tail = head, aPtr = a, bPtr = b;
while (aPtr != null && bPtr != null) {
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
tail.next = (aPtr != null ? aPtr : bPtr);
return head.next;
}
复杂度分析

时间复杂度:O(n)。
空间复杂度:O(1)。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode ans = null;
        for (int i = 0; i < lists.length; ++i) {
            ans = mergeTwoLists(ans, lists[i]);
        }
        return ans;
    }

    public ListNode mergeTwoLists(ListNode a, ListNode b) {
        if (a == null || b == null) {
            return a != null ? a : b;
        }
        ListNode head = new ListNode(0);
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
            if (aPtr.val < bPtr.val) {
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
        tail.next = (aPtr != null ? aPtr : bPtr);
        return head.next;
    }
}

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
class Solution {
    int maxSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        maxGain(root);
        return maxSum;
    }

    public int maxGain(TreeNode node) {
        if (node == null) {
            return 0;
        }
        
        // 递归计算左右子节点的最大贡献值
        // 只有在最大贡献值大于 0 时,才会选取对应子节点
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);

        // 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        int priceNewpath = node.val + leftGain + rightGain;

        // 更新答案
        maxSum = Math.max(maxSum, priceNewpath);

        // 返回节点的最大贡献值
        return node.val + Math.max(leftGain, rightGain);
    }
}



、、、
    
    
class Solution {
    int pathSum = Integer.MIN_VALUE;

    public int maxPathSum(TreeNode root) {
        dfs(root);
        return pathSum;
    }

    // dfs 返回以该节点为端点的最大路径和
    public int dfs(TreeNode node) {
        if (node == null) return 0;
        int left = dfs(node.left);
        int right = dfs(node.right);
        // 当前节点有四个选择:
        // 1)独立成线,直接返回自己的值 
        // 2)跟左子节点合成一条路径 
        // 3)跟右子节点合成一条路径
        int ret = Math.max(node.val, node.val + Math.max(left, right));
        // 4)以自己为桥梁,跟左、右子节点合并成一条路径
        pathSum = Math.max(pathSum, Math.max(ret, node.val + left + right));
        return ret;
    }
}
对于树的题目,有统一的思考方法,那就是站在树(子树)的顶端根节点root思考。

那么对于此题,我们思考如下问题:如果当前处在root节点,左右节点应该告诉我们什么信息才能得到答案?

根据题中对路径的定义,对于此题我们来回答以上问题。当我们遍历到树中某个节点时,我希望左子节点告诉我,在左子树中,以左子节点为开始(端点)的路径和最大为多少,同理我也希望右子节点告诉我类似的信息。

如果有了以上信息,再来思考最后一个问题:有了这个信息如何得到答案?

显然,对于当前节点有四个选择:

我自己就是一条路径
只跟左子节点合并成一条路径
只跟右子节点合并成一条路径
以自己为桥梁,跟左、右子节点合并成一条路径
需要注意的是,我们在递归求解的时候,第四种情况是不能作为递归的返回值的,因为它不符合我们对递归所期望返回值的定义(因为此时该子节点并不是拥有最大路径和路径的起点(端点)),但它也是一个可能的解,所以我们用一个全局变量记录上面四种值的最大值,递归结束后,该变量就是答案。

再次强调一下,当节点以自己为桥梁连接两边形成一条路径时,根据路径定义,其祖先节点不再可能加入到这条路径中,也就是说这种情况下,它是解的一种可能,但不符合我们递归返回值的定义。

想通上面的逻辑后,代码就呼之欲出了,看看最后的代码,核心的就六七行,却解决了一个Hard问题。👏

    
    在这段代码中,`maxGain`方法被定义为一个递归方法。在计算节点`node`的最大贡献值时,`maxGain`方法会重复调用自身以处理节点的子节点,直到满足某个条件后停止调用。递归是一种算法设计技术,指的是在函数或过程中直接或间接调用自身的方法。

在这个具体例子中,`maxGain`方法通过递归的方式计算节点的最大贡献值,具体实现如下:

1. 方法开始时首先判断当前节点是否为空,若为空则返回贡献值为02. 然后递归计算左子节点和右子节点的最大贡献值,即以左子节点和右子节点为根节点的子树的最大贡献值。
3. 对左右子节点的最大贡献值进行处理,若小于0,则取04. 计算当前节点的最大路径和,即当前节点值加上左右子节点的最大贡献值。
5. 更新整棵树的最大路径和`maxSum`。
6. 返回当前节点的最大贡献值,即当前节点值加上左右子节点最大贡献值中的较大值。

递归方法在解决树结构相关问题时经常使用,因为树的结构天然适合递归的特性。递归能够简化问题的表达,使得算法更加直观易懂。在递归中,每一次递归调用都在解决一个规模更小的子问题,最终这些子问题的解决会汇总为整体问题的解决。
    
    在递归方法中,程序员通常会定义一个递归停止的条件,这就是递归的终止条件。当递归方法执行到满足了这个终止条件时,递归调用会停止,避免无限循环调用自身。

在代码中的`maxGain(TreeNode node)`方法中,终止条件是当`node`节点为空时,即`if (node == null)`中返回0。这样做,保证了递归在遇到空节点时会停止递归调用。

通过在递归方法中设置终止条件,确保了递归方法的结束,避免了无限循环调用的情况。递归方法在每次调用时都会检查是否满足这个终止条件,一旦满足就会结束递归。这样设计能够确保递归的安全性和正确性,使得程序能够正确地处理所有情况,并得出正确的结果。
    对于一个函数或方法来说,即使它在逻辑上是调用了自身并且在返回前进行了return,只要它在执行过程中确实调用了自身(直接或间接地),并且这种调用是递进的,就可以称之为递归方法。

递归方法的本质在于函数或方法在执行过程中调用自身来解决问题。这种调用是通过创建一个新的函数栈帧来实现的,每次递归调用都会生成一个新的栈帧,将调用的参数和局部变量保存在其中。当递归层级达到限制或者满足终止条件时,这些栈帧会被逐个弹出,函数的执行顺序也就返回到调用栈的上一级。

递归的原理可以理解为函数调用形成一个递归链条,通过这种链条把复杂的问题分解成较小的子问题,最终解决问题并把结果依次传递回去,直到最终结果返回给最初的调用者。

在代码中,即使递归调用后会返回,但仍然称之为递归方法,因为它在解决问题的过程中多次调用了自身,这种调用是循环的,每次调用都在处理规模更小的子问题,最终实现了整个问题的解。递归在树状数据结构等问题中非常常见,因为很多问题天然适合递归的解法。
    对于一个函数或方法来说,即使它在逻辑上是调用了自身并且在返回前进行了return,只要它在执行过程中确实调用了自身(直接或间接地),并且这种调用是递进的,就可以称之为递归方法。

递归方法的本质在于函数或方法在执行过程中调用自身来解决问题。这种调用是通过创建一个新的函数栈帧来实现的,每次递归调用都会生成一个新的栈帧,将调用的参数和局部变量保存在其中。当递归层级达到限制或者满足终止条件时,这些栈帧会被逐个弹出,函数的执行顺序也就返回到调用栈的上一级。

递归的原理可以理解为函数调用形成一个递归链条,通过这种链条把复杂的问题分解成较小的子问题,最终解决问题并把结果依次传递回去,直到最终结果返回给最初的调用者。

在代码中,即使递归调用后会返回,但仍然称之为递归方法,因为它在解决问题的过程中多次调用了自身,这种调用是循环的,每次调用都在处理规模更小的子问题,最终实现了整个问题的解。递归在树状数据结构等问题中非常常见,因为很多问题天然适合递归的解法。
    递归和循环都是常见的控制流结构,用于处理重复执行的问题,但它们在实现方式和使用场景上有一些区别。

### 区别:

1. **实现方式**- 递归是通过在函数内部调用自身来解决问题的一种方法。递归调用会创建一个新的函数调用栈,每次调用都会占用一定的内存空间。
   - 循环是通过迭代执行一段代码块来重复执行特定的任务。循环控制结构会在代码块内部完成重复执行,不需要额外的函数调用栈。

2. **终止条件**- 递归需要定义明确的终止条件,以避免无限递归调用和栈溢出。
   - 循环通过控制条件(如`for`、`while`循环的终止条件)来确定循环执行的次数和终止条件。

3. **内存占用**- 递归可能会占用更多的内存,因为每次递归调用都会创建新的函数调用栈。
   - 循环通常不会占用过多的内存,因为它只需要维护循环变量等少量状态信息。

4. **代码可读性**- 适当使用时,递归可以使代码更加简洁和易理解,特别适用于解决涉及树状结构等问题。
   - 循环通常更直观,适用于明确知道循环次数的情况下。

5. **性能**- 递归有时候可能会因为函数调用的开销比较大而导致性能较差。
   - 循环通常比较高效,因为它不需要频繁地进行函数调用。

### 总结:
递归和循环是解决重复执行问题的两种方法,各自有优缺点。选择适当的方法取决于问题本身的性质,以及在实际应用中对可读性、性能等的要求。递归更加适合于解决涉及递归结构、树形结构等问题,而循环则更适用于简单迭代的情况。

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

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

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

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

示例 1:

img

输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
输出:4

由题目我们可以知道每分钟每个腐烂的橘子都会使上下左右相邻的新鲜橘子腐烂,这其实是一个模拟广度优先搜索的过程。所谓广度优先搜索,就是从起点出发,每次都尝试访问同一层的节点,如果同一层都访问完了,再访问下一层,最后广度优先搜索找到的路径就是从起点开始的最短合法路径。

回到题目中,假设图中只有一个腐烂的橘子,它每分钟向外拓展,腐烂上下左右相邻的新鲜橘子,那么下一分钟,就是这些被腐烂的橘子再向外拓展腐烂相邻的新鲜橘子,这与广度优先搜索的过程均一一对应,上下左右相邻的新鲜橘子就是该腐烂橘子尝试访问的同一层的节点,路径长度就是新鲜橘子被腐烂的时间。我们记录下每个新鲜橘子被腐烂的时间,最后如果单元格中没有新鲜橘子,腐烂所有新鲜橘子所必须经过的最小分钟数就是新鲜橘子被腐烂的时间的最大值。

以上是基于图中只有一个腐烂的橘子的情况,可实际题目中腐烂的橘子数不止一个,看似与广度优先搜索有所区别,不能直接套用,但其实有两个方向的思路。

一个是耗时比较大且不推荐的做法:我们对每个腐烂橘子为起点都进行一次广度优先搜索,用 dis[x][y][i] 表示只考虑第 i 个腐烂橘子为起点的广度优先搜索,坐标位于 (x,y) 的新鲜橘子被腐烂的时间,设没有被腐烂的新鲜橘子的 dis[x][y][i]=inf ,即无限大,表示没有被腐烂,那么每个新鲜橘子被腐烂的最短时间即为

min i

dis[x][y][i]

最后的答案就是所有新鲜橘子被腐烂的最短时间的最大值,如果是无限大,说明有新鲜橘子没有被腐烂,输出 −1 即可。

无疑上面的方法需要枚举每个腐烂橘子,所以时间复杂度需要在原先广度优先搜索遍历的时间复杂度上再乘以腐烂橘子数,这在整个网格范围变大的时候十分耗时,所以需要另寻他路。

方法一:多源广度优先搜索
思路

观察到对于所有的腐烂橘子,其实它们在广度优先搜索上是等价于同一层的节点的。

假设这些腐烂橘子刚开始是新鲜的,而有一个腐烂橘子(我们令其为超级源点)会在下一秒把这些橘子都变腐烂,而这个腐烂橘子刚开始在的时间是 −1 ,那么按照广度优先搜索的算法,下一分钟也就是第 0 分钟的时候,这个腐烂橘子会把它们都变成腐烂橘子,然后继续向外拓展,所以其实这些腐烂橘子是同一层的节点。那么在广度优先搜索的时候,我们将这些腐烂橘子都放进队列里进行广度优先搜索即可,最后每个新鲜橘子被腐烂的最短时间 dis[x][y] 其实是以这个超级源点的腐烂橘子为起点的广度优先搜索得到的结果。

为了确认是否所有新鲜橘子都被腐烂,可以记录一个变量 cnt 表示当前网格中的新鲜橘子数,广度优先搜索的时候如果有新鲜橘子被腐烂,则 cnt=cnt−1 ,最后搜索结束时如果 cnt 大于 0 ,说明有新鲜橘子没被腐烂,返回 −1 ,否则返回所有新鲜橘子被腐烂的时间的最大值即可,也可以在广度优先搜索的过程中把已腐烂的新鲜橘子的值由 1 改为 2,最后看网格中是否有值为 1 即新鲜的橘子即可。

class Solution {
    int[] dr = new int[]{-1, 0, 1, 0};
    int[] dc = new int[]{0, -1, 0, 1};

    public int orangesRotting(int[][] grid) {
        int R = grid.length, C = grid[0].length;
        Queue<Integer> queue = new ArrayDeque<Integer>();
        Map<Integer, Integer> depth = new HashMap<Integer, Integer>();
        for (int r = 0; r < R; ++r) {
            for (int c = 0; c < C; ++c) {
                if (grid[r][c] == 2) {
                    int code = r * C + c;
                    queue.add(code);
                    depth.put(code, 0);
                }
            }
        }
        int ans = 0;
        while (!queue.isEmpty()) {
            int code = queue.remove();
            int r = code / C, c = code % C;
            for (int k = 0; k < 4; ++k) {
                int nr = r + dr[k];
                int nc = c + dc[k];
                if (0 <= nr && nr < R && 0 <= nc && nc < C && grid[nr][nc] == 1) {
                    grid[nr][nc] = 2;
                    int ncode = nr * C + nc;
                    queue.add(ncode);
                    depth.put(ncode, depth.get(code) + 1);
                    ans = depth.get(ncode);
                }
            }
        }
        for (int[] row: grid) {
            for (int v: row) {
                if (v == 1) {
                    return -1;
                }
            }
        }
        return ans;
    }
}

//这段代码是一个解决问题的 Java 类 `Solution`,其中包含了一个名为 `orangesRotting` 的方法。这个方法的目的是模拟腐烂橘子的过程,并返回所有橘子都腐烂所需的最小时间。

让我们来逐行解释这段代码:

1. 首先定义了两个数组 `dr` 和 `dc`,分别表示在上、左、下、右四个方向上的行和列的变化量。

2. `orangesRotting` 方法获取一个二维数组 `grid` 作为输入参数,表示橘子的分布情况,其中值为 0 表示空单元格,1 表示新鲜的橘子,2 表示腐烂的橘子。

3. 创建了一个队列 `queue` 用于存储腐烂的橘子的位置,以及一个 `depth` 哈希表用于存储橘子在腐烂过程中的时间步长。

4. 遍历整个 `grid`,将初始腐烂的橘子的位置放入队列和 `depth` 中。

5. 使用 BFS(广度优先搜索)的方式模拟橘子的腐烂过程,每一轮遍历腐烂橘子周围的新鲜橘子,并将它们标记为腐烂状态,并将腐烂的橘子位置加入队列中。

6. 在更新橘子状态和步长时,通过 `depth` 记录腐烂每个新鲜橘子所需的时间步长。

7. 在遍历过程中,不断更新 `ans` 变量以记录最后一个橘子腐烂的时间步长。

8. 最后检查整个 `grid` 是否还有新鲜的橘子,如果有则返回 -1,否则返回 `ans`。

这段代码利用了 BFS 算法解决了腐烂橘子的问题,根据新鲜和腐烂橘子之间的关系,逐步将腐烂状态扩散,并记录了每个橘子腐烂所需的最小时间步长。
    在这段代码中,`r * C + c`、`code / C`、`code % C`、`nr * C + nc` 这些操作都是为了将二维数组中的行和列转换为一维数组中的索引,或者将一维数组中的索引还原为二维数组中的行和列。

具体解释如下:

1. `r * C + c`:
   - 这个表达式将二维坐标 `(r, c)` 转换为一维数组中的索引。在这段代码中,`r` 是行号,`c` 是列号,`C` 是二维数组的列数。通过这个表达式,可以将二维坐标转换为一维数组中的索引,便于在一维数组中处理二维数组的数据。

2. `code / C` 和 `code % C`:
   - `code / C` 表示将一维数组中的索引 `code` 转换回二维数组中的行号 `r`。
   - `code % C` 表示将一维数组中的索引 `code` 转换回二维数组中的列号 `c`。
   - 这两个操作是将一维数组中的索引重新映射回二维数组中的坐标位置。

3. `nr * C + nc`:
   - 这个表达式将二维坐标 `(nr, nc)` 转换为一维数组中的索引。同样,`nr` 和 `nc` 是新的行号和列号,通过这个表达式可以得到新坐标在一维数组中的索引位置。

在这段代码中,这些操作主要用于在处理二维数组时,方便地将坐标之间进行转换,以便在一维数组中对对应位置的元素进行操作和访问。

复杂度分析

时间复杂度:O(nm)。即进行一次广度优先搜索的时间,其中 n,m 分别为 grid 的行数与列数。

空间复杂度:O(nm)。需要额外的 dis 数组记录每个新鲜橘子被腐烂的最短时间,大小为 O(nm),且广度优先搜索中队列里存放的状态最多不会超过 nm 个,最多需要 O(nm) 的空间,所以最后的空间复杂度为 O(nm)。

N皇后问题

思路
都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二维矩阵还会有点不知所措。

首先来看一下皇后们的约束条件:

不能同行
不能同列
不能同斜线
确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。

下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:

从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。

回溯三部曲
按照我总结的如下回溯模板,我们来依次分析:

void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
递归函数参数
我依然是定义全局变量二维数组result来记录最终结果。

参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。

代码如下:

vector<vector> result;
void backtracking(int n, int row, vector& chessboard) {
递归终止条件
在如下树形结构中:

可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。

代码如下:

if (row == n) {
result.push_back(chessboard);
return;
}
单层搜索的逻辑
递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。

每次都是要从新的一行的起始位置开始搜,所以都是从0开始。

代码如下:

for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = ‘Q’; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = ‘.’; // 回溯,撤销皇后
}
}
验证棋盘是否合法
按照如下标准去重:

不能同行
不能同列
不能同斜线 (45度和135度角)
代码如下:

bool isValid(int row, int col, vector& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == ‘Q’) {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i–, j–) {
if (chessboard[i][j] == ‘Q’) {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i–, j++) {
if (chessboard[i][j] == ‘Q’) {
return false;
}
}
return true;
}
在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢?

因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

那么按照这个模板不难写出如下C++代码:

class Solution {
private:
vector<vector> result;
// n 为输入的棋盘大小
// row 是当前递归到棋盘的第几行了
void backtracking(int n, int row, vector& chessboard) {
if (row == n) {
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
chessboard[row][col] = ‘Q’; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = ‘.’; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) { // 这是一个剪枝
if (chessboard[i][col] == ‘Q’) {
return false;
}
}
// 检查 45度角是否有皇后
for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i–, j–) {
if (chessboard[i][j] == ‘Q’) {
return false;
}
}
// 检查 135度角是否有皇后
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i–, j++) {
if (chessboard[i][j] == ‘Q’) {
return false;
}
}
return true;
}
public:
vector<vector> solveNQueens(int n) {
result.clear();
std::vectorstd::string chessboard(n, std::string(n, ‘.’));
backtracking(n, 0, chessboard);
return result;
}
};
可以看出,除了验证棋盘合法性的代码,省下来部分就是按照回溯法模板来的。

总结
本题是我们解决棋盘问题的第一道题目。

如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。

这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了。

class Solution {
    List<List<String>> res = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        char[][] chessboard = new char[n][n];
        for (char[] c : chessboard) {
            Arrays.fill(c, '.');
        }
        backTrack(n, 0, chessboard);
        return res;
    }


    public void backTrack(int n, int row, char[][] chessboard) {
        if (row == n) {
            res.add(Array2List(chessboard));
            return;
        }

        for (int col = 0;col < n; ++col) {
            if (isValid (row, col, n, chessboard)) {
                chessboard[row][col] = 'Q';
                backTrack(n, row+1, chessboard);
                chessboard[row][col] = '.';
            }
        }

    }


    public List Array2List(char[][] chessboard) {
        List<String> list = new ArrayList<>();

        for (char[] c : chessboard) {
            list.add(String.copyValueOf(c));
        }
        return list;
    }


    public boolean isValid(int row, int col, int n, char[][] chessboard) {
        // 检查列
        for (int i=0; i<row; ++i) { // 相当于剪枝
            if (chessboard[i][col] == 'Q') {
                return false;
            }
        }

        // 检查45度对角线
        for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }

        // 检查135度对角线
        for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }
}

// 方法2:使用boolean数组表示已经占用的直(斜)线
class Solution {
    List<List<String>> res = new ArrayList<>();
    boolean[] usedCol, usedDiag45, usedDiag135;    // boolean数组中的每个元素代表一条直(斜)线
    public List<List<String>> solveNQueens(int n) {
        usedCol = new boolean[n];                  // 列方向的直线条数为 n
        usedDiag45 = new boolean[2 * n - 1];       // 45°方向的斜线条数为 2 * n - 1
        usedDiag135 = new boolean[2 * n - 1];      // 135°方向的斜线条数为 2 * n - 1
		//用于收集结果, 元素的index表示棋盘的row,元素的value代表棋盘的column
        int[] board = new int[n];
        backTracking(board, n, 0);
        return res;
    }
    private void backTracking(int[] board, int n, int row) {
        if (row == n) {
            //收集结果
            List<String> temp = new ArrayList<>();
            for (int i : board) {
                char[] str = new char[n];
                Arrays.fill(str, '.');
                str[i] = 'Q';
                temp.add(new String(str));
            }
            res.add(temp);
            return;
        }

        for (int col = 0; col < n; col++) {
            if (usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1]) {
                continue;
            }
            board[row] = col;
			// 标记该列出现过
            usedCol[col] = true;
			// 同一45°斜线上元素的row + col为定值, 且各不相同
            usedDiag45[row + col] = true;
			// 同一135°斜线上元素row - col为定值, 且各不相同
			// row - col 值有正有负, 加 n - 1 是为了对齐零点
            usedDiag135[row - col + n - 1] = true;
            // 递归
            backTracking(board, n, row + 1);
            usedCol[col] = false;
            usedDiag45[row + col] = false;
            usedDiag135[row - col + n - 1] = false;
        }
    }
}


alse;
}
}
return true;
}
}

// 方法2:使用boolean数组表示已经占用的直(斜)线
class Solution {
List<List> res = new ArrayList<>();
boolean[] usedCol, usedDiag45, usedDiag135; // boolean数组中的每个元素代表一条直(斜)线
public List<List> solveNQueens(int n) {
usedCol = new boolean[n]; // 列方向的直线条数为 n
usedDiag45 = new boolean[2 * n - 1]; // 45°方向的斜线条数为 2 * n - 1
usedDiag135 = new boolean[2 * n - 1]; // 135°方向的斜线条数为 2 * n - 1
//用于收集结果, 元素的index表示棋盘的row,元素的value代表棋盘的column
int[] board = new int[n];
backTracking(board, n, 0);
return res;
}
private void backTracking(int[] board, int n, int row) {
if (row == n) {
//收集结果
List temp = new ArrayList<>();
for (int i : board) {
char[] str = new char[n];
Arrays.fill(str, ‘.’);
str[i] = ‘Q’;
temp.add(new String(str));
}
res.add(temp);
return;
}

    for (int col = 0; col < n; col++) {
        if (usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1]) {
            continue;
        }
        board[row] = col;
		// 标记该列出现过
        usedCol[col] = true;
		// 同一45°斜线上元素的row + col为定值, 且各不相同
        usedDiag45[row + col] = true;
		// 同一135°斜线上元素row - col为定值, 且各不相同
		// row - col 值有正有负, 加 n - 1 是为了对齐零点
        usedDiag135[row - col + n - 1] = true;
        // 递归
        backTracking(board, n, row + 1);
        usedCol[col] = false;
        usedDiag45[row + col] = false;
        usedDiag135[row - col + n - 1] = false;
    }
}

}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值