最近在擼vue 和react的源碼,雖然晦澀難懂,可是卻發現新大陸,發現了數據結構和算法在前端的重要性,好比在react中,發現react的fiber樹,對應的其實是一個叫鏈表的數據結構,咱們es6中新出的Map的數據結構其實就是對應字典的數據結構而Set對應的就是集合的數據結構,他是一個無序且惟一的數據結構。而在vue 中也是大量的用到棧和隊列的數據結構,因而,遍尋資料,學習一番,記錄以下,若有錯誤,請大佬指點!前端
我在學習數據結構與算法時,請教了許多人,他們就告訴我直接刷題,因而我打開力扣準備開始刷題之旅,然而,刷了一陣以後發現,因爲沒有系統的知識體系,學得快,忘得也快,最大的感觸就是不能觸類旁通,終於意識到,知識體系的重要性了。vue
因而,從新翻開大學嶄新的數據結構與算法,溫習一遍。node
首先咱們的數據結構分那麼幾種,咱們必需要完全瞭解數據結構都有哪些,這樣在碰見算法時咱們才能清晰的知道應該用怎樣的數據結構去解決問題。react
棧是一種聽從後進先出(LIFO)原則的數據結構。
複製代碼
如上圖所示,棧頂和棧低,在上圖中,咱們能夠清晰的看到,先入棧的數據因爲被壓在棧底,若是想要出棧,那麼必須等前方的數字都出棧才能輪到他,因而就造成了咱們的後進先出的數據結構(強調一下棧是一種數據結構)es6
然而在咱們前端中是沒有棧這種數據結構的,可是咱們有萬能的數組,使用它能夠模擬出棧,而且還能衍生出棧的操做方法,咱們知道在es標準中數組有兩個標準方法push和pop其實,他們就能夠表示出棧和入棧的操做方法,好,廢話少說讓咱們開始吧!面試
class Stack {
constructor() {
this.stack = []
// 棧的長度
this.size = 0
}
// 入棧
push(i) {
this.stack[this.size] = i
this.size++
}
// 出棧
pop() {
if (this.stack.length > 0) {
const last = this.stack[--this.size]
this.stack.length--
return last
}
}
// 查看棧頂元素
peek() {
return this.stack[this.size - 1];
}
// 棧是否爲空
isEmpty() {
return stack.length === 0;
}
// 清空棧
clear() {
this.stack = []
}
// 查看棧元素
see(){
console.log(this.stack)
return this.stack
}
}
複製代碼
到此爲止,一個簡單的前端棧的實現就完成了算法
而在咱們前端中,咱們常說的調用堆棧,其實就是使用棧的數據結構,你會發現他徹底符合最早壓入最後彈出編程
隊列是一種聽從先進先出原則的數據結構。
複製代碼
如上圖所示,咱們發現最早進入的數據,只能先出去,他具備先進先出的特性。然而,在js的語法中一樣的沒有隊列的數據結構,可是咱們依然能夠用數據來模擬隊列的數據結構,因爲隊列是先進先出,那麼,也就是如上圖所示,咱們首先須要模擬原生的入隊push方法,再模擬原生的shift方法,ok 廢話少說開始吧!數組
class Queue {
constructor() {
// 建立一個隊列
this.queue = []
// 隊列長度
this.size = 0
}
// 入隊列
push(i) {
this.queue[this.size] = i
this.size++
}
// 出隊列
shift() {
if (this.queue.length === 0) {
return
}
const first = this.queue[0]
//將後面的賦值給前面的
for (let i = 0; i < this.queue.length - 1; i++) {
this.queue[i] = this.queue[i + 1]
}
this.queue.length--
return first
}
//獲取隊首
getFront() {
return this.queue[0];
}
//獲取隊尾
getRear() {
return this.queue[this.size - 1]
}
// 清空隊列
clear() {
this.queue = []
}
// 查看隊列元素
see() {
console.log(this.queue)
return this.queue
}
}
複製代碼
到此爲止一個簡單的隊列就實現了瀏覽器
其實,在前端中咱們的異步事件就徹底符合隊列的數據結構,先進先出,學到這裏,你還覺的前端不須要學習數據結構嗎?
用鏈式存儲的線性表統稱爲鏈表
複製代碼
上面就是鏈表的定義,有沒有感受晦澀難懂,其實他本質上就是一個多元素組成的列表,只不過他的元素存儲不連續,須要再用next指針關聯。這就是一個鏈表的結構,無圖無真相,上圖 上圖就是一個簡單的鏈表結構,就是這麼的樸實且無華,然而,在平常的使用中,人們發現鏈表的形態也是多種多樣的,因而就給他作了單向鏈表,和雙向鏈表的區分
如上圖所示,就是一個簡單的單項鍊表,單向鏈表(單鏈表)是鏈表的一種,它由節點組成,每一個節點都包含下一個節點的指針,下圖就是一個單鏈表,表頭爲空,表頭的後繼節點是"結點10"(數據爲10的結點),"節點10"的後繼結點是"節點20"(數據爲10的結點)
如上圖所示,雙向鏈表(雙鏈表)是鏈表的一種。和單鏈表同樣,雙鏈表也是由節點組成,它的每一個數據結點中都有兩個指針,分別指向直接後繼和直接前驅。因此,從雙向鏈表中的任意一個結點開始,均可以很方便地訪問它的前驅結點和後繼結點。通常咱們都構造雙向循環鏈表。
說完這些概念,咱們發現前端中也沒有鏈表這種數據結構,然而,咱們能夠用對象來實現一個簡單的鏈表結構,
var a = { name: '大佬1' }
var b = { name: '大佬1' }
var c = { name: '大佬1' }
var d = { name: '大佬1' }
var e = { name: '大佬1' }
// 創建鏈表
a.next=b
b.next=c
c.next=d
d.next=e
複製代碼
如此一來咱們的鏈表就創建完成,這是有人就會問了,鏈表的數據結構到底有啥好處呢,尚未數組方便呢? 咱們發現,鏈表的存儲不是連續的,他們經過next去關聯,因此咱們執行刪除的時候,只須要去改變next指針,即可天然而然的執行刪除操做,可是,你若是想在數組中去刪除,你會發現,至關的麻煩
固然,既然鏈表是一種數據結構,那麼,咱們必定會有相應的操做,下面咱們看看都有啥吧
// 遍歷鏈表
let p = a
while (a) {
console.log(a)
p = a.next()
}
// 插入鏈表
const f={name:'大佬1'}
//模擬在a元素後插入
a.next=f
f.next=b
// 刪除鏈表
// 在刪除f
a.next=b
複製代碼
上文中咱們模擬了在js中鏈表的遍歷,插入以及刪除,如此發現,其實鏈表的數據結構尚未咱們操做數組難呢
集合是由一組無序但彼此之間又有必定相關性的成員構成的, 每一個成員在集合中只能出現一次.他是一個無序且惟一的數據結構
複製代碼
在以前的數據結構中,前端都沒有對應的實現,終於的,咱們的集合在前端中有了本身的集合構造器,他就是大名鼎鼎的-Set,一般的咱們用它來執行數組去重,接下來咱們來看看他都有什麼魔力吧!
set是es6新出的一種數據結構,Set對象是值的集合,你能夠按照插入的順序迭代它的元素。 Set中的元素只會出現一次,即 Set 中的元素是惟一的。接下來讓咱們來看看set的基本操做
// 建立一個 集合
var s = new Set([1, 2, 3, '3', 4]);
//添加一個key
s.add(5);
//重複元素在Set中自動被過濾
s.add(5);
console.log(s);//Set {1, 2, 3, 4,5}
//刪除一個key
s.delete(2);
// 判斷是否有這個元素
s.has(3)
console.log(s);//Set{1, 3, "3", 4, 5}//注意數字3和字符串'3'是不一樣的元素。
複製代碼
既然是集合,那麼咱們怎麼能沒有求交集,並集呢?然而比較惋惜的是,set 並無給咱們提供對應的操做方法,不過,沒關係,咱們有數組,能夠利用數組求出交集
// 並集
const arr1=new Set([1,2,4])
const arr2=new Set([2,3,4,5])
new Set([...arr1, ...arr2])
//交集
const arr1=new Set([1,3,4,6,7,9])
const arr2=new Set([2,3,4,5,6,7])
const arr=new Set([...arr1].filter((item)=>{arr2.has(item)}))
複製代碼
字典 是一種以鍵-值對形式存儲惟一數據的數據結構
複製代碼
那你就會說了,這玩意不就是個對象嗎?拿他和對象有啥區別呢?咱們先來看怎樣使用,估計你就能一眼看出區別了,在es6中,咱們能夠用Map這個構造器去建立一個字典
廢話少說,直接上代碼
// 建立一個字典
const map=new Map([
['a',1],
['b',2]
]);
console.log(map);
複製代碼
上圖所示,就是咱們的字典的數據結構,你發現他的原型不一樣,而且存儲結構也不一樣,那麼有的同窗又問了,都有了對象了,並且能實現Map的功能,爲啥還要開發一個Map,這裏個人理解是,爲了讓對象的功能和做用更純粹,從而使得這門語言規範法,標準化,以及打開變函數式編程的大門,接下來,咱們來簡單的使用字典的增刪改查
// 字典的增刪改查
// 獲取長度
console.log(map.size);
// 增長key 能夠是個非原始類型
console.log(map.set([1,2],''));
map.set('name','李麗').set('age',18);
// 查詢
console.log(map.get('name'));
// 刪除
console.log(map.delete('name'));
// 是否含有
console.log(map.has('name'));
複製代碼
說了這麼多,咱們來看看字典的用處吧好比:求出數組中三個最大的三個數的下標
// 求最大的三個數 而且求出下標
// =======解題思路=======
//一、採用的是暴力求解法,那麼咱們能夠利用字典的特色存儲最大的三個值
//二、還有種比較好理解的辦法利用排序,在以前文章裏寫過
function maxThree(arr) {
var i = 0;
var j = 0;
var tmp = arr[0]
var v = 0
var b = new Map()
// 遍歷三次分別找到最大的三個值
while (i < 3) {
while (j < arr.length) {
if (tmp < arr[j] && !b.has(j)) {
tmp = arr[j]
v = j
}
j++
}
b.set(v, tmp)
i++
j = 0
tmp = 0
}
// console.log(b)
}
maxThree([1, 3, 4, 5, 6, 7, 9, 1, 1, 9, 2, 8, 3, 4, 5,])
複製代碼
圖是一種網絡結構的抽象模型,它是一組由邊鏈接的頂點組成
複製代碼
是否是有點蒙,接下來咱們直接上一個"圖" 如上圖所示,由一條邊鏈接在一塊兒的頂點稱爲相鄰頂點,A和B是相鄰頂點,A和D是相鄰頂點,A和C是相鄰頂點......A和E是不相鄰頂點。一個頂點的度是其相鄰頂點的數量,A和其它三個頂點相連,因此A的度爲3,E和其它兩個頂點相連,因此E的度爲2......路徑是一組相鄰頂點的連續序列,如上圖中包含路徑ABEI、路徑ACDG、路徑ABE、路徑ACDH等。簡單路徑要求路徑中不包含有重複的頂點,若是將環的最後一個頂點去掉,它也是一個簡單路徑。例如路徑ADCA是一個環,它不是一個簡單路徑,若是將路徑中的最後一個頂點A去掉,那麼它就是一個簡單路徑。若是圖中不存在環,則稱該圖是無環的。若是圖中任何兩個頂點間都存在路徑,則該圖是連通的,如上圖就是一個連通圖。若是圖的邊沒有方向,則該圖是無向圖,上圖所示爲無向圖,反之則稱爲有向圖, 上圖所示,就是一個有向圖,他們其實本質就是爲了表示任何的二元關係,而且給他抽象成一個圖的數據結構,比咱們的地鐵圖,就能夠抽象成圖的數據結構,而咱們的兩個地鐵站之間就是隻能用一條線連接,這就是表示多個二元關係
那麼在咱們的js中,一樣的沒有圖咱們一樣的可使用數組和對象來表示一個圖,若是用數組去表示,就會給起一個名字叫作鄰接矩陣,而咱們若是將數組和對象混用那麼就起個名字叫作鄰接表,本質上就是表示上的不一樣
如上圖所示咱們就將一個圖抽象成了鄰接矩陣表示,其實本質就是用一個二維數組去表示鏈接點之間的關係,好,廢話少說,咱們接下來用鄰接矩陣代碼去表示一個簡單的圖
好比說咱們要用鄰接矩陣去表示以下圖
var matrix=[
[0,1,0,0,0],
[0,0,1,1,0],
[0,0,0,0,1],
[1,0,0,0,0],
[0,0,0,1,0]
]
複製代碼
如上代碼所示,其實他的本質就是在橫向和縱向同時都有A、B、C、D、E 當橫向的與縱向的造成關聯時,則修改當前對應項爲1,若是就能夠用矩陣的形式表達各個頂點之間的關係,好了,到此鄰接矩陣表示法,到此爲止,其實難點並非畫一個鄰接矩陣,而是將抽象的業務邏輯轉化成一個鄰接矩陣去解決問題,好比在電商界大名鼎鼎的sku,因爲業務複雜度較高,全部,咱們寫的算法每每業務邏輯極其複雜,這時鄰接矩陣就能發揮威力 有興趣能夠看看大佬的分分鐘學會前端sku算法(商品多規格選擇)
提及鄰接表,表示起來就比鄰接矩陣直觀多了,鄰接矩陣至關的抽象,那麼上圖咱們怎樣用鄰接表去表示呢? 因爲鄰接表比較簡單,使用用例建立,
class Graph {
constructor() {
this.vertices = []; // 用來存放圖中的頂點
this.adjList = new Map(); // 用字典的數據結構來存放圖中的邊
}
// 向圖中添加一個新頂點
addVertex(v) {
if (!this.vertices.includes(v)) {
this.vertices.push(v);
this.adjList.set(v, []);
}
}
// 向圖中添加a和b兩個頂點之間的邊
addEdge(a, b) {
// 若是圖中沒有頂點a,先添加頂點a
if (!this.adjList.has(a)) {
this.addVertex(a);
}
// 若是圖中沒有頂點b,先添加頂點b
if (!this.adjList.has(b)) {
this.addVertex(b);
}
this.adjList.get(a).push(b); // 在頂點a中添加指向頂點b的邊
}
}
let graph = new Graph();
let myVertices = ['A', 'B', 'C', 'D', 'E'];
myVertices.forEach((v) => {
graph.addVertex(v);
});
graph.addEdge('A', 'B');
graph.addEdge('B', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');
graph.addEdge('E', 'D');
graph.addEdge('D', 'A');
console.log(graph);
複製代碼
如圖,鄰接表也完整映射出圖的關係
在計算機科學中,樹是一種十分重要的數據結構。樹被描述爲一種分層數據抽象模型,經常使用來描述數據間的層級關係和組織結構。樹也是一種非順序的數據結構。
複製代碼
如上圖所示,這就是一個樹形象展現,而在咱們前端中,實際上是和樹打交道最多的了,好比dom樹、級聯選擇、屬性目錄控件這是咱們最常聽見的名詞了吧,他其實就是一個樹的數據結構。在咱們前端js中一樣的也沒有樹的數據結構。可是咱們能夠用對象和數組去表示一個樹。
var tree={
value:1,
children:[
{
value:2,
children:[
{
value:3
}
]
}
]
}
複製代碼
如上所示,咱們就簡單的實現了一個樹的數據結構,可是你以爲這就夠了嗎?他是遠遠不夠的,在咱們的樹中,有着不少被大衆廣泛接受的算法,叫作深度優先遍歷(DFS),和廣度優先遍歷(BFS),首先咱們來一個dom 樹,而後再來分析它
如上圖所示,我將dom轉化成了樹形結構
深度優先遍歷顧名思義,就是緊着深度的層級遍歷,他是縱向的維度對dom樹進行遍歷,從一個dom節點開始,一直遍歷其子節點,直到它的全部子節點都被遍歷完畢以後再遍歷它的兄弟節點,如此往復,直到遍歷完他全部的節點
他的遍歷層級依次是:
div=>ul=>li=>a=>img=>li=>span=>li=>p=>button
複製代碼
用js 代碼表示以下:
//將dom 抽象成樹
var dom = {
tag: 'div',
children: [
{
tag: 'ul',
children: [
{
tag: 'li',
children: [
{
tag: 'a',
children: [
{
tag: 'img'
}
]
}
]
},
{
tag: 'li',
children: [
{
tag: 'span'
}
]
},
{
tag: 'li'
}
]
},
{
tag: 'p'
},
{
tag: 'button'
}
]
}
var nodeList = []
//深度優先遍歷算法
function DFS(node, nodeList) {
if (node) {
nodeList.push(node.tag);
var children = node.children;
if (children) {
for (var i = 0; i < children.length; i++) {
//每次遞歸的時候將 須要遍歷的節點 和 節點所存儲的數組傳下去
DFS(children[i], nodeList);
}
}
}
return nodeList;
}
DFS(dom, nodeList)
console.log(nodeList)
複製代碼
結果也至關的一致
所謂廣度優先遍歷,也是一樣的道理,就是緊着同級的遍歷,該方法是以橫向的維度對dom樹進行遍歷,從該節點的第一個子節點開始,遍歷其全部的兄弟節點,再遍歷第一個節點的子節點,完成該遍歷以後,暫時不深刻,開始遍歷其兄弟節點的子節點 他的遍歷層級依次是:
div=>ul=>p=>button=>li=>li=>li=>a=>span=>img
複製代碼
用js 代碼表示以下:
function BFS(node, nodeList) {
//因爲是廣度優先,for循環不是很優雅,咱們可使用隊列來解決
if (node) {
var q = [node]
while (q.length > 0) {
var item = q.shift()
nodeList.push(item.tag)
if (item.children) {
item.children.forEach(e => {
q.push(e)
})
}
}
}
}
BFS(dom, nodeList)
console.log(nodeList)``
複製代碼
那麼結果也顯而易見
在咱們以前的代碼中,描述的都是多叉樹,而在數據結構中,還有一個至關重要的概念,叫作二叉樹
二叉樹(Binary Tree)是一種樹形結構,它的特色是每一個節點最多隻有兩個分支節點,一棵二叉樹一般由根節點,分支節點,葉子節點組成。而每一個分支節點也經常被稱做爲一棵子樹。
複製代碼
說了這麼多概念,那麼二叉樹究竟是啥?咱們來看一張圖,相信就能賽過千言萬語 如圖所示長這樣就是二叉樹
以上就是二叉樹的概念,可是,在前端中,目前我尚未見到二叉樹的應用(若有大佬知道請告知),可是仍然不妨礙咱們來學習他,而在二叉樹中最有名的當屬先序遍歷,中序遍歷,以及後序遍歷
那麼在咱們前端中二叉樹應該怎麼表示呢?咱們能夠用object來表示一個二叉樹
const bt = {
val: 1,
left: {
val: 2,
left: {
val: 4,
left: null,
right: null,
},
right: {
val: 5,
left: null,
right: null,
},
},
right: {
val: 3,
left: {
val: 6,
left: null,
right: null,
},
right: {
val: 7,
left: null,
right: null,
},
},
}
複製代碼
以上數據結構就是一個簡單的二叉樹,他會有當前節點的值,以及一個左子樹,和右子樹,接下來,纔是重點部分,二叉樹的建立以及遍歷
// arr=[6,5,6,8,9,1,4,3,6] 將數組根據下標爲0的大小轉換成二叉樹
arr = [6, 5, 6, 8, 9, 1, 4, 3, 6]
class BinaryTreeNode {
constructor(key) {
// 左節點
this.left = null;
// 右節點
this.right = null;
// 鍵
this.key = key;
}
}
class BinaryTree {
constructor() {
this.root = null;
}
insert(key) {
const node = new BinaryTreeNode(key)
if (this.root === null) {
this.root = node
} else {
this.insertNode(this.root, node)
}
}
// 抽離遞歸比較部分邏輯
insertNode(node, newNode) {
if (node.key < newNode.key) {
if (node.left) {
this.insertNode(node.left, newNode)
} else {
node.left = newNode
}
} else {
if (node.right) {
this.insertNode(node.right, newNode)
} else {
node.right = newNode
}
}
}
}
const tree = new BinaryTree();
arr.forEach(key => {
tree.insert(key)
});
console.log(tree)
複製代碼
中序遍歷(inorder):先遍歷左節點,再遍歷本身,最後遍歷右節點,輸出的恰好是有序的列表
中序遍歷 有遞歸版本和 非遞歸的版本,此處使用遞歸版本
// 是對象中的一個方法
inorderTransverse(root, arr) {
if (!root) return;
this.inorderTransverse(root.left, arr);
arr.push(root.key);
this.inorderTransverse(root.right, arr);
}
// 使用
const arrTransverse = []
tree.inorderTransverse(tree.root, arrTransverse)
console.log(arrTransverse)
複製代碼
先序遍歷(preorder):先本身,再遍歷左節點,最後遍歷右節點
preorderTransverse(root, arr) {
if (!root) return;
arr.push(root.key);
this.preorderTransverse(root.left, arr);
this.preorderTransverse(root.right, arr);
}
複製代碼
後序遍歷(postorder):先左節點,再右節點,最後本身
// 因爲後序遍歷的非遞歸版本比較巧妙,咱們使用非遞歸版本
postorderTransverse(root, arr) {
// 建立一個棧
var stack = [];
// 將根節點壓入棧頂
stack.push(root);
while (stack.length > 0) {
let node = stack.pop();
// 利用unshift按照順序壓入數組
arr.unshift(node.key);
if (node.left) {
stack.push(node.left);
}
if (node.right) {
stack.push(node.right);
}
}
}
// 調用
const arrTransverse = []
tree.postorderTransverse(tree.root, arrTransverse)
複製代碼
本題來自力扣226題,而且是一個火爆的面試題,緣由是這是一道Homebrew包管理工具的做者,去谷歌面試寫不出來的題
// 1
// 2 3
// 4 5 6 7
// 將如上二叉樹翻轉過來
//===== 解題思路======
//一、若是想翻轉二叉樹,使用分治思想是一種比較好理解的方式
//二、主要就是遞歸每一層,遞歸到當前層只須要交換當前層的二叉樹便可
function invertTree(bt) {
if (!bt) {
return bt
}
//利用解構賦值交換兩個數
[bt.left, bt.right] = [invertTree(bt.right), invertTree(bt.left)];
return bt
}
console.log(invertTree(bt))
複製代碼
堆是什麼? 在前端中甚至都不多提過這個概念,其實,堆是一種特殊的徹底二叉樹
複製代碼
在以前的樹中咱們介紹了樹,一樣介紹了二叉樹,那什麼是徹底二叉樹呢?
如上圖所示,就是一個徹底二叉樹,也能夠叫滿二叉樹(有的文獻定義滿二叉樹不是徹底二叉樹,沒統一的標準和規範定義),可是徹底二叉樹不必定都是滿二叉樹好比
上圖中,就是一個徹底二叉樹,卻不是一個滿二叉樹,那徹底二叉樹的定義是啥呢?
若設二叉樹的深度爲k,除第 k 層外,其它各層 (1~k-1) 的結點數都達到最大個數,第k 層全部的結點都連續集中在最左邊,這就是徹底二叉樹。
複製代碼
說白了就是二叉樹的每層子節點必須填滿,最後一層若是不是滿的,那麼必須只缺乏右邊節點,那麼咱們又說,堆是一中特殊的徹底二叉樹,那麼他又什麼特色呢?
那麼在js須要使用數組來表示一個堆,爲啥呢?個人理解是因爲堆是一個徹底二叉樹,他的每一個節點必須填滿,那麼每一層就能在數組中找到固定的位置,這樣的話,就不須要對象了
接下來有人又會問了,堆有啥用,這麼複雜,其實我咱們看堆的結構就能發現,堆的時間複雜度是O(1) 它可以快速找到堆中的最大值和最小值,接下來咱們手寫一個實踐一個最小堆類吧
class MinHeep {
constructor() {
this.heap = []
}
// 插入方法
insert(val) {
this.heap.push(val)
this.shiftUP(this.heap.length - 1)
}
// 交換方法
swap(i, v) {
var temp = this.heap[i]
this.heap[i] = this.heap[v]
this.heap[v] = temp
}
// 上移方法
shiftUP(index) {
if (index === 0) { return }
var preIindex = this.getParentIndex(index)
if (this.heap[preIindex] > this.heap[index]) {
this.swap(preIindex, index)
}
}
getParentIndex(i) {
// 求商的方法
return (i - 1) >> 1
}
}
var h=new MinHeep()
h.insert(3)
h.insert(1)
h.insert(2)
h.insert(7)
h.insert(4)
console.log(h.heap)
複製代碼
如此,咱們就實現了最小堆,打印數組來看,你就會發現是,堆頂元素必定是最小的
相信不少人在刷算法的時候,聽到最多的一個詞就是時間複雜度和空間複雜度,那麼他究竟是什麼呢?
首先咱們來論一論算法究竟是什麼?算法就是操做數據、解決程序問題的一組方法,那麼既然是一組方法,他就有好的方法,和壞的方法,因而人麼就發明了兩個維度去計算好壞,一個就是時間維度,一個就是空間維度
時間維度:是指執行當前算法所消耗的時間,咱們一般用「時間複雜度」來描述。
空間維度:是指執行當前算法須要佔用多少內存空間,咱們一般用「空間複雜度」來描述。
複製代碼
簡而言之,就是你使用的for循環越多,那麼你的時間複雜度就越大,你聲明的變量越多那麼你的空間複雜度就越大。你覺得這就夠了嗎?貼心的大佬們還總結了一套關於時間複雜度和空間複雜度的表示方法。
目前行業中公認叫作「 大O符號表示法 」什麼意思呢?舉個例子
var a=1
a++
複製代碼
上述代碼中,咱們發現並並無for循環 他的執行步驟也不隨着某個變量的變化而變化,那麼他就叫作O(1),其實計算時間複雜度有一套比較沒法的公式,咱們在此不在贅述有興趣請移步大佬房間算法的時間與空間複雜度(一看就懂)
咱們只需記住時間複雜度量級有:
從上之下的時間複雜度愈來愈大,接下來來舉幾個例子,理解一下
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
複製代碼
咱們發現他的時間是隨着n的變化而變化 那麼他的時間複雜度就是O(n)
int i = 1;
while(i<n)
{
i = i * 2;
}
複製代碼
因爲i每次都是倍數遞增 那麼他的循環次數就會比n小,那麼他的時間複雜度就爲(logN) 那麼以此類推若是若是循環套循環就是平方階
空間複雜度度就比較簡單了,空間複雜度比較經常使用的有:O(1)、O(n)、O(n²), 說白了就是你聲明的變量多少,好比
//O(1)
var a=1
a++
// 聲明數組,有n個元素O(n)
var a=new Array(n)
複製代碼
看完了基礎知識,細再來盤點一下高級思想
咱們算法中,排序算法數一數二,也是面試重災區,不少人都掛在上面,其實排序算法理清楚之後至關簡單,也就分爲那麼幾種冒泡排序、選擇排序、插入排序、歸併排序、快速排序、搜索排序 等
一、比較相鄰的元素。若是第一個比第二個大,就交換他們兩個。 二、對每一對相鄰元素做一樣的工做,從開始第一對到結尾的最後一對。這步作完後,最後的元素會是最大的數。 三、針對全部的元素重複以上的步驟,除了最後一個。 四、持續每次對愈來愈少的元素重複上面的步驟,直到沒有任何一對數字須要比較。
function bubble(arr){
let tem = null;
//外層i控制比較的輪數
for(let i=0;i<arr.length;i++){
// 裏層循環控制每一輪比較的次數
for(let j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){
//當前項大於後一項,交換位置
tem = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tem;
}
}
}
return arr
}
複製代碼
因爲冒泡排序有兩個嵌套循環,因此他的時間複雜度爲O(n²),因爲這個時間複雜度至關的高,因此在排序算法中,冒泡排序屬於性能較差的因此工做中基本用不到,只是在面試中使用
一、首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 二、再從剩餘未排序元素中繼續尋找最小(大)元素,而後放到已排序序列的末尾。 三、重複第二步,直到全部元素均排序完畢。
//先選擇出最小的值組個比較調換位置
function selectSort(arr){
let length = arr.length;
for(let i=0;i<length-1;i++){
let min = i;
for(let j=min+1;j<length;j++){
if(arr[min]>arr[j]){
min = j
}
}
if(min!=i){
let temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
return arr
}
複製代碼
選擇排序咱們發現他也是兩個for循環,那麼相應的他也是O(n²),性能較差
一、從第二個數字往前比。 二、比他大的日後排,以此類推直接排到末尾
function insertSort(arr) {
let length = arr.length;
for(let i = 1; i < length; i++) {
let temp = arr[i];
let j = i;
for(; j > 0; j--) {
if(temp >= arr[j-1]) {
break; // 當前考察的數大於前一個數,證實有序,退出循環
}
arr[j] = arr[j-1]; // 將前一個數複製到後一個數上
}
arr[j] = temp; // 找到考察的數應處於的位置
}
return arr;
}
// example
let arr = [2,5,10,7,10,32,90,9,11,1,0,10]
console.log(insertSort(arr));
複製代碼
插入排序咱們發現他也是兩個for循環,那麼相應的他也是O(n²),性能較差
一、把數組劈成兩半,在遞歸的對子數組進行劈開的操做,知道分紅一個個單獨的數 二、把兩個數組合併成有序數組,在對有序數組進行合併直到所有子數組合併爲一個完整數組
function mergeSort(arr) {
// 劈開數組
if (arr.length === 1) { return arr }
const mid = Math.floor(arr.length / 2)
const left = arr.slice(0, mid)
const right = arr.slice(mid, arr.length)
// 遞歸
const orderLeft = mergeSort(left)
const orderRight = mergeSort(right)
const res = []
while (orderLeft.length || orderRight.length) {
// 利用隊列排序數字並壓入數組
if (orderLeft.length && orderRight.length) {
res.push(orderLeft[0] < orderRight[0] ?
orderLeft.shift() : orderRight.shift())
} else if (orderLeft.length) {
res.push(orderLeft.shift())
} else if (orderRight.length) {
res.push(orderRight.shift())
}
}
return res
}
var arr = [1, 4, 6, 7, 9, 5]
console.log(mergeSort(arr))
複製代碼
因爲分的操做是一個遞歸,而且是給劈成兩半那麼他的時間複雜度就是O(logN)因爲合併是一個while 的循環那麼整體的時間複雜度就是O(nlogN) ,如此一來歸併排序就達到了可用程度,因而大名鼎鼎的火狐瀏覽器的sort排序用的就是歸併排序這個算法。
一、首先須要分區,從數組送任意選擇一個基準,而後將先後的值跟跟基準去比較,若是比基準小,那麼放入左邊數組,不然放入右邊數組 二、遞歸的對子數組進行分區知道最後和合並排序號的子數組
var arr = [2, 4, 3, 5, 1]
function quickSort(arr) {
if (arr.length === 1 || arr.length === 0) { return arr }
const left = [], right = []
// 找到基準,暫時取下標0
const mid = arr[0]
// 注意因爲0被取了,從1開始
for (let i = 1; i < arr.length; i++) {
if (arr[i] < mid) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
// 遞歸
console.log(left, right)
return [...quickSort(left), mid, ...quickSort(right)]
}
console.log(quickSort(arr))
複製代碼
看完快速排序的代碼,是否是發現他跟歸併排序很像,沒錯,的思路確實很像,都是分治思想,一樣的他們的時間複雜度也都是O(nlogN) ,而且谷歌瀏覽器的sort排序用的就是快速排序,那說了這麼多,他們有什麼區別呢?
區別就是進行分組的策略不一樣,合併的策略也不一樣。歸併的分組策略:是假設待排序的元素存放在數組中,那麼把數組前面的一半元素做爲一組,後面一半做爲另外一組。而快速排序則是根據元素的值來分的,大於某個值的元素一組,小於某個值的元素一組。
快速排序在分組的時候已經根據元素的大小來分組了,而合併時,只須要把兩個分組合並起來就能夠了,歸併排序則須要對兩個有序的數組根據大小合併
說完排序算法,咱們再來鼓搗鼓搗搜索,搜索也是咱們面試的高頻考點,雖然工做中基本用不上,可是爲了裝逼,怎麼能不會呢?接下來咱們來看搜索都有那些?經常使用的通常就兩種順序搜索、二分搜索
順序搜索呢是一個很是低效的搜索方式,主要思路就是遍歷目標數組,發現同樣就返回 ,找不到就返回-1
// 掛載原型上不用傳兩個值
Array.prototype.sequentialSearch = function(item) {
for(let i = 0; i < this.length; i+=1) {
if(this[i] === item) {
return i;
}
}
return -1;
};
const res = [1,2,3,4,5].sequentialSearch(3)
console.log(res);
複製代碼
咱們發現順序搜索,其實就是一個單純的遍歷,那麼他的時間複雜度就是O(n)
二分搜索顧名思義,就是給數組劈開一半而後查找,如此一來就減小了數組查找次數,大大提升了性能
他首先從數組中間開始,若是命中元素那麼就返回結果,若是未命中,那麼就比較目標值和已有值的大小,若查找值小於中間值,則在小於中間值的那一部分執行上一步操做,反正同樣,可是必須有一個前提條件,數組必須是有序
Array.prototype.binarySearch=function( key) {
var low = 0;
var high = this.length - 1;
while (low <= high) {
var mid = parseInt((low + high) / 2);
if (key === this[mid]) {
return mid;
}
else if (key < this[mid]) {
high = mid - 1;
}
else if (key > this[mid]) {
low = mid + 1;
}
else {
return -1;
}
}
}
var arr = [1,2,3,4,5,6,7,8];
console.log(arr.binarySearch(3));
複製代碼
因爲每次搜索範圍都被縮小一半,那麼他的時間複雜度就是O(logN)
分而治之是什麼呢?
分而治之是算法設計中的一種方法,或者思想
他並非一種具體的數據結構,而是一種思想,就至關與在咱們編程中的範式,好比說咱們編程中有aop (切面編程)、oop(對象編程)、函數式編程、響應式編程,分層思想等,那麼,咱們的算法思想和編程思想的地位是一致的,可見他有多麼重要,要歸正傳!分而治之究竟是什麼?
他其實就是將一個待解決的問題分解成多個小問題遞歸解決,再將結果合併以解決原來的問題
複製代碼
其實分治思想咱們以前已經見過了,咱們的歸併排序和快速排序都是典型的分而治之的應用
動態規劃和分而治之相似,也是算法設計中的一種方法,一樣的他也是將一個問題分解成相互重疊的子問題,經過去求解子問題,來達到求解原來的問題的目的
複製代碼
看到這裏你是否是以爲跟分治思想同樣,其實他們有區別的,區別呢就是在分解這塊,分治思想是將問題分解成相互獨立的子問題,他們彼此是沒有重疊的,而動態規劃則是分解成相互重疊的子問題,他們相互之間是有關聯的,舉個例子,拿出來經典的斐波那契數列,他就是動態規劃的典型應用
斐波那契數列(Fibonacci sequence),又稱黃金分割數列、因數學家萊昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖爲例子而引入,故又稱爲「兔子數列」,指的是這樣一個數列:0、一、一、二、三、五、八、1三、2一、34
複製代碼
接下來咱們就來看看這個數列 如上圖所示,他是否是有一個規律 0+1=一、1+1=二、1+2=3....如此往復,咱們就能夠總結出來一個公式
咱們能夠發現他的當前元素和前兩個元素有着特殊的關聯,咱們給這種關聯定義成一個函數F,麼咱們是否是就能夠總結出來F(n)=F(n-1)+F(n-2)
如此一來咱們就用一個關聯的公式去去求出全部的斐波那契數列的值,這就是斐波那契數列的典型應用,其實啊,波那契數列在數學和生活以及天然界中都很是有用,在此我就不深刻研究了(主要我也到這了,在深刻露餡),若有興趣請移步 斐波那契數列爲何那麼重要,全部關於數學的書幾乎都會提到?
接下來,咱們來一道力扣和麪試的經典問題---爬樓梯問題
當前題來自力扣70題
//假設你正在爬樓梯。須要 n 階你才能到達樓頂。
//每次你能夠爬 1 或 2 個臺階。你有多少種不一樣的方法能夠爬到樓頂呢? 注意:給定 n 是一個正整數。
// 示例
// 輸入: 2
// 輸出: 2
// 解釋: 有兩種方法能夠爬到樓頂。
// 1. 1 階 + 1 階
// 2. 2 階
// =======分析=========
// 首先咱們假設只有3階,那麼我先定義好,我最後一步能夠是兩階,也多是一階,
// 若是最後一步是兩階,咱們就能知道以前就只有一種方法才能達到最後一步是兩階梯
// 若是最後一步是一階咱們就是知道以前有兩種方法能夠到達最後一階梯
// 因爲咱們的最後一步的階梯數定死了,因此,他的階梯數量就是以前的方法之和
// 如此咱們就能夠推導出f(n)=f(n-1)+f(n-2) 公式
var climbStairs = function (n) {
// 因爲咱們的公式是f(n)=f(n-1)+f(n-2),全部當n小於2的時候咱們直接返回,兼容邊界條件
if (n < 2) { return 1 }
// 咱們先定義初始的能求值的數組,下標0爲1是因爲咱們只有一階的時候也只有一種方法
const dp = [1, 1]
//套用公式
for (let i = 2; i < n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
// 返回當前階梯的方法
return dp[n]
};
複製代碼
貪心算法也是算法設計中的一種方法,他是經過每一個階段的局部最優選擇,從而達到全局最優
複製代碼
而後事實每每事與願違,雖然都是局部最優選擇,可是結果卻不必定最優,可是必定比最糟好。正是因爲永遠不會命中下下籤,並且看着仍是上上籤,使得的貪心算法這種設計思路留存到了今天。
ok,接下來舉個例子,咱們有1,2,5三種面值硬幣,如今要求是咱們使用最少的面值硬幣來湊成11塊錢,那若是使用貪心算法,咱們爲了使用的硬幣最少,上來是否是須要選個5,接着在選個5,最後選個1,如此一來咱們只須要三個硬幣就能湊齊11塊錢,固然在這種狀況下,他就是最優解,那麼若是咱們給硬幣換一下,咱們須要1,3,4三種面值硬幣,咱們須要湊夠6塊錢,若是按照貪心算法,那麼 組合就是4+1+1, 然而實際的最優解確實3+3只須要兩個就能夠了。
當前題來自力扣122題
//定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。
// 設計一個算法來計算你所能獲取的最大利潤。你能夠儘量地完成更多的交易(屢次買賣一支股票)。
// 注意:你不能同時參與多筆交易(你必須在再次購買前出售掉以前的股票)。
// 舉例輸入: [7,1,5,3,6,4] 輸出: 7
//解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能得到利潤 = 5-1 = 4 。
//隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能得到利潤 = 6-3 = 3 。S
//======= 解題思路 =========
// 一、因爲咱們在開始之初就能拿到股票的走勢數組,那麼也就是咱們已知股票的結果
// 二、既然已知股票結果,那麼利用貪心算法的原則,咱們只考慮局部最優解
// 三、只須要遍歷整個數組發現是上漲的交易日,咱們就進行買賣操做,降低的則不動,這樣永遠不會虧損
// 四、使用貪心算法的思路就是無論怎樣,我不能賠錢,這樣作的好處就是容易理解,若是使用動態規劃,還要總結公式
var maxProfit = function (prices) {
var profit = 0
for (var i = 1; i < prices.length; i++) {
tmp = prices[i] - prices[i - 1]
//只有上漲我纔買賣
if (tmp > 0) {
profit += profit
}
}
return profit
};
複製代碼
回朔算法也是算法設計當中的一種思想,是一種漸進式尋找並構建問題解決方式的一種策略
複製代碼
基本思路就是找一條可能的路去走,若是走不通,回到上一步,繼續選擇另外一條路,知道問題解決,回朔算法是一種暴力求解法,有着一種試錯的思想,由於其簡單粗暴而聞名,故而一般的也被稱爲通用求解法。
舉個例子,咱們在上學的時候都會碰見全排列問題,就是將1,2,3 用不一樣的順序去排列不能重複,而後讓求出有多少種排列方式,那麼這就是一道典型的用回朔算法思想去解決的問題。
解題思路
一、用遞歸模擬全部的狀況
二、遇到包含重複元素的狀況,就返回上一步(官話叫作回溯)
三、蒐集並返回全部的沒有重複的順序
//給定一個 沒有重複 數字的序列,返回其全部可能的全排列。
//輸入: [1,2,3]
//輸出:
//[
//[1,2,3],
//[1,3,2],
//[2,1,3],
//[2,3,1],
//[3,1,2],
//[3,2,1]
//]
//=========題目解析===========
//一、採用回溯算法求解
//二、將不重複數字一次放入數組的每一個位置,若是知足條件,取出來,不然回溯尋找下一組
//三、使用遞歸實現回溯思路
const permute = (nums) => {
const res = [];
var dfs = function (path) {
if (path.length === 3) { res.push(path); return }
nums.forEach(element => {
if (path.includes(element)) { return }
// 多層遞歸實現回溯
dfs(path.concat(element))
});
}
dfs([])
console.log(res)
};
permute([1, 2, 3])
複製代碼
本題來自力扣第三題
// 無重複最長子串
//輸入: s = "abcabcbb"
//輸出: 3
//解釋: 由於無重複字符的最長子串是 "abc",因此其長度爲 3。
//=======解題思路========
// 一、 這種題目是須要有解提套路的,好比,使用雙指針,好比使用滑動窗口
var lengthOfLongestSubstring = function (s) {
var l = 0, r = 0, max = 0;// 創建兩個指針先指向下標0
const mapObj = new Map()
while (r < s.length) {
// 兩層判斷防住重複元素不在滑動窗口內的狀況
if (mapObj.has(s[r]) && mapObj.get(s[r]) >= l) {
l = mapObj.get(s[r]) + 1
}
max = Math.max(r - l + 1, max)
mapObj.set(s[r], r)
r++
}
console.log(max)
};
lengthOfLongestSubstring('djcqwertyuhjjkkiuy')
複製代碼
本題來自力扣第一題 本題夢想開始的地方,初始看到本題第一反應就是暴力求解法,兩層遍歷,然而參考別人的套路發現使用Map直接能給時間複雜度降到O(n),果然自古套路得人心
//給定一個整數數組 nums 和一個目標值 target,請你在該數組中找出和爲目標值的那 兩個 整數,並返回他們的數組下標。
//你能夠假設每種輸入只會對應一個答案。可是,數組中同一個元素不能使用兩遍。
//給定 nums = [2, 7, 11, 15], target = 9
//由於 nums[0] + nums[1] = 2 + 7 = 9
//因此返回 [0, 1]
//=======解題思路=========
//一、 本題有暴力解法和非暴力解法
//二、 非暴力解法若是不是有人提拔,通常人很難想到
//三、 咱們須要想象咱們找到目結果就是湊對,至關於婚介所找對象
//四、 思路就是來的元素所有存檔,當有別的元素進來時,在已存檔的元素中去湊出目標結果
var twoSum = function (nums, target) {
var obj = new Map()
nums.forEach((item, i) => {
// 若是找到返回
if (obj.has(nums[i])) {
console.log([obj.get(nums[i]), i])
} else {
// 記錄對象,並存儲下標
obj.set(target - nums[i], i)
}
})
};
twoSum([2, 7, 11, 15], 18)
複製代碼
本題來自力扣第141題 環形鏈表
本題是對對我影響比較大的題,他很大程度上改變了個人想法和觀念,讓我明白,想要學好算法,就得考死記硬背,記住套路,真正的算法能力全是實打實練出來的
// 判斷是不是環形鏈表
//輸入:head = [3,2,0,-4], pos = 1
//輸出:true
//解釋:鏈表中有一個環,其尾部鏈接到第二個節點。
//========解題思路==========
//一、在我最初的時候也是暴力求解法也是正常人的思惟所能想到的
//二、歷全部節點,每次遍歷到一個節點時,判斷該節點此前是否被訪問過。
//三、然而我看了大佬們的清奇的解題思路震驚了,估計想幾天也想不到
//四、他們使用的就是快慢指針其實也是雙指針
//五、思路就是創建兩個指針,一個每次走兩個next,一個每次走一個next ,若是最後兩個相遇,說明必定是環形鏈表
/**
* @param {ListNode} head
* @return {boolean}
*/
var head = {
val: 3
}
var head1 = {
val: 2
}
var head2 = {
val: 0
}
var head3 = {
val: -4
}
head.next = head1
head1.next = head2
head2.next = head3
// head3.next = head1
var hasCycle = function (head) {
var slow = head
var fast = head
// 若是是環形指針就用戶按遞歸下去直到retrun 跳出函數
// 若是不是環形指針就會有盡頭,這是最巧妙的地方
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
// 表示相遇了,返回環形指針
if (slow == fast) {
console.log(true)
return true
}
}
console.log(false)
return false
};
hasCycle(head)
複製代碼
思慮清奇的套路題暫時到這,後續刷到慢慢更新
斷斷續續一個月算法的學習心得終於寫完了,以上是本人的學習算法的方法,首先了解基本的數據結構知識,再去有目的性的刷相關題目,這樣數據結構和算法的體系算是印在個人腦海中了,本覺得能在力扣大殺四方了,沒想到,仍是到處碰壁,終於明白,自古套路得人心是,想要將算法攻克,沒有速成辦法,他就像背單詞,今天你看懂了,可能明天就忘了,想要掌握算法,只有四個字---惟手熟爾,也就是你見的多了,天然就會了,由於咱們正常人的思惟,是遠遠想不到這些解題套路的,惟有看的多了,才能觸類旁通,之後的路還很長,寫此文章只爲記錄探索過程,以及引發還未入門同志的興趣,不對之處望大佬批評指正,路漫漫,其修遠兮,你們加油!