計算 N 以內的回數個數

若一個十進制整數按位從左向右讀和從右向左讀,得到的結果是相等的,就稱之爲回數。
即需要滿足形如 abcd == dcba 這樣的條件,例 121 是回數,123 不是。

請計算 N 以內回數的個數。

以上本來是我面試時出的一道代碼題。它是從最早【寫一個素數生成器】逐漸演化過來的,我發現他比寫素數更好的地方在於還可以考察到空間變換的能力。

多數實習生和普通的程序員都可以實現 O(N) 的遍歷版本,好一點的就是知道單獨寫一個函數進行判斷,再寫一個循環,可讀性較好。因爲招聘需求也不高,一般這樣的就算過了。

只有少數人(也是小公司難約好簡歷吧)能對【還能再快一點麼】的提問有所反應。而他們最多也就是能給出一個分治的思路,從沒有人能把代碼寫下來,更甭提測試和debug了。所以對於這個“我自己提出的問題”,我也從沒嘗試過做出解答。直到昨晚面了個“歷史最高分”。

那哥們看長相就是個愛寫代碼的,對這道題在提筆之前決定先跟我說一下他的思路:直接過掉“最蹉的方法”,描述了一個分治的思路(他是提出這個思路最快的人)。然後問我有沒有更好的方法呢?他是希望從我這裏得到一個指引,如果我回答是,他就去想那個答案,如果我回答否,他就開始寫代碼。

然後就尷尬了,因爲我也不知道有沒有更好的方法,甚至我都沒做過這道題。。。他自己又想了大約 10min 後,決定開始實現剛纔的那個思路。

他的思路是:

  1. 假設題目裏整數 N 一共有 x 位十進制數, 把問題分爲兩部分計算
  2. 一部分是位數低於 x 的部分,即 0-9 的排列組合
  3. 另一部分是正好 x 位整數且小於等於 N 的部分,這個比較噁心

他花半小時寫完了以後也沒怎麼檢驗,而我也着急回家就簡單看了一下代碼,覺得沒什麼大問題就給過了。沒管他還一幅“不行我要搜一下看別人都怎麼寫的”的氣勢。

晚上洗澡的時候我又想起這件事,覺得自己也應該做一下,萬一再遇到這種較真的同學免得毫無準備。

基於上面這個思路來分析:

爲了避免遍歷整數,我們要從回數的自身特性上尋找規律。

回數是一個對稱結構,而其每個元素都有一個大小至多爲 10 的值域,所以如果我們把它當成字符串來處理,複雜度就會大大的降低。

容易發現:對於長度爲 x 的整數字符串:

在每個字符的值域都是 0-9 的情況下,總共有 10**(ceil(x/2)) 種回數排列方法。(本文假設整數相除可以得到浮點數)

但有一個例外狀況是,在 x > 1 時,首位不能爲 0,所以公式其實是 9*10**(ceil(x/2)-1) 當 x > 1。對於 N < 10 的情況,就直接返回 N + 1 吧。

既然對於固定位數的整數,算法是這樣,那麼對於 x 位以內的整數,只要遞歸計算就可以了:

def count_palindrome_by_length(x):
	if x < 1:
		return 0
	elif x == 1:
		return 10
	else:
		return 9*(10**(ceil(x/2)-1)) + count_palindrome_by_length(x-1)

思路第二步完成。而對於思路里的第三步,就比較麻煩了。因爲對小於 N 的判斷,再次把處理對象拉回了整數。因爲每一位的值域也不再是固定的 0-9 或 1-9,他變成動態的了,當某一位的上位還可以進位的時候,本位的值域是 0-9,否則就是 N 對應位置的那個數。例:12345 的算法並不是 1 * 3 * 4 = 12。當前兩位是 11 時,第三位其實是 0-9;而當前兩位是 12 時,第三位是 0-3。

從整數的角度考慮這個問題,你會發現:對於整數 abcde 的前半部分——abc來說,對於任意小於 abc (大於等於 100)的整數,將之鏡像得到的回數,總是小於 abcde 的,因爲這個數小於 abc00。所以最後只需要再判斷 abcde 自己是不是回數就可以了。

def count_palindrome_with_fixed_length(n):
    if n < 10:
        return 10
    chars = list(str(n))
    half = chars[:int(ceil(len(chars)/2))]
    count = int(''.join(half)) - 10**(len(half) - 1)
    # 上式由此: count = (int(''.join(half)) - 1) - 10**(len(half) - 1) + 1 化簡而來
    if is_palindromic(n):
        count += 1
    return count

其中 is_palindromic 是直接判斷某個數字是否是回數的函數。

寫到這裏就會發現,原來第二步也可以使用第三步的思路來實現,進而合併爲一套相同的邏輯。因爲第二步的條件,不過是第三步中參數只有 9 的特殊情況罷了。感興趣的同學可以自己組合一下,這裏不再贅述。

按位算法完。

我隨後Google了一下這個問題,發現除了上面這種方式以外,還有人會用一種類似篩法的東西:

def gen_palindeome(num):
    """given 12, return [121, 1221]"""
    strn = str(num)
    palindeome = []
    palindeome.append(int(strn + ''.join(reversed(strn))))
    if len(strn) > 1:
        palindeome.append(int(strn + ''.join(reversed(strn[:len(strn) - 1]))))
    return palindeome


def count_palindrome(n):
    count = 1
    for i in range(1, n + 1):
        if i<10 and is_palindromic(i):
            count += 1
        new_palindeome = gen_palindeome(i)
        for num in new_palindeome:
            if num <= n:
                count += 1
        if i > 10 and new_palindeome[-1] >= n:
            break
    return count

這種方法可讀性要好得多,不過效率上卻差了一大截。之所以其在計算素數上顯得效率很高是因爲素數的生成並不像回數那樣直觀,可以按位去湊。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章