我寫了七、八年的 「算法博客」,出版了一本《算法的樂趣》,一門《算法應該怎麼「玩」?》課程,所有介紹算法的例子都是用 C++ 編寫的。
很多讀者來向我吐槽:「好好的一本算法書,為什麼要用 C++?」 或者 「C++ 很強大,Java 也很優秀,我選 Python」。
所以在本文裡,我非常詳細的講述了用 Java 或 C++ 寫算法時候的優劣勢,你可以參考一下來判斷自己喜歡用哪種語言寫算法。
PS:
下文中,上面的代碼是 C++ 的實現方式,下面的是 Java 的實現方式。
C++ 語法使用的是 C++11之後的版本,Java 使用的是 Java6 之後的版本。
1、C++ 和 Java 語法特性的相似性同為 C 語言家族的 Java 和 C++ 語言層面的相似性是有客觀基礎的。我通常這樣理解:Java 是跨平臺的 C++,是一種更好的 C++(是不是有點拉仇恨的感覺)。
2、基本數據類型C++ 的基本數據類型有:int、unsigned int、long、unsigned long、short、unsigned short、char、unsigned char、bool、float 和 double;
相應的,Java 也有 8 種基本數據類型,分別是:byte、short、int、long、float、double、char 和 boolean。
大部分情況下,兩種語言的基本數據類型可以根據下表進行一對一的轉換,但是也有差異,需要特別注意。
首先是 char,C++ 的 char 是 8 比特無符號整數,順便表示了 ASCII 字符;Java 的 char 是 16 比特,天生就可以表示寬字符集的字符。
另一個需要注意的是 long 類型,C++ 的 long 是不可移植類型,在不同的系統上其長度不一樣,可能是 32 位,也可能是 64 位,所以 C++ 程式設計師應儘量避免使用 long。
Java 的 long 比較單純,無論是 32 位的系統還是 64 位的系統,它都表示 64 位整數。
反過來,Java 會用 d 或 D 表示一個直接數字是 double 類型的浮點數,比如 200.0d 或(200.0D),但是 C++ 不支持,因為 C++ 默認一個浮點型的直接數字就是 double 類型。
C++ 用字面量符號 f 或 F 表示一個直接數字是 float 類型浮點數,比如 250.1 f(或 250.1F),這一點 Java 也是一樣的。
C++ 用字面量符號 l 或 L 表示 long,用 UL 表示 unsigned long。
3、字符串很多 C++ 程式設計師喜歡的用 char* 或 char 類型的數組存儲字符串,這其實是 C 語言用戶帶過來的習慣,我給出的 C++ 算法實現對字符串一般都用 std::string,對應 Java 的 String。
std::string 和 String 的用法對照如下表所示:
4、基本語法雖然 Java 的語法和 C++ 十分地相似,但是語言層面還有一些不同。C++ 允許全局函數的存在,Java 則不允許,不過 Java 也留了個口子,就是用靜態成員函數。Java 沒有指針,對象的傳遞和返回都是用的引用的方式,並且不需要像 C++ 那樣用 「&」 做特殊的語法標記。
大多數介紹 Java 的書籍開篇就是類和抽象,然後才是基本的語法,這和 Java 上等人的氣質是一致的,連這都不會,咋做程式設計師?C++ 應該多提升一下氣質,少用點指針和全局函數。本文為了對比 C++ 和 Java 的相似性,所以就從基本語法結構開始介紹。
運算符和賦值二者的運算符幾乎一樣,甚至 「++」 和 「—」 運算符都一樣有前綴式和後綴式兩種形式,意義也一樣;運算符的優先級規則也是一樣的。賦值語句兩者基本上是一樣的,看看每一行結尾的 「;」 你就知道它們有多相似。
條件判斷與循環條件判斷方面,C++ 與 Java 的 if 語句、switch 語句用法都相同;邏輯表達式的結構和語法、邏輯運算符的優先級也都相同。
C++ 的三種基本循環方式是 while 循環、do…while 循環和 for 循環,Java 都支持,甚至連關鍵字和 break、continue 控制語句的意義也一樣。
C++11 版本引入了一種根據範圍循環的語法,一般理解和 Java 的增強 for 循環類似,比如這種 C++ 循環形式:
Java 與之對應的形式是:
C++ 的基於範圍的 for 循環也可用於 C++ 的標準庫對象,用於取代老舊的迭代器循環方式:
同樣,Java 的增強 for 循環也支持基於 Collection 的遍歷,理解起來不成問題:
傳統的 C++ 語言是用迭代器對標準庫的容器進行遍歷,比如:
C++ 的容器都有 begin() 和 end() 接口,分別得到起始位置的迭代器的值和結束位置的迭代器的值,很多標準庫的算法都會用到迭代器。
C++ 用當前迭代器的值是否等於 end() 代表的結束位置迭代器的值來判斷是否遍歷結束。
Java 的 Collection 也有迭代器的機制,Java 用 hasNext() 判斷是否遍歷結束。
C++ 直接用 「 * 」 提領迭代器,得到對象本身的引用,Java 用迭代器的 next() 接口得到對象本身的引用。以上 C++ 代碼可以翻譯成如下 Java 代碼:
除了以上的 for 循環語句,C++ 還支持 for_each() 形式的遍歷 + 處理操作,也是配合迭代器使用,for_each() 的前兩個參數是一對迭代器,代表循環的起始位置和結束位置。
第三個參數是一個可調用對象,即函數對象(C++11 版本之後,這個參數還可以是一個 Lambda 表達式),舉個慄子:
Java 沒有與之對應的泛型函數接口,但是 Java 的很多 Collection 都支持 forEach() 接口:
C++ 的 for_each()其實用起來並不好用,自從 C++11 之後,除了懷舊派 C++ 程式設計師,其他人應該很少會再用 for_each() 了,基於範圍的 for 循環簡直絲滑的不要不要的。
5、函數C++ 的函數結構和 Java 也一樣,函數調用的形參和實參對應方式也一樣,也無需多做說明。
6、數組C++ 和 Java 都支持原生數組,並且數組索引都是從 0 開始。C++ 中定義數組的同時就分配了存儲空間,所以在定義時要指定長度,使用 new 動態申請內存時,要指定長度。
但是一種情況除外,那就是靜態初始化數組的形式,因為此時編譯器知道需要多少空間存儲這些數據,如下是 C++ 定義數組的常用形式:
Java 如果僅僅是聲明一個數組,可以不指定長度,因為此時並不分配存儲空間,需要分配空間的時候,用 new。與之對應的 Java 語言的形式是:
C++ 中二維數組的每一維長度必須相同,因為 C++ 的二維數組實際上只是一塊連續的存儲空間而已,甚至可以用一維數組的下標遍歷全部二維數組的存儲空間。
Java 沒這要求,因為 Java 的每一維都是可以單獨申請存儲空間的。但是二者在使用形式上是一樣的。C++ 定義和初始化二維數組一般有這幾種形式:
與之對應的 Java 語言初始化二維數組的形式是:
C++ 也支持動態內存形式的二維數組,一般有兩種使用方法,Java 都有與之對應的習慣用法:
與之對應的 Java 的方法是:
這代碼相似度很高。C++ 代碼最後要用 delete[] 手動釋放為數組申請的內存,Java 是不需要的。
C++ 還可以利用二維數組在內存中是連續存儲這一特性,使用時用下標計算將一維數組當成二維數組使用,計算的方法是:a\[i]\[j] = b[i * 2 + j],如下代碼示例:
遇到這樣的代碼,需要根據上述對應關係,小心地理解算法代碼的意圖。一些棋盤類遊戲通常喜歡用一維數組存儲二維的邏輯棋盤結構,好在 Java 也可以這麼做,轉換起來也沒什麼難度。
7、枚舉與 C 相比,C++ 強化了類型差異,枚舉變量和整數變量之間不能互相賦值,但是使用方法依然是直接使用枚舉值,沒有限制域。
C++11 之後,開始支持強類型枚舉,這一點就和 Java 很像了,越來越像一家人了:
C++ 代碼中一般用 std::cin 和 std::cout 進行控制臺的輸入和輸出。也有一些半吊子 C++ 程式設計師會在 C++ 代碼中混用 C 語言的 printf() 列印輸出信息。
不過話說回來,很多語言都支持 printf 方式的格式化輸出,比如 Java、 Python,為啥 C++ 就不能提供一個呢?比如以下代碼接受用戶輸入一個字符串和一個整數,並將其輸出出來:
將其翻譯成 Java,是這個樣子的:
上述代碼示例中,C++ 和 Java 的輸入分隔符都是空格或回車,如果希望輸入帶空格的一整行內容怎麼辦?
C++ 提供了 getline() 函數,getline() 會從緩衝區中取輸入流,直到遇到結束符。
結束符默認是 '\n',實際上是 getline() 函數有三個參數,第三個參數可指定結束符:
Java 也有與之對應的 Buffer IO 方式,請看:
C++ 程式設計師有時候也會用 std::cin::get() 函數,這個函數也是從緩衝區中讀入一行,直到遇到結束符,和 getline() 函數一樣,這個函數也可以指定結束符,如果不指定,默認是'\n'。
但是 std::cin::get() 函數有個小個性,就是它不從緩衝區中讀出結束符,而是將結束符留在緩衝區中。
為了適應它的這個小個性,C++ 程式設計師通常會在後面跟一個 get,將結束符讀出並丟棄掉,所以代碼看起來有點怪怪的:
理解了這一點,看懂 C++ 代碼也就不難了。當然,無論是 C++ 還是 Java,其 I/O 系統都非常複雜,有流式 I/O,也有緩衝區 I/O,操作的數據可以是控制臺 I/O,也可以是文件 I/O。
9、類和封裝首先說說 C++ 的 struct,Java 沒有與之對應的相似物的,但是完全可以用 class 來替換這個概念。為什麼這麼說呢?
因為在 C++ 中,struct 的位置有點尷尬,它是個 POD 吧,但它的成員又可以用非 POD 的數據類型,比如 std::string,甚至還可以定義虛接口,一旦有了這些東西,它就算不上 POD 了,除了成員默認是公有之外,和 class 沒有太大差別。
在我看來,C++ 保留 struct 的主要意義是為了兼容舊的 C 或 C++ 的庫,這些庫中很多接口用到了 struct,其次是純粹作為一種 POD 的 value type 來使用。
我的算法代碼中也會用到 struct,大概是為了懷舊吧,其實完全可以用 class 代替,當然也可以很容易地翻譯成 Java 的 class。來看個例子,對於這個 struct:
可以很輕鬆的轉成 class:
自從 C++11 發布以後,我就覺得 C++ 和 Java 的 class 越來越像了,分開這麼多年後,C++ 終於也支持 final 和 override 了。
從語法層面看,二者的差異很小,就小規模的算法而言,也很少會用到繼承和重載之類的情況,所以,Java 程式設計師看懂 C++ 的 class 定義與實現一點都不難。
少有的一些差異,比如 C++ 的函數可以設置參數默認值,或者 C++ 的抽象機制採用的虛函數要使用 virtual 關鍵字等。先看一個典型的 C++ 類的定義與實現:
C++ 的類成員訪問控制採用分節控制,用 public: 或 protected: 作為分節的標誌,如果沒有分節標誌的類成員,則是默認的 private: 控制。
C++ 的成員函數可以有默認值,並且構造函數也支持默認值。以 Bucket 類為例,構造函數第二個參數默認值是 0,即在構造 Bucket 對象時,可以只確定一個參數 capicity,也可以在確定 capicity 參數的同時,確定 Bucket 的水量,比如:
Java 不支持參數默認值,但是可以通過重載函數解決這個問題,即增加一個只有 capicity 參數的構造函數:
C++ 沒有抽象基類的語法,但是又抽象基類的概念,一般當一個類中有一個純虛函數的時候,這個類是不能被直接實例化的,它就類似於是一個抽象基類,比如:
C++ 的函數有很多類型修飾,比如常見的 const,C++11 後新增了 final 和 override,但是 = 0 一直是一個比較奇怪的存在,它表明這個函數沒有實現,需要在派生類中實現,同時,也說明這個類是不能被實例化的。
對於這樣的機制,Java 可以理解為這就是個抽象基類:
C++ 的繼承體系的語法與 Java 類似,只是語法形式上不同,Java 採用關鍵字:extends。
C++ 對於基類聲明的虛函數,繼承類中不需要再用 virtual 關鍵字修飾,當然,加了 virtual 關鍵字也沒錯誤。Java 也一樣,abstract 關鍵字再繼承類中可以省去。
從 Shape 抽象類派生一個 Circle 類,C++ 的典型代碼是:
Circle 構造函數後面的 :Shape(color),表示對基類的初始化,對於 Java 語言來說,相當於調用 super(color)。
以上代碼翻譯成 Java 語言,應該是這樣的形式:
C++ 有時候也會將一個類聲明為 final,意味著它不希望被其他類繼承,從語法上做了限制,比如:
有時候,是某個不希望被派生類重載,比如:
這些對於 Java 程式設計師來說,並不陌生,語法上只是 final 關鍵字的位置不同,理解上應該不存在任何問題。
10、總結本文介紹了 C++ 和 Java 在基本語法層面的對應關係,因為算法代碼涉及的語言方面深度有限,所以本文介紹的內容也比較基礎。
通過對比發現不管是用 C++ 還是用 Java 來寫算法,差別基本不大,如果朋友們對算法想再深度了解,可以看一下《算法應該怎麼「玩」?》。
▼掃碼跟我一起學
這門課選擇了 30 多個簡單且實用的算法實例,基本覆蓋了各種算法比賽中常見的題目以及生活中一些有趣的算法實現。側重通過多個例子,來引導大家掌握常見的算法設計思想。
你會用哪種語言來寫算法呢?在評論區告訴我吧~對算法感興趣的同學,點擊 閱讀原文,試讀了解。