6.1. 問題描述
6174數字黑洞是印度數學家卡普雷卡爾於1949年發現的,又稱爲卡普雷卡爾黑洞,其規則描述如下。
任意取一個4位的整數(4個數字不能完全相同),把4個數字由大到小排列成一個大的數,又由小到大排列成一個小的數,再把兩數相減得到一個差值。之後對這個差值重複前面的變換步驟,經過若干次重複就會得到6174。
例如,對整數8848按規則進行變換操作,其過程如下。
- 重排取大數:將8848的4個數字按從大到小排列組成一個最大數8884。
- 重排取小數:將8848的4個數字按從小到大排列組成一個最小數4888。
- 求取差值:用大數減去小數得到差值3996。
- 重複變換:之後對差值繼續按上述步驟進行變換。操作的過程爲:
9963-3699=6264
6642-2466=4176
7641-1467=6174。
經過4次變換之後,就將自然數8848變換爲6174這個黑洞數字,並且繼續變換也會一直是6174。
請編寫一個程序,驗證卡普雷卡爾黑洞。
6.2. 算法分析
在程序設計中,當解決複雜問題時,通常採用“自頂向下,逐步求精”的模塊化設計思想,將一個複雜的任務逐層分解爲若干個功能單一的子任務,如果某個子任務仍然複雜,還可以繼續分解爲更小的子任務,直到每個子任務簡單明瞭。簡單地說就是,化整爲零,各個擊破。
面對編程驗證卡普雷卡爾黑洞這個任務,根據其規則描述,可分解爲輸入數字、檢測數字合法性、黑洞變換3個子任務。其中,黑洞變換這個子任務稍顯複雜,又可以劃分出分解數字、取大數、取小數這3個較小的子任務。分解後的各個子任務呈現爲一個樹狀結構,可以使用功能結構圖來表示,如圖14-1所示。
經過分解,各個子任務已經足夠簡單。每個子任務稱爲一個功能模塊,能夠單獨進行設計、編碼和測試。各功能模塊的實現步驟描述如下。
在上圖中,主程序之下有三個模塊,主程序先調用輸入數字模塊接收用戶通過鍵盤輸入的一個整數,再調用檢測數字模塊檢測這個整數是否合法。如果檢測通過,調用黑洞變換模塊進行數字變換操作;否則,提示“輸入的整數不合法”,並結束程序。
主程序模塊流程圖如下:
至於其他子模塊的流程圖,這裏不再給出,有興趣的童鞋可以去原書《Python趣味編程:從入門到人工智能》查看。
6.3. 編程解題
在上述算法分析中,採用“自頂向下,逐步求精”的模塊化設計思想,將驗證數字黑洞的任務分解爲多個小的功能模塊,每個模塊功能單一,易於編程實現。我們來看看python程序怎麼實現:
1 ''' 2 程序:卡普雷卡爾黑洞,6174數字黑洞 3 作者:蘇秦@小海豚科學館公衆號 4 來源:圖書《Python趣味編程:從入門到人工智能》 5 ''' 6 #主程序 7 def main(): 8 n = input('請輸入4位數字不完全相同的整數:') 9 if check(n): 10 blackhole(n) 11 else: 12 print('輸入的整數不合法') 13 14 #檢測數字 15 def check(n): 16 if not n.isnumeric(): 17 return False 18 elif len(n) != 4: 19 return False 20 elif n == n[0] * 4: 21 return False 22 else: 23 return True 24 25 #黑洞變換 26 def blackhole(n): 27 print('變換過程:') 28 while n != '6174': 29 a = list(n) 30 b = max_number(a) 31 c = min_number(a) 32 n = str(b - c) 33 print('%s - %s = %s' % (b, c, n)) 34 print('變換結束!') 35 36 #取大數 37 def max_number(a): 38 a.sort(reverse=True) 39 num = int(''.join(a)) 40 return num 41 42 #取小數 43 def min_number(a): 44 a.sort() 45 num = int(''.join(a)) 46 return num 47 48 #程序入口 49 if __name__ == '__main__': 50 main()
從上面的代碼可以看出,“檢測數字”等模塊被定義成了獨立的代碼塊,這就是python的自定義函數,通常它是這個樣子的:
其中如果不需要參數,函數名後面的括號可以留空,多個參數用逗號(,)隔開。
現在我們把上面的python代碼轉換成julia代碼:
1 #= 2 程序:卡普雷卡爾黑洞,6174數字黑洞 3 作者:蘇秦@小海豚科學館公衆號 4 來源:圖書《Python趣味編程:從入門到人工智能》 5 =# 6 "主程序" 7 function main() 8 n = input("請輸入4位數字不完全相同的整數:") 9 if check(n) 10 blackhole(n) 11 else 12 print("輸入的整數不合法") 13 end 14 end 15 16 #檢測數字 17 function check(n) 18 #= 19 julia中沒有判斷字符串是否爲數字的內置函數 20 只能用這種方式實現了。(isnumeric,isdigit 21 函數的參數都是字符不是字符串) 22 =# 23 if tryparse(Int, n)===nothing 24 return false 25 elseif length(n) != 4 26 return false 27 elseif n == repeat(n[1],4) 28 return false 29 else 30 return true 31 end 32 end 33 34 #黑洞變換 35 function blackhole(n) 36 println("變換過程:") 37 while n != "6174" 38 a = split(n,"") 39 b = max_number(a) 40 c = min_number(a) 41 n = string(b - c) 42 #julia的參數字符串比python簡潔些 43 println("$b - $c = $n") 44 end 45 print("變換結束!") 46 end 47 48 #取大數 49 function max_number(a) 50 a=sort(a,rev=true) 51 #將數組元素組合成字符串 52 num=parse.(Int,join(a,"")) 53 return num 54 end 55 56 #取小數 57 function min_number(a) 58 a=sort(a) 59 num=parse.(Int,join(a,"")) 60 return num 61 end 62 63 """ 64 #這是重寫的實現類似Python的input函數 65 參數:string類型,默認值爲空 66 返回值:string類型 67 """ 68 function input(prompt::String="")::String 69 print(prompt) 70 return chomp(readline()) 71 end 72 main()
從上面的代碼中我們可以看出,julia的自定義函數的格式是這樣的:
同樣的,如果不需要參數,函數名後面的括號可以留空,多個參數用逗號(,)隔開。另外參數可以定義類型和默認值,函數可以定義返回值類型等。例如:
function input(prompt::String="")::String
另外,大家可能發現上面的python和julia代碼中都有一個main函數,而且python還有“if __name__ == '__main__':”這種寫法。事實上,與C、C++、java等靜態語言強制使用main函數作爲程序入口函數不同的是,python其實是沒有main函數的,這裏使用main函數作爲程序的起始,只是一種結構化編程的方式,同理,julia也一樣。當然,python中“if __name__ == '__main__':”使得名稱爲main的函數體中的代碼語句,只能在當前文件被單獨運行的時候被執行,如果當前python文件被作爲外部模塊引用的時候,main還是中的代碼語句不會被執行,所以,通常會把一些測試類的代碼寫在main函數裏面。當然,作爲工程化開發時,不建議這樣做,測試代碼應該寫到單獨的文件中去。
這裏有必要說一下julia的代碼註釋,Julia自v0.4版本之後,便提供了內置的文檔註釋系統,可以很方便地對函數、類型、對象及語句進行描述,直接支持Markdown格式[[1]],能夠生成非常實用、方便的說明文檔。
最簡單的註釋是一句話就可說清楚的單行註釋方式,以井號#作爲註釋內容的前綴,一般放於語句的上一行或當前行尾部。請參考上面的代碼,此處不再贅述。
對於多行註釋,一般在註釋內容的上下行使用#=與=#兩個標記符進行界定,請參考上面代碼的開頭的程序、作者聲明。
另外,類型聲明、函數與宏定義等上一行中單獨出現的字符串對象都會被認爲是註釋內容,上面main函數的註釋就是這樣的。當然,多行字符串也是支持的,而且遵循Markdown語法格式。請參看上面input函數的註釋。