本系列內容來源《學習Javascipt數據結構與算法》,源文件使用es5代碼編寫,在這里我使用ES6來編寫相關實例
下面內容我是按書上順序寫的,不過并不是完全一致,加入了我自己的理解
環境安裝與配置
// 項目目錄如下
-dist
-src
-main.js
index.html
目錄下運行npm init -yes建立package.json文件
安裝http-server
// 只是學習,沒必要安裝到全局
npm i --save-dev http-server
...
// 啟動在當前目錄下的服務器
./node_modules/http-server/bin/http-server
- 安裝babel
npm i --save-dev babel-cli babel-preset-es2015
- 創建.babelrc文件,為babel轉換設置相關規則
{
"presets": ["es2015"],
"plugins": []
}
數組相關方法
// 下面數組方法,都基于下面3個數組
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let arr3 = [7, 8, 9];
let arr4 = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let arr5 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
let arr6 = [1, 2, 3, 2, 1, 3];
let isEven = function (x) {
return (x % 2 === 0);
}
let isEven2 = function (x) {
console.log(x % 2 === 0);
}
下面方法[0]表示不改變原始數組,[1]表示改變原始數組
- concat:對數組進行連接,返回一個新數組 [0]
// 下面輸出[1, 2, 3, 4, 5, 6, 7, 8, 9]
// arr1,arr2,arr3不變
arr1.concat(arr2, arr3);
- join: 使用指定符號連接數組數據,返回一個字符串,默認為‘,’ [0]
arr1.join(); // '1,2,3'
arr1.join('-'); // '1-2-3';
- pop: 刪除并返回數組的最后一個元素 [1]
arr1.pop(); // 3,此時arr1變為[1, 2]
- push: 向數組的末尾添加一個或更多元素,并返回新的長度。 [1]
arr1.push(4); // 3, arr1為[1, 2, 4]
- shift: 與pop功能相似,這個方法時刪除并返回第一個元素 [1]
arr1.shift(); // 1, 此時arr1為[2, 4]
- unshift: 與push功能相似,向數組開頭添加一個或更多元素,并返回新的長度 [1]
arr1.unshift(1); // 返回3,此時arr1為[1, 2, 4]
- reverse: 顛倒數組中元素的順序。 [1]
arr1.reverse(); // [4, 2, 1]
- sort: 對數組進行排序,可以接受函數,設置排序的規則 [1]
arr4.reverse(); // 先故意倒序,[9, 8, 7...]
arr4.sort(); // 對數組進行排序, [1, 2, 3...]
- slice: 返回指定范圍的數組元素(可以接受兩個參數,開始位置start和結束位置end,包含開始位置的元素,不包含結束位置的元素,下標從0開始算) [0]
slice(start, end);
arr4.slice(0, 4); // [1, 2, 3, 4]
- splice: 向/從數組中添加/刪除/替換項目,然后返回被刪除的項目 [1]
splice(index,howmany,item);
// howmany設為0就不會刪除元素,該方法變成向指定位置添加元素
// 此時arr4為[1, "a", 2, 3, 4, 5, 6, 7, 8, 9]
arr4.splice(1, 0, 'a'); // 沒有元素刪除,此時返回為[]
...
// howmany設為1就會刪除元素,不過此時方法看起來是替換元素
// 此時arr4為[1, "b", 2, 3, 4, 5, 6, 7, 8, 9]
arr4.splice(1, 1, 'b'); // 返回["a"],這是被刪除的元素
...
// howmany設為2以上的數字,就能體現出刪除元素的功能了
// 此時arr4為[1, "b", 3, 4, 5, 6, 7, 8, 9]
arr4.splice(1,2,'b'); // ["b", 2]
- map: 對數組的進行迭代,然后把每一次的執行結果組成一個新數組返回 [0]
// isEven參數x接收了arr5每個元素,并進行判斷,并把判斷結果返回成一個數組
// 返回新數組[false, true, false, true, false, true, false, true, false, true, false, true, false, true, false]
arr5.map(isEven);
- forEach: 與map方法類似,對數組進行迭代,區別在于不會返回新的數組 [0]
arr6.forEach(isEven); // 沒有任何返回
arr6.forEach(isEven2); // 在控制臺會輸出每一個元素判斷后的值
- filter: 對數組進行迭代,把符號條件的元素,組合成一個新數組 [0]
arr6.filter(isEven); // [2, 4, 6, 8, 10, 12, 14]
- some: 對數組進行迭代,只要某個數組元素符合條件,就會返回true [0]
arr6.some(isEven); // true,數組中存在偶數
- every: 對數組進行迭代,要求每個數組元素都要符合條件,才會返回true [0]
arr6.every(isEven); // false,數組中有奇數,不符合每個元素都是偶數的條件
- reduce: 對數組元素進行迭代,數組元素按順序進行兩兩操作,并把結果繼續傳遞當作下次計算的第一個元素,直至遍歷到最后一個數組元素,并返回最后計算結果 [0]
reduce(累積變量, 當前變量, 當前位置, 原數組);
累積變量默認是數組第一個元素
arr2.reducce(function(a, b) {
console.log(a, b);
return a * b;
});
...
// 這時控制臺輸出為,20就是數組元素4,5的計算結果,最后返回的是20和6的計算結果
// 計算規則可以隨意定位,并不局限于四則運算
4 5
20 6
< 120
...
arr2.reduce(function (a, b) {
return a + '-' + b;
});
// 這是控制臺輸出'4-5-6'
- reduceRight: 規則和reduce一致,區別在于reduce是按數組順序進行計算,reduceRight是按數組逆序進行計算 [0]
累積變量默認是數組最后一個元素
arr2.reduceRight(function (a, b) {
return a + '-' + b;
});
// '6-5-4'
- indexOf: 數組迭代,查找數組中是否有指定元素,有的話返回出現的位置(數組下標,從0開始),同時終止迭代,否則遍歷完整個數組,如果整個數組都沒有指定數組,該值返回-1 [0]
arr2.indexOf(6); // 2
arr2.indexOf(60); // -1
...
arr2.indexOf(5, 2); // -1,接受第二個參數表示從什么位置開始搜索
arr2.indexOf(5, 1); // 1
- lastIndexOf: 和indexOf方法類似,區別在于這個方法是查詢元素最后出現的位置 [0]
arr6.indexOf(1); // 0
arr6.lastIndexOf(1); // 4
棧
棧是一種遵從后進先出(LIFO)原則的有序集合,新添加的或者待刪除的元素都保存在棧的末尾,稱為棧頂,另一端叫棧底
創建棧
// 原書是使用es5的寫法,我這里換為es6
class Stack {
constructor () {
this.items = [];
}
push (element) {
this.items.push(element);
}
pop () {
return this.items.pop();
}
peek () {
return this.items[this.items.length - 1];
}
isEmpty () {
return this.items.length === 0;
}
size () {
return this.items.length;
}
clear () {
this.items = [];
}
print () {
console.log(this.items.toString());
}
}
let stack = new Stack();
console.log(stack.isEmpty());
在終端運行下面命令
babel ./src/main.js -o ./dist/main.js
如果想簡單一些,可以把上面命令寫到package.json的script中,通過npm run運行
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": " babel ./src/main.js -o ./dist/main.js"
},
編譯完成后我們在index.html文件引入編譯后的main.js,在之前啟動的服務器刷新下,就能看到控制臺輸出true(也就是stack.isEmpty()的值),在控制臺我們可以嘗試使用定義好的方法來驗證相關功能,一個基本的棧就建立完成,總結下我們這個棧一共有如下幾個功能:
- push: 向棧頂添加元素
- pop: 移除棧頂元素
- peek: 返回棧頂元素
- isEmpty: 判斷棧中是否有元素
- clear: 清空棧中所有元素
- size: 返回棧中有多少元素
把十進制數字轉為二進制
js本身有方法使用toString(2)就能得到一個二進制,不過這并不是底層算法,下面用算法,把一個十進制數字轉為二進制
// 以10為例,下面的計算并不是嚴格意義上的計算公式,只是一種計算思路
10 / 2 = 5; // 余數為0
5 / 2 = 2; // 余數為1
2 / 2 = 1; // 余樹為0
1 / 2 = 0; // 余數為1,所以10的二進制為1010
// 以11為例
11 / 2 = 5; // 余數為1
5 / 2 = 2; // 余數為1
2 / 2 = 1; // 余數為0
1 / 2 = 0; // 余數為1
從上面的過程我們分析整個計算過程應該是先算出要轉換的數字和2相除的整數為多少,如果這個整數值為0就終止整個計算過程,如果不是就繼續與2相除,直至結果為0,期間產生的余數就是對應2進制的值
// 10進制轉2進制算法代碼
// 方法是使用上面棧定義的方法
let divideBy2 = function (decNumber) {
let remStack = new Stack();
let rem = 0;
let binaryString = '';
while (decNumber > 0) {
rem = decNumber % 2 // 記錄當前余樹是多少
remStack.push(rem); // 存入棧中
decNumber = parseInt(decNumber / 2); // 與2除取整
}
while (!remStack.isEmpty()) {
binaryString += remStack.pop().toString();
}
return binaryString;
};
漢諾塔
漢諾塔(又稱河內塔)問題是源于印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞著64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。并且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤(來源百度百科)。
漢諾塔的解法主要是對分治遞歸的理解,這個理解可以參考盜夢空間的多層夢境,從第一層夢境到第四層夢境,再從夢境中醒過來的過程。
書上和網上查到都提到使用棧解決漢諾塔,不過實現后發現其實使用棧也不過是記錄數據的方式,并沒有什么特別的地方(簡單的東西說的那么高大上,琢磨了半天...)
遞歸解法
// 這里用了class的寫法
// 使用時new hanoi(4);表示要移動圓盤數,別太大,會卡死
class hanoi {
constructor (disc) {
this.disc = disc; // 設定要移動多少圓盤
this.src = 'A'; // 設定源支柱名詞
this.aux = 'B'; // 輔助支柱名詞
this.dst = 'C'; // 設定目標支柱名詞
}
descMove ({disc = this.disc, src = this.src, aux = this.aux, dst = this.dst} = {}) {
// 這個函數的意義是表示,某個圓盤,要從src(源支柱)移動到dst(目標支柱),aux是輔助支柱,
if (disc) {
// 至少有一個圓盤才會進行下面的邏輯
// 下面函數進行的意義是指,如果至少是兩個圓盤(因為是一個圓盤時,回調當前函數時disc為0,不會有任何輸出)
// 整個算法體現了分治遞歸的思路,我們是假設有三個支柱,A是源支柱,B是輔助支柱,C是目標支柱
// 最終的目的是把A支柱上的所有圓盤移動到C支柱上,如果正向解這個問題,會很復雜,而且會涉及很多判斷
// 使用分治的思路,把問題簡化,
// 整體來看漢諾塔的算法是拆分成如下過程(最終結束的三步)
// 先把(n-1)的圓盤,移動到輔助支柱
// 把n圓盤移動到目標支柱
// 再把(n-1)的圓盤,以輔助支柱為源支柱,源支柱為輔助支柱,進行再一次的遞歸,直至圓盤為1
// 反過來看就是移動的過程
this.descMove({disc : disc - 1, src: src, aux : dst, dst : aux});
console.log(`移動${disc}號圓盤,從${src}移動到${dst}`);
this.descMove({disc: disc - 1, src : aux, aux : src, dst: dst});
}
}
}
結合棧的解法
算法思路還是使用遞歸思路,區別在于使用棧來存儲
// 這里使用了上面定義的棧
// 這里可以先驗證pillarSrc和pillarDst
// 運行hanoi(3, pillarSrc, pillarAux, pillarDst);
// 再次查看兩個棧的值,就能發現模擬的圓盤,從源支柱,移動到目標支柱上
let pillarSrc = new Stack(); // 源支柱
let pillarAux = new Stack(); // 輔助支柱
let pillarDst = new Stack(); // 目標支柱
let n = 3; // 初始時有多少圓盤
while (n--) {
pillarSrc.push(n); // 在源支柱插入數據
}
let hanoi = (dist, src, aux, dst) => {
console.log(dist);
if (dist === 1) {
// 當只有一個圓盤時,把圓盤從源支柱移動到目標支柱
moveStack(src, dst);
} else {
hanoi(dist - 1, src, dst, aux);
moveStack(src, dst);
hanoi(dist - 1, aux, src, dst);
}
}
let moveStack = (src, dst) => {
if (!src.isEmpty()) {
let moveElem = src.pop(); // 把源棧移出棧頂
dst.push(moveElem); // 把從源棧演出的元素插入到目標支柱
}
}