Java咖啡館(5)——Java語言基礎

一、黃蓉難倒瑛姑的數學題

  看過《射鵰英雄傳》的朋友,一定被黃蓉的機靈鬼怪、冰雪聰明深深打動。比如黃蓉遇上神算子瑛姑,給她出的三道題目中有一題是這樣的:今有物不知其數,三三數之剩二,五五數之剩三,七七數之剩二,問物幾何?也就是說,有一個未知數,這個數除以三餘二,除以五餘三,除以七餘二,問這個數是多少?在一部武俠鉅著中巧妙嵌入中國古代數學精粹,金老先生果真才高八斗啊!

  小提示

  快去嚐鮮Eclipse 3 Release

  經過幾近吊胃口的n長時間的測試迭代,Eclipse.org終於按時放出正式版,對於Java愛好者而言,不諦是夏日的盛大禮物。

  Eclipse 3的新功能非常多,增加了許多體貼的細節功能。趕快閱讀Eclipse 3.0 New and Noteworthy:http://download.eclipse.org/downloads/drops/R-3.0-200406251208/eclipse-news-R3.html,親自品味一下這個新版本!

二、Java v.s. 神算子

  可惜宋朝沒有Java,否則英姑一定不會被黃蓉難倒。

  打開Eclipse,新建一個Java項目,名稱叫做“黃蓉的題目”。然後新建一個Java類,名字叫做JavaCalc,記得在“public static void main(String[] args)”選項前打上勾。按照圖1所示輸入Java程序,共五行代碼(圖1)。

  運行程序,你就會得到如圖2所示結果(圖2)。

  通過驗算,23果然就是滿足題意的一個解。程序編寫的具體操作步驟不再贅述了,如果你還不熟悉,趕快翻翻前幾期的《Java咖啡館》好好複習一下吧!

  短短五行程序就解決號稱神算子英姑撓破頭皮都沒想出來的問題,是不是很神奇呢?其實,這些代碼包含了許多Java語言特性,比如變量、操作符、流程控制語句。或許這些代碼對你而言還有些神祕,結合這道題,再看看下面的介紹,馬上就會領悟到其中奧妙。

  1.變量(variable)

  變量是用標識符表示的(擁有名字)用來存儲值(擁有內涵)的物體。代碼:

int X;

  就是一個變量聲明語句,宣告X是一個int類型的變量。

  爲了聲明一個變量,你必須明確提供這個變量的類型和名稱。

  變量的類型是用來確定變量可以存儲的數據類型和可以對該變量進行的操作。比如上面代碼中,X是int類型,即一個整數,只能夠把整數賦給這個變量,比如X=0。你也只能對X進行算術運算,比如加、減、乘、除以及取餘數等。

  Java的變量分爲“原始類型(primitive type)”及“引用類型(reference type)”兩大類。其中“原始類型”的變量保存的是擁有特定大小和類型的簡單數據,比如一個整數、一個字符、一個布爾值(以邏輯學家Geogre Boole命名的變量類型,只有兩種值——真或假,屬於經典的二值邏輯)等。與“原始類型”不同,“引用類型”是一個參照的概念,指向內存中某個具體對象(的地址)(見圖3)(圖3+4  左側爲“原始類型”,右側爲“引用類型”,通過對比,我們可以看到它們的不同)。


   我們現在接觸“原始類型”比較多,足夠完成複雜任務。“引用類型”在面向對象概念中起了重要作用。

  爲什麼Java語言中要這麼分兩種變量類型呢?這是出於對執行效率的考慮。“引用類型”服務於對象,是面向對象的基礎設施,用面向對象的方法構架和設計系統很漂亮,但是“引用類型”的執行效率遠不及“原始類型”。從而,Java實際上是“原始類型”和“引用類型”結合使用的計算機語言,從這點上講,Java並非純粹的面嚮對象語言。當然,Java提供了與int類型(原始類型)遙相呼應的Java類——Integer類型(引用類型),它們可以互相轉換,新版本的Java還提供了自動轉換的功能,這是後話了,暫且不提。

  Java的原始類型列舉如下:

類型
描述
大小
byte 8-bit長的整數 8-bit
short 短整數 16-bit
int 整數 32-bit
long 長整數 64-bit
float 單精度浮點數 32-bit IEEE 754
double 雙精度浮點數 64-bit IEEE 754
char 單個字符 16-bit Unicode字符
boolean 布爾值 true或false

  值得一提的是,Java語言明確規定了變量類型的大小,爲跨平臺鋪平道路。

  變量名就是變量的名字,你是通過變量名訪問實際變量的,所以變量名有嚴格的規定。在Java中,變量名必須是:

  ★標識符,即符號開頭的字符串。
   ★不能是關鍵字、布爾值(true或者false)以及保留字null。
   ★在作用域內惟一,即一個作用域內變量名必須惟一。

  看起來規矩很多,其實也很合理。比如給小孩起名字,總是以百家姓作爲姓氏,如果名字叫做“¥皓”,別人一定認爲是開玩笑或者是奇怪的網名。當然,小孩的名字也不能叫做“總統”、“皇帝”之類的關鍵詞,以免引起誤會。說到作用域惟一,也很好理解:中國叫做吳宗憲的人太多了,學校裏(一個作用域)會用學號惟一標識你的身份,而不會逼迫你改名爲吳宗憲2004等。

  雖然合法的都能夠成爲變量名,但給變量起一個有意義的名字,是一個良好的素質,免得閱讀代碼時,別人看不懂,自己也看不懂。舉個例子,給變量起變量名正如起暗號,最好不要起“打死我也不說”這樣的暗號,不然,有你好受的……

  最後說說作用域。作用域是指變量在程序內部能夠被訪問到的區域。比如我們的程序中,變量X在整個main方法中都能夠被訪問到,非常直觀。你可以藉助Eclipse的力量體驗一下這個概念:如果作用域不對,Eclipse會毫不猶豫地警告你。

  定義了變量就能夠通過“=”給它賦值,正如程序中所做的。

  2.運算符

  運算符用來進行函數運算。Java中的運算符可以分爲算術運算符、關係運算符、條件運算符、位移與邏輯運算符、賦值運算符以及其他一些運算符組成。

  下面,用“(X % 3 == 2) && (X % 5 == 3) && (X % 7 == 2)”來理解Java運算符的概念。

  剛纔已經說過了,“X”是一個變量,“%”是取模的算術運算符,“X % 3”即X模3,就是X除以3的餘數;“==”是關係運算符,當“==”兩邊相等時返回true,否則就是false。從而,“X % 3 == 2”的意思就很明白了——如果X除以3的餘數是2,則這個表達式的值就是true,否則爲false。一對“()”是用來表示運算的優先級,即括號裏面的表達式首先被計算,然後纔是括號外的。“&&”讀作and,是條件運算符,是邏輯運算裏面的合取,即當且僅當“&&”兩邊都是true的時候,表達式爲true,否則爲false。現在回過頭看上面這個表達式的意思就是:X除以3餘2並且X除以5餘3並且X除以7餘2,表達式的結果顯然是一個布爾值,不是true就是false。

  3.流程控制

  程序是以文件形式保存在磁盤上的。如果沒有流程控制,Java解釋器將從頂向下、從左到右執行語句。而用流程控制語句,你可以選擇性地執行部分語句,重複執行一些語句,或者改變語句的順序流程,從而完成千變萬化的工作。

  首先介紹if流程控制語句。if語句允許你選擇性地執行某些語句,跟“如果怎樣則如此這般”的意思一樣。比如,我們的程序是這樣寫的:

if ((X % 3 == 2) && (X % 5 == 3) && (X % 7 == 2)) { 
   System.out.print("這個數字是:"); 
   System.out.print(X);
}

  聯繫上面關於運算符的知識,很明顯,這句話的意思是:如果“X除以3餘2並且X除以5餘3並且X除以7餘2”,則“把這個數字打印出來”,是不是跟日常生活的語法非常接近?

  下面看看外層的for流程控制語句。for語句是循環語句,用來遍歷一段數值。基本語法形式如下:

for (初始化; 終止判斷; 增量) {
    執行語句
}

  在循環執行前,初始化部分將首先被執行。比如我們的代碼中,初始化語句是:X = 0,表示把0賦給變量X,即從0開始測試是否滿足題意。之後,執行語句將被調用,即上面的if語句——若當前的X(這時是0)滿足題意便打印出來。然後,增量語句將被執行,即X++,意思是把X的值增加1,即我們要嘗試1是否滿足題意的解。最後,終止判斷語句將被執行,這裏是“X < 100”,表示若X小於100則重複以上步驟,否則停止循環,執行後面的代碼。

  可以看到,控制整個循環的主要部分是增量和終止判斷,沒有增量的變化,就無法達到依次嘗試0到99這些數字是否滿足題意;如果沒有終止判斷,循環就無法停止,可能進入所謂的死循環甚至死機——嚇唬你的,有Eclipse在,即使寫出造成死循環的豆腐渣代碼,按下Eclipse中的停止按鈕,照樣安然無事。可惜我們的防汛工作卻沒有這樣的救命按鈕,必須腳踏實地,來不得半點馬虎。
OK,現在真相已經大白了!我們的Java代碼不過是從0到99依次嘗試,看看是否“三三數之剩二,五五數之剩三,七七數之剩二”,如果是則把該數字打印出來。藉助電腦幾近暴力的速度,從而在短短1秒內解開黃蓉的謎題!黃老邪估計也不得不悄悄地低頭買下有《Java咖啡館》連載的《電腦愛好者》雜誌閉門修煉大腦,哈哈哈……

三、陽春白雪

  其實,這道題目頗有淵源。

  相傳,春秋戰國時,雲夢山鬼谷洞中住了一個奇人名曰鬼谷子,精通天文、地理、兵法、算術,是兵家之府庫,縱橫家之鼻祖——孫臏、龐涓、蘇秦、張儀、毛遂等都是他的學生。鬼谷子稱自己的算術研究爲鬼谷算,又叫隔牆算。這道題目便是鬼谷算中比較著名的題目之一,後現於《孫子算經》,又稱爲韓信點兵、秦王暗點兵、剪管術、大衍求一術等等。

  我國宋代學者對這類題目鑽研已頗爲精深,總結出了“三人同行七十稀,五樹梅花廿一枝,七子團圓正半月,去百零五便得知”這樣的口訣,意思是說“以三三數之,餘數乘以七十;五五數之,餘數乘以二十一;七七數之,餘數乘十五。三者相加,如不大於一百零五,即爲答數;否則須減去一百零五或其倍數。”這樣,很快就能知道答案爲23。

  不得不承認,跟上面的那些古人的方法相比,我們的程序雖然能夠比他們更快地得到23這個解,但是嚴格來講,我們的程序並不能算作是一個算法,不過是小學生級別的“暴力破解”而已。學習編程不能夠僅僅停留在這種程度,讓我們開動大腦,玩玩智慧遊戲。

  設這個數字是x,把題目寫成方程式就是這樣:

3a + 2 = x
5b + 3 = x
7c + 2 = x

  你看,三個聯立方程四個變量,不定方程肯定有無窮答案,23只是100以內惟一的一個解。
心算快的朋友一下子就可以這樣得到答案:除以三和除以七都餘二,則這個數字除以二十一必定也是餘二,二十三除以二十一餘二,而且二十三除以五恰好餘三,問題解決了。不過,如果不是3、5、7等小數字,心算再快也不夠用啊。

  其實,早在春秋年間,已經有了解題的算法,也就是西方數學家所謂的中國剩餘定理(Chinese Remainder Theory)。具體的推理過程涉及太多抽象代數的知識,這裏只寫出最後的通解公式:

x = 70 * 2 + 21 * 3 + 15 * 2 + 105 * n

  當n=-2時,便得到x=23這個最小解。

  Just do it

  試試看把中國剩餘定理的算法用Java編寫出程序,打印前1000個滿足題意的數字;然後用我們最初的算法打印前1000個滿足題意的程序(提示:只要改變for語句的終止判斷,到104918結束),比較兩者之間的速度差別。

  再擴展開去,中國剩餘定理在符號計算中起着重要作用。比如我們都知道2/3,有理數是一種精確的表示。但用Java表示2/3就會變成0.6666667這樣的數值數,是不精確的表示。不過,符號計算會帶來巨大的複雜度,必須放到一個域中才能夠限制住難度,這就要用到中國剩餘定理。老祖宗給我們留下豐厚的智慧遺產,有興趣的朋友可以看看計算代數這樣的課程,繼承並且發揚光大。

  說了這麼多看似無用的陽春白雪,東漸肯定又要給我衛生眼球看。實際上,我是想說明,學習Java編程和學習計算機科學有一個相通處,那就是我們追求的是優美算法,而計算機的高速只適合驗證,甚至有的問題,即使計算機速度增長得再快,也不及問題複雜度的增長速度,這就牽涉到計算複雜度的問題。從兩個程序的速度差別你就完全可以體會到。

  好了,就此打住。金老先生看到他優美的武俠鉅著在這裏當做呆板的高等數學課程講解,一定痛心疾首找我打官司(求之不得啊,正好請他老人家簽名)。也罷,其實想不通道理也不必鬱悶,畢竟這些東西弄得我一北大數學院在讀博士的哥們也頭疼腦熱得很。Java編程更偏向工科,以上的知識恐怕偌大一個Windows操作系統裏面也只有安全部分用到了(Windows安全漏洞百出並非算法不好,而且程序沒有寫好哦),所以Java愛好者能夠掌握Java的編程理念,通過嚴謹而優美的方法學打造工程奇觀,同樣雄偉得很。

四、小結

  這回我們介紹了Java語言最最基礎的部分,限於篇幅,無法詳細展開,請讀者自行閱讀免費書籍Thinking in Java以及Java Tutorial裏面的相關章節鞏固知識。如果想實踐,可以編寫一個求10000以內所有質數的小程序自我考察一下。

  其實,金老先生的《射鵰英雄傳》裏面還有其他的數學謎題,有機會我們再介紹一些解法。
歡迎大家繼續到我的網誌http://garychan.3322.org/進行交流。網誌是一個共同學習的好方法,通過交流,互相取長補短,分享創新的思維,共同進步。如果你對《Java咖啡館》某篇文章有感觸想寫幾句,或者對今後連載的題材有什麼要求,首先請註冊爲網誌用戶,然後就能夠登陸並且發言了。等待你的參與。

 
發佈了3 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章