2018/8/20更 新增了二叉樹的相關概念和內容javascript
2018/8/18更 修改了評論區中指出的錯誤,謝謝反饋( ̄▽ ̄)"html
終於來了,算法相關的。 其實我的理解,前端崗位對於算法的要求與其餘IT崗位相比,是低得多的。 可是小白我經歷瞭如螞蟻金服、網易這樣的大廠教作人以後,仍是以爲,對於一些基本算法、思想的掌握仍是必須的。 而後,就把本身遇到的、學到的算法相關的再總結一下,方便本身隨時備戰面試。前端
系列傳送門:java
1.VK的秋招前端奇遇記(一)node
3.VK的秋招前端奇遇記(三)github
5.番外篇:前端面試&筆試算法 Algorithm正則表達式
JS自己數組的sort方法,能夠知足平常業務操做中不少的場景了,因此我認爲這也是爲何基本面試會直接讓寫一個快速排序
,由於好像其餘排序方法在JS中彷佛沒什麼意義了。算法
可是在拼多多的面試中,面試官仍是讓我手寫選擇排序
冒泡排序
和快速排序
的僞代碼。 既然有機會總結,乾脆就所有寫一遍好了,從基本排序到高級排序來講。
基本排序的基本思想很是相似,重排列時用的技術基本都是一組嵌套的for循環: 外循環遍歷數組的每一項,內循環則用於比較元素。
最笨最基本最經典點的方法,無論學什麼語言,說到排序,第一個接觸的就是它了吧。基本思想什麼的太經典了,就不復數了,直接用例子說明過程吧:
E A D B H
複製代碼
通過一次排列後,變成
A E D B H
複製代碼
前兩個元素互換了,接下來變成:
A D E B H
複製代碼
第二個和第三個互換,繼續:
A D B E H
複製代碼
第三個和第四個互換,最後,第二個和第三個元素還會互換一次,獲得最終的順序爲:
A B D E H
複製代碼
好了,其實基本思想就是逐個的比較,下面就實現一下:
function bubleSort(arr) {
var len = arr.length;
for (let outer = len ; outer >= 2; outer--) {
for(let inner = 0; inner <=outer - 1; inner++) {
if(arr[inner] > arr[inner + 1]) {
let temp = arr[inner];
arr[inner] = arr[inner + 1];
arr[inner + 1] = temp;
}
}
}
return arr;
}
複製代碼
這裏有兩點須要注意:
>=2
時便可中止;inner
與inner+1
,所以,臨界條件是inner<outer -1
在比較交換的時候,就是計算機中最經典的交換策略,用臨時變量temp
保存值,可是面試官問過我,ES6有沒有簡單的方法實現? 有的,以下:
arr2 = [1,2,3,4];
[arr2[0],arr2[1]] = [arr2[1],arr2[0]] //ES6解構賦值
console.log(arr2) // [2, 1, 3, 4]
複製代碼
因此,剛纔的冒牌排序能夠優化以下:
function bubleSort(arr) {
var len = arr.length;
for (let outer = len ; outer >= 2; outer--) {
for(let inner = 0; inner <=outer - 1; inner++) {
if(arr[inner] > arr[inner + 1]) {
[arr[inner],arr[inner+1]] = [arr[inner+1],arr[inner]]
}
}
}
return arr;
}
複製代碼
選擇排序是從數組的開頭開始,將第一個元素和其餘元素做比較,檢查完全部的元素後,最小的放在第一個位置,接下來再開始從第二個元素開始,重複以上一直到最後。
有了剛纔的鋪墊,我以爲不用再演示了,很簡單嘛: 外層循環從0開始到length-1
, 而後內層比較,最小的放開頭,走你:
function selectSort(arr) {
var len = arr.length;
for(let i = 0 ;i < len - 1; i++) {
for(let j = i ; j<len; j++) {
if(arr[j] < arr[i]) {
[arr[i],arr[j]] = [arr[j],arr[i]];
}
}
}
return arr
}
複製代碼
簡單說兩句:
i
表示第幾輪,arr[i]
就表示當前輪次最靠前(小)的位置;i
開始,依次日後數,找到比開頭小的,互換位置便可結束,收工!!
插入排序核心--撲克牌思想: 就想着本身在打撲克牌,接起來一張,放哪裏無所謂,再接起來一張,比第一張小,放左邊,繼續接,多是中間數,就插在中間....依次
其實每種算法,主要是理解其原理,至於寫代碼,都是在原理之上瓜熟蒂落的事情:
function insertSort(arr) {
for(let i = 1; i < arr.length; i++) { //外循環從1開始,默認arr[0]是有序段
for(let j = i; j > 0; j--) { //j = i,將arr[j]依次插入有序段中
if(arr[j] < arr[j-1]) {
[arr[j],arr[j-1]] = [arr[j-1],arr[j]];
} else {
break;
}
}
}
return arr;
}
複製代碼
分析: 注意這裏兩次循環中,i
和j
的含義:
i
是外循環,依次把後面的數插入前面的有序序列中,默認arr[0]
爲有序的,i
就從1開始j
進來後,依次與前面隊列的數進行比較,由於前面的序列是有序的,所以只須要循環比較、交換便可break
,由於前面是都是有序的序列,因此若是當前要插入的值arr[j]
大於或等於arr[j-1]
,則無需繼續比較,直接下一次循環就能夠了。乍一看,好像插入排序速度還不慢,可是要知道: 當序列正好逆序的時候,每次插入都要一次次交換,這個速度和冒泡排序是同樣的,時間複雜度O(n²); 固然運氣好,前面是有序的,那時間複雜度就只有O(n)了,直接插入便可。
排序算法 | 平均時間複雜度 | 最壞時間複雜度 | 空間複雜度 | 是否穩定 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 是 |
選擇排序 | O(n²) | O(n²) | O(1) | 不是 |
直接插入排序 | O(n²) | O(n²) | O(1) | 是 |
好了,這張表如何快速記憶呢? 方法就是一開始寫的基本排序算法 。 一開始就說到,基本思想就是兩層循環嵌套,第一遍找元素O(n),第二遍找位置O(n),因此這幾種方法,時間複雜度就能夠這麼簡便記憶啦!
若是全部排序都像上面的基本方法同樣,那麼對於大量數據的處理,將是災難性的,老哥,只是讓你排個序,你都用了O(n²)。 好吧,因此接下來這些高級排序算法,在大數據上,能夠大大的減小複雜度。
快速排序能夠說是對於前端最最最最重要的排序算法,沒有之一了,面試官問到排序算法,快排的機率能有80%以上(我瞎統計的...信不信由你)。
因此快排是什麼呢?
快排是處理大數據最快的排序算法之一。它是一種分而治之的算法,經過遞歸的方式將數據依次分解爲包含較小元素和較大元素的不一樣子序列。該算法不斷重複這個步驟直至全部數據都是有序的。
簡單說: 找到一個數做爲參考,比這個數字大的放在數字左邊,比它小的放在右邊; 而後分別再對左邊和右變的序列作相同的操做:
function quickSort(arr) {
if(arr.length <= 1) {
return arr; //遞歸出口
}
var left = [],
right = [],
current = arr.splice(0,1); //注意splice後,數組長度少了一個
for(let i = 0; i < arr.length; i++) {
if(arr[i] < current) {
left.push(arr[i]) //放在左邊
} else {
right.push(arr[i]) //放在右邊
}
}
return quickSort(left).concat(current,quickSort(right)); //遞歸
}
複製代碼
希爾排序是插入排序的改良算法,可是核心理念與插入算法又不一樣,它會先比較距離較遠的元素,而非相鄰的元素。文字太枯燥,仍是看下面的動圖吧:
在實現以前,先看下剛纔插入排序怎麼寫的:
function insertSort(arr) {
for(let i = 1; i < arr.length - 1; i++) { //外循環從1開始,默認arr[0]是有序段
for(let j = i; j > 0; j--) { //j = i,將arr[j]依次插入有序段中
if(arr[j] < arr[j-1]) {
[arr[j],arr[j-1]] = [arr[j-1],arr[j]];
} else {
continue;
}
}
}
return arr;
}
複製代碼
如今,不一樣之處是在上面的基礎上,讓步長按照三、二、1來進行比較,至關因而三層循環和嵌套啦。
insertSort(arr,[3,2,1]);
function shellSort(arr,gap) {
console.log(arr)//爲了方便觀察過程,使用時去除
for(let i = 0; i<gap.length; i++) { //最外層循環,一次取不一樣的步長,步長鬚要預先給出
let n = gap[i]; //步長爲n
for(let j = i + n; j < arr.length; j++) { //接下類和插入排序同樣,j循環依次取後面的數
for(let k = j; k > 0; k-=n) { //k循環進行比較,和直接插入的惟一區別是1變爲了n
if(arr[k] < arr[k-n]) {
[arr[k],arr[k-n]] = [arr[k-n],arr[k]];
console.log(`當前序列爲[${arr}] \n 交換了${arr[k]}和${arr[k-n]}`)//爲了觀察過程
} else {
continue;
}
}
}
}
return arr;
}
複製代碼
直接看這個三層循環嵌套的內容,會稍顯複雜,這也是爲何先把插入排序寫在前面作一個對照。 其實三層循環的內兩層徹底就是一個插入排序,只不過原來插入排序間隔爲1
,而希爾排序的間隔是變換的n
, 若是把n
修改成1
,就會發現是徹底同樣的了。
運行一下看看
var arr = [3, 2, 45, 6, 55, 23, 5, 4, 8, 9, 19, 0];
var gap = [3,2,1];
console.log(shellSort(arr,gap))
複製代碼
結果以下:
(12) [3, 2, 45, 6, 55, 23, 5, 4, 8, 9, 19, 0] //初始值
當前序列爲[3,2,23,6,55,45,5,4,8,9,19,0]
交換了45和23
當前序列爲[3,2,23,5,55,45,6,4,8,9,19,0]
交換了6和5
當前序列爲[3,2,23,5,4,45,6,55,8,9,19,0]
交換了55和4
當前序列爲[3,2,23,5,4,8,6,55,45,9,19,0]
交換了45和8
當前序列爲[3,2,8,5,4,23,6,55,45,9,19,0]
交換了23和8
當前序列爲[3,2,8,5,4,23,6,19,45,9,55,0]
交換了55和19
當前序列爲[3,2,8,5,4,23,6,19,0,9,55,45]
交換了45和0
當前序列爲[3,2,8,5,4,0,6,19,23,9,55,45]
交換了23和0
當前序列爲[3,2,0,5,4,8,6,19,23,9,55,45]
交換了8和0
當前序列爲[0,2,3,5,4,8,6,19,23,9,55,45]
交換了3和0
當前序列爲[0,2,3,5,4,8,6,9,23,19,55,45]
交換了19和9
當前序列爲[0,2,3,4,5,8,6,9,23,19,55,45]
交換了5和4
當前序列爲[0,2,3,4,5,6,8,9,23,19,55,45]
交換了8和6
當前序列爲[0,2,3,4,5,6,8,9,19,23,55,45]
交換了23和19
當前序列爲[0,2,3,4,5,6,8,9,19,23,45,55]
交換了55和45
複製代碼
wait? 不是還有不少排序算法的嗎?怎麼不繼續了? 是的,其實排序是很深奧的問題,若是研究透各個方法的實現、性能等等,內容恐怕多到爆炸了...並且這個也主要是爲前端常見算法 問題的總結,我的以爲到這裏就差很少了
排序算法 | 平均時間複雜度 | 最壞時間複雜度 | 空間複雜度 | 是否穩定 |
---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | O(1) | 是 |
選擇排序 | O(n²) | O(n²) | O(1) | 不是 |
直接插入排序 | O(n²) | O(n²) | O(1) | 是 |
快速排序 | O(nlogn) | O(n²) | O(logn) | 不是 |
希爾排序 | O(nlogn) | O(n^s) | O(1) | 不是 |
若是不考慮穩定性,快排彷佛是接近完美的一種方法,但惋惜它是不穩定的。 那什麼是穩定性呢?
通俗的講有兩個相同的數A和B,在排序以前A在B的前面,而通過排序以後,B跑到了A的前面,對於這種狀況的發生,咱們管他叫作排序的不穩定性,而快速排序在對存在相同數進行排序時就有可能發生這種狀況。
/* 好比對(5,3A,6,3B ) 進行排序,排序以前相同的數3A與3B,A在B的前面,通過排序以後會變成 (3B,3A,5,6) 因此說快速排序是一個不穩定的排序 /* 複製代碼
穩定性有什麼意義? 我的理解對於前端來講,好比咱們熟知框架中的虛擬DOM的比較,咱們對一個<ul>
列表進行渲染,當數據改變後須要比較變化時,不穩定排序或操做將會使自己不須要變化的東西變化,致使從新渲染,帶來性能的損耗。
遞歸,其實就是本身調用本身。
不少時候咱們本身以爲麻煩或者感受 "想象不過來",主要是本身和本身較真,由於交給遞歸,它本身會幫你完成須要作的。
遞歸步驟:
talk is cheap,show me code!
斐波那契數列,每一個語言講遞歸都會從這個開始,可是既然搞前端,就搞點不同的吧,從對象的深度克隆(deep clone)提及
Deep Clone :實現對一個對象(object)的深度克隆
//所謂深度克隆,就是當對象的某個屬性值爲object或array的時候,要得到一份copy,而不是直接拿到引用值
function deepClone(origin,target) { //origin是被克隆對象,target是咱們得到copy
var target = target || {}; //定義target
for(var key in origin) { //遍歷原對象
if(origin.hasOwnProperty(key)) {
if(Array.isArray(origin[key])) { //若是是數組
target[key] = [];
deepClone(origin[key],target[key]) //遞歸
} else if (typeof origin[key] === 'object' && origin[key] !== null) {
target[key] = {};
deepClone(origin[key],target[key]) //遞歸
} else {
target[key] = origin[key];
}
}
}
return target;
}
複製代碼
這個能夠說是前端筆試/面試中常常常常遇到的問題了,思路是很清晰的:
剩下的事情,交給JS本身處理就行了,咱們不用考慮內部的層層嵌套,想太多
接下來,列舉一些本身在最近筆試、面試中遇到的,須要使用遞歸實現的問題
Array的方法flat不少瀏覽器還未能實現,請寫一個flat方法,實現扁平化嵌套數組,如:
Array
var arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]
複製代碼
這個問題的實現思路和Deep Clone很是類似,這裏實現以下:
Array.prototype.flat = function() {
var arr = [];
this.forEach((item,idx) => {
if(Array.isArray(item)) {
arr = arr.concat(item.flat()); //遞歸去處理數組元素
} else {
arr.push(item) //非數組直接push進去
}
})
return arr; //遞歸出口
}
複製代碼
好了,能夠測試一下:
arr = [[2],[[2,3],[2]],3,4]
arr.flat()
// [2, 2, 3, 2, 3, 4]
複製代碼
神祕力量的新解法
在評論區的一位小夥伴,提出了更好的辦法,很簡潔、方便,只用一句話就能夠實現需求哦(不過你這樣去解答一道網易的「編程題」,不以爲讓人家有點難堪嘛~哈哈)
arr.prototype.flat = function() {
this.toString().split(',').map(item=> +item )
}
複製代碼
好了,驚歎完以後,大概說下實現吧:
'2,2,3,2,3,4'
['2','2','3','2','3','4']
2,2,3,2,3,4
這個問題,詳細的解釋能夠在我以前的文章(從Co剖析和解釋generator的異步原理)去看一下,若是對ES6的iterator和generator不太瞭解的,能夠跳過這個問題。
好比實現以下的功能:
const co = require('co');
co(function *() {
const url = 'http://jasonplacerholder.typecoder.com/posts/1';
const response = yield fetch(url);
const post = yield response.json();
const title = post.title;
console.log('Title: ',title);
})
複製代碼
剖析:
done
,表明結束,能夠出去function run(generat) {
const iterator = generat();
function autoRun(iteration) {
if(iteration.done) {return iteration.value} //出口
const anotherPromise = iteration.value;
anoterPromise.then(x => {
return autoRun(iterator.next(x)) //遞歸條件
})
}
return autoRun(iterator.next())
}
複製代碼
有一樓梯共M級,剛開始時你在第一級,若每次只能跨上一級或二級,要走上第M級,共有多少種走法?
分析: 這個問題要倒過來看,要到達n級樓梯,只有兩種方式,從(n-1)級 或 (n-2)級到達的。因此能夠用遞推的思想去想這題,假設有一個數組s[n], 那麼s[1] = 1(因爲一開始就在第一級,只有一種方法), s[2] = 1(只能從s[1]上去 沒有其餘方法)。
那麼就能夠推出s[3] ~ s[n]了。
下面繼續模擬一下, s[3] = s[1] + s[2], 由於只能從第一級跨兩步, 或者第二級跨一步。
function cStairs(n) {
if(n === 1 || n === 2) {
return 1;
} else {
return cStairs(n-1) + cStairs(n-2)
}
}
複製代碼
嗯嗯,沒錯呢,其實就是斐波納契數列沒跑了
二分查找,是在一個有序的序列裏查找某一個值,與小時候玩的猜數字遊戲很是相啦:
A: 0 ~ 100 猜一個數字
B: 50
A: 大了
B: 25
A: 對頭,就是25
複製代碼
所以,思路也就很是清楚了,這裏有遞歸和非遞歸兩種寫法,先說下遞歸的方法吧:
function binaryFind(arr,target,low = 0,high = arr.length - 1) {
const n = Math.floor((low+high) /2);
const cur = arr[n];
if(cur === target) {
return `找到了${target},在第${n+1}個`;
} else if(cur > target) {
return binaryFind(arr,target,low, n-1);
} else if (cur < target) {
return binaryFind(arr,target,n+1,high);
}
return -1;
}
複製代碼
接下來,使用循環來作一下二分查找,其實思路基本一致:
function binaryFind(arr, target) {
var low = 0,
high = arr.length - 1,
mid;
while (low <= high) {
mid = Math.floor((low + high) / 2);
if (target === arr[mid]) {
return `找到了${target},在第${mid + 1}個`
}
if (target > arr[mid]) {
low = mid + 1;
} else if (target < arr[mid]) {
high = mid - 1;
}
}
return -1
}
複製代碼
這裏對基本概念就不詳細複習了,在各大資料中有更詳盡的介紹,這裏就只簡單介紹基本概念和術語,而後使用JavaScript實現一個二叉樹,並封裝其方法。
如圖所示,一棵樹最上面的幾點稱爲根節點,若是一個節點下面鏈接多個節點,那麼該節點成爲父節點,它下面的節點稱爲子節點,一個節點能夠有0個、1個或更多節點,沒有子節點的節點叫葉子節點。
二叉樹,是一種特殊的樹,即子節點最多隻有兩個,這個限制可使得寫出高效的插入、刪除、和查找數據。在二叉樹中,子節點分別叫左節點和右節點。
二叉查找樹是一種特殊的二叉樹,相對較小的值保存在左節點中,較大的值保存在右節點中,這一特性使得查找的效率很高,對於數值型和非數值型數據,好比字母和字符串,都是如此。如今經過JS實現一個二叉查找樹。
二叉樹的最小元素是節點,因此先定義一個節點
function Node(data,left,right) {
this.left = left;
this.right = right;
this.data = data;
this.show = () => {return this.data}
}
複製代碼
這個就是二叉樹的最小結構單元
function BST() {
this.root = null //初始化,root爲null
}
複製代碼
BST初始化時,只有一個根節點,且沒有任何數據。 接下來,咱們利用二叉查找樹的規則,定義一個插入方法,這個方法的基本思想是:
BST.root === null
,那麼就將節點做爲根節點BST.root !==null
,將插入節點進行一個比較,小於根節點,拿到左邊的節點,不然拿右邊,再次比較、遞歸。這裏就出現了遞歸了,由於,老是要把較小的放在靠左的分支。換言之
最左變的葉子節點是最小的數,最右的葉子節點是最大的數
function insert(data) {
var node = new Node(data,null,null);
if(this.root === null) {
this.root = node
} else {
var current = this.root;
var parent;
while(true) {
parent = current;
if(data < current.data) {
current = current.left; //到左子樹
if(current === null) { //若是左子樹爲空,說明能夠將node插入在這裏
parent.left = node;
break; //跳出while循環
}
} else {
current = current.right;
if(current === null) {
parent.right = node;
break;
}
}
}
}
}
複製代碼
這裏,是使用了一個循環方法,不斷的去向子樹尋找正確的位置。 循環和遞歸都有一個核心,就是找到出口,這裏的出口就是當current 爲null的時候,表明沒有內容,能夠插入。
接下來,將此方法寫入BST便可:
function BST() {
this.root = null;
this.insert = insert;
}
複製代碼
這樣子,就可使用二叉樹這個自建的數據結構了:
var bst = new BST();
bst.insert(10);
bst.insert(8);
bst.insert(2);
bst.insert(7);
bst.insert(5);
複製代碼
可是這個時候,想要看樹中的數據,不是那麼清晰,因此接下來,就要用到遍歷了。
咱們知道,樹的遍歷主要包括
這個只是爲了好記憶,咱們拿下面的圖作一個遍歷
前序遍歷: 56 22 10 30 81 77 92
中序遍歷: 10 22 30 56 77 81 92
後序遍歷: 10 30 22 77 92 81 56
這裏發現了一些規律:
56
左邊(10/22/30)的必定是左子樹的,右邊的(77/81/92)必定是右子樹的。這裏就又用到以前的遞歸了,中序遍歷要求: 左!根!右
function inOrder(node) {
if(node !== null) {
//若是不是null,就一直查找左變,所以遞歸
inOrder(node.left);
//遞歸結束,打印當前值
console.log(node.show());
//上一次遞歸已經把左邊搞完了,右邊
inOrder(node.right);
}
}
//在剛纔已有bst的基礎上執行命令
inOrder(bst.root);
複製代碼
經過遞歸,實現了中序遍歷,上面打印出的結果以下:
2
5
7
8
10
複製代碼
若是剛纔的遞歸過程搞清楚,那這個就再簡單不過了
function preOrder(node) {
if(node !== null) {
//根左右
console.log(node.show());
preOrder(node.left);
preOrder(node.right);
}
}
複製代碼
ok,趁熱打鐵,就把後序遍歷的方法也一併寫入,以下:
function postOrder(node) {
if(node !== null) {
//左右根
postOrder(node.left);
postOrder(node.right);
console.log(node.show())
}
}
複製代碼
好了,能夠去嘗試兩種方法打印出來的結果了:
preOrder(bst.root);
postOrder(bst.root);
複製代碼
在二叉樹這種數據結構中進行數據查找是最方便的,如今咱們就對查找最小值、最大值和特定值進行一個梳理:
清楚思路後,就動手來寫:
//最小值
function getMin(bst) {
var current = bst.root;
while(current.left !== null) {
current = current.left;
}
return current.data;
}
//最大值
function getMax(bst) {
var current = bst.root;
while(current.right !== null) {
current = current.right;
}
return current.data;
}
複製代碼
最大、最小值都是很是簡單的,下面主要看下如何經過
function find(target,bst) {
var current = bst.root;
while(current !== null) {
if(target === current.data) {
return true;
}
else if(target > current.data) {
current = current.right;
} else if(target < current.data) {
current = current.left;
}
}
return -1;
}
複製代碼
其實核心,仍然是經過一個循環和判斷,來不斷的向下去尋找,這裏的思想其實和二分查找是有點相似的。
哇...
沒想到今天去整理排序 花了這麼久...嗯..然而這篇文章已經夠長了
接下來我會把以前筆試遇到的題目和一些經常使用的算法問題,一一記錄,前端不少算法都是和數組、字符串處理息息相關的,因此對正則表達式、數組經常使用方法的掌握也很重要,簡單總結下知識點:
內容會持續更新,最快的固然仍是在github上,而後會同步到掘金,github傳送門