逻辑迷宫——数据结构与算法(Java版)

提示:文章内容参考 Datawhale 2024年9月组队学习之Leetcode基础算法篇
Datawhale官网
刷题手册


何为枚举

”在汉语中:本义是树干,引申为条状物。有逐一、单个的意思。“”在汉语中,则有抬起、提出等含义。
那么枚举大白话就是将所有可能的情况一一给它列出来。

在计算机算法中,枚举就是把所有可能的情况一个一个地列举出来,然后逐个进行分析判断,找出最终那个符合条件的解。
作为一种算法,必定有其优点,也有其不足之处。
那么枚举法的优点显而易见:

  • 简单直接甚至称得上是简单粗暴
    • 对于一些问题,特别是那些规模较小、可能解的数量也相对较少的问题,枚举算法非常容易理解和实现。不需要用到那些看起来就头大的复杂的数学推导或高级的算法知识,即使是初学者也能够很快掌握并运用。
  • 确定性高
    • 因为我们了解到枚举算法是逐一检查所有可能的解,那么只要问题有解,并且在枚举的范围内,就一定能够找到解。不存在不确定性或随机性,结果是可靠的。

而枚举的不足之处:

  • 效率低下
    • 当问题的规模较大或可能解的数量非常多时,枚举算法的效率会变得非常低。因为它需要逐个检查每一个可能的解,时间复杂度通常很高。
  • 占用资源
    • 枚举大量的可能解需要占用大量的计算资源,包括内存和处理器时间。在一些资源受限的环境下,可能会导致系统性能下降甚至崩溃。

由此来看,我们什么时候用枚举法呢:

  • 问题规模小,可能解的数量有限
  • 问题的脉络非常明了,方便进行枚举
  • 对算法效率要求不高
  • 没有更好的算法可用,可以作为保底算法

综上看来:枚举算法用起来简单直观,但缺点也较为明显,可以结合实际情况,与其他更为高效的算法结合使用,以期获得求解的效率。


两数之和(0001)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。

# Java_枚举法
class Solution {
    /**
     * twoSum方法接收一个整数数组nums和一个目标值target
     * 它通过两层循环枚举数组中的每对数,以找出哪对数之和等于目标值target
     * 
     * @param nums 输入的整数数组
     * @param target 目标值,我们需要找到数组中两个数的和等于这个值
     * @return 返回一个包含两个整数索引的数组,这两个索引对应的数之和等于目标值
     *         如果没有这样的数对,则返回null
     */
    public int[] twoSum(int[] nums, int target) {
        // 外层循环从第一个元素开始,到倒数第二个元素结束
        for(i=0;i<nums.length-1;i++){
            // 内层循环从外层循环的下一个元素开始,到数组的最后一个元素结束
            for(j=1+i;j<nums.length;j++){
                // 检查当前的两个元素之和是否等于目标值
                if(nums[i]+nums[j]==target){
                    // 如果等于,返回这两个元素的索引
                    return new int[]{i,j};
                }
            }
        }
        // 如果没有找到满足条件的两个数,返回null
        return null;
    }
}

计数质数(0204)

给定整数 n ,返回所有小于非负整数 n 的质数的数量 。

# Java_枚举法
class Solution {
    /**
     * 计算小于给定非负整数n的质数数量
     * 
     * @param n 非负整数,函数将计算小于此数的质数数量
     * @return 小于n的质数数量
     */
    public int countPrimes(int n) {
        // 对于小于3的数,没有质数
        if (n == 0 || n == 1 || n == 2) {
            return 0;
        } else if (n == 3) {
            // 数字3本身是质数
            return 1;
        } else if (n == 4) {
            // 数字2和3都是质数
            return 2;
        } else {
            // 初始化质数计数为2,因为2和3是已知的最小质数
            int count = 2;
            // 从5开始检查每个奇数是否为质数,因为偶数除了2都不是质数
            for (int i = 5; i < n; i += 2) {
                // 如果数字i是质数,则增加计数
                if (isPrime(i) == 1) {
                    count++;
                }
            }
            return count;
        }
    }

    /**
     * 判断给定数字是否为质数
     * 
     * @param num 要判断的数字
     * @return 如果数字是质数返回1,否则返回0
     */
    public static int isPrime(int num) {
        // 数字小于2不是质数
        if (num < 2) {
            return 0;
        } else {
            // 只需检查到sqrt(num)即可,因为如果num有因子,必然有一个小于等于其平方根
            for (int i = 2; i <= Math.sqrt(num); i++) {
                // 如果找到一个因子,则num不是质数
                if (num % i == 0) {
                    return 0;
                }
            }
        }
        // 如果没有找到因子,则num是质数
        return 1;
    }
}
# Java_筛选法(埃拉托斯特尼筛法)
class Solution {
    /**
     * 计算小于给定非负整数n的质数数量
     * @param n 非负整数,函数将计算小于此数的质数数量
     * @return 小于n的质数数量
     */
    public int countPrimes(int n) {
        return countPrimesNums(n);
    }
    /**
     * 使用埃拉托斯特尼筛法计算小于给定非负整数n的质数数量
     * 埃拉托斯特尼筛法是一种高效地找出一定范围内所有质数的算法
     * @param n 非负整数,函数将计算小于此数的质数数量
     * @return 小于n的质数数量
     */
    public static int countPrimesNums(int n) {
        // 初始化一个布尔数组,用于标记每个数是否为质数
        boolean[] isPrime = new boolean[n];
        // 默认所有数都是质数(先将2到n-1的数标记为质数)
        for (int i = 2; i < n; i++) {
            isPrime[i] = true;
        }
        // 从2开始遍历到sqrt(n),标记合数
        for (int i = 2; i * i < n; i++) {
            // 如果i是质数
            if (isPrime[i]) {
                // 将i的倍数标记为合数
                for (int j = i * i; j < n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        // 统计质数数量
        int count = 0;
        for (int i = 2; i < n; i++) {
            // 如果i是质数,则计数加一
            if (isPrime[i]) {
                count++;
            }
        }
        // 返回质数数量
        return count;
    }
}

统计平方和三元组的数目(1925)

一个平方和三元组 (a,b,c) 指的是满足 a 2 + b 2 = c 2 a^2 + b^2 = c^2 a2+b2=c2 的 整数三元组 a,b 和 c 。
给你一个整数 n ,请你返回满足 1 < = a , b , c < = n 1 <= a, b, c <= n 1<=a,b,c<=n的平方和三元组的数目。

class Solution {
    /**
     * 计算并返回满足勾股定理的整数triplets数量
     * 
     * @param n 指定范围的上限,triplets中的a、b、c必须小于或等于n
     * @return 满足条件的整数triplets的数量
     */
    public int countTriples(int n) {
        // 初始化计数器
        int count = 0;
        // 对输入值n进行边界条件检查
        if (n <= 4) {
            // 如果n小于等于4,则不存在满足条件的triplets,返回0
            return 0;
        } else {
            // 三重循环遍历所有可能的triplets组合
            for (int a = 3; a <= n - 2; a++) {
                for (int b = a + 1; b <= n - 1; b++) {
                    for (int c = b + 1; c <= n; c++) {
                        // 检查当前组合是否满足勾股定理
                        if (a * a + b * b == c * c) {
                            // 如果满足,计数器增加
                            count++;
                        }
                    }
                }
            }
        }
        // 返回计数器的值乘以2,因为每个triplet都被计算了两次(a,b,c和c,b,a)
        return count * 2;
    }
}

在既定时间做作业的学生人数(1450)

给你两个整数数组 startTime(开始时间)和 endTime(结束时间),并指定一个整数 queryTime 作为查询时间。
已知,第 i 名学生在 startTime[i] 时开始写作业并于 endTime[i] 时完成作业。
请返回在查询时间 queryTime 时正在做作业的学生人数。形式上,返回能够使 queryTime 处于区间 [startTime[i], endTime[i]](含)的学生人数。

class Solution {
    /**
     * 计算在查询时间点正在忙于学习的学生数量
     * 
     * @param startTime 学生开始学习的时间数组
     * @param endTime 学生结束学习的时间数组
     * @param queryTime 查询的时间点
     * @return 在查询时间点正在忙于学习的学生数量
     */
    public int busyStudent(int[] startTime, int[] endTime, int queryTime) {
        // 初始化学生计数器
        int count = 0;
        // 遍历所有学生的学习时间
        for (int i = 0; i < startTime.length; i++) {
            // 检查学生是否在查询时间点正在学习
            if (startTime[i] <= queryTime && endTime[i] >= queryTime) {
                // 如果学生在查询时间点正在学习,计数器增加
                count++;
            }
        }
        // 返回在查询时间点正在忙于学习的学生数量
        return count;
    }
}

网络信号最好的坐标(1620)

给你一个数组 towers 和一个整数 radius 。
数组 towers 中包含一些网络信号塔,其中 t o w e r s [ i ] = [ x i , y i , q i ] towers[i] = [x_i, y_i, q_i] towers[i]=[xi,yi,qi] 表示第 i 个网络信号塔的坐标是 ( x i , y i ) (x_i, y_i) (xi,yi) 且信号强度参数为 q i q_i qi 。所有坐标都是在 X − Y X-Y XY坐标系内的整数坐标。两个坐标之间的距离用欧几里得距离计算。
整数 radius 表示一个塔能到达的最远距离 。如果一个坐标跟塔的距离在 radius 以内,那么该塔的信号可以到达该坐标。在这个范围以外信号会很微弱,所以 radius 以外的距离该塔是不能到达的 。
如果第 i 个塔能到达 (x, y) ,那么该塔在此处的信号为 ? q i / ( 1 + d ) ? ?q_i / (1 + d)? ?qi/(1+d)? ,其中 d 是塔跟此坐标的距离。一个坐标的信号强度是所有能到达该坐标的塔的信号强度之和。
请你返回数组 [ c x , c y ] [c_x, c_y] [cx,cy] ,表示信号强度最大的整数坐标点 ( c x , c y ) (c_x, c_y) (cx,cy) 。如果有多个坐标网络信号一样大,请你返回字典序最小的非负坐标。
注意:
1、坐标 ( x 1 , y 1 ) (x_1, y_1) (x1,y1) 字典序比另一个坐标 ( x 2 , y 2 ) (x_2, y_2) (x2,y2)小,需满足以下条件之一:
要么 x 1 < x 2 x_1 < x_2 x1<x2 ,要么 x 1 = = x 2 且 y 1 < y 2 x_1 == x_2 且 y_1 < y_2 x1==x2y1<y2
2、 ? v a l ? ?val? ?val? 表示小于等于 val 的最大整数(向下取整函数)。

class Solution {
    /**
     * 计算并返回最佳坐标点数组,包含x坐标和y坐标
     * 
     * @param towers 二维数组,每个子数组表示一个信号塔的位置和信号强度
     * @param radius 信号的有效半径
     * @return int[] 包含最佳坐标点的x坐标和y坐标的数组
     */
    public int[] bestCoordinate(int[][] towers, int radius) {
        // 使用哈希表存储每个点的信号强度总和
        Map<String, Integer> pointSignalStrength = new HashMap<>();
        // 存储最大信号强度值
        int maxSignal = 0;
        // 存储最大信号强度的坐标点及其信号强度
        int[] bestPoint = new int[] { 0, 0, 0 };

        // 遍历所有信号塔
        for (int[] tower : towers) {
            int tx = tower[0];
            int ty = tower[1];
            int tq = tower[2];

            // 遍历当前信号塔周围所有可能的坐标点
            for (int x = tx - radius; x <= tx + radius; x++) {
                for (int y = ty - radius; y <= ty + radius; y++) {
                    // 计算当前点到信号塔的距离
                    double distance = Math.sqrt(Math.pow(x - tx, 2) + Math.pow(y - ty, 2));
                    // 如果距离在信号有效范围内
                    if (distance <= radius) {
                        // 计算当前点从该信号塔接收到的信号强度
                        int signal = (int) Math.floor(tq / (1 + distance));
                        // 使用字符串表示坐标点,便于哈希表存储
                        String point = x + "," + y;
                        // 累加当前点的总信号强度
                        pointSignalStrength.put(point, pointSignalStrength.getOrDefault(point, 0) + signal);

                        // 如果当前点的信号强度大于之前记录的最大信号强度
                        if (pointSignalStrength.get(point) > maxSignal) {
                            maxSignal = pointSignalStrength.get(point);
                            bestPoint[0] = x;
                            bestPoint[1] = y;
                            bestPoint[2] = signal;
                        } else if (pointSignalStrength.get(point) == maxSignal) {
                            // 如果当前点的信号强度等于最大信号强度,比较坐标点大小,选取更小的坐标点
                            if (x < bestPoint[0] || (x == bestPoint[0] && y < bestPoint[1])) {
                                bestPoint[0] = x;
                                bestPoint[1] = y;
                                bestPoint[2] = signal;
                            }
                        }
                    }
                }
            }
        }
        // 如果最大信号强度为0,说明没有有效的信号点,返回[0,0]
        if (bestPoint[2] == 0) {
            return new int[]{0,0};
        } else {
            // 返回最大信号强度的坐标点
            return new int[]{bestPoint[0],bestPoint[1]};
        }
    }
}

公因子的数目(2427)

给你两个正整数 a 和 b ,返回 a 和 b 的公因子的数目。
如果 x 可以同时整除 a 和 b ,则认为 x 是 a 和 b 的一个 公因子 。

class Solution {
    /**
     * 计算两个整数a和b的公共因子数
     * 公共因子是指能同时整除a和b的数
     * 
     * @param a 整数a
     * @param b 整数b
     * @return 返回a和b的公共因子数
     */
    public int commonFactors(int a, int b) {
        // 初始化计数器为1,因为1是所有整数的公共因子
        int count = 1;
        // 循环从2开始,直到小于等于a和b中较小的那个数
        // 这是因为一个数不可能有大于它自身的公共因子
        for (int i = 2; i <= (a >= b ? b : a); i++) {
            // 如果i同时是a和b的因子,则增加计数器
            if (a % i == 0 && b % i == 0) {
                count++;
            }
        }
        // 返回计数器的值,代表a和b的公共因子数
        return count;
    }
}

文件组合(LCR180)

待传输文件被切分成多个部分,按照原排列顺序,每部分文件编号均为一个正整(至少含有两个文件)。传输要求为:连续文件编号总和为接收方指定数字 target 的所有文件。请返回所有符合该要求的文件传输组合列表。
注意,返回时需遵循以下规则:
1、每种组合按照文件编号升序排列;
2、不同组合按照第一个文件编号升序排列。

class Solution {
    /**
     * 寻找所有加和等于目标值的连续正整数组合
     * 
     * @param target 目标值,所有组合的和应等于此值
     * @return 包含所有满足条件的连续正整数组合的二维数组
     */
    public int[][] fileCombination(int target) {
        // 存储结果的列表
        List<int[]> combinations = new ArrayList<>();
        // 从1开始尝试找到所有可能的组合
        for (int i = 1; i < target; i++) {
            // 初始化组合的和为当前的起始值
            int sum = i;
            // j表示当前组合中的下一个数字
            int j = i + 1;
            // 当组合的和小于目标值时,继续添加数字
            while (sum < target) {
                // 将j加到组合的和中
                sum += j;
                // 移动到下一个数字
                j++;
            }
            // 如果组合的和正好等于目标值,记录这个组合
            if (sum == target) {
                // 创建一个数组来存储当前找到的组合,长度为组合中数字的数量
                int[] combination = new int[j - i];
                // 遍历从i到j-1的每个数字,将其存储到数组中
                for (int k = i, index = 0; k < j; k++, index++) {
                    combination[index] = k;
                }
                // 将当前组合添加到结果列表中
                combinations.add(combination);
            }
        }
        // 将列表转换为数组,以便返回
        return combinations.toArray(new int[combinations.size()][]);
    }
}
class Solution {
    /**
     * 计算所有可能的文件组合
     * 
     * @param target 目标数字,表示需要组合的文件总大小
     * @return 返回一个二维数组,包含所有可能的文件组合
     */
    public int[][] fileCombination(int target) {
        // 先计算可能的组合数量
        int combinationCount = 0;
        for (int i = 1; i < target; i++) {
            int sum = 0;
            for (int j = i; j <= target; j++) {
                sum += j;
                if (sum == target) {
                    combinationCount++;
                }
            }
        }
        // 根据组合数量初始化结果数组
        int[][] result = new int[combinationCount][];
        int count = 0;
        for (int i = 1; i < target; i++) {
            int sum = 0;
            for (int j = i; j <= target; j++) {
                sum += j;
                if (sum == target) {
                    int[] combination = new int[j - i + 1];
                    for (int k = i, index = 0; k <= j; k++, index++) {
                        combination[index] = k;
                    }
                    result[count++] = combination;
                }
            }
        }
        return result;
    }
}

统计圆内格点数目(2249)

给你一个二维整数数组 circles ,其中 c i r c l e s [ i ] = [ x i , y i , r i ] circles[i] = [x_i, y_i, r_i] circles[i]=[xi,yi,ri]表示网格上圆心为$ (x_i, y_i)$ 且半径为 r i r_i ri的第 i 个圆,返回出现在至少一个圆内的格点数目 。
注意:
1、格点是指整数坐标对应的点。
2、圆周上的点也被视为出现在圆内的点。

class Solution {
    /**
     * 计算给定圆列表覆盖的不相交整点数量
     * 
     * @param circles 二维数组,每个子数组表示一个圆的坐标和半径,格式为[x, y, r]
     * @return 不相交整点的总数
     */
    public int countLatticePoints(int[][] circles) {
        // 初始化整点计数器
        int count = 0;
        // 创建一个能够存储一维数组的数据结构,同时其中的元素不能重复,进而便于统计不同一维数组的数量
        Set<String> uniqueArrays = new HashSet<>();
        // 遍历给定的每个圆
        for (int i = 0; i < circles.length; i++) {
            // 遍历每个圆内的每个点,x和y分别为点的坐标
            for (int x = circles[i][0] - circles[i][2]; x <= circles[i][0] + circles[i][2]; x++) {
                for (int y = circles[i][1] - circles[i][2]; y <= circles[i][1] + circles[i][2]; y++) {
                    // 检查点是否在当前圆内
                    if (Math.pow((x - circles[i][0]), 2) + Math.pow((y - circles[i][1]), 2) <= Math.pow(circles[i][2],
                            2)) {
                        // 将点的坐标转换为字符串格式,便于存储和检查重复
                        String point = x + "," + y;
                        // 将点添加到集合中,自动处理重复问题
                        uniqueArrays.add(point);
                    }
                }
            }
        }
        // 返回不相交整点的数量
        return uniqueArrays.size();
    }
}

最大正方形(0221)

在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。

# 动态规划
class Solution {
    /**
     * 寻找矩阵中最大的正方形的面积
     * 
     * @param matrix 二维字符数组,表示输入的矩阵,其中'1'表示该位置是有效的
     * @return 返回最大正方形的面积
     */
    public int maximalSquare(char[][] matrix) {
        // 矩阵的行数
        int rows = matrix.length;
        // 如果矩阵为空,则直接返回0
        if (rows == 0) return 0;
        // 矩阵的列数
        int cols = matrix[0].length;
        // 用于动态规划的二维数组,dp[i][j]表示以matrix[i][j]为右下角的最大正方形的边长
        int[][] dp = new int[rows][cols];
        // 记录最大正方形的边长
        int maxSide = 0;

        // 遍历矩阵中的每个元素
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 如果当前元素是'1',表示可以形成正方形的一部分
                if (matrix[i][j] == '1') {
                    // 如果是边界元素,则最大正方形的边长为1
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1;
                    } else {
                        // 计算以当前元素为右下角的最大正方形的边长
                        dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;
                    }
                    // 更新最大正方形的边长
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        // 返回最大正方形的面积,即边长的平方
        return maxSide * maxSide;
    }
}
# 枚举法(时间超限)
class Solution {
    /**
     * 寻找最大的正方形
     * 
     * @param matrix 二维字符数组,表示矩阵
     * @return 返回最大正方形的面积
     */
    public int maximalSquare(char[][] matrix) {
        // 用于存储最大正方形的面积
        int max = 0;
        // 遍历矩阵的每一个元素
        for (int y = 0; y < matrix.length; y++) {
            for (int x = 0; x < matrix[y].length; x++) {
                // 如果当前元素为'1',则以此元素为正方形的一个角
                if (matrix[y][x] == '1') {
                    // side表示正方形的边长,初始为1
                    int side = 1;
                    // expandable用于判断是否可以扩展正方形
                    boolean expandable = true;
                    // 当可以扩展时,进行循环
                    while (expandable) {
                        // 如果扩展后超出矩阵边界,则无法继续扩展
                        if (y + side >= matrix.length || x + side >= matrix[y].length) {
                            expandable = false;
                            break;
                        }
                        // 检查当前正方形范围内的所有元素是否都为'1'
                        for (int i = y; i <= y + side; i++) {
                            for (int j = x; j <= x + side; j++) {
                                // 如果发现'0',则无法形成更大的正方形
                                if (matrix[i][j] == '0') {
                                    expandable = false;
                                    break;
                                }
                            }
                            // 如果发现无法扩展,跳出内层循环
                            if (!expandable) {
                                break;
                            }
                        }
                        // 如果当前正方形可扩展,则尝试扩展边长
                        if (expandable) {
                            side++;
                        }
                    }
                    // 更新最大正方形的面积
                    max = Math.max(max, side * side);
                }
            }
        }
        // 返回最大正方形的面积
        return max;
    }
}

和为K的子数组(0560)

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。

class Solution {
    /**
     * 统计数组中和为k的子数组数量
     * 
     * @param nums 输入的整数数组
     * @param k 指定的和值
     * @return 返回数组中和为k的子数组数量
     */
    public int subarraySum(int[] nums, int k) {
        // 初始化满足条件的子数组数量为0
        int count = 0;
        
        // 外层循环遍历数组,以每个元素作为起始点
        for (int i = 0; i < nums.length; i++) {
            // 初始化以当前元素为起始的子数组的和为0
            int sum = 0;
            
            // 内层循环从当前元素开始,遍历数组的剩余部分
            for (int j = i; j < nums.length; j++) {
                // 累加当前元素到子数组的和
                sum += nums[j];
                
                // 如果子数组的和正好等于k,则满足条件的子数组数量增加1
                if (sum == k) {
                    count++;
                }
            }
        }
        
        // 返回满足条件的子数组数量
        return count;
    }
}

何为递归

“递”:传递、依次进行的过程;
“归”:回到原处、合并、归属。
因此“递归”可以理解为依次传递并最终返回的过程

什么是递归
想象一下,你现在正在玩一个俄罗斯套娃。当你打开最大的娃娃时,里面有一个稍微小一点的娃娃;再打开又会发现里边有一个更小的……如此重复,直到打开最小的那个娃娃,发现里边什么也没有。
由此,递归可以简单定义为一种方法或者过程,其中函数通过调用自身来将问题分解成更小的相同的问题来解决的情况。

递归的基本组成部分
一个递归函数通常包含两个部分:

  • 基准情况(Base Case):这是递归调用停止的地方,即问题可以被直接解决而不需要进一步的递归。没有正确的基本情况会导致无限递归,最终可能导致程序崩溃。
  • 递归情况(Recursive Case):这是将问题分解为更小的问题,并对这些较小的问题进行递归调用的过程。每次递归调用都应使问题向基本情况靠近。

工作原理
递归函数的工作原理类似于一个。每当函数调用自身时,当前的状态(包括变量值等)会被保存在一个新的栈帧中。当达到基本情况时,递归开始“回溯”,即开始从最内层的调用返回到最初的调用点。在这个过程中,每个栈帧中的状态被恢复,直到最初的调用完成。

递归的优缺点

  • 优点
    • 代码简洁:递归可以使一些问题的解决方案非常简洁和直观,特别是对于那些具有明显递归结构的问题。
    • 易于理解:对于一些问题,递归的解决方案更容易理解和推导,因为它通常与问题的自然描述非常接近。
  • 缺点
    • 性能问题:递归调用会占用大量的栈空间,对于较大的问题规模,可能会导致栈溢出错误。此外,递归调用通常比迭代解决方案效率低,因为每次递归调用都需要一些额外的开销。
    • 如果没有正确设置基本情况,可能会导致无限递归。

递归的优化思路

  • 尾递归优化:尾递归是指递归调用是函数执行的最后一个操作。在这种情况下,编译器可以在某些编程语言中优化递归调用,使其不会增加新的栈帧,从而节省内存。
  • 记忆化(Memoization):记忆化是一种缓存技术,通过将已经计算过的结果存储起来,避免重复计算。这在解决动态规划问题时特别有用。
  • 迭代替代:有时,将递归转换为迭代可以显著提高性能,因为迭代不需要多次函数调用,也不会增加栈帧。
  • 递归深度限制:例如在Python等语言中,默认的递归深度是有限的(通常是1000)。如果需要更深的递归,可以调整递归深度限制,但要注意这可能会导致栈溢出。

反转字符串(0344)

class Solution {
    public void reverseString(char[] s) {
        int left=0,right=s.length-1;
        for(left=0;left<s.length/2;left++,right--){
            char letter;
            letter=s[left];
            s[left]=s[right];
            s[right]=letter;
        }
        return;
    }
}

杨辉三角①(0118)

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> result = new ArrayList<>();
        if (numRows == 1) {
            List<Integer> list = new ArrayList<>();
            list.add(1);
            result.add(list);
            return result;
        } else if (numRows == 2) {
            List<Integer> list1 = new ArrayList<>();
            list1.add(1);
            List<Integer> list2 = new ArrayList<>();
            list2.add(1);
            list2.add(1);
            result.add(list1);
            result.add(list2);
            return result;
        } else {
            List<Integer> list1 = new ArrayList<>();
            list1.add(1);
            result.add(list1);

            List<Integer> list2 = new ArrayList<>();
            list2.add(1);
            list2.add(1);
            result.add(list2);

            for (int i = 2; i < numRows; i++) {
                List<Integer> list = new ArrayList<>();
                list.add(1);
                for (int j = 1; j < i; j++) {
                    list.add(result.get(i - 1).get(j - 1) + result.get(i - 1).get(j));
                }
                list.add(1);
                result.add(list);
            }
            return result;
        }
    }
}

杨辉三角②(0119)

class Solution {
    public List<Integer> getRow(int rowIndex) {
        List<List<Integer>> result = new ArrayList<>();
        
        List<Integer> list1 = new ArrayList<>();
        list1.add(1);
        result.add(list1);
        if (rowIndex ==0) {
                return result.get(rowIndex);
        }
        List<Integer> list2 = new ArrayList<>();
        list2.add(1);
        list2.add(1);
        result.add(list2);
        if (rowIndex ==1) {
                return result.get(rowIndex);
        }
        for (int i = 2; i <= rowIndex; i++) {
                List<Integer> list = new ArrayList<>();
                list.add(1);
                for (int j = 1; j < i; j++) {
                    list.add(result.get(i - 1).get(j - 1) + result.get(i - 1).get(j));
                }
                list.add(1);
                result.add(list);
        }
        return result.get(rowIndex);
    }
}

斐波那契数列(0509)

class Solution {
    public int fib(int n) {
        if(n==0){
            return 0;
        }else if(n==1){
            return 1;
        }else {
            List<Integer> list =new ArrayList<>();
            list.add(0);
            list.add(1);
            for(int i=2;i<=n;i++){
                list.add(list.get(i-2)+list.get(i-1));
            }
            return list.get(n);
        }
    }
}

反转链表(0206)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev =null;
        ListNode current =head;
        while(current!=null){
            ListNode next =current.next;
            current.next =prev;
            prev = current;
            current=next;
        }
        return prev;
    }
}

合并两个有序链表(0021)

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode virtualNode=new ListNode(-1);
        ListNode cur=virtualNode;
        while(list1!=null&&list2!=null){
            if(list1.val<list2.val){
                cur.next=list1;
                list1=list1.next;
            }else if(list1.val>=list2.val){
                cur.next=list2;
                list2=list2.next;
            }
            cur=cur.next;
        }
        if(list1!=null){
            cur.next=list1;
        }else{
            cur.next=list2;
        }
        return virtualNode.next;
    }
}

爬楼梯(0070)

class Solution {
    public int climbStairs(int n) {
        if (n <= 1) {
            return 1;
        }
        int prev1 = 1, prev2 = 1; 
        for (int i = 2; i <= n; i++) {
            int current = prev1 + prev2;
            prev2 = prev1;
            prev1 = current;
        }
        return prev1;
    }
}

破冰游戏(LCR87)

# 列表
class Solution {
    public int iceBreakingGame(int num, int target) {
        // 初始化一个列表来表示成员
        List<Integer> members = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            members.add(i);
        }

        int index = 0; // 从0号成员开始计数

        // 循环直到只剩下一个成员
        while (members.size() > 1) {
            // 计算下一个需要离开的成员位置
            index = (index + target - 1) % members.size();
            // 移除该成员
            members.remove(index);
        }

        // 返回最后一个剩下的成员编号
        return members.get(0);
    }
}
# 超时
class Solution {
    public int iceBreakingGame(int num, int target) {
        // Initialize an array to keep track of whether a member is still in the game
        boolean[] person = new boolean[num];
        // Initialize variables
        int count = 0; // Number of members who have left the game
        int index = 0; // Current index in the circle
        // Continue until only one member is left
        while (count < num - 1) {
            // Count target members who are still in the game
            int step = 0;
            while (step < target) {
                if (!person[index]) {
                    step++;
                }
                if (step == target) {
                    break;
                }
                index = (index + 1) % num;
            }
            // Mark the member at the current index as having left the game
            person[index] = true;
            count++;
            // Move to the next member
            index = (index + 1) % num;
        }
        // Find the last remaining member
        for (int i = 0; i < num; i++) {
            if (!person[i]) {
                return i;
            }
        }
        // This line should never be reached
        return -1;
    }
}

何为分治

“分”有分开、划分的意思;
“治”则有治理、管理的意思。
那么“分治”就是通过将一个整体划分为多个部分,然后分别对这些部分进行管理和处理,最终达到对整个系统或问题的有效治理和解决

什么是分治
分治(Divide and Conquer)是一种重要的算法设计策略。它的核心思想是将一个大问题分解为若干个规模较小、相互独立且与原问题形式相同的子问题,然后分别解决这些子问题,最后将子问题的解合并起来得到原问题的解。
打个比方,假如你要整理一个非常杂乱的大房间,你可以把这个大任务分成几个小任务,比如先整理书架,再整理衣柜,然后整理桌子等。每个小任务相对于整理整个大房间来说要简单得多,当你把这些小任务都完成后,整个房间也就整理好了。

基本原理

  • 分解(Divide)
    • 将原问题分解为若干个规模较小的子问题。这些子问题的形式通常与原问题相同,只是规模更小。
    • 分解的方式取决于具体的问题,但一般要保证子问题之间相互独立,即解决一个子问题不会影响其他子问题的解。
    • 例如,在归并排序中,将一个数组分成两个子数组,每个子数组的排序问题与原数组的排序问题形式相同,只是规模减半。
  • 解决(Conquer)
    • 对每个子问题进行求解。如果子问题足够小,可以直接求解,这被称为基本情况(base case)。
    • 如果子问题仍然较大,可以继续使用分治策略将其进一步分解为更小的子问题,直到达到基本情况。
    • 例如,在计算整数数组的和时,如果数组只有一个元素,那么这个元素就是数组的和,这是基本情况。如果数组有多个元素,可以将数组分成两个子数组,分别计算两个子数组的和,然后将两个子数组的和相加得到原数组的和。
  • 合并(Combine)
    • 将子问题的解合并起来得到原问题的解。合并的过程可能需要根据问题的具体情况进行特定的操作。
    • 例如,在归并排序中,将两个已排序的子数组合并成一个有序的数组。

优缺点

  • 优点
    • 简化问题:通过将大问题分解为小问题,可以使问题变得更容易理解和处理。
    • 提高效率:对于某些问题(比如一些具有递归结构的问题,),分治算法能够提供比其他算法更高的效率,尤其是在处理大规模数据时。
    • 易于实现并行化:由于子问题是相互独立的,因此可以很容易地在多核处理器或多台计算机上并行执行,进一步加速计算过程。
  • 局限性
    • 额外的空间开销:分解和合并子问题的过程可能需要额外的时间和空间开销,特别是当子问题的规模较大时。
    • 递归调用的成本:分治算法通常需要递归调用,这可能会导致栈空间的消耗较大,对于大规模问题可能会出现栈溢出的情况。

多数元素(0169)

class Solution {
    public int majorityElement(int[] nums) {
        HashMap<Integer, Integer> list = new HashMap<>();
        for (int num : nums) {
            if (list.containsKey(num)) {
                list.put(num, list.get(num) + 1);
            } else {
                list.put(num, 1);
            }
        }
        int majorityElement = nums[0];
        int maxCount = 0;
        for (Map.Entry<Integer, Integer> elem : list.entrySet()) {
            if (elem.getValue() > maxCount) {
                maxCount = elem.getValue();
                majorityElement = elem.getKey();
            }
        }
        return majorityElement;
    }
}

Pow(x,n)(0050)

# 时间超限
class Solution {
    public double myPow(double x, int n) {
        if(x==0){
            return 0.0;
        }
        if(n==0){
            return 1.0;
        }
         if(n<0){
            return 1/myPow(x,-n);
        }
         if(n%2==0){
            double half=myPow(x,n/2);
        }
        return x*myPow(x,n-1);
    }
}
# 位运算
class Solution {
    public double myPow(double x, int n) {
        if(x==0){
            return 0.0;
        }
        if(n==0){
            return 1.0;
        }
         if(n<0){
            x=1/x;
            n=-n;
        }
        double result=1.0;
        while(n>0){
            if(n%2==1){
                result*=x;
            }
            x=x*x;
            n=n/2;
        }
        return result;
    }
}

何为回溯

“回”指的是返回、倒退,意味着从当前位置或状态返回到之前的某个位置或者状态
“溯”在此处意思是追溯、追寻,表示沿着某种路径或线索倒回去寻找或验证
“回溯”综合起来就是“返回并追溯

和DFS算法类似,但是回溯算法更复杂一些。回溯算法的搜索过程是递归的,每次选择一个解,然后递归地尝试下一个解,直到找到一个满足条件的解或者所有的解都被尝试过。

回溯的基本思想

  • 选择:从问题的所有可能解中选择一个。
  • 试探:根据选择的解进行尝试,看是否满足问题的约束条件。
  • 递归:如果当前的选择满足了所有的约束条件,那么就继续选择下一个解;如果不满足,则回溯到上一步,改变选择。
  • 结束:当所有可能的解都已经被尝试过,或者找到了满足条件的解时,算法结束。

实现步骤

  • 定义解空间:首先,需要确定问题的解空间,也就是所有可能解的集合。这通常是通过定义一个递归函数来完成的,该函数表示了解空间中的每一个节点。
  • 选择候选解:在解空间中选择一个候选解进行尝试。这通常涉及到对当前节点的所有子节点进行迭代。
  • 递归探索:如果候选解满足问题的约束条件,则将此解加入部分解中,并递归地探索下一层的解。
  • 撤销选择:如果当前的选择无法产生可行解(即达到了解空间的边界,或者违反了约束条件),则撤销选择,回溯到上一状态,尝试其他候选解。
  • 终止条件:当达到解空间的某个叶节点时,检查是否找到了一个完整的解。如果是,则记录下来;如果不是,则继续回溯。

实现优化技巧

  • 剪枝:在搜索过程中,如果发现当前的部分解不可能成为最终解的一部分,就可以立即停止对该路径的探索,从而节省计算资源。
  • 状态标记:回溯算法通常会构建一个状态标记存在,每个节点代表一个可能的状态,边表示从一个状态转移到另一个状态。
  • 递归与非递归实现:虽然回溯算法可以通过递归自然地实现,但是也可以通过使用栈等数据结构来实现非递归版本。

全排列①(0046)

class Solution {
    List<List<Integer>> result = new LinkedList<>();

    public List<List<Integer>> permute(int[] nums) {
        LinkedList<Integer> track =new LinkedList<>();
        // backTrack(nums,track);
        DFS(nums,new boolean[nums.length],new LinkedList<>());
        return result;

    }
    //方法一
    public void backTrack(int[] nums,LinkedList<Integer> track){
        if(track.size()==nums.length){
            result.add(new LinkedList<>(track));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(track.contains(nums[i])){
                continue;
            }
            track.add(nums[i]);
            backTrack(nums,track);
            track.removeLast();
        }
    }
    //方法二
    public void DFS(int[] nums,boolean[] visited,LinkedList<Integer> track){
        if(track.size()==nums.length){
            result.add(new LinkedList<>(track));
            return ;
        }
        //遍历nums数组,发现未被使用的数字,对其标记为使用,并加入stack
        for(int i=0;i<nums.length;i++){
            if(!visited[i]){
                track.push(nums[i]);
                visited[i]=true;
                DFS(nums,visited,track);
                visited[i]=false;
                track.pop();
            }
        }
    }
}

全排列②(0047)

import java.util.*;

class Solution {
    List<List<Integer>> result = new LinkedList<>();
    int[][] newNums;

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums); // Sort the array to handle duplicates
        newNums = new int[nums.length][2];
        for (int i = 0; i < nums.length; i++) {
            newNums[i][0] = nums[i];
            newNums[i][1] = 0;
        }
        LinkedList<Integer> track = new LinkedList<>();
        backTrack(nums, track);
        return result;
    }

    public void backTrack(int[] nums, LinkedList<Integer> track) {
        if (track.size() == nums.length) {
            result.add(new LinkedList<>(track));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            // Skip used elements or duplicates
            if (newNums[i][1] == 1 || (i > 0 && nums[i] == nums[i - 1] && newNums[i - 1][1] == 0)) {
                continue;
            }
            track.add(nums[i]);
            newNums[i][1] = 1;
            backTrack(nums, track);
            track.removeLast();
            newNums[i][1] = 0; // Reset the state
        }
    }
}
class Solution {

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> result = new LinkedList<>();
        DFS(nums,new boolean[nums.length],new LinkedList<>(),result);
        return result;
    }
    public void DFS (int[] nums,boolean[] visited,LinkedList<Integer> stack,List<List<Integer>> result){
        if(stack.size()==nums.length){
            result.add(new ArrayList<>(stack));
            return ;
        }
        for(int i=0;i<nums.length;i++){
            if(i>0&&nums[i]==nums[i-1]&&!visited[i-1]){
                continue;
            }
            if(!visited[i]){
                stack.push(nums[i]);
                visited[i]=true;
                DFS(nums,visited,stack,result);
                visited[i]=false;
                stack.pop();
            }
        }
    }
}

组合(0077)

class Solution {
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new LinkedList<>();
        DFS(1,n,k,new LinkedList<>(),result);
        return result;
    }
    public void DFS(int start,int n,int k,LinkedList<Integer> stack,List<List<Integer>> result){
        if(stack.size()==k){
            result.add(new ArrayList<>(stack));
            return;
        }
        for(int i=start;i<=n;i++){
            //优化:剪枝->缺的数字要小于备用数字
            if(k-stack.size()>n-i+1){
                continue;
            }
            //
            stack.push(i);
            DFS(i+1,n,k,stack,result);
            stack.pop();
        }
    }
}

组合总和①(0039)

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> result =new ArrayList<>();
        DFS(0,candidates,target,new LinkedList<>(),result);
        return result;
    }
    public void DFS(int start,int[] candidates,int target,LinkedList<Integer> stack,List<List<Integer>> result){
        if(target<0){
            return;
        }else if(target==0){
            result.add(new ArrayList<>(stack));
            return;
        }
        for(int i=start;i<candidates.length;i++){
            int candidate=candidates[i];
            //优化:剪枝->距离目标差的的数字要大于备用数字
            if(target<candidate){
                continue;
            }
            //
            stack.push(candidate);
            DFS(i,candidates,target-candidate,stack,result);
            stack.pop();
        }
    }
}

组合总和②(0040)

class Solution {
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);
        List<List<Integer>> result =new ArrayList<>();
        DFS(0,candidates,target,new boolean[candidates.length],new LinkedList<>(),result);
        return result;
    }
    public void DFS(int start,int[] candidates,int target,boolean[] visited,LinkedList<Integer> stack,List<List<Integer>> result){
        if(target<0){
            return;
        }else if(target==0){
            result.add(new ArrayList<>(stack));
            return;
        }
        for(int i=start;i<candidates.length;i++){
            int candidate=candidates[i];
            //优化:剪枝->距离目标差的的数字要大于备用数字
            if(target<candidate){
                continue;
            }
            //
            if(i>0&&candidate==candidates[i-1]&&!visited[i-1]){
                continue;
            }
            visited[i]=true;
            stack.push(candidate);
            DFS(i+1,candidates,target-candidate,visited,stack,result);
            stack.pop();
            visited[i]=false;
        }
    }
}

组合总和③(0216)

class Solution {
    public List<List<Integer>> combinationSum3(int k, int target) {
        List<List<Integer>> result = new LinkedList<>();
        DFS(1,target,k,new LinkedList<>(),result);
        return result;
    }
    public void DFS(int start,int target,int k,LinkedList<Integer> stack,List<List<Integer>> result){
        if(target==0&&stack.size()==k){
            result.add(new ArrayList<>(stack));
            return;
        }
        for(int i=start;i<=9;i++){
            //优化:剪枝->缺的数字要小于备用数字
            if(k-stack.size()>9-i+1){
                continue;
            }
            if(target<i){
                continue;
            }
            if(stack.size()==k){
                continue;
            }
            //
            stack.push(i);
            DFS(i+1,target-i,k,stack,result);
            stack.pop();
        }
    }
}

N皇后①(0051)

class Solution {
    List<List<String>> result = new LinkedList<>();

    public List<List<String>> solveNQueens(int n) {
        // y
        boolean[] lie = new boolean[n];
        // x+y
        // 0 1 2 3
        // 1 2 3 4
        // 2 3 4 5
        // 3 4 4 6
        boolean[] zuoXie = new boolean[2 * n - 1];
        // n-1-(x-y)
        // 0 -1 -2 -3 3 4 5 6
        // 1 0 -1 -2 2 3 4 5
        // 2 1 0 -1 1 2 3 4
        // 3 2 1 0 0 1 2 3
        boolean[] youXie = new boolean[2 * n - 1];
        char[][] table = new char[n][n];
        // 初始化
        for (char[] t : table) {
            Arrays.fill(t, '.');
        }
        DFS(0, n, table, lie, zuoXie, youXie);
        return result;

    }

    public void DFS(int x, int n, char[][] table, boolean[] lie, boolean[] zuoXie, boolean[] youXie) {
        if (x == n) {
            List<String> onceList =new ArrayList<>();
            for (char[] row : table) {
                onceList.add(new String(row));
            }
            result.add(onceList);
            return;
        }
        for (int y = 0; y < n; y++) {
            if (lie[y] || zuoXie[x + y] || youXie[n - 1 - (x - y)]) {
                continue;
            }
            table[x][y] = 'Q';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = true;
            DFS(x + 1, n, table, lie, zuoXie, youXie);
            table[x][y] = '.';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = false;
        }
    }
}

N皇后②

class Solution {
    List<List<String>> result = new LinkedList<>();
    int count=0;
    public int totalNQueens(int n) {
        // y
        boolean[] lie = new boolean[n];
        // x+y
        // 0 1 2 3
        // 1 2 3 4
        // 2 3 4 5
        // 3 4 4 6
        boolean[] zuoXie = new boolean[2 * n - 1];
        // n-1-(x-y)
        // 0 -1 -2 -3 3 4 5 6
        // 1 0 -1 -2 2 3 4 5
        // 2 1 0 -1 1 2 3 4
        // 3 2 1 0 0 1 2 3
        boolean[] youXie = new boolean[2 * n - 1];
        char[][] table = new char[n][n];
        // 初始化
        for (char[] t : table) {
            Arrays.fill(t, '.');
        }
        DFS(0, n, table, lie, zuoXie, youXie);
        return count;

    }

    public void DFS(int x, int n, char[][] table, boolean[] lie, boolean[] zuoXie, boolean[] youXie) {
        if (x == n) {
            // List<String> onceList =new ArrayList<>();
            // for (char[] row : table) {
            //     onceList.add(new String(row));
            // }
            // result.add(onceList);
            count++;
            return;
        }
        for (int y = 0; y < n; y++) {
            if (lie[y] || zuoXie[x + y] || youXie[n - 1 - (x - y)]) {
                continue;
            }
            table[x][y] = 'Q';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = true;
            DFS(x + 1, n, table, lie, zuoXie, youXie);
            table[x][y] = '.';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = false;
        }
    }
}

八皇后(面试题 08.12)

class Solution {
    List<List<String>> result = new LinkedList<>();

    public List<List<String>> solveNQueens(int n) {
        // y
        boolean[] lie = new boolean[n];
        // x+y
        // 0 1 2 3
        // 1 2 3 4
        // 2 3 4 5
        // 3 4 4 6
        boolean[] zuoXie = new boolean[2 * n - 1];
        // n-1-(x-y)
        // 0 -1 -2 -3 3 4 5 6
        // 1 0 -1 -2 2 3 4 5
        // 2 1 0 -1 1 2 3 4
        // 3 2 1 0 0 1 2 3
        boolean[] youXie = new boolean[2 * n - 1];
        char[][] table = new char[n][n];
        // 初始化
        for (char[] t : table) {
            Arrays.fill(t, '.');
        }
        DFS(0, n, table, lie, zuoXie, youXie);
        return result;

    }

    public void DFS(int x, int n, char[][] table, boolean[] lie, boolean[] zuoXie, boolean[] youXie) {
        if (x == n) {
            List<String> onceList =new ArrayList<>();
            for (char[] row : table) {
                onceList.add(new String(row));
            }
            result.add(onceList);
            return;
        }
        for (int y = 0; y < n; y++) {
            if (lie[y] || zuoXie[x + y] || youXie[n - 1 - (x - y)]) {
                continue;
            }
            table[x][y] = 'Q';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = true;
            DFS(x + 1, n, table, lie, zuoXie, youXie);
            table[x][y] = '.';
            lie[y] = zuoXie[x + y] = youXie[n - 1 - (x - y)] = false;
        }
    }
}

子集(0078)

class Solution {
    List<List<Integer>> result =new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        LinkedList<Integer> track = new LinkedList<>();
        DFS(nums,0,track);
        return result;
    }
    public void DFS(int[] nums,int start,LinkedList<Integer> track){
        result.add(new LinkedList<>(track));
        for(int i=start;i<nums.length;i++){
            track.push(nums[i]);
            DFS(nums,i+1,track);
            track.pop();
        }
    }
}

何为贪心

贪心算法的概念:
贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下的最优决策的算法设计策略。它期望通过一系列局部最优选择,最终达到全局最优解。
但要注意的是,贪心算法并不总是能得到全局最优解,不过在一些特定问题中它非常高效。
贪心算法的特点

  • 简单直观:贪心算法通常比较容易理解和实现。它的基本思想就是在每一步都做出看似最好的选择,不需要考虑太多复杂的情况。
    例如,在找零钱的问题中,如果要找给顾客一定金额的零钱,我们可以每次都选择面值最大的硬币,这就是一种贪心策略。
  • 高效性:由于贪心算法的简单性,它通常运行速度很快。对于一些规模较小或者有特殊结构的问题,贪心算法可以在很短的时间内给出解决方案。
    比如在一些任务调度问题中,如果我们按照任务的截止时间或者收益等因素进行贪心选择,可以快速地得到一个可行的调度方案。
  • 局部最优选择:贪心算法在每一步都会做出当时看起来最好的选择,而不是考虑未来的后果。这种选择是基于当前问题的状态,而不会考虑下一步之后的情况。
  • 不可回溯性:一旦做出选择,就不会再改变。这意味着贪心算法不会尝试不同的路径来寻找可能更好的解决方案。

工作原理:

  • 确定问题的最优子结构:贪心算法通常依赖于问题具有最优子结构性质。这意味着一个问题的最优解可以由其子问题的最优解组合而成。
    例如,在背包问题中,如果我们知道如何选择前 n-1 个物品的最优组合,那么在考虑第 n 个物品时,我们可以根据这个子问题的解来做出当前的最优选择。
  • 设计贪心策略:找到一个合适的贪心策略是贪心算法的关键。贪心策略是在每一步选择中做出的局部最优决策规则。
    比如在活动选择问题中,我们可以按照活动的结束时间进行排序,然后每次都选择结束时间最早且与已选活动不冲突的活动,这就是一种贪心策略。
  • 证明贪心策略的正确性:对于一些问题,我们需要证明贪心策略确实能够得到全局最优解。这通常需要一些数学推理或者通过反证法来证明。
    然而,并不是所有的贪心算法都能保证得到全局最优解,所以在使用贪心算法时,我们需要谨慎地分析问题的性质和贪心策略的有效性。

局限性:
尽管贪心算法在某些情况下非常有效,但它并不适用于所有优化问题。有些问题需要全局考虑才能找到最优解,这时贪心算法可能会得到次优解。例如,在某些情况下,贪心算法可能会过早地选择一个看似好的选项,从而错过了最终的最优解。


零钱兑换①(0322)

# 正序暴力递归(时间超限)
import java.util.Arrays;

class Solution {
    private int minCount;

    public int coinChange(int[] coins, int amount) {
        // Reset minCount for each call
        minCount = Integer.MAX_VALUE;
        Arrays.sort(coins);
        exChange(0, coins, amount, 0);
        return minCount == Integer.MAX_VALUE ? -1 : minCount;
    }

    public void exChange(int index, int[] coins, int reminder, int count) {
        if (reminder == 0) {
            minCount = Math.min(minCount, count);
            return;
        }
        for (int i = index; i < coins.length; i++) {
            if (coins[i] <= reminder) {
                exChange(i, coins, reminder - coins[i], count + 1);
            } else {
                break; // Since coins are sorted, no need to check further
            }
        }
    }
}
# 贪心算法(局部最优解)
import java.util.Arrays;

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 对硬币数组进行排序,方便从大到小选择硬币
        Arrays.sort(coins);
        int count = 0;

        // 从面值最大的硬币开始选择
        for (int i = coins.length - 1; i >= 0; i--) {
            // 使用尽可能多的当前面值的硬币
            while (amount > coins[i]) {
                amount -= coins[i];
                count++;
            }
            if(amount==coins[i]){
                amount=0;
                count++;
                break;
            }
        }

        // 如果最后无法凑成金额,返回-1
        if (amount > 0) {
            return -1;
        } else {
            return count;
        }
    }
}
# 动态规划
class Solution {
    public int coinChange(int[] coins, int amount) {
        // 创建一个大小为 (coins.length + 1) x (amount + 1) 的二维数组 dp
        int[][] dp = new int[coins.length + 1][amount + 1];
        // 初始化 dp 数组
        for (int i = 0; i <= coins.length; i++) {
            for (int j = 0; j <= amount; j++) {
                if (j == 0) {
                    dp[i][j] = 0; // 金额为 0 时,硬币数为 0
                } else {
                    dp[i][j] = amount + 1; // 初始化为一个较大值
                }
            }
        }
        // 填充 dp 数组
        for (int i = 1; i <= coins.length; i++) {
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i - 1]) {
                    // 如果当前金额 j 大于等于当前硬币面额 coins[i-1]
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
                } else {
                    // 如果当前金额 j 小于当前硬币面额 coins[i-1]
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        // 如果 dp[coins.length][amount] 的值大于 amount,说明无法组成该金额
        return dp[coins.length][amount] > amount ? -1 : dp[coins.length][amount];
    }
}

零钱兑换②(0518)

# 正序暴力递归(时间超限)
class Solution {
    public int change(int amount, int[] coins) {
        return exChange(0,coins,amount);
    }
    public int exChange(int index,int[] coins,int reminder){
        if(reminder<0){
            return 0;
        }else if(reminder==0){
            return 1;
        }else{
            int count =0;
            for(int i=index;i<coins.length;i++){
                count+=exChange(i,coins,reminder-coins[i]);
            }
            return count;
        }
    }
}
# 倒序暴力递归(时间超限)
class Solution {
    public int change(int amount, int[] coins) {
        return exChange(coins.length-1,coins,amount);
    }
    public int exChange(int index,int[] coins,int reminder){
        if(reminder<0){
            return 0;
        }else if(reminder==0){
            return 1;
        }else{
            int count =0;
            for(int i=index;i>=0;i--){
                count+=exChange(i,coins,reminder-coins[i]);
            }
            return count;
        }
    }
}

零钱兑换(LCR103)

# 贪心算法(局部最优解)
import java.util.Arrays;

class Solution {
    public int coinChange(int[] coins, int amount) {
        // 对硬币数组进行排序,方便从大到小选择硬币
        Arrays.sort(coins);
        int count = 0;

        // 从面值最大的硬币开始选择
        for (int i = coins.length - 1; i >= 0; i--) {
            // 使用尽可能多的当前面值的硬币
            while (amount >= coins[i]) {
                amount -= coins[i];
                count++;
            }
        }

        // 如果最后无法凑成金额,返回-1
        if (amount > 0) {
            return -1;
        } else {
            return count;
        }
    }

}
# 动态规划
class Solution {
    public int coinChange(int[] coins, int amount) {
        // 创建一个大小为 (coins.length + 1) x (amount + 1) 的二维数组 dp
        int[][] dp = new int[coins.length + 1][amount + 1];
        // 初始化 dp 数组
        for (int i = 0; i <= coins.length; i++) {
            for (int j = 0; j <= amount; j++) {
                if (j == 0) {
                    dp[i][j] = 0; // 金额为 0 时,硬币数为 0
                } else {
                    dp[i][j] = amount + 1; // 初始化为一个较大值
                }
            }
        }
        // 填充 dp 数组
        for (int i = 1; i <= coins.length; i++) {
            for (int j = 1; j <= amount; j++) {
                if (j >= coins[i - 1]) {
                    // 如果当前金额 j 大于等于当前硬币面额 coins[i-1]
                    dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i - 1]] + 1);
                } else {
                    // 如果当前金额 j 小于当前硬币面额 coins[i-1]
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        // 如果 dp[coins.length][amount] 的值大于 amount,说明无法组成该金额
        return dp[coins.length][amount] > amount ? -1 : dp[coins.length][amount];
    }
}

位运算

什么是位运算
在计算机中,所有的数据都是以二进制的形式存储的
位运算是针对整数在二进制位级别上进行的运算操作,而不是对数值进行传统的算术运算。

  • 按位与(&)
    • 如果两个相应的二进制位都为1,则结果位为1;否则为0。
    • 例如:5 & 3(即 0101 & 0011)的结果是 0001,即1。
  • 按位或(|)
    • 如果两个相应的二进制位中至少有一个为1,则结果位为1;否则为0。
    • 例如:5 | 3(即 0101 | 0011)的结果是 0111,即7。
  • 按位异或(^)
    • 如果两个相应的二进制位不同,则结果位为1;如果相同,则结果位为0。
    • 例如:5 ^ 3(即 0101 ^ 0011)的结果是 0110,即6。
  • 按位取反(~)
    • 将二进制数的每一位取反,0 变为 1,1 变为 0。
    • 例如,~5(二进制为 0101),结果为 -6(在计算机中以补码形式存储,二进制为 1010,考虑有符号整数的补码表示)。
  • 左移(<<)
    • 将二进制表示向左移动指定的位数,右边空出的位置用0填充。
    • 例如:5 << 1(即 0101 << 1)的结果是 1010,即10,也就是乘了2的1次方。
  • 右移(>>)
    • 将二进制表示向右移动指定的位数,左边空出的位置根据数字的符号位填充,对于正数填0,负数填1。
    • 例如:5 >> 1(即 0101 >> 1)的结果是 0010,即2,也就是除以2取整。
  • 无符号右移(>>>)
    • 无论正负数,都将二进制表示向右移动指定的位数,左边空出的位置用0填充。
    • 例如:-5的32位二进制的补码是: 11111111111111111111111111111011 11111111 11111111 11111111 11111011 11111111111111111111111111111011
      • 右移1位:-5 >>> 1(即 -0101 >>> 1) 01111111111111111111111111111101 01111111 11111111 11111111 11111101 01111111111111111111111111111101结果: 2147483643 2147483643 2147483643
      • 右移31位:-5 >>> 31 00000000000000000000000000000001 00000000 00000000 00000000 00000001 00000000000000000000000000000001结果: 1 1 1
      • 右移32位:-5 >>> 32 11111111111111111111111111111011 11111111 11111111 11111111 11111011 11111111111111111111111111111011结果: − 5 -5 5

七进制数(0504)

class Solution {
    public String convertToBase7(int num) {
        StringBuilder sb =new StringBuilder();
        boolean isNegative=false;

        if(num==0){
            return "0";
        }else if(num<0){
            isNegative = true;
            num=-num;
        }

        while(num>0){
            sb.insert(0,num%7);
            num=num/7;
        }
        if(isNegative){
            sb.insert(0,'-');

        }
        return sb.toString();
    }
}

数字转换为字符串(0405)

class Solution {

    public String toHex(int num) {
        if (num == 0) {
            return "0";
        }

        char[] hexChars = "0123456789abcdef".toCharArray();
        StringBuilder sb = new StringBuilder();

        // 处理负数的情况
        for (int i = 0; i < 8 && num != 0; i++) {
            sb.insert(0, hexChars[num & 0xF]);
            num >>>= 4;
        }

        return sb.toString();
    }
}

颠倒二进制位(0190)

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
        if(n==0){
            return 0;
        }
        String numStr = Integer.toBinaryString(n);
        StringBuilder num = new StringBuilder(numStr);
           // 如果二进制字符串长度小于 32,在前面补 0 使其长度为 32
        while (num.length() < 32) {
            num.insert(0, "0");
        }
        String revNum = num.reverse().toString();
        Long finalNum = Long.parseLong(revNum,2);
        return finalNum.intValue();
    }
}

十进制整数的反码(1009)

class Solution {
    public int bitwiseComplement(int n) {
        if(n==0){
            return 1;
        }
        StringBuilder numStr = new StringBuilder();
        while(n>0){
            numStr.insert(0,n%2==0?'1':'0');
            n>>=1;
        }
        int number=0;
        for(int i=0;i<numStr.length();i++){
            if(numStr.charAt(i)=='1'){
                number+=powerNum(numStr.length()-1-i);

            }
        }
        return number;
    }
    public int powerNum(int n){
        return 1 << n;
    }
}
# 反码与掩码的神奇运算
class Solution {
    public int bitwiseComplement(int n) {
        // 如果输入为 0,其按位取反是 1
        if (n == 0) {
            return 1;
        }
        int temp = n;
        int mask = 0;
        // 循环用于计算掩码
        while (temp > 0) {
            // 将 temp 右移一位,逐步检查每一位
            temp >>= 1;
            // mask 左移一位再或上 1,不断构建全为 1 的掩码
            mask = (mask << 1) | 1;
        }
        // 先对输入 n 按位取反,再与掩码进行与操作,得到最终的按位补码结果
        return ~n & mask;
    }
}

位1的个数(0191)

# 直接调用函数
class Solution {
    public int hammingWeight(int n) {
        return Integer.bitCount(n);
    }
}
# 有趣的小方法
class Solution {
    public int hammingWeight(int n) {
        int count = 0;
        while (n > 0) {
            count += n & 1;
            n >>= 1;
        }
        return count;
    }
}

位1的个数(LCR133)

# 直接调用函数
class Solution {
    public int hammingWeight(int n) {
        return Integer.bitCount(n);
    }
}

比特位计数(0338)

class Solution {
    public int[] countBits(int n) {
        int[] count = new int[n+1];
        count[0]=0;
        for(int i=1;i<=n;i++){
            count[i]=Integer.bitCount(i);
        }
        return count;
    }
}

比特位计数(LCR003)

class Solution {
    public int[] countBits(int n) {
        int[] count = new int[n+1];
        count[0]=0;
        for(int i=1;i<=n;i++){
            count[i]=Integer.bitCount(i);
        }
        return count;
    }
}

丢失的数字(0268)

class Solution {
    public int missingNumber(int[] nums) {
        int[] count = new int[nums.length+1];
        for(int i=0;i<nums.length;i++){
            count[nums[i]]=1;
        }
        int fin=0;
        for(int i=0;i<=nums.length;i++){
            if(count[i]==0){
                fin=i;
                break;
            }
        }
        return fin;
    }
}

错误的集合(0645)

public class Solution {
    public int[] findErrorNums(int[] nums) {
        int n = nums.length;
        int[] result = new int[2];
        boolean[] seen = new boolean[n + 1];
        
        // 遍历数组,标记出现过的数字
        for (int num : nums) {
            if (seen[num]) {
                result[0] = num; // 重复的数字
            }
            seen[num] = true;
        }
        
        // 查找未出现的数字
        for (int i = 1; i <= n; i++) {
            if (!seen[i]) {
                result[1] = i; // 缺失的数字
                break;
            }
        }
        
        return result;
    }
}

完结✿✿ヽ(°▽°)ノ✿,后续会继续完善补充……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stars_niu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值