Day25 打卡第二十五天

文章讲述了如何使用回溯算法解决两道编程题目:组合总和III,要求找到所有和为n的k个数的组合,每个数字1到9最多使用一次;电话号码的字母组合,给定数字字符串,返回所有可能的字母组合。在解题过程中,回溯法被用来生成所有可能的路径,同时避免重复和无效的路径。

216. 组合总和 III

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次 

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。
class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void trackbacking(int k , int n , int start, int sum){
        if(path.size() == k){
            if(sum == n) res.push_back(path);
            return ;
        }
        for(int i = start; i <= 9 - (k - path.size()) + 1; i++){
            path.push_back(i);
            sum += i;
            trackbacking(k, n, i + 1, sum);
            sum -= i;
            path.pop_back();
            
        }      
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        trackbacking(k, n, 1, 0);
        return res;
    }
};

index和sum的初始化放在回溯函数的参数中

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
public:
    vector<string> res;
    string s = {};
    const string lettermap[10] = {"","",
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
    }; 
    void backtracking(const string &digits,int index){
        if(index == digits.size()){
            res.push_back(s);
            return ;
        }
        string letter = lettermap[digits[index] - '0'];
        for(int i = 0; i < letter.size(); i++ ){
            s.push_back(letter[i]);
            backtracking(digits, index + 1);
            s.pop_back();
        }

    }

    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return res;
        backtracking(digits,0);
        return res;
    }
};

注意回溯函数的索引是代表回溯的深度,以及单层遍历内容应该是最内层循环所要处理的逻辑。

如下是改进后的,再看看如下代码怎么样: @JvmStatic fun getWorkDayPoints(currTimeMillis: Long, startHour: Int, startMinute: Int, endHour: Int, endMinute: Int, confirmHour: Int, confirmMinute: Int) : WorkingDayTimePoints { val timePoints = WorkingDayTimePoints() val calendar = Calendar.getInstance() calendar.timeInMillis = currTimeMillis var year = calendar[Calendar.YEAR] var month = calendar[Calendar.MONTH] + 1 // 月份0开始 var day = calendar[Calendar.DAY_OF_MONTH] val hour = calendar[Calendar.HOUR_OF_DAY] val minute = calendar[Calendar.MINUTE] // 比较分钟数 val start = startHour * 60 + startMinute val end = endHour * 60 + endMinute val confirm = confirmHour * 60 + confirmMinute // 工作日是否跨? 只要下班点和确认点其中任意一个大于上班点均为跨 if (start > end || start > confirm) { // 00:00 ~ 确认时间之前 工作日算前一 if ((hour * 60 + minute) < confirm) { calendar.add(Calendar.DAY_OF_MONTH, -1) year = calendar[Calendar.YEAR] month = calendar[Calendar.MONTH] + 1 // 月份0开始 day = calendar[Calendar.DAY_OF_MONTH] } } // 工作日 timePoints.workDay = String.format(Locale.getDefault(Locale.Category.FORMAT), "%04d-%02d-%02d", year, month, day) // 工作日对应的毫秒值 calendar.set(year, month - 1, day, 0, 0, 0) val dayTimeMillis = calendar.timeInMillis // 工作日上班打卡点 timePoints.workStartTime = dayTimeMillis + start * 60 * 1000 // 1的毫秒值 val dayMillis = 86400000 // 工作日下班打卡点 timePoints.workEndTime = dayTimeMillis + end * 60 * 1000 + (if (start > end) dayMillis else 0) // 工作日确认打卡点 timePoints.workConfirmTime = dayTimeMillis + confirm * 60 * 1000 + (if (start > confirm) dayMillis else 0) timePoints.currUsePointTime = timePoints.workStartTime if (currTimeMillis >= timePoints.workEndTime) { timePoints.currUsePointTime = timePoints.workEndTime } if (currTimeMillis >= timePoints.workConfirmTime) { timePoints.currUsePointTime = timePoints.workConfirmTime } return timePoints }
09-17
<think> 我们分析改进后的代码: 改进点: 1. 使用calendar.add(Calendar.DAY_OF_MONTH, -1)来安全地减少一(自动处理跨月、跨年) 2. 在减少一后,重新获取year, month, day,保证日期正确 3. 使用calendar.set来设置日期,并获取毫秒值,避免使用LocalDate解析字符串,减少开销 但仍然存在一些问题: 1. **时区问题**:Calendar.getInstance()使用的是默认时区,而代码中设置日期时(calendar.set)也是使用同一个Calendar实例,因此时区一致。但注意,如果默认时区发生变化,可能会有影响。不过,这里没有显式指定时区,但整个计算过程使用同一个Calendar实例,所以内部是一致的。但考虑到不同设备时区设置,最好显式指定时区。 2. **跨判断逻辑**:判断条件`if (start > end || start > confirm)`,然后当当前时间在确认时间之前,就将日期减一。这里有一个假设:跨只可能跨一。如果确认时间在两后,这个逻辑就不对了。不过,根据通常的打卡规则,确认时间一般不会超过24小时,所以可能够用。另外,注意这里的判断:只要下班点或确认点有一个大于上班点,就算跨。然后,在跨的情况下,如果当前时间(小时和分钟)在确认时间之前,就算前一。这个逻辑在确认时间跨(比如凌晨2点)的时候是合理的。但是,如果下班时间跨(比如夜班从晚上8点到第二早上8点),而确认时间不跨(比如下午6点),那么即使当前时间在下班时间之前(但已经是第二凌晨)也不会被减一?这里需要分析: 示例:假设上班时间20:00(start=20*60=1200),下班时间8:00(第二,end=8*60=480),确认时间18:00(confirm=18*60=1080)。 因为start>end(1200>480)所以进入跨分支。 然后判断当前时间是否小于confirm(1080分钟,即18:00)。注意,当前时间是日历中的时间(即实际日期时间)。假设当前时间是第二凌晨2点(即2:00,分钟数为120),那么120<1080,所以会执行减一,得到前一。这是正确的,因为凌晨2点属于前一的工作日(从20:00到第二8:00的工作日)。 但是,如果当前时间是第二20:00(即20:00,分钟数为1200)?那么1200<1080?不成立(1200<1080为假),所以不会减一,那么工作日就被算成第二。然而,第二20:00实际上已经属于下一个工作日了(如果是连续夜班,但这里每个工作日是独立的)。所以这里逻辑需要根据业务来,但通常一个工作日不会超过24小时,所以确认时间一般会在工作日内。因此,这个逻辑可能满足要求。 3. **当前打卡点计算**:`timePoints.currUsePointTime`的计算逻辑:先设为上班时间,然后如果当前时间超过下班时间则设为下班时间,如果超过确认时间则设为确认时间。这里注意,确认时间一般在下班时间之后(比如下班后30分钟确认),所以这个顺序是合理的。但是,如果确认时间在下班时间之前,那么当当前时间超过下班时间,会被设置为下班时间,然后即使超过确认时间也不会再设置为确认时间(因为第二个if条件在第一个if之后,且当前时间已经大于下班时间必然大于确认时间?不一定,如果确认时间在下班时间之后,那么没问题;但如果确认时间在下班时间之前,那么第二个if会覆盖第一个if?因为代码中两个if是独立的,且第二个if在第一个if之后,所以如果当前时间大于确认时间,那么第二个if会覆盖currUsePointTime为确认时间。但是,如果确认时间在下班时间之前,那么下班时间点之后,当前时间肯定大于确认时间(因为下班时间点已经大于确认时间),所以会覆盖为确认时间。这合理吗?需要业务确认。通常,确认时间应该在下班时间之后。所以,这里建议增加注释,明确要求确认时间必须在下班时间之后,或者调整逻辑(例如,按时间顺序判断:先上班,再下班,再确认)。 4. **变量提取**:`start * 60 * 1000` 可以提前计算为毫秒数,避免重复计算多次(虽然这里只用到一次,但end和confirm也都需要转换,可以统一提取一个变量,比如startMillis = start * 60 * 1000L,注意要转成Long,避免溢出)。 5. **常量提取**:`86400000` 可以写成 `24 * 60 * 60 * 1000` 更清晰,或者定义为常量。同时,注意这个值在夏令时切换时可能不是准确的24小时,但一般情况下,我们不考虑夏令时,因为打卡时间点通常是固定的钟表时间。 6. **参数校验**:仍然没有对输入时间参数进行校验(例如,小时超过24,分钟超过60等)。 7. **变量重新赋值**:在跨分支中,对year, month, day进行了重新赋值。但在后面,又用calendar.set(year, month-1, day, 0,0,0)来设置日期。这里其实可以不用重新给year,month,day赋值,因为后面设置calendar的时候已经用到了重新计算的calendar(在add之后)。而且,后面字符串格式化用的year, month, day就是重新赋值后的值,这是正确的。但是,在calendar.set之前,calendar已经被add修改了,然后我们又用year(重新赋值的)、month(重新赋值的)、day(重新赋值的)来设置calendar,这相当于覆盖了calendar的日期部分。这里可以优化:在确定好日期后,直接使用calendar的时间(不需要重新获取year,month,day)来格式化日期字符串,避免重复赋值和设置。 改进建议: 步骤: 1. 使用calendar.add之后,不需要重新获取year,month,day,可以直接使用calendar来格式化日期。 2. 或者,在计算工作日日期时,可以重新构建一个Calendar实例,避免修改原始calendar(因为原始calendar还保存着当前时间的信息?但后面并没有用到原始calendar,所以可以接受)。 但是,代码中在设置calendar.set(year, month-1, day, 0,0,0)之前,calendar已经修改为减一后的日期(如果需要减一)。然后,我们用这个日期(year,month,day)再设置calendar,实际上是将calendar的时间设置为那一的0点。这里注意,year,month,day是从calendar中获取的,而calendar在add之后已经改变,所以这样设置没问题。不过,这里可以更简洁:在确定日期后,直接设置calendar的时间为当的0点,然后获取毫秒值。 另一种做法:在确定工作日日期后,将calendar设置为该日期的0点0分0秒,然后直接从这个calendar获取年月日来格式化字符串,这样就不需要额外的year,month,day变量。 8. **代码冗余**:在跨分支中,重新获取了year,month,day,然后又在后面用这些值去设置calendar,其实可以直接使用calendar的时间来设置workDay字符串。 建议修改: 去掉year,month,day变量,在最后需要获取日期字符串的时候,从calendar中获取(注意,在设置0点之后,calendar的日期就是工作日日期)。 修改步骤: - 去掉year,month,day的声明 - 在调整日期之后(包括减一或者不减),设置calendar的时间为当的0点(calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE,0); ... 或者 calendar.set(year, month-1, day,0,0,0) 但这里我们已经没有这些变量了,所以直接设置小时分钟秒为0) - 然后用calendar.get(Calendar.YEAR)等获取年月日来格式化字符串。 但是注意:在跨分支中,我们使用了calendar.add,所以calendar的日期已经是减一后的日期(如果需要减一)。然后我们再将这个calendar设置为0点,然后获取年月日。 所以,调整后的代码结构: 1. 获取当前时间的calendar 2. 计算start, end, confirm的分钟数 3. 判断是否跨并且当前时间在确认时间之前:如果需要,则calendar.add(Calendar.DAY_OF_MONTH, -1) 4. 设置calendar的时间为0点0分0秒(此时calendar的日期就是工作日的日期) 5. 用calendar获取年月日,格式化workDay字符串 6. 获取dayTimeMillis = calendar.timeInMillis 7. 计算各个时间点(加上分钟数转换的毫秒数,以及如果需要跨再加一毫秒数) 另外,注意设置0点后,calendar的时间就是工作日的0点。 9. **currUsePointTime逻辑**:如第3点所述,需要确保确认时间在下班时间之后,否则逻辑可能出错。或者,可以改为按时间顺序排序三个时间点,然后判断当前时间落在哪个区间。但根据业务,可能打卡点有固定的顺序(上班、下班、确认),所以建议添加注释,要求传入的确认时间必须在下班时间之后。 10. **命名**:`currUsePointTime`仍然意义不明确,建议改为更清晰的名称,例如`currentCheckPointTime`。 总结:改进后的代码解决了日期跨月跨年的问题,但仍然存在一些可读性和健壮性问题。 §§相关问题§§ 1. 在打卡时间计算中,如何确保跨时区(如夏令时)不会导致时间点计算错误? 2. 如果业务要求支持多个确认点(如多次打卡确认),应如何重构代码结构? 3. 为什么在设置calendar为0点后,还要单独加上分钟数来计算打卡点?直接设置具体小时分钟是否更简单? 4. 如何设计单元测试来覆盖跨、跨月、跨年等边界情况? 5. 当打卡时间超过24小时(如连续工作30小时)时,当前逻辑是否能正确计算打卡点? </think> 改进后的代码解决了日期跨月/跨年问题,但仍有优化空间: ### 主要改进 1. **安全日期计算** 使用 `calendar.add(Calendar.DAY_OF_MONTH, -1)` 替代直接 `day--`,正确处理跨月/跨年场景 2. **减少冗余计算** 复用 `start/end/confirm` 分钟数值,避免重复转换 3. **统一时间基准** 通过 `calendar.set()` 直接计算毫秒值,消除字符串解析开销 ### 现存问题 ```kotlin // 问题1:未校验时间参数有效性 val start = startHour * 60 + startMinute // 当startHour=25时会溢出 // 问题2:跨判断逻辑缺陷 if (start > end || start > confirm) { // 未处理 end/confirm 均小于start但间隔超24小时的情况 if ((hour*60 + minute) < confirm) { // 依赖当前时间而非日期 calendar.add(Calendar.DAY_OF_MONTH, -1) } } // 问题3:时区不一致风险 val calendar = Calendar.getInstance() // 默认时区 // 但dayTimeMillis计算未显式指定时区 ``` ### 关键优化建议 1. **增加参数校验** ```kotlin require(startHour in 0..23) { "无效小时数" } require(startMinute in 0..59) { "无效分钟数" } // 同样校验end/confirm参数 ``` 2. **重构跨判断逻辑** ```kotlin val needsAdjust = (start > end || start > confirm) && (hour * 60 + minute) < confirm if (needsAdjust) calendar.add(Calendar.DAY_OF_MONTH, -1) ``` 3. **显式指定时区** ```kotlin calendar.timeZone = TimeZone.getTimeZone("Asia/Shanghai") // 或使用业务相关时区 ``` 4. **提取时间常量** ```kotlin private const val MILLIS_PER_MINUTE = 60_000L private const val MILLIS_PER_DAY = 86_400_000L ``` 5. **简化日期格式化** ```kotlin timePoints.workDay = SimpleDateFormat("yyyy-MM-dd").format(calendar.time) ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值