JS数据结构学习笔记

176 阅读5分钟

一、队列

933. 最近的请求次数

1.队列是先进先出的结构 。(JS引擎利用队列执行异步任务,计算最近的请求次数,树、图中广度优先遍历)

2.JS中没有队列,可以用Array实现队列的所有功能。

3.常用操作:push、shift、queue[0]

二、链表

2. 两数相加

83. 删除排序链表中的重复元素

141. 环形链表

206. 反转链表

237. 删除链表中的节点

1. 原型链: 本质是链表;原型链上的节点:各种原型对象;通过__proto__属性连接各种原型对象。对象、函数、数组 ,js变量中除了对象这个数据类型,其他的都是先指向自己的原型对象,再指向Object的原型对象。

(1)面试一:instanceof的原理用代码实现

知识点:如果A沿着原型链能找到B.prototype,那么A instanceOf B为true;

解法:遍历A的原型链,如果能找到B的原型,则返回true,否则返回false

const instanceOf = (A,B) => {
    while(p){
        if(p === B){
            return true;
        }
        p = p.prototype;
    }
    return false;
}

(2)面试二 下面代码中的foo.a, foo.b, F.a, F.b是多少

知识点:如果A对象上没有找到x属性,那么会沿着原型链找x属性。

解法:明确foo和F变量的原型链,沿着原型链找a属性和b属性。

var foo = {};
    F = () => {};
Object.prototype.a = 'value a'; //foo.a 为 value a, F.a为value a
Function.prototype.b = 'value b';//F.b为value b, foo.b为null,因为对象的原型链上没有函数的原型。

三、集合

349. 两个数组的交集

  1. 集合:无序且唯一的数据结构, ES6中的集合名为Set; 集合的常用操作:去重、判断某元素是否在集合中、求交集

  2. Set 使用Set对象:new add delete has size; 迭代Set: 多重迭代方法、set与Array互转、求差集/交集

let my = new Set();

my.add(1);
my.add(2);
my.add('some text');
my.add({a:1, b:2})

my.delete(2);

for(let item of my) console.log(item);

// set数据结构中key、value为同一个值。
for(let [key, value] of my.entries()) console.log(key, value);

//set 转换为 array
const myarr = [...my];
const myArr = Array.from(my);

//array 转为 set
const myset2 = new Set([1,2,3,4]);

//求交集
const intersection = new Set([...my].filter(x => myset2.has(x)));

//求差集
const difference = new Set([...my].filter(x => !myset2.has(x)));

字典

#3 无重复字符的最长子串

#1 两数之和

#20 有效的括号

#349 两个数组的交集

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

  • 分层数据的抽象模型
  • 前端工作中常见的包括:DOM、树、级联选择、树形控件
  • JS中没有树,但可以用Object和Array来构建树
  • 常用操作:深度/广度、先中后序遍历

1. 深度优先遍历

算法口诀:

  • 访问根节点;
  • 对根节点的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.val);
    root.children.forEach(dfs);
}

dfs(tree);

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 bfs = (root) => {
    const q = [root];
    while(q.length !== 0){
        const n = q.shift();
        console.log(n.val);
        n.children.forEach(element => {
            q.push(element);
        });
    }
}

bfs(tree)

二叉树

  • 树的每个节点最多只能有两个子节点
  • 在JS中常用Object来模拟二叉树
  • 所谓前中后序遍历,遍历的是二叉树的根节点

递归版

1. 先序遍历

算法口诀:

  • 访问根节点;
  • 对根节点的左子树进行先序遍历;
  • 对根节点的右子树进行先序遍历
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
        }
    }
}

// **先序遍历算法**
const preorder = (root) => {
    if(!root) return;
    console.log(root.val);
    preorder(root.left);
    preorder(root.right);
}
preorder(bt)

2. 中序遍历

算法口诀:

  • 对根结点的左子树进行中序遍历;
  • 访问根节点;
  • 对根节点的右子树进行中序遍历
const inorder = (root) => {
    if(!root) return;
    inorder(root.left);
    console.log(root.val);
    inorder(root.right);
}

3. 后序遍历

算法口诀:

  • 对根节点的左子树进行后序遍历
  • 对根节点的右子树进行后序遍历
  • 访问根节点
const postorder = (root) => {
    if(!root) return;
    postorder(root.left);
    postorder(root.right);
    console.log(root.val)
}

非递归版(面试更偏向考察)

1. 先序遍历

因为栈结构后进先出,而访问节点顺序为先左后右,所以先把右节点push进去。

const preorder1 = (root) => {
    if(!root) return;
    const stack = [root];
    while(stack.length){
        const n = stack.pop();
        console.log(n.val);
        // 因为栈结构后进先出,而访问节点顺序为先左后右,所以先把右节点push进去。
        if(n.right) stack.push(n.right.val);
        if(n.left) stack.push(n.left.val);
    }
}

2. 中序遍历

左根右,从当前根元素开始循环压入所有左子节点,当到叶子节点时,弹出当前叶子结点,此时栈顶为上一个左节点,若其也没有右子树,那么弹出该节点(即刚刚弹出的元素的父节点)

const inorder1 = (root) => {
    if(!root) return;
    const stack = [];
    let p = root;
    while( stack.length || p) {
        //先把当前根节点的左子节点压入栈
        while(p){
            stack.push(p);
            p = p.left;
        }
        //当当前栈顶元素无左、右子树时,弹出栈顶元素即当前的根节点
        const n = stack.pop();
        console.log(n.val);
        // 开始搜寻刚刚弹出的根节点的右子树
        p = n.right;
    }
}

3. 后序遍历

先序遍历输出顺序是根左右 后序需要的是左右根,所以将先序遍历的左右顺序调换,使得到根右左,再逆序输出(需要第二个栈)即可

//先序遍历输出顺序是根左右 后序需要的是左右根 
//所以将先序遍历的左右顺序调换,使得到根右左,再逆序输出(需要第二个栈)即可
const postorder1 = (root) => {
    if(!root) return;
    const outputStack = [];
    const stack = [root];
    while(stack.length){
        const n = root;
        //逆序入第二个栈
        outputStack.push(n);
        //调换先序遍历的入栈顺序,栈后进先出,第一次变换要得到右左,所以先压左后压右
        if(n.left) stack.push(n.left);
        if(n.right) stack.push(n.right);

    }
}