基础数据结构

之前有简单接触过数据结构,概念性的问题直接跳过,重点记录刷题过程中比较常用且重要的思路和知识点,比较突出个人薄弱的部分

数组

一维数组

初始化

const arr = new Array()
const arr = (new Array(7)).fill(1)

遍历

map,forEach,for循环

性能上看,for 循环遍历起来是最快的,推荐使用

二维数组

初始化

const arr =(new Array(7)).fill([])

注意,如果入参类型是引用型,那么fill在填充坑位时填充的其实就是入参的引用。因此当你修改第0行第0个元素的值时,第1-6行的第0个元素的值也都会跟着发生改变。

直接用一个 for 循环来解决

const len = arr.length
for(let i=0;i<len;i++) {
    // 将数组的每一个坑位初始化为数组
    arr[i] = []
}

遍历

N 维数组需要 N 层循环来完成遍历

栈和队列

常用pop,push

队列

常用shift,push

链表

创建链表结点

需要一个构造函数

function ListNode(val) {
    this.val = val;
    this.next = null;
}

增加节点

img

// 如果目标结点本来不存在,那么记得手动创建
const node3 = new ListNode(3)     
// 把node3的 next 指针指向 node2(即 node1.next)  先后面那条线
node3.next = node1.next
// 把node1的 next 指针指向 node3
node1.next = node3

节点删除

img

node1.next = node3.next 

在涉及链表删除操作的题目中,重点不是定位目标结点,而是定位目标结点的前驱结点。做题时,完全可以只使用一个指针(引用),这个指针用来定位目标结点的前驱结点。

// 利用 node1 可以定位到 node3
const target = node1.next  
node1.next = target.next

复杂的都

常规来说,数组都对应着一段连续的内存,删除增加对应的时间复杂度是 O(n)。但是js中,若数组中定义了不同类型的元素

const arr = ['haha', 1, {a:1}]

它对应的就是一段非连续的内存。此时,JS 数组不再具有数组的特征,其底层使用哈希映射分配内存空间,是由对象链表来实现的。

链表增删操作的复杂度是常数级别的复杂度,为 O(1)。 但链表在读取某个特定节点时,必须整个遍历

小结:链表插入/删除效率较高,而访问效率较低数组访问效率较高,而插入效率较低

树和二叉树

二叉树定义和传值

在 JS 中,二叉树使用对象来定义。它的结构分为三块:

  • 数据域
  • 左侧子结点(左子树根结点)的引用
  • 右侧子结点(右子树根结点)的引用

定义二叉树构造函数时,我们需要把左侧子结点和右侧子结点都预置为

// 二叉树结点的构造函数
function TreeNode(val) {
    this.val = val;
    this.left = this.right = null;
}

img
当需要新建一个二叉树结点时,直接调用构造函数、传入数据域的值就行了:

const node  = new TreeNode(1)

二叉树遍历

按照实现方式的不同,遍历方式可以分为以下两种:

  • 递归遍历(先、中、后序遍历) 以根节点遍历时机为准 常考
    • 结点 -> 左子树 -> 右子树 先
    • 左子树 -> 结点 -> 右子树 中
    • 左子树 -> 右子树 -> 结点 后
  • 迭代遍历(层次遍历)

img

创建

请添加图片描述

const root = {
    val: "A",
    left: {
        val: "B",
        left: {
            val: "D"
        },
        right: {
            val: "E"
        }
    },
    right: {
        val: "C",
        right: "F"
    }
}

遍历-递归

编写一个递归函数之前,大家首先要明确两样东西:

  • 递归式
  • 递归边界
先序

请添加图片描述

// 所有遍历函数的入参都是树的根结点对象
function preorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历左子树 
    preorder(root.left)  
    // 递归遍历右子树  
    preorder(root.right)
}
中序
// 所有遍历函数的入参都是树的根结点对象
function inorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 递归遍历左子树 
    inorder(root.left)  
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
    // 递归遍历右子树  
    inorder(root.right)
}

在leedcode中会要求输出一个数组,这里注意要在里面嵌套一个递归函数

/**
 * @param {TreeNode} root
 * @return {number[]}
 */

var inorderTraversal = function (root) {
    const res = []

    function xunhuan(n){
        if (!n) return
        xunhuan(n.left)
        res.push(n.val)
        xunhuan(n.right)
    }
    xunhuan(root)

    return res
};
后序

img

function postorder(root) {
    // 递归边界,root 为空
    if(!root) {
        return 
    }
     
    // 递归遍历左子树 
    postorder(root.left)  
    // 递归遍历右子树  
    postorder(root.right)
    // 输出当前遍历的结点值
    console.log('当前遍历的结点值是:', root.val)  
}

时间复杂度

反映的不是算法的逻辑代码到底被执行了多少次,而是随着输入规模的增大,算法对应的执行总次数的一个变化趋势

只保留次数最高那一项,并且将其常数系数无脑改为1

举例:

function fn(arr) {
    var len = arr.length  
    
    for(var i=1;i<len;i=i*2) {
        console.log(arr[i])
    }
}

这个算法中,我们关心的就是 console.log(arr[i]) 到底被执行了几次,换句话说,也就是要知道 i<n( len === n) 这个条件是在 i 递增多少次后才不成立的。

假设 i 在以 i=i*2的规则递增了 x 次之后,i<n 开始不成立(反过来说也就是 i>=n 成立)。那么此时我们要计算的其实就是这样一个数学方程:

2^x >= n

x解出来,就是要大于等于以 2 为底数的 n 的对数:

img

也就是说,只有当 x 小于 log2n 的时候,循环才是成立的、循环体才能执行。注意涉及到对数的时间复杂度,底数和系数都是要被简化掉的。那么这里的 O(n) 就可以表示为:

O(n) = logn

常见的时间复杂度按照从小到大的顺序排列,有以下几种:

img

空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,是内存增长的趋势
常见的空间复杂度有 O(1)O(n)O(n^2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值