本篇將是一個系列,重點講述在外力很少的情況下如何自學編程,以及需要注意的一些地方。
一般來說,一些所謂的『高手』或者老師會告訴人們演算法是非常非常重要以至於會不會演算法就是你會不會編程的唯一標準。不過事實上並非如此。掌握演算法固然是好,只是大部分程式並不需要高深的演算法,而且招人的時候僅僅要求會演算法的公司也是很少的(而且很難進)。我並不是學院派的人,所以雖然我本人也推崇學習演算法,但並不推崇一開始就學習演算法。
剛開始學編程的人總是不知道自己應該從哪裡入手。實際上這是一個相當重要的問題。在我看來,學好變成有若干條件:
·興趣
·數學/英語
·財力
首先談一談興趣。那些為了生計而尋找捷徑學習編程的人並不在本篇的考慮範圍之內,這些人我通常是不管的。興趣是非常重要的一個條件,但是興趣也是可以培養的。對編程的濃厚興趣可以讓自己自發地尋找各種各樣的書籍、發現自己知識結構上的弱點、跟同行有效地進行交流等等。那些沒有興趣的人遇到了一個問題只會上論壇或者QQ群上要代碼(而且多數脾氣暴躁)。
數學和英語在一開始並沒有什麼影響,但是在你學有所成之後,開始接觸複雜的內容的時候,數學能力就開始起作用了。很多電腦論文都是使用數學語言寫的,對數學沒有熱情或者不夠敏感的人將會很難跨過這個門檻學習一些書本上沒有的東西。英語同樣也是重要的,因為並不是所有的文章或書籍都會翻譯成中文,或者及時翻譯成中文。
財力並不是重點,不過至少在養活自己的同時要有閑散資金來不停地支付網路費用、書籍、電腦、外圍裝置等必須物品。
至於知識結構方面我個人的見解可能跟流行的觀點有所出入。目前人們總是把知識結構比喻為一個金字塔,最下面是基礎,上面一層一層更加深入而且更加專業的知識,最上是領域知識。老師們會說要學好基礎,首先學好語言和演算法,然後慢慢往上走。我自己並不這麼認為。個人認為『從左至右』的學習方法是更加有效而且不會錯過什麼東西的,只是不能速成。
從左至右是什麼意思呢?想象一個金字塔,最左邊仍然是最底層的【基礎】,再往左就涉及到更多的【基礎】以及更多的上層內容了。這樣一步一步下去就會有【基礎】--【上層】--【基礎】--【上層】這樣的不斷迴圈。這麼做的好處是成果快,能夠培養起興趣和成就感,而且基礎隨著應用的需要慢慢積累,等到學有所成的時候,基礎也覆蓋完了,上層的東西也看得差不多了,就可以超越金字塔自己翱翔了。
好了,那麼如何培養興趣呢?
人總是對有趣的東西比較感興趣的,而且這種東西如果不難入門的話,那麼接受起來更加容易,跟容易培養成就感,也就更有興趣了。根據實際情況,個人推薦剛開始接觸的時候應該學習C#,理由如下:
·C#的書籍非常多,語言核心簡單易懂,類庫豐富
·C#製作介面簡單
·C#屏蔽了有關作業系統和底層的大部分事情,可以讓學習的人專註於自己感興趣的內容
如果不是特別著急的話,一開始就對著C/C++的數組啊指標啊什麼亂七八糟的東西我覺得沒什麼必要,反正將來自然就知道了。我在這裡推薦C#的另一個重要原因是Microsoft Visual Studio .NET的C#編輯器有一個無敵美好的提示列表(按了一個“.”之後彈出來的),這對於初學者來說是相當好的一個工具。
一開始學習C#的時候應該首先掌握基本的少量文法,也就是說掌握條件陳述式、分支語句、函式宣告以及數組,外加少量庫的運用以及計算上的知識。然後開始學習製作介面,最後學GDI+。
學習GDI+是有很多好處的。不過在學習之前應該找本相關的書來看。GDI+有一些比較進階的功能如半透明效果和畫刷等等,容易組合出一些非常漂亮的圖形來。學會GDI+的基本操作之後,就可以慢慢接觸一些圖形濾鏡、分形、三維的內容了。使用平面工具繪製簡單的三維圖形是一件非常有意思的事情,而且非常鍛煉數學能力,所得到的效果也是『令人震撼』的。
隨後應該學習字串處理。典型的字串處理有分析INI檔案、對一個四則運算式子進行操作等等的內容。雖然C#處理器字串出來比C++稍微蹩腳一點,不過在這個時候忽略這個問題是相當有用的,至少不用陷入無窮的指標漩渦裡面去。
等圖形和字串都少有涉獵之後,就可以開始開發有趣的程式了。譬如用C#些動畫、開發畫函數圖的工具、自己設計一種高度簡化的HTML然後進行渲染製作自己的協助程式、或者開發簡單的影像處理軟體之類。稍微聰明一點的人,如果每天都有機會寫很多代碼的話,大概半年到一年就可以走到這裡了。
為什麼我會選擇圖形和字串兩種東西呢?為了培養興趣,首先要有成就感。圖形跟字串都是跟作業系統本身沒關係的東西,而且操作起來也沒什麼注意事項,因此入門比較簡單。如果漸漸深入的話會激發起學習資料結構、演算法、甚至是數學英語的熱情。如果可以使用這條主線貫穿整個編程的初級階段的話,得到的將會是紮實的基礎以及靈活的頭腦。
好了,今天就先說到這裡。下次再寫續篇。在此解答一下大家有可能提出來的疑問。
·資料庫和網路都很熱門,要不要學呢?
--這兩門技術掌握了也是很好的一件事情,而且作為入門的話也未嘗不可。只是如果一開始就往資料庫和網路的路走的話,將來可能會錯過一些學習作業系統底層以及複雜的演算法的機會,因為這兩種東西不會讓你有學習大部分有深度的知識的動力。
·演算法為什麼不一開始學習呢?
--學會了演算法,但是沒有有趣問題給你解決的話,那學來幹什嗎?而且學習演算法的最終目的是讓自己擁有設計演算法的能力,很多人都忽略了這一點。
·學會了GDI+和字串之後能不能找工作呢?
--不能。做人切勿急躁,學編程沒有個三五年還是不要把自己看得太厲害的好。
·接下來應該學習什麼樣的東西呢?
接著上一篇文章繼續往下講。如果按照上一篇文章走下去的話,現在估計做了有些小軟體了吧。字串和圖形都容易做大,而且對於潛意識上喜歡數學的最有希望的程式員們也是有吸引力的。但是這兩種東西卻不容易做好。等到程式到了一定規模的時候,維護和效率這兩大問題就會凸顯出來。心急吃不了熱豆腐,為瞭解決維護和效率這兩個經常會出現的問題,我們需要學習演算法和架構。這兩種東西是可以同時學的,但是一篇文章說不了多少東西,那麼就從演算法開始吧。
程式員是需要開闊眼界的,光C#一門也是不行的,畢竟程式運行在各種平台上,有各種各樣的語言。譬如Win32上的native C/C++、Delphi等,.NET上的C#和VB.NET,還有自成體系的Java,然後就是運行在mainframe上的COBOL,剩下的還有各種各樣的函數式語言、指令碼語言等等。熟悉了C#的人從Delphi入手不會很困難,從C/C++入手也可以了。這兩門原本是本地語言的語言在編寫程式的時候需要我們注意多一些的東西,典型的就是記憶體管理。這還是需要多加練習的,在這裡就不多說了。
說到演算法,在這裡首先向大家推薦《演算法導論》第二版。一年前我去買的時候,發現了中文版,但是中文版那個時候仍然有一些章節沒有翻譯。不知道現在怎麼樣了。英語好的同志們可以去買英文版。
演算法與資料結構是經常出現在一起的。每一種演算法總會有在各種不同資料結構上的實現,用於處理不同的問題。在不同的語言上面,各種各樣針對實際問題的資料結構也有一些巧妙的做法和通用的做法。我們可以用STL,可以用System.Collections.Generic,也可以自己寫。這根據實際情況而定。我們並不是不能做的比STL好,只是STL已經相當好了,滿足了大多數人的需要。在特定的情況下,面對非常特殊的問題,有時候我們就要自己實現資料結構。使用上一篇文章說的辦法來聯絡的話,到了這個時候已經寫了不少代碼了,用了不少並不複雜的數學知識了,鍛煉了理論與實際相聯絡的基礎。有了這些基礎,我們學習演算法和資料結構會比較簡單。
常用的資料結構有鏈表、列表、堆棧、隊列、二叉樹、平衡樹、堆、雜湊表和圖等,除此之外還有各種各樣的變形,但是萬變不離其宗。圍繞著這些資料結構還有各種各樣的演算法。典型的有排序演算法、搜尋演算法、尋路演算法、網路流等等。還有一些屬於策略的演算法,譬如貪心演算法、動態規劃等等。屬於策略的演算法經常用於製造新的演算法,要慢慢體會,勤加思考才行。至於這些資料結構和演算法的實際內容我並不打算在這篇文章講。《演算法導論》用了半本書來說這些問題,還是看書的好,文章不夠詳細。
至於我們如何選擇演算法呢?就如同我剛才強調的一樣,我們需要聯絡理論與實際的經驗,我們要用數學的眼光來看待我們需要解決的問題。如果我們找到了一種簡潔的表示來描述我們的問題的話,我們同時也找到瞭解決問題需要的資料結構的雛形。當然這個數學並不是指數學分析這些,我覺得更接近於抽象代數。扯遠了啊,一般來說我們並不需要鑽研這些學科,我們只需要有感覺就好了。培養感覺的一個捷徑就是學習數學。當然不學習也可以,經驗也能知道我們做事情,只不過走的路要長一些。至於讀者希不希望學習數學就自己決定吧,沒有普適的道路。找到了資料結構的雛形之後,剩下的就是尋找演算法了。有一些演算法可以在書裡面找到(譬如ACM很喜歡考的題目),有一些演算法可以在論文中找到(譬如專門為了對付一些複雜問題而製造出來的不具有通用性的演算法),剩下的就要靠我們自己去推導了。
那麼,我們如何學習演算法呢?我們是為瞭解決實際問題才學習演算法的,是為了為將來自己遇到問題的時候有個指導方向才學習演算法的,我們並不是為了學習演算法而學習演算法。我見過兩種不同的學習演算法的人。第一種是直覺閱讀演算法並學習,以後碰到問題再尋找。另一種則是僅僅將演算法稍微瞭解一下然後就放開,以後遇到問題的時候再翻開相應的演算法來學習。兩種方法適應於兩種不同的人,並沒有什麼大的優劣之分。於是我們根據自己的興趣或者需要,終於必須掌握一種演算法了。那麼這個時候我們可以找資料來看,就跟閱讀文章一樣消化裡面的知識,然後就寫一些小程式來實驗實驗(或者叫做原型,那些做軟體工程的人都喜歡這麼說)。這種小程式屬於拋棄型原型,寫完即扔的,目的是為了讓自己在瞭解了演算法的內容之後,檢驗一下自己是否已經真的明白了執行這個演算法所需要的所有細節問題。等到覺得自己已經能控制這個演算法的時候,我想也就差不多了吧。
有些人可能會覺得演算法很複雜,因為書裡面的演算法都是非常複雜的。但是演算法的目的是為了快,因此有一些好的演算法跟資料結構,結合的時候可能會變得相當簡單,但是並不是很容易想到。在這裡我舉幾個簡單的例子。
喜歡做圖形的朋友們,大概都喜歡做遊戲吧,嘿嘿。我們小時候在做那種簡單的2D遊戲的時候,總是要計算一大堆人之間是否相互接觸,或者很多人放出的魔法是否跟敵人碰撞到。如果我們的地圖上有100個人,每個人放了兩招,兩兩檢驗是否碰撞(以便判斷是否應該實施攻擊)的話就需要檢查20000次。這顯然是不行的。那麼我們可以使用分而治之的原理來做。我們可以把地圖切成很多個地區,地區包含著人和魔法。每當人和魔法的移動越過地區的邊界的時候,人和魔法就把自己從前一個地區斷開,連結到新的地區裡面去。這個時候地區就儲存了兩個鏈表,一個是人,另一個是魔法。好了,如何檢查魔法和人互相碰撞呢?只需要檢查同一個地區裡面的就行了。如果這100人都在25個地區裡面,平均每個地區有4個人8個魔法,那麼兩兩檢驗的話只需要檢查4×8×25=800次,相對於前面的暴力演算法節省了96%的時間。當然這隻是理想狀態。
在這裡舉另一個例子。我們都覺得C#、VB和Java很神奇吧,東西new了都不用delete,多舒服。假設我們現在要實現這種功能的話,我們需要維護所有已經new了的記憶體空間,並執行一種搜尋演算法來判斷哪一些記憶體空間是再也不可能被訪問的然後標記,最後刪掉所有被標記的空間。於是我們需要一個記憶體管理器,用來申請、標記和釋放。如何做比較合適呢?
我們的記憶體管理器需要根據設定的長度返回一段控制代碼來代表記憶體空間,然後需要可以通過控制代碼來訪問記憶體,最後標記並一起刪除這些控制代碼。為什麼要控制代碼呢?因為如果直接返回指標的話,語言執行久了會產生很多記憶體片段,而且new和delete也不夠快。現在,我們需要以下幾個資料結構:
·一個記錄所有被new了而且delete過的控制代碼的列表,用於迅速獲得沒有正在被使用的控制代碼。
·一個記錄了所有正在使用的控制代碼的列表,記錄指標以及長度。這張表是一個數組,控制代碼是索引。
new的時候,我們查詢第一張表拿出一個閒置控制代碼。如果列表為空白的話那麼將第二個表變大(這個時候所有控制代碼都被使用)並且將第一個閒置(也就是原來的表接下去的第一個新空間)控制代碼所對應的記錄標記使用。然後我們分配的總是最末尾的地方
delete的時候,我們查詢所有標記了使用控制代碼,看看是否有被mark,有的話就標記為不使用並將控制代碼放置入第一張表。
mark的時候,我們查詢這個控制代碼所對應的記錄,然後mark。
collect的時候,這是一個操作,將所有記憶體片段清除。我們只需要順序遍曆第二章表,將有用的內容挪動到前面一大塊無用的空間裡面,複製一下資料然後修改一下起始指標即可。
圖示一下:
空閑控制代碼:1 2
控制代碼記錄:<0,0..9><1,NULL><2,NULL><3,40..43>
記憶體空間:[第0-9個位元組佔用][10-39不佔用][40-43被佔用][此處為末尾]
好了,我們需要申請一個記憶體空間,我們拿到了控制代碼4,需要10個位元組。
空閑控制代碼:1 2
控制代碼記錄:<0,0..9><1,NULL><2,NULL><3,40..43><4,44..53>
記憶體空間:[第0-9個位元組佔用][10-39不佔用][40-43被佔用][44-53被佔用][此處為末尾]
現在我們標記3並刪除:
空閑控制代碼:1 2 3
控制代碼記錄:<0,0..9><1,NULL><2,NULL><3,NULL><4,10..19>
記憶體空間:[第0-9個位元組佔用][10-19佔用,從控制代碼4挪過來的][此處為末尾]
分析一下時間複雜度吧,這裡分析的是絕大部分情況,根據資料結構的實際實現偶爾會有少許偏差。
new為O(1),因為從空閑控制代碼獲得內容為O(1),分配末尾記憶體為O(1),找到記錄並標記為O(1)
mark為O(1),因為找到記錄並標記為O(1)
delete為O(1),因為只需要標記
collect為O(n),因為遍曆控制代碼記錄O(n),挪動內容,就算最多也就挪動整段記憶體空間,也是O(n)
從句並獲得記憶體位址也是O(1)
我們僅僅需要在記憶體不夠的情況下才動用win32的api分配一塊新的大記憶體,這樣來看的話在大部分情況下我們的記憶體管理器的分配比作業系統做得還快,這也是為什麼C#作為託管語言並沒有明顯慢下來的一個原因。當然還有一些其他原因,譬如.NET虛擬機器會把一些Managed 程式碼臨時編譯成本地代碼等等。
至於第三個例子,就看這裡吧,為了做一個大作業而弄出來的利用動態規劃是顯得簡單尋路演算法。
說到這裡本篇也快結束了。舉著兩個例子只為了說明以下問題:
·演算法往往跟執行效率有很大關係
·好的資料結構才能發揮演算法應有的威力
·要根據實際情況來選擇,甚至自己思考演算法
·演算法並不都是複雜的
其實,對於資料結構和演算法不熟悉或者根本沒聽說過的話,也並不是就不能寫出一些稍微有點規模的程式,只是寫出來的程式可能會很亂。演算法在一個程式員的發展道路上看還是最好學一學。