快乐数是指一个正整数通过不断替换为各位数字平方和,最终结果为1的数;判定核心是检测是否出现重复值以判断循环,而非依赖迭代次数阈值。

什么是快乐数?先看判定逻辑
快乐数的定义是:对一个正整数不断替换为它各位数字的平方和,最终结果会变成 1;如果进入循环但始终不等于 1,则不是快乐数。关键点在于——你不需要预设循环长度,也不用数学证明收敛性,只需检测是否出现重复值。
常见错误现象:std::set 没清空、用 int 存平方和导致溢出(比如 999999999 的平方和是 9×81 = 729,其实安全,但若误写成平方而非平方和就崩)、递归没设终止条件导致栈溢出。
- 判定必须基于「是否重复出现」,而不是「是否 > 100 次迭代」这类经验阈值
- 所有正整数的平方和上限很低:10 位数最大为
9999999999→ 平方和最多是 10×81 = 810,所以实际状态空间 ≤ 810 - 因此用
std::unordered_set或布尔数组(大小 811)做闭环检测足够且高效
用 unordered_set 检测循环的写法
这是最直观、不易出错的实现方式。核心就是把每次算出的和存进集合,下次再遇到就说明进环了。
bool isHappy(int n) {
std::unordered_set<int> seen;
while (n != 1 && seen.find(n) == seen.end()) {
seen.insert(n);
int next = 0;
while (n) {
int d = n % 10;
next += d * d;
n /= 10;
}
n = next;
}
return n == 1;
}- 注意内层
while (n)是取各位数字的标准写法,别写成for (char c : to_string(n))——字符串转换开销大,且在嵌入式或禁用 STL 字符串的环境里不可行 -
seen.find(n) == seen.end()比!seen.count(n)稍快(避免二次哈希),但差别微小;两者都可接受 - 不要用
std::set,红黑树插入是 O(log n),而unordered_set平均 O(1),这里 n ≤ 810,差异不大,但习惯要对
用 Floyd 判圈法(龟兔赛跑)省空间
如果你在意内存(比如写 firmware 或硬实时系统),可以用双指针代替哈希表。原理是:快指针走两步、慢指针走一步,若存在环,它们必相遇。
立即学习“C++免费学习笔记(深入)”;
int getNext(int n) {
int sum = 0;
while (n) {
int d = n % 10;
sum += d * d;
n /= 10;
}
return sum;
}
<p>bool isHappy(int n) {
int slow = n, fast = n;
do {
slow = getNext(slow);
fast = getNext(getNext(fast));
} while (slow != fast && slow != 1 && fast != 1);
return slow == 1 || fast == 1;
}-
getNext必须抽成函数,否则内联展开易读性差且难调试 - 循环条件里的
slow != 1 && fast != 1是必须的,否则可能错过刚到达 1 就终止的机会 - 这个写法空间复杂度 O(1),但代码稍长;实测在 LeetCode 上运行时间与哈希法基本一致,无明显性能优势
- 容易踩的坑:漏掉初始
do-while的第一次调用,或把fast = getNext(getNext(fast))错写成getNext(fast)两次调用(顺序不对)
边界和特殊值验证要点
1 是快乐数,10、100 也是(因为 1²+0²=1);2 不是,路径是 2 → 4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4,回到 4 形成环。
- 测试时至少覆盖:
1、2、7(7→49→97→130→10→1,是快乐数)、19(经典快乐数) - 不要只测个位数,
999算出来是 243,再算下去会进主循环,能暴露取位逻辑 bug - 如果用
long long接中间结果,纯属过度——int足够(最大中间值 810)
实际部署时,如果判定逻辑被高频调用(比如筛 1e6 以内所有快乐数),建议预计算打表,或者用布尔数组替代 unordered_set(索引 0~810),避免哈希冲突带来的常数波动。但单次判定,直接上 unordered_set 最省心。


















