JavaScript版数据结构与算法(一)栈、队列、链表、集合、树

一、前言

为什么要学习数据结构与算法?最重要的就是面试要考算法,另外就是如果在实际工作当中,能够使用算法优化代码,会提升代码质量和运行效率,作为一名前端人员可能在实际中用的并不是特别多。数据结构与算法是分不开的,数据结构是计算机存储、组织数据的方式,算法是一系列解决问题的清晰指令,程序就是数据结构+算法。算法刷题大家都知道,就是力扣。刷题顺序推荐按类型刷题,比如栈相关的题,一次刷好几道,巩固巩固。刷题过程中需要重点关注的是通用套路、时间/空间复杂度分析和优化。其实这跟高中初中做数学题很像,有通用的套路可循,但是需要多复习,多看错题,做题的时候尽量把这道题考察的知识点都总结出来。
要学习的数据结构大致分为下面几类:

  • 有序:栈、队列、链表
  • 无序:集合、字典
  • 有相互连接关系:树、堆(特殊的树)、图

学习的算法大致分为下面几类:

  • 链表:遍历链表、删除链表节点。
  • 树、图:深度/广度优先遍历。
  • 数组:冒泡/选择/插入/日并/快速排序、顺序/二分搜索。

关于时间复杂度:
用O()函数表示,定性描述算法运行时间

  • O(1)
    代码只会执行一次,没有任何循环
    let i = 1;
    i += 1;
    
  • O(n)
    for循环中的代码会执行n次
    for (let i = 0; i < n; i += 1) {
    	console. log(i);
    }
    
  • O(1) + O(n) = O(n)
    代码上下顺序执行是两个时间复杂度相加,只算增长趋势较大的时间复杂度,增长趋势较小的时间复杂度可以忽略
    let i = 1;
    i += 1;
    for (let j = 0; j < n; j += 1) {
    	console. log(i);
    }
    
  • O(n) * O(n) = O(n^2)
    两个方法嵌套的时间复杂度需要两个时间复杂度相乘
    for (let i=0;i<n;i+=1){
    	for (let j = 0; j < n; j += 1) {
    		console. log(i, j);
    	}
    }
    
  • O(logN)
    let i = 1;
    while(i < n){
    	console.log(i);
    	i *= 2;
    }
    

空间复杂度
用O()函数表示
算法在运行过程中临时占用存储空问大小的量度,越小越好

  • O(1)
    只有一个变量
    let i = 0;
    i += 1
    
  • O(n)
    在内存中声明了n个变量
    const list = [];
    for (let i=0; i<n;i+=1){
    	list.push(i);
    }
    
  • O(n^2)
    就是一个矩阵
    const matrix = [];
    for(let i = 0;i < n;i += 1){
    	matrix.push([])
    	for(let j=0; j<n; j +=1){
    		matrix[i].push(j)
    	}
    }
    

二、栈

栈就是一个后进先出的数据结构
JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第1张图片
javascript中没有栈这种数据结构,但是可以用Array实现栈,模仿栈的操作。

const stack = [];
// 入栈
stack.push(1);
stack.push(2);
// 出栈
// pop()将最后的元素删除,并且返回
const item1 = stack.pop()
console.log(item1)
// 出栈
const item2 = stack.pop()
console.log(item2)

什么场景下用栈?
所有后进先出的场景,例如:

  • 十进制转二进制
    需要用十进制数不断除以2,并且倒序取余数,后算出的余数要排在前面,因此可以将余数依次存入栈中,再出栈,就可以实现余数倒序输出
    JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第2张图片
/**
 * 十进制转二进制
 * */
// 1、数字除以2 得到商和余数
// 2、余数push到栈中
// 3、商继续除以2 得商和余数
// 4、余数继续入栈
const trans = function (num) {
    const stack = []
    let n = num;
    while (n > 0) {
        // 获取商
        const shang = Math.floor(n / 2);
        // 获取余数
        const yushu = n % 2;
        // 余数入栈
        stack.push(yushu);
        // n重新赋值
        n = shang;
    }
    return stack.reverse().join('')
}
  • 判断字符串的括号是否有效
    在敲代码的时候,代码编辑器经常要判断括号是否正常闭合,无效的括号会给我们错误提示。这个算法就是编辑器中常用的判断括号是否有效的算法。
    越靠后的左括号,对应的右括号越靠前。
    左括号入栈,右括号出栈,最后栈空了就是合法的。
    JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第3张图片

  • 函数调用堆栈
    最后调用的函数,是最先执行完的
    greeting() start -> [1] 操作 -> sayHi() start -> sayHi() end -> [2] 操作 -> greeting() end -> [3] 操作
    JS解释器使用栈来控制函数的调用顺序。
    JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第4张图片

三、队列

先进先出
javascript中没有队列,但能用数组实现队列

// 创建队列
const queue = [];
// 入队
queue.push(1)
queue.push(2)
// 出队
const item1 = queue.shift()
console.log(item1)
const item2 = queue.shift()
console.log(item2)

应用场景:
所有先进先出的场景
食堂打饭
js异步任务队列(事件循环)
异步任务会放在任务队列中,先放进队列的先执行
计算最近请求次数

四、链表

多个元素组成的链表
但是元素存储不连续,用 next 指针连在一起。

数组 VS 链表
数组:增删非首尾元素时往往需要移动元素。
链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可。

javascript中可以用 object 模拟链表

前端中的链表–原型链
原型链的本质是链表。
原型链上的节点是各种原型对象,比如Function.prototype, Object.prototype…
原型链通过_proto_属性连接各种原型对象。
原型链知识点
如果 A 沿着原型链能找到 B.prototype,那么A instanceof B 为true。
如果在 A 对象上没有找到x 属性,那么会沿着原型链找 x 属性。
面试题一
instanceof 的原理,并用代码实现。

// 如果 A 沿着原型链能找到 B.prototype,那么A instanceof B 为true。
function instanceOf(A, B){
    let p = A;
    while(p){
        if(p == B.prototype) return true;
        p = p.__proto__;
    }
    return false;
}

五、集合

集合是一种无序且唯一的数据结构。
栈、队列、链表都是有序的数据结构,并且元素都是可以重复的。
前端中的集合: Set
常用操作:
数组去重
[...new Set([1, 2, 3, 1, 2])]
判断某元素是否在集合中
set.has(3)
求两个集合的交集

const set = new Set([1,2,3,2,1])
const set2 = new Set([2,3,4])
// 筛选出set中有,并且set2里面也有的元素
const set3 = new Set([...set].filter(s=>set2.has(s)))
console.log(set3)

字典
与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式来存储。
ES6 中有字典,名为 Map。
字典的常用操作:键值对的增删改查。

// 增删改查
const m = new Map();
m.set('a', 'aa')
m.set('b', 'bb')
m.delete('b')
// 清空
// m.clear();
// 改直接覆盖set
m.set('a', '啊啊')

// 求两个数组中都存在的元素,要求去重
const arr1 = [1,2, 2,3,4]
const arr2 = [2,3,2,4]
// 字典中的key也是唯一的,所以遍历数组一创建字典就不会有重复的key
const map = new Map();
arr1.forEach(a=>{
    map.set(a, true)
})
const res = []
arr2.forEach(a=>{
    if(map.get(a)) {
        res.push(a)
        map.delete(a)
    }
})
console.log(res)

六、树

树是一种分层数据的抽象模型
前端的树:DOM树、级联选择、树形控件
JS中没有树,只能用Array和Object模拟
树的常用操作:深度/广度优先遍历、二叉树的先中后序遍历。
深度优先遍历:尽可能深的搜索树的分支
广度优先遍历:先访问离根节点最近的节点
下面一图,左侧是深度优先遍历的访问顺序,右侧是广度优先遍历的访问顺序
JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第5张图片
深度优先遍历算法口诀 (最为重要)
1、访问根节点
2、对根节点的 children 挨个进行深度优先遍历。

// 深度优先遍历
const tree={
    val: 'a',
    children:[
        {
            val: 'b',
            children:[
                {
                    val: 'd',
                    children:[]
                },
                {
                    val: 'e',
                    children:[]
                }
            ]
        },
        {
            val: 'c',
            children:[
                {
                    val: 'f',
                    children:[]
                },
                {
                    val: 'g',
                    children:[]
                }
            ]
        }
    ]
}


const dfs = (root) =>{
    // 访问根节点
    console.log(root);
    // 对根节点的children依次进行深度优先遍历
    root.children.forEach(dfs);
}

console.log(dfs(tree))

广度优先遍历算法口诀
1、新建一个队列,把根节点入队。
2、把队头出队并访问。
3、把队头的 children 挨个入队。
4、重复第二、三步,直到队列为空。

// 广度优先遍历
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: []
                },
                {
                    val: 'e',
                    children: []
                }
            ]
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: []
                },
                {
                    val: 'g',
                    children: []
                }
            ]
        }
    ]
}
const bfs = (root) => {
    // 新建一个队列 根节点入队
    const q = [root]
    while (q.length > 0) {
        // 队头出队并访问
        const n = q.shift();
        console.log(n)
        // 队头的children挨个入队
        n.children.forEach(c => q.push(c));
    }
}
bfs(tree)

树在前端中的应用

  • 访问json数据中的所有节点
    使用深度优先遍历实现,深度优先遍历就是先访问根节点,然后对每个根节点进行深度优先遍历
// 访问json中所有节点值
const json = {
    a: {
        b: {
            c: 3
        }
    },
    d: [1, 2]
}

// 深度优先遍历 使用path记录每个节点的路径
const dfs = (n, path) =>{
	// 访问当前节点
    console.log(n, path)
    // 使用Object.keys遍历n的所有子节点
    Object.keys(n).forEach(k=>{
        dfs(n[k], path.concat(k))
    })
}

dfs(json, [])

JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第6张图片

七、二叉树

树中每个节点最多只能有两个子节点。
在JS中通常用 Object 来模拟二叉树。

const binaryTree = {
    val: '1',
    left: {
        val: '2',
        left: null,
        right: null
    },
    right: {
        val: '3',
        left: null,
        right: null
    }
}

二叉树的遍历主要有三种:先序遍历、中序遍历、后序遍历
二叉树的先序遍历
根->左->右
JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第7张图片

1、访问根节点。
2、对根节点的左子树进行先序遍历。
3、对根节点的右子树进行先序遍历。

const binaryTree = {
    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
        }
    }
}

const preorder = root => {
    if (!root) return;
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
}
preorder(binaryTree)

二叉树的中序遍历
左->根->右
JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第8张图片
1、对根节点的左子树进行中序遍历。
2、访问根节点。
3、对根节点的右子树进行中序遍历。
首先对于整棵树而言,根节点是5,要先对于左子树2进行中序遍历,找2 的左子树,是1,对1中序遍历,1没有左子树,所以1是最先访问的节点
当前节点是1,访问1的根节点,就是2,第二个访问的是2
然后对2这棵树的右子树进行中序遍历,即4,先找4的左子树,即3,访问3,即第三个访问的是3
3访问完,要访问3的根节点,第四个访问的是4
4没有右子树,所以结束访问,2也结束了访问
此时就要访问2节点所在的根节点,即5,第五个访问的是5
在找5的右子树,进行中序遍历,它的右子树6没有左子树,所以先访问根节点6,即第六个访问的是6
访问6之后需要访问6树的右子树,即7,因此第七个访问的是7

二叉树的后序遍历
左->右->根
JavaScript版数据结构与算法(一)栈、队列、链表、集合、树_第9张图片
1、对根节点的左子树进行后序遍历。
2、对根节点的右子树进行后序遍历。
3、访问根节点。

// 将二叉树定义为一个独立可复用的模块
const binaryTree = require('./binaryTree')
const postorder = (root) => {
    if (!root) return;
    // 1 左子树进行后序遍历
    postorder(root.left);
    // 2 右子树进行后序遍历
    postorder(root.right);
    // 3 访问根节点
    console.log(root.val);
}
postorder(binaryTree)

你可能感兴趣的:(数据结构与算法,javascript,链表,开发语言)