1. 程式人生 > 其它 >202. 快樂數

202. 快樂數

202. 快樂數

題目:202.快樂數(簡單)

題目描述

編寫一個演算法來判斷一個數 n 是不是快樂數。 「快樂數」定義為:

  • 對於一個正整數,每一次將該數替換為它每個位置上的數字的平方和。

  • 然後重複這個過程直到這個數變為 1,也可能是 無限迴圈 但始終變不到 1。

  • 如果 可以變為 1,那麼這個數就是快樂數。

如果 n 是快樂數就返回 true ;不是,則返回 false 。

示例 1:

輸入:19
輸出:true
解釋:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1

示例 2:

輸入:n = 2
輸出:false

提示:

  • 1 <= n <= 2^31 - 1

題解

思路:尋找無線迴圈發生的情況,即迴圈將出現在每次計算的平方和當中,因此我們需要用一個容器來記錄每一次的平方和。因為我們需要快速地判斷當前的平方和是否已經存在容器中了,所以我們可以選擇使用雜湊演算法。選取 unordered_set 比較合適。

程式碼:C++版本

bool isHappy(int n) {
unordered_set<int> sums;
if (n == 1) return true;
while (n != 1){
int sum = 0;
while (n != 0) {
int tmp = n % 10;
sum += tmp*tmp;
n = n / 10;
}
//快樂數
if (sum == 1) return true;
//沒找到,插入
else if (sums.find(sum) == sums.end()){
sums.insert(sum);
}
//找到,結束迴圈
else if (sums.find(sum) != sums.end()){
break;
}
n = sum;
}
return false;
}

官方解法

官方解法

方法一:用雜湊集合檢測迴圈 我們可以先舉幾個例子。我們從 7 開始。則下一個數字是 49(因為 7^2=49),然後下一個數字是 97(因為 4^2+9^2=97)。我們可以不斷重複該的過程,直到我們得到 1。因為我們得到了 1,我們知道 7 是一個快樂數,函式應該返回 true。

再舉一個例子,讓我們從 116 開始。通過反覆通過平方和計算下一個數字,我們最終得到 58,再繼續計算之後,我們又回到 58。由於我們回到了一個已經計算過的數字,可以知道有一個迴圈,因此不可能達到 1。所以對於 116,函式應該返回 false。

根據我們的探索,我們猜測會有以下三種可能。

  • 最終會得到 1。

  • 最終會進入迴圈。

  • 值會越來越大,最後接近無窮大。 第三個情況比較難以檢測和處理。我們怎麼知道它會繼續變大,而不是最終得到 1 呢?我們可以仔細想一想,每一位數的最大數字的下一位數是多少。

    DigitsLargestNext
    1 9 81
    2 99 162
    3 999 243
    4 9999 324
    13 9999999999999 1053

對於 3 位數的數字,它不可能大於 243。這意味著它要麼被困在 243 以下的迴圈內,要麼跌到 1。4 位或 4 位以上的數字在每一步都會丟失一位,直到降到 3 位為止。所以我們知道,最壞的情況下,演算法可能會在 243 以下的所有數字上迴圈,然後回到它已經到過的一個迴圈或者回到 1。但它不會無限期地進行下去,所以我們排除第三種選擇。 即使在程式碼中你不需要處理第三種情況,你仍然需要理解為什麼它永遠不會發生,這樣你就可以證明為什麼你不處理它。

演算法

演算法分為兩部分,我們需要設計和編寫程式碼。

  • 給一個數字 n,它的下一個數字是什麼?

  • 按照一系列的數字來判斷我們是否進入了一個迴圈。

第 1 部分我們按照題目的要求做數位分離,求平方和。 第 2 部分可以使用雜湊集合完成。每次生成鏈中的下一個數字時,我們都會檢查它是否已經在雜湊集合中。

  • 如果它不在雜湊集合中,我們應該新增它。

  • 如果它在雜湊集合中,這意味著我們處於一個迴圈中,因此應該返回 false。

我們使用雜湊集合而不是向量、列表或陣列的原因是因為我們反覆檢查其中是否存在某數字。檢查數字是否在雜湊集合中需要 O(1) 的時間,而對於其他資料結構,則需要 O(n) 的時間。選擇正確的資料結構是解決這些問題的關鍵部分。

class Solution {
private int getNext(int n) {
int totalSum = 0;
while (n > 0) {
int d = n % 10;
n = n / 10;
totalSum += d * d;
}
return totalSum;
}

public boolean isHappy(int n) {
Set<Integer> seen = new HashSet<>();
while (n != 1 && !seen.contains(n)) {
seen.add(n);
n = getNext(n);
}
return n == 1;
}
}

複雜度分析 確定這個問題的時間複雜度對於一個「簡單」級別的問題來說是一個挑戰。如果您對這些問題還不熟悉,可以嘗試只計算 getNext(n) 函式的時間複雜度。

  • 時間複雜度:O(243⋅3+logn+loglogn+logloglogn)... = O(logn)。

    • 查詢給定數字的下一個值的成本為 O(logn),因為我們正在處理數字中的每位數字,而數字中的位數由 logn 給定。

    • 要計算出總的時間複雜度,我們需要仔細考慮迴圈中有多少個數字,它們有多大。 我們在上面確定,一旦一個數字低於 243,它就不可能回到 243 以上。因此,我們就可以用 243 以下最長迴圈的長度來代替 243,不過,因為常數無論如何都無關緊要,所以我們不會擔心它。

    • 對於高於 243 的 n,我們需要考慮迴圈中每個數高於 243 的成本。通過數學運算,我們可以證明在最壞的情況下,這些成本將是 O(logn)+O(loglogn)+O(logloglogn)...。幸運的是,O(logn) 是占主導地位的部分,而其他部分相比之下都很小(總的來說,它們的總和小於logn),所以我們可以忽略它們。

  • 空間複雜度:O(logn)。與時間複雜度密切相關的是衡量我們放入雜湊集合中的數字以及它們有多大的指標。對於足夠大的 n,大部分空間將由 n 本身佔用。我們可以很容易地優化到O(243⋅3)=O(1),方法是隻儲存集合中小於 243 的數字,因為對於較高的數字,無論如何都不可能返回到它們。

方法二:快慢指標法 通過反覆呼叫 getNext(n) 得到的鏈是一個隱式的連結串列。隱式意味著我們沒有實際的連結串列節點和指標,但資料仍然形成連結串列結構。起始數字是連結串列的頭 “節點”,鏈中的所有其他數字都是節點。next 指標是通過呼叫 getNext(n) 函式獲得。

意識到我們實際有個連結串列,那麼這個問題就可以轉換為檢測一個連結串列是否有環。因此我們在這裡可以使用弗洛伊德迴圈查詢演算法。這個演算法是兩個奔跑選手,一個跑的快,一個跑得慢。在龜兔賽跑的寓言中,跑的慢的稱為 “烏龜”,跑得快的稱為 “兔子”。

不管烏龜和兔子在迴圈中從哪裡開始,它們最終都會相遇。這是因為兔子每走一步就向烏龜靠近一個節點(在它們的移動方向上)。

演算法

我們不是隻跟蹤連結串列中的一個值,而是跟蹤兩個值,稱為快跑者和慢跑者。在演算法的每一步中,慢速在連結串列中前進 1 個節點,快跑者前進 2 個節點(對 getNext(n) 函式的巢狀呼叫)。 如果 n 是一個快樂數,即沒有迴圈,那麼快跑者最終會比慢跑者先到達數字 1。 如果 n 不是一個快樂的數字,那麼最終快跑者和慢跑者將在同一個數字上相遇。

class Solution {

public int getNext(int n) {
int totalSum = 0;
while (n > 0) {
int d = n % 10;
n = n / 10;
totalSum += d * d;
}
return totalSum;
}

public boolean isHappy(int n) {
int slowRunner = n;
int fastRunner = getNext(n);
while (fastRunner != 1 && slowRunner != fastRunner) {
slowRunner = getNext(slowRunner);
fastRunner = getNext(getNext(fastRunner));
}
return fastRunner == 1;
}
}

複雜度分析

  • 時間複雜度:O(logn)。該分析建立在對前一種方法的分析的基礎上,但是這次我們需要跟蹤兩個指標而不是一個指標來分析,以及在它們相遇前需要繞著這個迴圈走多少次。

    • 如果沒有迴圈,那麼快跑者將先到達 1,慢跑者將到達連結串列中的一半。我們知道最壞的情況下,成本是 O(2⋅logn)=O(logn)。

    • 一旦兩個指標都在迴圈中,在每個迴圈中,快跑者將離慢跑者更近一步。一旦快跑者落後慢跑者一步,他們就會在下一步相遇。假設迴圈中有 k 個數字。如果他們的起點是相隔 k−1 的位置(這是他們可以開始的最遠的距離),那麼快跑者需要 k-1 步才能到達慢跑者,這對於我們的目的來說也是不變的。因此,主操作仍然在計算起始 n 的下一個值,即 O(logn)。

  • 空間複雜度:O(1),對於這種方法,我們不需要雜湊集來檢測迴圈。指標需要常數的額外空間。

方法三:數學

程式碼隨想錄

程式碼隨想路題解

思路

這道題目看上去貌似一道數學問題,其實並不是! 題目中說了會 無限迴圈,那麼也就是說求和的過程中,sum會重複出現,這對解題很重要! 正如:關於雜湊表,你該瞭解這些!中所說,當我們遇到了要快速判斷一個元素是否出現集合裡的時候,就要考慮雜湊法了 所以這道題目使用雜湊法,來判斷這個sum是否重複出現,如果重複了就是return false, 否則一直找到sum為1為止。 判斷sum是否重複出現就可以使用unordered_set。 還有一個難點就是求和的過程,如果對取數值各個位上的單數操作不熟悉的話,做這道題也會比較艱難 C++程式碼如下:

class Solution {
public:
// 取數值各個位上的單數之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果這個sum曾經出現過,說明已經陷入了無限迴圈了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};