作为一名计算机专业的学生,算法刷题是提升编程能力的必经之路。今天我将分享在准备复试过程中遇到的几道典型编程题目,包括阶乘因子统计、数字串处理、回文数判断等常见题型。这些题目看似基础,但蕴含着许多值得深入探讨的编程技巧和算法思想。
题目要求计算n!(n的阶乘)中末尾有多少个连续的0。这个问题看似简单,但实际上需要理解数学原理才能高效解决。我们知道,末尾的0是由因子10产生的,而10=2×5。在阶乘中,2的因子数量远多于5的因子数量,因此问题转化为计算n!中5的因子个数。
最直观的解法是对每个数从1到n,统计其包含的5的因子数量,然后累加:
cpp复制int countTrailingZeros(int n) {
int cnt = 0;
for(int i = 1; i <= n; i++) {
int temp = i;
while(temp % 5 == 0) {
cnt++;
temp /= 5;
}
}
return cnt;
}
这种方法的时间复杂度是O(nlogn),对于小规模的n足够使用,但当n很大时效率不高。
更高效的解法是利用数学规律:n!中5的因子数量等于n/5 + n/25 + n/125 + ...,直到n/5^k为0为止。这是因为每隔5个数会出现一个5的倍数,每隔25个数会出现一个25的倍数(贡献两个5因子,但其中一个已经在5的倍数中统计过),以此类推。
优化后的代码如下:
cpp复制int countTrailingZeros(int n) {
int cnt = 0;
while(n > 0) {
n /= 5;
cnt += n;
}
return cnt;
}
这个算法的时间复杂度是O(logn),效率显著提升。在实际编程竞赛中,这种数学优化往往能带来性能的飞跃。
注意:虽然2的因子数量更多,但统计5的数量已经足够,因为2的数量总是过剩的。如果题目要求的是n!的二进制表示末尾有多少个0,那么就需要统计2的因子数量了。
题目要求找出数组中最长的连续相同数字序列,并返回该数字及其长度。这是一个典型的线性扫描问题,可以使用单次遍历解决。
关键思路是维护两个变量:current记录当前正在统计的数字,cnt记录当前数字的连续长度。同时用res和max_cnt记录最终结果。算法从第二个元素开始遍历,比较当前元素与current:
cpp复制int findLongestConsecutive(const vector<int>& a) {
if(a.empty()) return -1; // 处理空数组情况
int res = a[0], current = a[0];
int max_cnt = 1, cnt = 1;
for(int i = 1; i < a.size(); i++) {
if(current == a[i]) {
cnt++;
} else {
current = a[i];
cnt = 1;
}
if(max_cnt < cnt) {
max_cnt = cnt;
res = current;
}
}
return res; // 返回最长连续数字
// 如果需要同时返回长度,可以使用pair或结构体
}
在实际编码中,有几个边界情况需要注意:
这个算法的空间复杂度是O(1),时间复杂度是O(n),是最优解法。类似的思路可以应用于许多字符串处理问题,如最长无重复字符子串等。
回文数是指正读反读都相同的数字,如121、1331等。判断一个整数是否是回文数,可以将数字逆序后与原数比较:
cpp复制bool isPalindrome(int n) {
if(n < 0) return false; // 负数不是回文数
int reversed = 0, original = n;
while(n > 0) {
reversed = reversed * 10 + n % 10;
n /= 10;
}
return original == reversed;
}
数字逆序是一个基础但重要的操作,实现时需要注意:
改进后的逆序函数:
cpp复制int reverseNumber(int n) {
int reversed = 0;
while(n != 0) {
// 检查溢出
if(reversed > INT_MAX/10 || reversed < INT_MIN/10) return 0;
reversed = reversed * 10 + n % 10;
n /= 10;
}
return reversed;
}
题目要求将浮点数保留一位小数并进行四舍五入。利用整数运算的特性可以避免浮点数精度问题:
cpp复制double roundToOneDecimal(double num) {
int temp = num * 10; // 将小数点后第一位变为整数部分
double decimal = num * 10 - temp; // 获取小数点后第二位
if(decimal >= 0.5) {
temp++;
}
return static_cast<double>(temp) / 10;
}
处理浮点数时常见问题:
更健壮的实现可以考虑使用标准库函数:
cpp复制#include <cmath>
#include <iomanip>
double roundStandard(double num) {
return std::round(num * 10) / 10;
}
// 输出时控制精度
std::cout << std::fixed << std::setprecision(1) << num;
题目要求计算在n年中,每个月的13号落在星期一到星期日的次数,特别是落在星期五的次数(黑色星期五)。这是一个典型的日期计算问题,需要处理闰年和月份天数。
核心算法思路:
实现代码:
cpp复制#include <vector>
#include <iostream>
using namespace std;
bool isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
int getDaysInMonth(int year, int month) {
if(month == 2) return isLeap(year) ? 29 : 28;
if(month == 4 || month == 6 || month == 9 || month == 11) return 30;
return 31;
}
vector<int> countBlackFridays(int n) {
vector<int> counts(7, 0); // 0-6对应周日到周六
int firstDay = 1; // 1900-01-01是周一(1)
for(int y = 1900; y < 1900 + n; y++) {
for(int m = 1; m <= 12; m++) {
int thirteenthDay = (firstDay + 12) % 7;
counts[thirteenthDay]++;
firstDay = (firstDay + getDaysInMonth(y, m)) % 7;
}
}
return counts;
}
处理日期相关问题时的常用技巧:
题目描述:一条路上有编号0到L的树,给出若干区间需要移走这些区间的树,最后计算剩下的树的数量。有两种解法:
方法一:使用pair存储区间,排序后合并重叠区间
cpp复制#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;
int remainingTrees(int L, const vector<pair<int, int>>& ranges) {
vector<pair<int, int>> sortedRanges = ranges;
sort(sortedRanges.begin(), sortedRanges.end());
int removed = 0;
int start = sortedRanges[0].first;
int end = sortedRanges[0].second;
for(const auto& range : sortedRanges) {
if(range.first <= end) { // 有重叠
end = max(end, range.second);
} else { // 无重叠
removed += end - start + 1;
start = range.first;
end = range.second;
}
}
removed += end - start + 1; // 最后一段
return L + 1 - removed; // 总树数是L+1
}
方法二:使用unordered_set记录所有被移除的树
cpp复制#include <unordered_set>
int remainingTreesWithSet(int L, const vector<pair<int, int>>& ranges) {
unordered_set<int> removedTrees;
for(const auto& range : ranges) {
for(int i = range.first; i <= range.second; i++) {
removedTrees.insert(i);
}
}
return L + 1 - removedTrees.size();
}
两种方法各有优劣:
pair排序法:
unordered_set法:
在实际应用中,应根据问题规模选择合适的算法。如果L很大(如1e9),但区间数量少(如1e5),则第一种方法更优;如果L较小(如1e6),则第二种方法可能更简单直接。
在刷题过程中,我总结了一些有用的技巧:
算法能力的提升需要持续练习和总结。每解决一个问题,不仅要写出代码,更要理解背后的思想和可能的变种。例如,学会了统计阶乘中的因子5,可以扩展到统计任意质因子的数量;掌握了最长连续数字的解法,可以推广到最长递增序列等问题。