1. 題目來源
鏈接:快樂數
來源:LeetCode
2. 題目說明
編寫一個算法來判斷一個數是不是“快樂數”。
一個“快樂數”定義爲:對於一個正整數,每一次將該數替換爲它每個位置上的數字的平方和,然後重複這個過程直到這個數變爲 1,也可能是無限循環但始終變不到 1。如果可以變爲 1,那麼這個數就是快樂數。
示例 :
輸入: 19
輸出: true
解釋:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
3. 思維啓迪
3.1 單鏈表
這個有趣的算法思維涉及到單鏈表,下面非常簡單的介紹下單鏈表相關知識:
直觀理解單鏈表這種數據結構,就是把內存中的若干個存儲單元,連成了一條鏈,邏輯結構呢就如下圖所示(這個五顏六色的顏色只是單純爲了好看些…):
能發現這些像指針一樣的小箭頭在程序中怎麼去實現呢?嗯,沒錯,就是拿指針實現這些小箭頭的指向關係。
鏈表結構的末尾將指向空地址,即上圖的 NULL
。
而單鏈表存在一條非常關鍵的性質:當前節點唯一指向下一個節點。 這個性質就能幫助我們快樂的解決快樂數問題。
3.2 單鏈表判環
單鏈表是否存在環是一個非常經典的問題,就如下圖所示:
如果讓咱們自己來 “肉眼判環”,這個結果就顯而易見,但是要把這個扔給計算機來判斷,那它可就是 “目光狹窄” 的判斷不了了。
其實判斷方法比較簡單,我們給個生活上的例子,你和博爾特在一個賽道上同時出發,假設你比博爾特跑的快,那麼當你從後面相遇到博爾特時,可以很自然的給他說一句 “嗨,我的老夥計,你怎麼跑的這麼慢,我都超你一圈了~” 那麼就證明了這個賽道是環形賽道。
同理可以引用到單鏈表中,就是經典的快慢指針解法。我們設置兩個指針,一個走得快,一個走得慢,如下圖:
如果 P
、Q
走着走着相遇了,那麼就能證明這個鏈表就存在環。如果不能相遇的話,那麼就肯定是 P
指針首先走到鏈表的末尾,即指到最後的 NULL
。
3.3 提煉其精華所在
鏈表中是否有環是一個算法技巧,但是它的價值有限,因爲它僅僅只是解決了一個數據結構的問題。
算法和算法思維是絕不等價的兩者。 一個算法可能 30 分鐘就能學會,但是算法思維是需要長期的鍛鍊的。下面來看看根據單鏈表判環能引出那種算法思維。提到思維的話,我們就需要將我們剛剛所掌握的東西做一個泛化的理解:
泛化鏈表結構:如果我們 把鏈表的節點看成問題中的狀態 的話,它可以代表問題中的一個數字、一個階段等等,是很泛化的東西。而鏈表的唯一指向關係它又代表什麼呢?它其實代表着 狀態與狀態之間是唯一確定轉換 的。也就是說從當前狀態是唯一確定轉換到下一個狀態的。
如果問題中存在這種特性,那麼我們就能夠用鏈表思維進行概括。
3.4 快樂數與單鏈表的聯繫
根據快樂數的轉換規則,能得到:
這個轉換關係是確定的,即如果當前狀態確定了,那麼下一個狀態就是確定的。那麼在思維邏輯上就能將其抽象爲鏈表結構,如下圖所示:
這個鏈表結構就更加清晰了,我們根據題目的意思就將 1 作爲鏈表的結尾即 NULL
。
那麼在單鏈表中一直走不到空地址意味着什麼?就意味着鏈表有環唄。那麼這個快樂數問題就被抽象爲鏈表中是否有環的問題。即,如果這個鏈表有環那麼就不是快樂數,如果鏈表沒環,能指到空地址 1 的話那就說明這個數就是快樂數。
3.5 算法規模,是不是死循環?
從上面我們得到了解決這個問題的算法思維,但是會不會出現這個鏈表太長了,有上個幾千、幾萬、幾十萬的鏈表單元,影響我們找不到結果呢?我們來考慮一下這個算法的規模。
首先,如果輸入值爲 int
,那麼能知道 int
最多也就是一個以 2 開頭的 10 位的數字。接着我們來考慮這樣一個問題,在 int
數據範圍中,哪一個數字 n
它所對應的下一個數字 n
是最大的?
我們能構造得到 1 999 999 999
,那麼只有構造出 1 個 1 ,9 個 9 的數字在 int
範圍內就是最大的,那麼下一個節點時多少呢?根據快樂數的定義能得到 ,那麼 730 就是在整形範圍之內任何一個數字所能映射到的下一個數字都不會超過 730,也就意味着當前所抽象出來的鏈表結構中節點數目最多不會超過 730 個,如果快指針一次走兩步、慢指針一次走一步的話,那麼慢指針走的最多,也只不過走了 步,而快指針就是走了 步。
所以至此就證明完了,在整形快樂數中進行單鏈表判環的操作的話,操作步驟是有限的,就取個整吧,最多最多也就 2000 步了。所以這個方案是高效可行的。
3.6 代碼展示
// 執行用時 :0 ms, 在所有 C++ 提交中擊敗了100.00%的用戶
// 內存消耗 :8 MB, 在所有 C++ 提交中擊敗了89.59%的用戶
class Solution {
public:
bool isHappy(int n) {
int p = n, q = getNext(n);
while (q != 1) {
p = getNext(p);
q = getNext(getNext(q));
if (p == q) return false;
}
return true;
}
int getNext(int x) {
int n = 0;
while (x) {
n += (x % 10) * (x % 10);
x /= 10;
}
return n;
}
};
4. 總結
好久沒有回到這種做題的快樂感了,或許和做這個快樂數有關吧 😃。其實做這個快樂數題目什麼的並不重要,而重要的是需要體會一點:如何從具體的算法推導出算法思維的。這個思維的變換很重要!這也就是真正的算法之美!