(万字好文!)你可能须要的数据结构与算法最详细的入门教材了!

前言

最近在撸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

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这个构造器去建立一个字典

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转化成了树形结构

深度优先遍历(DFS)

深度优先遍历顾名思义,就是紧着深度的层级遍历,他是纵向的维度对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)
复制代码

结果也至关的一致

广度优先遍历(BFS)

所谓广度优先遍历,也是一样的道理,就是紧着同级的遍历,该方法是以横向的维度对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),其实计算时间复杂度有一套比较没法的公式,咱们在此不在赘述有兴趣请移步大佬房间算法的时间与空间复杂度(一看就懂)

咱们只需记住时间复杂度量级有:

  • 常数阶O(1)
  • 对数阶O(logN)
  • 线性阶O(n)
  • 线性对数阶O(nlogN)
  • 平方阶O(n²)
  • 立方阶O(n³)
  • K次方阶O(n^k)
  • 指数阶(2^n)

从上之下的时间复杂度愈来愈大,接下来来举几个例子,理解一下

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

本题来自力扣第一题 本题梦想开始的地方,初始看到本题第一反应就是暴力求解法,两层遍历,然而参考别人的套路发现使用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)
复制代码

思虑清奇的套路题暂时到这,后续刷到慢慢更新

最后

断断续续一个月算法的学习心得终于写完了,以上是本人的学习算法的方法,首先了解基本的数据结构知识,再去有目的性的刷相关题目,这样数据结构和算法的体系算是印在个人脑海中了,本觉得能在力扣大杀四方了,没想到,仍是到处碰壁,终于明白,自古套路得人心是,想要将算法攻克,没有速成办法,他就像背单词,今天你看懂了,可能明天就忘了,想要掌握算法,只有四个字---惟手熟尔,也就是你见的多了,天然就会了,由于咱们正常人的思惟,是远远想不到这些解题套路的,惟有看的多了,才能触类旁通,之后的路还很长,写此文章只为记录探索过程,以及引发还未入门同志的兴趣,不对之处望大佬批评指正,路漫漫,其修远兮,你们加油!

相关文章
相关标签/搜索