算法一:JS版数据结构

234 阅读10分钟

一、时间复杂度

算法的时间复杂度是一个函数,它定性描述该算法的运行时间。

O(1):
let i = 0;
i += 1
只执行一次,所以O(1)
O(n)
for(let i = 0; i < n; i +=1 ){}

上面两个加在一起:O(1)+ O(n) = O(n)
n足够大,则可以忽略1
O(n2)
for(let i = 0; i < n; i +=1 ){
  for(let j = 0; j < n; j +=1 ){
    console.log()
  }
}

O(n) x O(n) = O(n2)
相乘和相加不一样
O(logN)
let i = 1;
whie(i < n) {
 i *= 2; 
}

二、空间复杂度

算法在运行过程中临时占用存储空间大小的量度

O(1):
只声明单个变量
let i = 0;
i += 1
O(n):
数组加了n个值,相当于添加n个内存单元
const list = [];
for(let i = 0;i < n; i+= 1){
  list.push(i)
}

O(n2):
const matrix = [];
for(let i = 0; i < n; i +=1){
  matrix.push([])
  for(let j = 0; j < n; j +=1){
    matrix[i].push(j)
  }
}

三、数据结构:栈

一个后进先出的数据结构

push:入栈 pop:出栈

应用场景:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈

场景一
const stack = [];
stack.push(1);
stack.push(2);
const item1 = stack.pop();
const item2 = stack.pop();

场景二:js中的函数调用堆栈
const func1 = () => {
    func2();
};
const func2 = () => {
    func3();
};
const func3 = () => {};

func1();

leetcode题号20:有效的括号

解法1
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if(s.length % 2 !== 0 || typeof s !== 'string') return false;
    let stack = [];
    let validCharacterMap = {
        '(': true,
        '{': true,
        '[': true
    };
    let closeCharacterMap = {
        '()': true,
        '{}': true,
        '[]': true
    };
    for(let i = 0; i < s.length; i++){
        let current = s[i];
        if (validCharacterMap[current]){
            stack.push(current);
        } else {
            let key = stack[stack.length - 1] + current;
            if(closeCharacterMap[key]){
                stack.pop();
            } else {
                return false;
            }
        }
    }
    return stack.length === 0;
};
解法2
/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    if(s.length % 2 !== 0 || typeof s !== 'string') return false;
    let stack = [];
    let chMap = {
        ')': '(',
        '}': '{',
        ']': '['
    };
    for(let i = 0; i < s.length; i++){
        if (!chMap[s[i]]){
            stack.push(s[i]);
        } else {
            if(stack[stack.length - 1] === chMap[s[i]]){
                stack.pop();
            } else {
                return false;
            }
        }
    }
    return stack.length === 0;
};
总结

此题的解题思路是入栈和出栈。
左括号代表入栈,对应的右括号代表出栈,对于stack数组进行push()和pop()操作。最后数组长度为0的话,则满足题目要求,return true。
第一次写好后,虽然题目中的测试用例通过了,但是提交之后没有通过如下测试用例。
输入:"([}}])"
主要是因为在判断右括号不是同类型后,没有直接return false,代码还在执行下去,倒数两个字符‘])’刚好和正数两个字符是同类型闭合,就执行pop()最后变成空数组。
还有一个需要注意的是,审题不够仔细,“字符串可被认为是有效字符串”题目有提到了,但看了一眼没注意,写的时候自然也没考虑到这点。但测试用例是通过的,所以没什么问题。
之后审题要认真一些,一些边界情况要多考虑。
还有一点,执行之后执行用时虽然挺快的,但内存消耗偏大,大概是因为定义了两个对象存储括号吧。
看官方解题法,用的是哈希映射(HashMap)存储,此外他的思路是:哈希映射的键为右括号,值为相同类型的左括号。
这个比我的设计要好。

四、数据结构:队列

一个先进先出的数据结构

1、应用场景:食堂排队打饭、JS异步中的任务队列、计算最近请求次数

2、JS异步中的任务队列:JS是单线程,无法同时处理异步中的并发任务

输入:inputs = [[],[1],[100],[3001],[3002]]
输出:[null,1,2,3,3]

const queue = [];
queue.push(1);
queue.push(2);
const item1 = queue.shift();
const item2 = queue.shift();

3、leetcode题号933:最近请求的次数

代码:本质队列问题

var RecentCounter = function() {
    this.q = []
};

/** 
 * @param {number} t
 * @return {number}
 */
RecentCounter.prototype.ping = function(t) {
    this.q.push(t)
    while(this.q[0] < t-3000){
        this.q.shift()
    }
    return this.q.length;
};

/**
 * Your RecentCounter object will be instantiated and called as such:
 * var obj = new RecentCounter()
 * var param_1 = obj.ping(t)
 */
总结

队列问题,虽然写出来了,但是因为之前看过题解。
有几个点掌握的不好:
1、构造函数this.q的用法
2、怎么快速分析问题,转化为代码逻辑,这需要锻炼

五、数据结构:链表

多个元素组成的列表;元素存储不连续,用next指针连在一起

1、优点:增删非首尾元素,不需要移动元素,只需要更改next的指向即可

2、代码

场景一:
const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while (p) {
    console.log(p.val);
    p = p.next;
}

// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;

// 删除
c.next = d;

场景二:
const json = {
    a: { b: { c: 1 } },
    d: { e: 2 },
};

const path = ['a', 'b', 'c'];

let p = json;
path.forEach((k) => {
    p=p[k];
});

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

let a = () => {}
typeof a === 'function' true
a instanceof Function true
a instanceof Object true

如果在A对象上没有找到x属性,那么会沿着原型链找x属性

const obj = {}
Object.prototype.x = 'x'
const func = () => {}
Function.prototype.y = 'y'

4、使用链表指针获取JSON的节点值

const json = {
  a: {b: {c: 1}},
  d: {e: 2}
};

const path = ['a', 'b', 'c'];

let p = json
path.foreach(key => {
  p = p[key]
})

5、leetcode:237、206、2、83、141、

面试题

1、instanceof的原理,并用代码实现 知识点:如果A沿着原型链能找到B.prototype,那么A instanceof B为true

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

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

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

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

let foo = {}
let F = function() {};
Object.prototype.a = 'value a'
Function.prototype.b = 'value b'

console.log(foo.a)
console.log(foo.b)

console.log(F.a)
console.log(F.b)
总结

1、链表里的元素存储是不连续的,通过next()连接
2、Javascript中没有链表,但可以用Object模拟链表
3、链表常用操作:修改next()、遍历链表
4、js中的原型链也是一个链表,
5、使用链表指针可以获取JSON的节点值

六、数据结构:集合

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

1、代码

// 去重
const arr = [1, 1, 2, 2];
const arr2 = [...new Set(arr)];

// 判断元素是否在集合中
const set = new Set(arr);
const has = set.has(3);

// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)));

2、set操作
使用Set对象:new、add、delete、has、size
迭代Set:多种迭代方法、Set与Array互传、求交集/差集

let mySet = new Set();

mySet.add(1);
mySet.add(5);
mySet.add(5);
mySet.add('some text');
let o = { a: 1, b: 2 };
mySet.add(o);
mySet.add({ a: 1, b: 2 });

const has = mySet.has(o);

mySet.delete(5);

for(let [key, value] of mySet.entries()) console.log(key, value);

const myArr = Array.from(mySet);

const mySet2 = new Set([1,2,3,4]);

const intersection = new Set([...mySet].filter(x => mySet2.has(x)));
const difference = new Set([...mySet].filter(x => !mySet2.has(x)));

3、leetcode:349

七、数据结构:字典

与集合类似,字典也是一种存储唯一值的数据结构,但它是以键值对的形式存储

Es6中有字典,名为Map

const m = new Map();

// 增
m.set('a', 'aa');
m.set('b', 'bb');

// 删
m.delete('b');
// m.clear();

// 改
m.set('a', 'aaa');

2、leetcode:349、20、1、3、76

八、数据结构:树

树:一种分层数据的抽象模型。(前端工作中常见的树:Dom树、级联选择、树形控件……)
JS中没有树,但是可以用Object和Array构建树。
树的常用操作:深度/广度优先遍历、先中后序遍历

//时间复杂度O(n),n为节点个树,空间复杂度O(n),即递归的空间开销(树的高度),最坏的情况下树是链表,所以是O(n),平均空间复杂度O(logn)
//前序遍历:
var preorderTraversal = function(root, res = []) {
    if (!root) return res;
    res.push(root.val);
    preorderTraversal(root.left, res)
    preorderTraversal(root.right, res)
    return res;
};

//中序遍历:
var inorderTraversal = function(root, res = []) {
    if (!root) return res;
    inorderTraversal(root.left, res);
    res.push(root.val);
    inorderTraversal(root.right, res);
    return res;
};

//后序遍历:
var postorderTraversal = function(root, res = []) {
    if (!root) return res;
    postorderTraversal(root.left, res);
    postorderTraversal(root.right, res);
    res.push(root.val);
    return res;
};


作者:zz1998
链接:https://leetcode.cn/problems/binary-tree-preorder-traversal/solution/dai-ma-jian-ji-yi-chong-huan-bu-cuo-de-j-gyxc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1、树的深度/广度优先遍历

深度优先遍历:尽可能深的探索树的分支
遍历方法(递归):
1、访问根节点;2、对根节点的children挨个进行深度优先遍历 深度和广度优先案例

dfs:
const dfs = (root) => {
    console.log(root.val);
    root.children.forEach(dfs);
};

dfs(tree);

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

bfs:
const bfs = (root) => {
    const q = [root];
    while (q.length > 0) {
        const n = q.shift();
        console.log(n.val);
        n.children.forEach(child => {
            q.push(child);
        });
    }
};

bfs(tree);
Json:
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'd',
                    children: [],
                },
                {
                    val: 'e',
                    children: [],
                }
            ],
        },
        {
            val: 'c',
            children: [
                {
                    val: 'f',
                    children: [],
                },
                {
                    val: 'g',
                    children: [],
                }
            ],
        }
    ],
};
2、二叉树的先中后序遍历

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

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

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

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

(非递归版)
// const preorder = (root) => {
//     if (!root) { return; }
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         console.log(n.val);
//         if (n.right) stack.push(n.right);
//         if (n.left) stack.push(n.left);
//     }
// };

preorder(bt);

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

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

(非递归版)
// const inorder = (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;
//     }
// };

inorder(bt);

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

postorder:

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

(非递归版)
// const postorder = (root) => {
//     if (!root) { return; }
//     const outputStack = [];
//     const stack = [root];
//     while (stack.length) {
//         const n = stack.pop();
//         outputStack.push(n);
//         if (n.left) stack.push(n.left);
//         if (n.right) stack.push(n.right);
//     }
//     while(outputStack.length){
//         const n = outputStack.pop();
//         console.log(n.val);
//     }
// };

postorder(bt);
JSON:
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,
        },
    },
};
3、leecode

104、111、102、94、112 算法题待补充

遍历JSON的所有节点值
const json = {
    a: { b: { c: 1 } },
    d: [1, 2],
};

const dfs = (n, path) => {
    console.log(n, path);
    Object.keys(n).forEach(k => {
        dfs(n[k], path.concat(k));
    });
};

dfs(json, []);
渲染Antd树组件

九、数据结构:图

图是网络结构的抽象模型,是一组由边连接的节点。
图可以表示任何二元关系,比如道路、航班…… JS中没有图,但是可以用Object和Array构建图

1、图的表示法:邻接矩阵、邻接表、关联矩阵……

2、图的常用操作:深度优先遍历、广度优先遍历

深度优先遍历:
1、访问根节点;
2、对根节点的没访问过的相邻节点挨个进行深度优先遍历

dfs:
const visited = new Set();
const dfs = (n) => {
    console.log(n);
    visited.add(n);
    graph[n].forEach(c => {
        if(!visited.has(c)){
            dfs(c);
        }
    });
};

dfs(2);
Json:
const graph = {
    0: [1, 2],
    1: [2],
    2: [0, 3],
    3: [3]
};

广度优先遍历:
1、新建一个队列,把根节点入队;2、把队头出队并访问;3、把队头的没访问过的相邻节点入队;4、重复二三步,直到队列为空

bfs:
const visited = new Set();
const q = [2];
while (q.length) {
    const n = q.shift();
    console.log(n);
    visited.add(n);
    graph[n].forEach(c => {
        if (!visited.has(c)) {
            q.push(c);
        }
    });
}

leetcode

65、417、133

十、数据结构:堆

一种特殊的完全二叉树
结构特性:二叉堆,树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),并且最后一层的叶节点尽可能都是左侧子节点。
堆特性:所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子节点。

JS中通常用数组表示堆:
左侧子节点的位置是2index+1
右侧子节点的位置是2
index+2
父节点的位置是(index-1)/2的商
堆能高效找出最大值和最小值,O(1),找出第K个最大(小)元素

实现最小堆

将值插入堆的底部,即数组的尾部
然后上移:将这个值和他的父节点进行交换,直到父节点小于等于这个插入的值
大小为K的堆中插入元素的时间复杂度为O(logK)

class MinHeap {
    constructor() {
        this.heap = [];
    }
    swap(i1, i2){
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    getParentIndex(i){
        return (i - 1) >> 1; //Math.floor((i -1)/2)
    }
    shiftUp(index){
        if(index === 0) return;
        const parentIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this.shiftUp();
        }
    }
    insert(value){
        this.heap.push(value);
        this.shiftUp(this.heap.length -1);
    }
}

const h = new MinHeap();
h.insert(3)
h.insert(2)
h.insert(1)

删除堆顶

用数组尾部元素替换堆顶(直接删除堆顶会破坏堆数据结构)
然后下移:将新堆顶和他的子节点进行交换,直到子节点大于等于这个新堆顶
大小为K的堆中删除堆顶的时间复杂度为O(logK)

class MinHeap {
    constructor() {
        this.heap = [];
    }
    swap(i1, i2){
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp;
    }
    getParentIndex(i){
        return (i - 1) >> 1; //Math.floor((i -1)/2)
    }
    getLeftIndex(i){
        return i * 2 + 1;
    }
    getRightIndex(i){
        return i * 2 + 2;
    }
    shiftUp(index){
        if(index === 0) return;
        const leftIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    shiftDown(index){
        if(index === 0) return;
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if(this.heap[leftIndex] < this.heap[index]) {
            this.swap(leftIndex, index);
            this.shiftDown(leftIndex);
        }
        if(this.heap[rightIndex] < this.heap[index]) {
            this.swap(rightIndex, index);
            this.shiftDown(rightIndex);
        }
    }
    insert(value){
        this.heap.push(value);
        this.shiftUp(this.heap.length -1);
    }
    pop(){
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
}

const h = new MinHeap();
h.insert(3)
h.insert(2)
h.insert(1)
h.pop()

获取堆顶和堆的大小

peek(){
	return this.heap[0];
}
size(){
	return this.heap.length;
}

leetcode

215、347、23

ps: vscode小技巧:打断点,点击F5就能运行