Skip to content
On this page

数据结构

​ 一个后进先出的数据结构。在JS中,使用数组实现栈结构。

javascript
const stack = [];
stack.push(1);
const items = stack.pop();

使用场景:需要后进先出的场景,如十进制转二进制、判断字符括号是否有效和函数调用堆栈等

相关题目:力扣第20题

JS中的函数调用堆栈

​ 多个函数的互相调用就是入栈和出栈的操作。

队列

​ 一个先进先出的数据结构,使用数组可以实现队列的结构。

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

应用场景:需要先进先出的场景,例如食堂排队打饭、js异步中的任务队列、计算最近请求次数

题目:力扣第933题

js异步中的任务队列

​ 异步任务会被放到webAPIs执行,然后代码会继续执行。

javascript
// 先打印2再打印1
setTimeout(() => console.log(1), 0);
console.log(2)

链表

​ 多个元素组成的列表,元素存储不连续,使用next指针连在一起。我们可以使用对象模拟一个链表。

javascript
const a = {val: 'a'};
const b = {val: 'b'};
a.next = b;
// 遍历链表
let p = a;
while(p) {
    console.log(p);
    p = p.next;
}
// 插入 a c b
const c = {val: 'c'};
c.next = b;
a.next = c;
// 删除 a b
a.next = b

推荐题目:力扣第237、206、2、83、141题

JS中的原型链

​ 本质是链表,原型链上的节点是各种原型对象,如Object.prototype、Function.prototype,原型链通过__proto__属性连接各种原型对象。

​ 如果A沿着原型链能找到B.prototype,那么A instanceof B为true。所有的对象的原型对象都是Object。如果在A对象上没有找到x属性,那么会沿着原型链找x属性。

javascript
func instanceof Object;  // true

面试题1

​ instanceof的原理,并用代码实现。

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

面试题2

​ 输出什么?

javascript
var foo = {}, F = function(){};
Object.prototype.a = 'a';
Function.prototype.b = 'b';

console.log(foo.a);  // a
console.log(foo.b);  // undifind
console.log(F.a); // a
console.log(F.b); // b

获取JSON的节点值

javascript
const json = {
    a: {
        b: {
            c: 2
        }
    },
    d: 3   
}
const path = ['a', 'b', 'c'];
let p = json;
path.forEach(k => {
    p = p[k];
})

集合

​ 一种无序且唯一的数据结构,ES6中有集合(Set)这一数据结构。集合常用操作有去重、判断某元素是否在集合中、求交集......

javascript
// 去重
const arr = [1, 1, 1, 2];
const arr2 = [...new Set(arr)];
// 判断元素是否在集合中
const set = new Set(arr)
console.log(set.has(1)); // true
// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set].filter(item => set2.has(item)))

推荐题目:力扣第349题

Set操作

  • 使用Set对象:new、add、delete、has、size
  • 迭代Set:多种迭代方法、Set和Array互转、求交集/差集
javascript
const mySet = new Set();
mySet.add(1);
mySet.add('hello');
mySet.add({a: 1});
console.log(mySet.has(3));
mySet.delete(1);
// 迭代set 三种效果一样
for (let item of mySet) {
    console.log(item);
}
for (let item of mySet.keys()) {
    console.log(item);
}
for (let item of mySet.values()) {
    console.log(item);
}
// set转array
const arr1 = [...mySet];
const arr2 = Array.from(mySet);
// array转Set
const mySet2 = new Set([1, 2, 3, 4])
// 求交集
const mySet3 = new Set([...mySet].filter(x => mySet2.has(x)));
// 求差集
const mySet4 = new Set([...mySet].filter(x => !mySet2.has(x)));

字典

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

javascript
const m = new Map();
// 增
m.set('a', 'aa');
// 删
m.delete('a');
m.clear();
// 改
m.set('a', 'aaa');
// 查
m.get('a')

推荐题目:力扣第349、20、1、3、76题

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

javascript
const tree = {
    val: 'a',
    children: [
        {
            val: 'b',
            children: [
                {
                    val: 'c',
                    children: []
                },
                {
                    val: 'd',
                    children: []
                }
            ]
        },
        {
            val: 'e',
            children: []
        }
    ]
}

推荐题目:力扣第104、111、102、94、112题。

深度/广度优先遍历

​ 深度优先遍历是指尽可能深的搜索树的分支;广度优先遍历是指先访问离根节点最近的节点。

​ 深度优先遍历算法口诀为:先访问根节点,再对根节点的children挨个进行深度优先遍历。

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

​ 广度优先遍历的算法口诀为:先新建一个队列,把根节点入队,然后将队头出队并访问最后把队头的children挨个入队,重复上面的第二第三步,直到队列为空。

javascript
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);
        });
    }
};

二叉树的先中后序遍历

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

javascript
const bt = {
    val: 'a',
    left: {
        val: 'b',
        left: null,
        right: null
    },
    right: {
        val: 'c',
        left: null,
        right: null
    }
}

先序遍历

​ 先访问根节点,再对根节点的左子树进行遍历,最后对根节点的右子树进行遍历。

javascript
// 遍历
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);
    }
};

中序遍历

​ 先对根节点的左子树进行遍历,再访问根节点,最后对根节点的右子树进行遍历。

javascript
// 遍历
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;
    }
};

后序遍历

​ 先对根节点的左子树进行遍历,再对根节点的右子树进行遍历,最后访问根节点。

javascript
// 遍历
const postOrder = (root) => {
    if(!root) return;
    postOrder(root.left);
    postOrder(root.right);
    console.log(root.val);
};
// 非遍历
const postOrder = (root) => {
    if(!root) return;
    const stack = [root];
    const outputStack = [];
    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);
    }
};

遍历JSON的所有节点值

javascript
const json = {
    a: {
        b: {
            c: 2
        }
    },
    d: [1, 3]
}

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

​ 图是网络结构的抽象模型,是一组由边连接的节点,图可以表示任何二元关系。在JS中,我们可以用Array和Object构建图,图的表示法有:邻接矩阵、邻接表、关联矩阵......。图的操作有深度与广度优先遍历。

深度与广度优先遍历

​ 深度优先遍历是指尽可能深的搜索树的分支;广度优先遍历是指先访问离根节点最近的节点。

​ 深度优先遍历算法口诀为:先访问根节点,再对根节点的没访问过的相邻节点挨个进行深度优先遍历。

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

​ 广度优先遍历的算法口诀为:先新建一个队列,把根节点入队,然后将队头出队并访问最后把队头的没访问过的相邻节点挨个入队,重复上面的第二第三步,直到队列为空。

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

推荐题目:力扣第65、417、133题

​ 一种特殊的完全二叉树。所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。在JS中使用数组表示堆,采用广度优先遍历的方式。左侧子节点的位置是2 * index + 1右侧子节点的位置为2 * index + 2。父节点的位置是(index -1)/2。堆能够高效、快速地找到最大最小值(时间复杂度为O(1))。

第k个最大元素

  • 构建一个最小堆,并将元素依次插入堆中
  • 当堆的容量超过k,就删除堆顶
  • 插入结束后,堆顶就是第k个最大元素

实现最小堆类

  • 在类里,声明一个数组,用来装元素
  • 主要方法:插入、删除堆顶、获取堆顶、获取堆大小
javascript
class MinHeap {
    constructor() {
        this.heap = [];
    }
    // 获取父节点的index
    getParentIndex(i) {
        return (i - 1) >> 1;
    }
    // 获取左节点的index
    getLeftIndex(i) {
        return i * 2 + 1;
    }
    // 获取右节点的index
    getRightIndex(i) {
        return i * 2 + 2;
    }
    // 交换操作
    swap(i1, i2) {
        const temp = this.heap[i1];
        this.heap[i1] = this.heap[i2];
        this.heap[i2] = temp
    }
    // 上移操作
    shiftUp(index) {
        if(index == 0) return ;
        const parentIndex = this.getParentIndex(index);
        if(this.heap[parentIndex] > this.heap[index]) {
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    // 下移操作
    shiftDown(index) {
        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);
    }
    // 获取堆顶
    peek() {
        return this.heap[0];
    }
    // 获取堆的大小
    size() {
        return this.heap.length;
    }
}

推荐题目:力扣第215、347、23题

排序和搜索

​ 排序是把某个乱序的数组变成升序或者降序的数组,搜索是找出数组中某个元素的下标。在JS中,搜索使用数组的sort方法,搜索使用数组的indexOf方法。

排序算法

​ 排序算法有冒泡排序、选择排序、插入排序、归并排序和快速排序等。

图解:排序

冒泡排序

​ 比较所有相邻元素,如果第一个比第二个大,则交换它们,这样一轮下来就可以保证最后一个数是最大的,执行n-1轮,就可以完成排序。

javascript
Array.prototype.bubbleSort = function() {
    for(let i = 0; i < this.length - 1; i++) {
        for(let j = 0; j < this.length - 1 - i; j++) {
            if(this[j] > this[j + 1]) {
                const temp = this[j];
                this[j] = this[j + 1];
                this[j + 1] = temp;
            }
        }
    }
}
const arr = [5, 4, 3, 1, 2];
arr.bubbleSort();

​ 冒泡排序有两层嵌套循环,时间复杂度为O(n ^ 2)。

选择排序

​ 找到数组中最小值,选中它并将其放置在第第一位,接着找到第二小的值,将其放置在第二位,以此类推,执行n-1轮。

javascript
Array.prototype.selectionSort = function() {
    for(let i = 0; i < this.length - 1; i++) {
        let indexMin = i;
        for(let j = i; j < this.length; j++) {
            if(this[j] < this[indexMin]) {
                indexMin = j;
            }
        }
        if(indexMin !== i) {
            const temp = this[i];
            this[i] = this[indexMin];
            this[indexMin] = temp;
        }
    }
    
}
const arr = [5, 4, 3, 1, 2];
arr.selectionSort();

​ 选择排序也是两层嵌套循环,时间复杂度和冒泡排序一样。

插入排序

​ 从第二个数开始往前比,比它大就往后排,以此类推进行到最后一个数。

javascript
Array.prototype.insertSort = function() {
    for(let i = 1; i < this.length; i++) {
        const temp = this[i];
        let j = i;
        while(j > 0) {
            if(this[j - 1] > temp) {
                this[j] = this[j - 1];
            } else {
                break;
            }
            j --;
        }
        this[j] = temp;
    }
}
const arr = [5, 4, 3, 1, 2];
arr.insertSort();
console.log(arr);

​ 插入排序时间复杂度和前面两个排序一样,但是对于小型的数组,其性能比较好。

归并排序

​ 把数组劈成两半,再递归地对子数组进行“分”操作,直到分成一个个单独的数(分)。把两个数合并为有序数组,再对有序数组进行合并,直到全部子数组合并为一个完整数组(合)。

​ 怎么合并两个数组?新建一个空数组用于存放最后排序后的数组。比较两个有序数组的头部,较小者出队并推入新建数组中,如果两个数组还有值,就重复第二步。

javascript
Array.prototype.mergeSort = function() {
    const rec = (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 = rec(left);
        const orderRight = rec(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;
    }
    const res =  rec(this);
    res.forEach((n, i) => {
        this[i] = n;
    })
}
const arr = [5, 4, 3, 1, 2];
arr.mergeSort();
console.log(arr);

​ 归并排序的时间复杂度为O(logN)(分)+ O(n)(合)= O(n * logN)。

快速排序

​ 从数组中任意选择一个“基准”, 所有比基准小的元素放在基准前面,比基准大的元素放在基准的后面(分区)。递归地对基准前后的子数组进行分区(递归)。

javascript
Array.prototype.quickSort = function() {
    const rec = (arr) => {
        if(arr.length <= 1) return arr;
        const left = [];
        const right = [];
        const mid = arr[0];
        for(let i = 1; i < arr.length; i++) {
            if(arr[i] < mid) {
                left.push(arr[i]);
            } else {
                right.push(arr[i]);
            }
        }
        return [...rec(left), mid, ...rec(right)];
    };
    const res = rec(this);
    res.forEach((n, i) => {
        this[i] = n;
    })
}

const arr = [5, 4, 3, 1, 2];
arr.quickSort();
console.log(arr);

​ 递归的时间复杂度为O(logN),分区的复杂度是O(n),总共的时间复杂度为O(n * logN)。

搜索算法

​ 搜索算法包括顺序搜索和二分搜索。

顺序搜索

​ 先遍历数组,然后找到跟目标值相等的元素,就返回它的下标。遍历结束后没有找到搜索的目标值就返回-1。

javascript
Array.prototype.sequentialSearch = function(target) {
    for(let i = 0; i < this.length; i++) {
        if(this[i] === target) {
            return i;
        }
    }
    return -1;
}
const res = [1, 3, 4, 2, 5].sequentialSearch(3);
console.log(res);

​ 遍历数组是一个循环操作,时间复杂度为O(n)。

二分搜索

​ 从数组的中间元素开始,如果中间元素正好是目标值,则搜索结束。如果目标值大于或者小于中间元素,则在大于或小于中间元素的那一半数组中搜索。

javascript
Array.prototype.binarySearch = function(target) {
    // 保证有序  从小到大
    // this.sort();
    let low = 0;
    let high = this.length - 1;
    while(low <= high) {
        const mid = Math.floor((low + high) / 2);
        const element = this[mid];
        if(element < target) {
            low = mid + 1;
        } else if(element > target) {
            high = mid - 1;
        } else {
            return mid;
        }
    }
    return -1;
}
const res = [1, 2, 3, 4, 5].binarySearch(4);
console.log(res);

​ 该算法的时间复杂度为O(logN),每一次搜索都使搜索范围减小一半。

推荐题目:力扣第21、374题

分而治之

​ 分而治之是算法设计中的一种方法。它将一个问题分成多个和原问题相似的小问题,递归解决小问题,再将结果合并以解决原来的问题。其常见的使用场景有:

  • 归并排序
  • 快速排序
  • 二分搜索
  • 翻转二叉树
  • ......

推荐题目:力扣第374、226、100、101题

动态规划

​ 动态规划是算法设计中的一种方法。将一个问题分解为相互重叠的子问题,通过反复求解子问题,来解决原来的问题。具体体现有:斐波那契数列问题。

和分而治之的区别在于:子问题是否独立。如果独立就是分而治之,不独立就是动态规划。

推荐题目:第70、198题

贪心算法

​ 贪心算法是算法设计中的一种方法。期盼能通过每个阶段的局部最优选择,从而达到全局的最优,结果不一定是最优。使用的负面场景有:

  • 零钱兑换
  • 天龙八部的珍珑棋局

推荐题目:力扣第455、122题

回溯算法

​ 回溯算法是算法设计中的一种方法。是一种渐进式寻找并构建问题解决方式的策略。回溯算法会先从一个可能的动作开始解决问题,如果不行,就回溯并选择另一个动作,直到将问题解决。使用情况为:有很多的假设,这些可能里有正确的假设也有错误的假设,需要使用递归。使用场景有:

  • 全排列
  • 子集
  • ......

推荐题目:力扣第46、78题

Released under the MIT License.