本書看完,對常見的數據結構與算法從概念上有了更深入的理解。
書中關於數組、棧和隊列、鏈表、字典、散列、集合、二叉樹、圖、排序、檢索、動態規划、貪心算法都有詳細的介紹。算是一本不錯的學習書籍。
棧和隊列:都可以通過數組來模擬。棧的規則是先進后出,隊列的規則是先進先出。
鏈表:有雙向與單向鏈表之分,默認有一個頭指針,指向下一個節點,就像鏈條一樣。
數組 vs 鏈表:
- 數組查找速度快:數組能夠通過索引快速找到一個元素,高效快速。而鏈表沒有索引,查找不是很便捷。
- 鏈表增加、刪除性能高:數組增加、刪除一個元素,后面的元素都需要移動一個位置,數據量大時,改前面的數據,很耗性能。而鏈表只需改動節點的指向索引,非常高效。
字典:簡單理解就是key/value的鍵值對存儲的對象。
散列:基於數組設計,長度預定。借助散列函數將每個鍵值映射為一個唯一的數組索引。
集合:js中的map、set。
二叉樹:非線性的數據結構,以分層的方式存儲。用來存儲具有層級關系的數據。比如文件系統。具有根節點、左子樹、右子樹、葉子節點。樹的層數就是樹的深度。樹的遍歷:
- 中序:按照節點上的鍵值,升序訪問所有節點
- 先序:先訪問根節點,然后以同樣的方式訪問左子樹和右子樹。
- 后序:先訪問葉子節點,從左子樹到右子樹,再到根節點。
圖:由邊的集合及頂點的集合組成。地圖就是一種圖。如果一個圖的頂點是有序的,則稱之為有向圖,反之為無向圖。
常見的排序:冒泡排序、選擇排序、插入排序
高級排序:希爾排序、歸並排序。
檢索:二分查找算法,比排序查找算法高效。
動態規划
使用遞歸方案能解決的問題,都能夠使用動態規划技巧來解決。
動態規划:解決相似子問題,並保存子問題的解,通過子問題的解從而解決整個問題。子問題的解通常存儲在數組中便於訪問
動態規划,聽過它的鼎鼎大名,但一直沒深入了解過。
書中的例子:
1.計算裴波那契數列
斐波那契數列:0,1,1,2,3,5,8,13...
該序列是由前兩項數值相加而成的
遞歸實現:效率低,有太多值在遞歸調用中被重新計算。
function recurFib (n) {
if (n < 2) {
return n;
}
return recurFib(n - 1) + recurFib(n - 2);
}
動態規划實現:將數組的每個元素賦值為前兩個元素之和,循環結束,最后一個元素值即為最終計算得到的斐波那契數值
function dynFib(n) {
var val = [];
for (var i = 0; i <= n; i++) { // 0 - n:含 n+1 個
val[i] = 0;
}
if (n == 1 || n == 2) {
return 1;
}
val[1] = 1;
val[2] = 2;
for (var i = 3; i <= n; i++) {
val[3] = val[i-1] + val[i-2];
}
return val[n-1];
}
迭代實現:不保存中間結果
function iterFib (n) {
var last = 1;
var nextLast = 1;
var result = 1;
for (var i = 2; i < n; i++) {
result = last + nextLast;
nextLast = last;
last = result;
}
return result;
}
2.尋找兩個字符串的最長公共子串
動態規划:使用一個二維數組存儲兩個字符串相同位置的字符比較結果。初始數組每一個元素被設置為0,每次在這兩個數組的相同位置發現了匹配,就將該數組對應行和列的元素加1,否則保持為0
function lcs (word1, word2) {
// 初始化二維數組,每項為0
var max = 0;
var index = 0;
var lcsarr = new Array(word1.length + 1);
for (var i = 0; i <= word1.length + 1; i++) {
lcsarr[i] = new Array(word2.length + 1);
for (var j = 0; j <= word2.length + 1; j++) {
lcsarr[i][j] = 0;
}
}
// 保存字符匹配的記錄,如果兩個字符串相應位置的字符進行了匹配,當前數組元素的值將被設置為前一次循環中數組元素保存的值加1,最后如果變量max的值比現在存儲在數組中的當前元素小,max的值將被賦值給這個元素,變量index的值將被設置為i的當前值。這兩個變量將在函數的最后一部分用於確定從哪里開始獲取最長公共子串。
for (var i = 0; i <= word1.length; i++) {
for (var j = 0; j <= word2.length; j++) {
if (i == 0 || j == 0) {
lcsarr[i][j] = 0;
} else {
if (word1[i - 1] == word2[j - 1]) {
lcsarr[i][j] = lcsarr[i - 1][j - 1] + 1;
} else {
lcsarr[i][j] = 0;
}
}
if (max < lcsarr[i][j]) {
max = lcsarr[i][j];
index = i;
}
}
}
// 確認從哪里開始構建這個最長公共子串。以變量index減去變量max的差值作為起始點,以變量max的值作為終點。
var str = '';
if (max == 0) {
return '';
} else {
for (var i = index - max; i <= max; i++) {
str += word2[i];
}
return str;
}
}
console.log(lcs('raven', 'havoc')); // av
關於第二部分,記錄字符串匹配結果,可以優化,減少遍歷次數
for (var i = 1; i <= word1.length; i++) {
for (var j = 1; j <= word2.length; j++) {
if (word1[i - 1] == word2[j - 1]) {
lcsarr[i][j] = lcsarr[i - 1][j - 1] + 1;
}
if (max < lcsarr[i][j]) {
max = lcsarr[i][j];
index = i;
}
}
}
書中還有關於如何用遞歸與動態規划解決背包問題,甚至用貪心算法解決部分背包問題。
背包問題:有一個保險箱,保險箱中的物品規格和價值不同。你需要將保險箱中的寶物放入一個你的背包,希望背包裝進的寶貝價值最大。如:保險箱中有5件物品,尺寸:3,4,7,8,9,價值:4,5,10,11,13,背包的容積為16。最優解是:選取第三件和第五件,總尺寸是16,總價值是23。
如果感興趣的,可以翻開此書看看。