在前端工程師中,常常有一種聲音,我們為什么要學數據結構與算法,沒有數據結構與算法,我們一樣很好的完成工作。
實際上,算法是一個寬泛的概念,我們寫的任何程序都可以稱為算法,甚至往冰箱里放大象,也要通過開門,放入,關門這樣的規划,我們也可以視作為一種算法。可以說:簡單的算法是人類的本能。而算法的知識的學習則是吸取前人的經驗。對於復雜的問題進行歸類,抽象,幫助我們脫離刀耕火種的時代,系統掌握一個算法的過程。
隨着自身知識的增長,不論是做前端,服務端還是客戶端,任何一個程序員都會開始面對更加復雜的問題,算法和數據結構就變得更不可或缺了。
前端工程師應該是最需要重視算法和數據結構基礎的人。數據結構和算法的又沒有照顧到入門的需要,所以前端工程師如果自身不重視算法和數據結構這樣的基礎知識,很可能陷入一直重復勞動很少有成長的這樣職業發展困境。未來的網頁UI,絕不是靠幾個選擇器操作加鏈接就能應付的。越來越復雜的產品和基礎庫,需要堅實的數據結構與算法才能駕馭。
在過去的幾年中,得益於Node.js和SpiderMonkey等平台,javascript越來越多的用於服務器端編程,介於javascript已經走出了瀏覽器,程序員發現他們需要更多的傳統語言(比如C++和JAVA),提供的工具。這些工具包括傳統的數據結構(如鏈表,棧,隊列,圖等)也包括傳統的排序和查找算法。本系列博文在使用javascript進行服務器端編程時,如何使用這些數據結構和算法。
javascript程序員會發現本系列內容很有用,因為本書討論了在javascript語言的限制下,如何實現數據結構和算法。這些限制包括:數組即對象,無處不在的全局變量,基於原型的對象模型等。javascript作為一種編程語言,名聲不大好,本系列博文將展示javascript好的一面,去實現如何高效的數據結構和算法。
在那些學校沒有學習過計算機的程序員來說,唯一熟悉的數據結構就是數組。在處理一些問題時,數組無疑是很好的選擇,但對於很多復雜的問題,數組就顯得太過簡陋。大多數程序員都原因承認這樣一個事實:對於很多編程問題,當他們提出一個合適的數據結構,設計和實現解決這些問題的算法就變得手到擒來。
二叉查找數(BST)就是這樣一個例子。設計二叉差找樹的目的就是為了方便查找一組數據中的最小值和最大值,由於這個數據結構自然引申出一個查找算法,該算法比目前最好的查詢算法效率還高。不熟悉二叉查找樹的程序員可能會使用一個更簡單的數據結構,但效率上就打了個折扣。
學習算法非常重要,因為解決同樣的問題,往往可以使用多種算法。對於高效程序員來說,知道那種算法效率高非常重要。比如,現在至少有六七種排序算法,如果知道快速排序比選擇排序效率更高,那么就會讓排序過程變得高效。又比如,實現一個線性查找的算法很簡單,但是如果知道有時二分查找可能比線性查找快兩倍以上,那么你勢必會寫出一個更好的程序。
深入學習數據結構和算法,不僅可以知道那種數據結構和算法更高效,還會知道如何找出最適合解決手頭問題的數據結構和算法。寫程序,尤其適用javascript寫程序時 ,經常要權衡。
javascript的編程環境和模型
本章描述了javascript編程環境和基本的編程模塊,后續章節將使用這些知識定義數據結構和實現各種算法。
1.javascript歷來都是在瀏覽器里運行的程序語言,然而在過去的幾年中,這些情況發生了變化,javascript可以作為桌面程序執行。或者在服務器上執行。介紹一種編程環境:javascript shell,這是MOzilla提供的綜合javascript編程環境SpiderMonkey中的一部分。
2.聲明和初始化變量
javascript變量默認是全局變量,嚴格的說,甚至不需要在使用前進行聲明。如果對一個事先未聲明的javascript變量進行初始化,該變量就變成了一個全局變量。所有,我們在編程中,遵循c++或java等編譯型語言的習慣,在使用變量前進行聲明,這樣做的好處就是:聲明的變量都是局部變量。本章稍后將部分詳細討論變量的作用域。
在javascript中聲明變量,需要使用關鍵字var,后面跟變量名,或后面跟隨一個賦值表達式。
var number; var name; var rate = 1.2; var greeting = "hello world!"; var flag = false;
3.javascript中的算術運算符和數學庫函數
javascript使用標准的算術運算符
+ 加 , - 減 , * 乘 , / 除 , % 去余
javascript同時擁有一個數學庫,用來完成一些高級運算,比如平方根,絕對值和三角函數。算術運算符遵循標准的運算順序,可以用括號來改變運算順序。
var x = 3; var y = 1.1; console.log(x + y) console.log(x * y) console.log((x + y) * (x - y)); var z = 9; console.log(Math.sqrt(z)); console.log(Math.abs(y/x))
如果計算精度不必像上面那樣精確,可以將數字轉換為固定精度
var x = 3; var y = 1.1; var z = x * y; console.log(z.toFixed(2));
2.判斷結構
根據布爾表達式的值,判斷結構讓程序可以執行到那句可以執行的程序的語句。常用的有if和switch語句
if有如下三種形式
- 簡單的if語句
- if-else語句
- if-else-if語句
下面演示簡單的if語句
var mid = 25; var height = 50; var low = 1; var current = 13; var found = -1; if (current < mid) { mid = (current - low) / 2 }
下面演示if - else語句
var mid = 25; var height = 50; var low = 1; var current = 13; var found = -1; if (current < mid) { mid = (current - low) / 2; } else { mid = (current + height) / 2; }
下面演示if - else -if
var mid = 25; var height = 50; var low = 1; var current = 13; var found = -1; if (current < mid) { mid = (current - low) / 2; } else if (current > mid) { mid = (current + height) /2 } else { found = current }
另外一個判斷結構是switch語句,在有多個簡單選擇時,使用該語句的代碼更加清晰。
3.循環結構
常用的兩種循環結構:while循環和for循環。
如果希望條件為真時,只執行一組語句,就選擇while循環,下面展示了while循環的工作原理
while循環
var number = 1; var sum = 0; while (number < 11) { sum += number; ++number } console.log(sum) //55
如果希望按執行次數執行一組語句,就選擇for循環。下面是for循環求1到10的整數累加和
var number = 1; var sum = 0; for (var number =1 ; number < 11; number++){ sum += number; } console.log(sum)
訪問數組時,經常使用到for循環,如下:
var number = [3,7,12,22,100]; var sum = 0; for (var i = 0; i < number.length; ++i) { sum += number[i]; } console.log(sum)//144
4.函數
javascript提供了兩種自定義函數的方式,一種有返回值,一種沒有返回值(這種函數有時叫做子程或者void函數)。
下面展示了如何自定義一個有返回值的函數如何在javascript調用該函數。
function factorial (number) { var product = 11; for (var i = number; i >= 1; --i) { product *= 1 } return product; } console.log(factorial(4))
下面的例子展示如何定義一個沒有返回值的函數,使用該函數並不是為了得到它的返回值,而是為了執行函數中定義的操作。
javascript中的子程或者void函數
function curve(arr, amount) { for (var i = 0; i < arr.length; ++i) { arr[i] += amount; } } var grades = [77,73,74,81.90]; curve(grades,5); console.log(grades)
javascript中,函數的參數傳遞方式都是按值傳遞,沒有按引用傳遞的參數。但是javascript有保存引用的對象,比如數組,如上例,它們是按引用傳遞的。
5.變量作用域
變量的作用域是指一個變量在程序中那些對象可以訪問。javascript中的變量作用域被定義為函數作用域。
這是指變量的值在定義該變量的函數內是可見的。並且定義在該函數內的嵌套函數也可以訪問該變量。
在主程序中,如果在函數的外部定義一個變量,那么該變量擁有全局作用域,這是指可以在包括函數體內的程序的任何部分訪問該變量。下面用一段簡短的程序展示作用域的工作原理。
function showScope(){ return scope; } var scope = "global"; console.log(scope); //global console.log(showScope()); //global
showScope()函數內定義的變量scope擁有局部作用域,而在主程序中定義變量scope是一個全局變量。盡管兩個變量名字相同,但它們的作用域不同,在定義他們的地方訪問時得到的值也不一樣。
這些行為都是正常且符合預期的,但是如果在定義變量時省略了關鍵字var,那么一切都變了。javascript允許在定義變量時不使用關鍵字var,但是這樣做的后果是定義的變量自動擁有了全局作用域,即使你在一個函數內定義該變量,它也是全局變量。
下面的例子是濫用全局變量的惡果。
function showScope(){ scope = "local"; return scope; } scope = "global"; console.log(scope); //global console.log(showScope()); //local console.log(scope) //local
上面的例子中,由於showScope()函數內定義變量scope時省略了關鍵字var。所以在將字符串"local"賦值給該變量時,實際上是改變了主程序中scope變量的值。因此,在定義變量時,應該總是var開始,避免發生類似錯誤。
前面我們提到,javascript擁有的是函數作用域,其含義是javascript沒有塊級作用域,這一點有別於其它很多現代的編程語言。使用塊級作用域,可以在一段代碼中定義變量,該變量只在塊內可見,離開這段代碼塊就不見了。在C++或者java的for循環中,我們經常看到這樣的例子
for (int i = 1; i <= 10; ++i) { count << "hello world!" << end; }
雖然javascript沒有塊級作用域,但在我們寫for循環時,我們假設它有:
for (var i = 1; i <=10; ++i) { console.log("hello,world!") }
這樣做的原因是,不希望自己養成壞編程習慣的幫手!
6.遞歸
javascript中允許函數遞歸調用,前面定義過的factorial()函數也可以使用遞歸方式定義
function factorial(number) { if (number == 1) { return number } else { return number * factorial(number - 1) } } console.log(factorial(5)) //120
當一個函數遞歸調用時,當遞歸沒有完成時,函數的計算結果暫時會被掛起。為了說明這個過程,這里用一副圖展示以5作為參數,調用factorial()函數時函數執行的過程。
5 * factorial(4) 5 * 4 * factorial(3) 5 * 4 * 3 * factorial(2) 5 * 4 * 3 * 2 * factorial(1) 5 * 4 * 3 * 2 * 1 5 * 4 * 3 * 2 5 * 4 * 6 5 * 24 120
對於大多數情況,javascript有能力處理層次較深的遞歸調用;但保不齊有些算法需要的遞歸深度超出了javascript的處理能力,這時候,我們就需要尋求改算法的一種迭代方式的解決方案了。任何可以被遞歸定義的函數,都可以被改寫為迭代的程序,這點要牢記於心。
對象和面向對象編程
數據結構都被要實現為對象。javascript提供了很多種方式來創建和使用對象。下面做展示,創建對象,並用於創建和使用對象中的方法和屬性。
對象通過如下方式創建:定義包含屬性和方法聲明的構造函數,並在構造函數后緊跟方法的定義。下面是一個檢查銀行賬戶對象的構造函數:
function Checking(amount){ this.balance = amount; //屬性 this.deposit = deposit; //方法 this.withdraw = withdraw;//方法 this.toString = toString;//方法 }
this關鍵字用來將方法和屬性綁定到一個對象的實例上,下面看看我們對上面聲明過的方法是如何定義的
function deposit(amount) { this.balance += amount; } function withdraw(amount) { if (amount <= this.balance) { this.balance -= amount; } if (amount > this.balance) { console.log("Insufficient funds"); } } function toString(){ return "Balance:" + this.balance; }
這里,我們又一次的使用this關鍵字和balance屬性,以便讓javascript解釋器指定我們引用的是那個對象的balance屬性
下面給出checking對象的完整定義和測試
function Checking(amount){ this.balance = amount; //屬性 this.deposit = deposit; //方法 this.withdraw = withdraw;//方法 this.toString = toString;//方法 } function deposit(amount) { this.balance += amount; } function withdraw(amount) { if (amount <= this.balance) { this.balance -= amount; } if (amount > this.balance) { console.log("余額不足"); } } function toString(){ return "余額:" + this.balance; } var account = new Checking(500); account.deposit(1000); console.log(account.toString());//余額:1500 account.withdraw(750); console.log(account.toString());//余額:750 account.withdraw(800); //余額不足 console.log(account.toString());//余額:750
ps:編寫出讓人容易閱讀的代碼和編寫出讓計算機能正確執行的代碼同等重要,作為負責任的程序員,必須將這一點牢記於心。