從上大學第一天開始接觸編程,老師便給咱們講過各式各樣的算法。從各類查找、排序,到遞歸、貪心等算法,大一的時候一直在和這些算法搏鬥。直到工做後,爲了應付面試,仍不得不回過頭去啃算法書或者去刷一些算法習題,纔可以拾回一些上學時的記憶。爲何算法就這麼難以記住呢?或者說,爲什麼計算機的算法不能更直觀一些呢?java
由於計算機的算法就是反人性的,從本質上來講,這是計算機的思惟方式和人腦思惟方式的區別而形成的。面試
人腦思惟的機制至今沒有一個肯定的理論,暫時認爲是化學物質和電信號的做用。雖然沒有科學的解釋,可是咱們每一個人都有一顆大腦,咱們每一個人均可以感覺到本身的思惟方式。算法
而計算機則是人類創造的,從設計之初它便不是以模擬人腦爲目的,所以它有其獨特的工做方式,只有理解了計算機的工做方式,才能夠學會以它的方式去思考, 才能夠寫出最適合計算機運行的程序代碼。編程
咱們經過一個具體的例子,來講明人腦和計算機的思惟方式不一樣,假設咱們想要從一個已經排好序的數組中找出一個特定的數字。數組
已知排序好的數組是1 2 3 5 7 13 34 67 90 127 308,咱們但願找到是否13這個數在數組內。緩存
人腦是如何去完成任務的呢?函數
人腦處理這樣的問題幾乎是「做弊」的,咱們能夠一目十行,咱們在眼鏡一掃視的狀況下就發現了13,因此若是我問本身我是如何找到13的,我只能說我「看見」了。性能
而計算機是如何來完成這個任務呢?學習
最簡單也是最笨的算法就是從數組開始一個一個的讀入數組,我相信每一個學習過編程基礎的同窗均可以寫出相似下面的代碼。優化
boolean isNumInArray(int num, int[] array) { for (int i = 0; i < array.length; i++) { if (array[i] == num) { return true; } } return false; }
計算機須要從數組的第一個元素開始,一個一個的去查當前的數組的元素,和13相比,看看是否是相等。爲了找出13這個數,計算機要作6次循環操做,而人幾乎是瞬間就看到了答案。
爲什麼計算機解決問題的方式這麼「笨」呢?咱們先得從計算機的工做原理提及。
CPU做爲計算機的最核心的部件,也是算法的主要運載體。
CPU並不會像人同樣思考,它只懂得一些基本的指令。每個CPU都有其指令集,指令集是存儲在CPU內部,對CPU運算進行指導和優化的硬程序。通俗一點說,指令集就是CPU的全部思惟方式。好比常見的指令集中都會有ADD指令,這個指令能夠將兩個寄存器中的值相加,並將存儲到另外一個寄存器中;與此相對應的也會有SUB指令,用於將兩個寄存器值相減。若是你去查閱各類CPU指令集的手冊,會發現基本上都會包含基本的加減乘除指令,以及向內存中存、取數據的指令。而常見的CPU指令集,最多也就是幾百條指令。也就是說CPU只會這幾百個命令。
而人腦相對於CPU,有強大的記憶和聯想能力,好比你看到1+1,就想到2,看到紅燈,就會想到停下來,看到門,就知道去開門把手,這些都是你不假思索能夠馬上反映出來的東西。
因此,CPU會的東西(指令)比人少多了,那CPU豈不是很笨?沒錯,CPU就是很笨,可是CPU的優勢也是人腦所沒法比擬的:
綜上所述,CPU是一個既笨又快的傢伙。
計算機的常見存儲有寄存器、高速緩存、內存、硬盤等。
寄存器就至關於人腦中馬上能夠想起來的東西,CPU所作的一切運算都是針對於在寄存器中的數據進行的。寄存器存儲了計算機當前要作什麼計算(指令寄存器),要計算的數據(數據寄存器),計算到哪一步了(段寄存器)等信息。不管是最先的有寄存器的CPU仍是最新最強的的CPU,它們的寄存器數量最多也只有幾十個(特殊狀況有幾百個),也就是說CPU同一時刻可以馬上使用過的信息也就是這幾十個數字。
內存則是計算機的主力存儲設施,它能夠存儲運行中的程序的信息,內存至關於圖書館的書架,CPU須要用某一段內存中的數據是,須要經過LOAD指令,同時附上一個書架編號(內存地址),而後內存控制器能夠將對應的地址的數據經過總線傳輸給CPU,CPU則將載入的結果放入寄存器中使用。內存存取的速度遠小於寄存器,可是訪問分佈在內存各個區間的數據的速度基本是相等的。
因爲大部分時候CPU須要讀取連續的一段內存來進行運算,所以一般CPU會有高速緩存將最近使用過的內存整塊緩存起來,而使得CPU沒必要每執行一步就須要去讀一次內存。高速緩存的速度介於寄存器和內存之間,但遠高於內存。高速緩存的大小通常在幾兆到十幾兆之間。
硬盤屬於外部存儲,老式的機械硬盤中會有一個可轉的磁頭,在讀取磁盤文件的時候須要將磁頭轉到對應的位置,磁盤的速度遠低於內存,而且若是磁盤的磁頭若是停留在某個位置時,隨機磁盤上不一樣位置的信息,會受到磁頭運動的物理速度限制而出現速度不均等的狀況。新式的固態硬盤採用了和內存類似的存儲介質,在隨機訪問的性能上提高很大。
因此,計算機有一顆只能記得一點點事情的小腦殼(寄存器),可是可以擁有相對較大的快速記憶(緩存),擁有遠超過人類的知識儲備(內存),而且還隨身攜帶了巨大的移動圖書館(硬盤),因此從存儲上來看,計算機像是一個有先天缺陷的雨人(Rain Man)。
因此,咱們來分析一下round 1中爲什麼計算機到底作了怎樣的操做?
首先咱們看咱們函數的定義
boolean isNumInArray(int num, int[] array)
在調用函數的底層實現中,參數是被分配到兩個寄存器中。isNumInArray
這個函數,在被調用時,第一個參數num
的值13
會被載入到寄存器(r1), 的第二個參數array
,傳入CPU的時候就只是array
在內存中的地址信息,被存儲在另外一個寄存器(r2)。
而在第四行array[i] == num
時,CPU須要作三件事才能夠完成這工做:
而根據操做3的結果,若是結果不相等,則CPU須要將循環計數器i
加上1存入寄存器r4,再次進行上面的計算。所不一樣的是,第二到第N次的步驟二會比第一次要快不少,由於整個數組的內容已經被高速緩存所捕獲。
因此,咱們能夠看出爲什麼計算機在解決這個問題上顯得如此愚笨:
計算機在上一輪和人腦的PK中敗下陣來,然而這並非很公平,由於數組的數量只有短短的幾個,而計算機能夠存儲的上限遠不止於如此。因而咱們開始第二次的比拼。 此次咱們將輸入擴大
1 2 3 5 7 13 34 67 90 127 308 502 ... 2341245 ... (100萬個
查找的數變成了2341245。
此次人腦和計算機的表現又如何呢?
對於一個普通人,咱們假設這100萬個數字是打印在一本字典裏的,那麼他如何找出100萬個有序數組中的某個數字呢?
這時人類引以自豪的「一目十行」的能力已經微乎其微,當數字的位數增大時,且不說一眼比較一個數字是否和目標數字相同已經困難,即便真的有一目十行的本事,在100萬這樣的數字面前也是微乎其微。
因而乎,咱們老老實實的去從頭至尾比較數字,一頁一頁的翻開,去看當前的頁中有沒有數字,沒有的話就去翻下一頁。
這個思路是否是很熟悉?沒錯,這就是計算機的思惟,和咱們上一節中所描述的計算機編碼幾乎是同樣的,除了人能夠一眼多看幾個數據外。
然而,人類在比較大數是否相等的速度,以及翻字典的速度可遠遠比不上計算機去讀完這100萬個數的速度,一樣是「笨鳥」,計算機每秒百萬次的運算能力幾乎能夠在瞬間就完成這樣的任務。
也就是說,在大規模輸入的狀況下,人腦的思惟方式「退化」成和計算機近似,可是被計算機壓倒性的性能優點給擊敗。
在第二輪中,人腦敗給了計算機,但這樣的比拼無疑於兩隻笨鳥比誰更快。有沒有聰明一些的方法呢?
沒錯,咱們學過二分查找(Binary Search)的算法能夠派上用場了。
步驟一:有這麼有一本打印了100萬個數字的字典擺在咱們的面前,咱們不知道要找的數字會在哪裏,那麼咱們先折半打開字典(不用那麼精確也不要緊),看當前頁的第一個數字和最後一個數字,咱們要找的數字是否在這個範圍內,若是在那麼咱們能夠繼續在當前頁找這個數字。
步驟二:若是當前頁的第一個數字仍是比咱們要找的數字大,那麼咱們能夠將字典的後半部分撕了(由於咱們要找的數字不可能在後半部分了),繼續上面的步驟。
步驟三:若是當前頁的最後一個數字比咱們要找的數字小,那麼咱們能夠將字典的前半部分撕了(理由同上),繼續步驟一。
這樣咱們會講這本字典越撕越薄,最壞的狀況下咱們會撕到最後一頁,這一頁要麼有這個數字,要麼沒有這個數字,可是咱們保證按照上面的步驟進行咱們不會錯過任何可能含有這個數字一頁。
這個邏輯和計算機算法中的二分查找原理是同樣的,咱們來看看實際的算法代碼是如何實現的
boolean isNumInArray(int num, int[] array, int start, int end) { if(num < arr[start] || key > arr[end] || start > end){ return false; } int middle = (start + end) / 2; //找到對摺點 if(array[middle] > num) { return isNumInArray(arr, key, start, middle - 1); //撕掉後一半 } else if(array[middle] < num){ return isNumInArray(arr, key, middle + 1, end); //撕掉前一半 }else { return middle; } }
咱們能夠看出,和人類的思惟方式比,計算機不會翻「一頁」,它只會翻看一個數字,可是其餘的思惟方式是如出一轍的。利用這樣的算法,人類雖然從結果上仍是比計算機要慢,可是雙方都找到了最適合的方法,達到自我效率的最大提高。
那麼咱們回過頭來看,爲何我要假設這100萬數字打印在字典上呢?由於字典和計算機內存的模型很像。
計算機能夠經過內存地址來直接訪問內存,這一點和經過字典的頁碼來翻到某一頁,這一點是近似的。
在計算機編碼中咱們能夠知道數組的長度,而經過折半的方法找到中間的數,字典有厚度,咱們能夠經過厚度減半來找到中間的頁碼,這一點也是類似的。
試想同樣,若是100萬的數字不是打印在字典,而是印在一條公路上,咱們是否還能夠用上一節的算法來人肉二分查找?答案是不能夠,由於跑到公路的一半會消耗你不少的體力,若是採用二分法查找比起round 1中的最笨辦法只會讓你耗費更多的體力。由於公路這一存儲的概念,對應的便不是內存的模型,而是磁帶(Tape)的模型,那麼對於這樣的模型,我相信不管是人或者是計算機, 都須要調整算法,來達到最高的效率。
經過以上的例子,咱們能夠看到,計算機的算法反人性,是由於計算機不是一個「正常人」,它有本身的缺陷,也有本身的長處。不少時候咱們覺的算法不直觀,不是由於咱們的思惟能力比計算機差,而偏偏是由於做爲人類咱們同時接觸的信息太多,所會的東西也太多而阻塞了咱們的思惟。那麼這種時候,不妨將本身「墮落」成一臺「鼠目寸光」和「所知甚少」的計算機,這時可能會有更清晰的思路。
本文已獨家受權給腳本之家(ID:jb51net)公衆號發佈