2-Javascript 数据结构与算法-字典 树

160 阅读9分钟

字典 Map

  • 与集合类似,但它是以键值对的形式存储的。
  • ES6中有字典,名为 Map
// 增 set

// 删除 delete

// 改(覆盖) set

// 清空 clear

// 交集 可用用过字典来完成数组的交集,思想就是先将A数组迭代到字典里面,然后在迭代B数组,每层循环的值判断是否存在于字典里面,存在则push到交集数组里面

总结

  • 与集合类似,字典也是一种存储唯一值得数据结构,但它是以键值对的形式来存储的
  • ES6中有字典,名为Map
  • 算法用到字典的有 两个数组的交集无重复字符的最长子串两数之和

树 Three

  • 一种分层数据的抽象模型
  • 前端工作中常见的树包括:DOM树、级联选择(城市区选择 slect)、树形控件
  • JS中没有树,但是我们可用Object和Array构建树
  • 树的常用操作:深度/广度优先遍历、先中后序遍历(二叉树)。

image.png

深度优先遍历:就像我们平常看书一样,先看第一章,然后是第一章的内容。一页一页看的。

广度优先遍历:就像我们平常看书一样,先把整本书的目录全部看一遍,然后去看第一章的内容。

根据经验之谈,建议大家拿到一本书之后先以广度优先遍历的策略去阅读书。

深度优先遍历算法口诀(递归)

  1. 访问根节点
  2. 对根节点的children依次进行深度优先遍历

背了【访问根节点、对根节点的children依次进行深度优先遍历】走到那里都不怕深度优先遍历

let tree = {
  val: 'a',
  children: [
    {
      val: 'b',
      children: [
        {
          val: 'd',
          children: []
        },
        {
          val: 'e',
          children: []
        }
      ]
    },
    {
      val: 'c',
      children: [
        {
          val: 'f',
          children: []
        },
        {
          val: 'g',
          children: []
        }
      ]
    }
  ]
}

// 树的深度优先遍历
function dfs(tree) {
  // 访问根节点
  console.log(tree.val)
  // 依次递归
  if (tree.children.length > 0) {
    tree.children.forEach((child) => {
      dfs(child)
    })
  }
}

dfs(tree)
// a
// b
// d
// e
// c
// f
// g

广度优先遍历算法口诀(队列)

  1. 新建一个队列,把根节点入队
  2. 把队头出队并访问
  3. 把队头的children挨个入队
  4. 重复第二、三步,直到队列为空

实现广度优先遍历,就要用到 队列whileshift()

let tree = {
  val: 'a',
  children: [
    {
      val: 'b',
      children: [
        {
          val: 'd',
          children: []
        },
        {
          val: 'e',
          children: []
        }
      ]
    },
    {
      val: 'c',
      children: [
        {
          val: 'f',
          children: []
        },
        {
          val: 'g',
          children: []
        }
      ]
    }
  ]
}

// 广度优先遍历
function bfs(tree) {
  let queue = [tree]
  while (queue.length > 0) {
    const n = queue.shift()
    console.log(n.val)
    n.children.forEach((child) => {
      queue.push(child)
    })
  }
}

bfs(tree)

// a
// b
// c
// d
// e
// f
// g

二叉树的先中后序遍历(递归版本)

  • 树中每个节点最多只能有两个子节点
  • 树的前序遍历, 中序遍历,后序遍历,都属于深度优先遍历。
  • 在JS中通常用Object来模拟二叉树

image.png

  • 先序遍历算法口诀(每个根节点开始都是一个递归)

    1.访问根节点

    2.对根节点的左子树进行先序遍历

    3.对根节点的右子树进行现需遍历

function preorder(bt) {
  //  访问根节点
  //  对根节点的左子树进行先序遍历
  //  对根节点的右子树进行先序遍历
  console.log(bt.val)

  if (bt.left) {
    preorder(bt.left)
  }

  if (bt.right) {
    preorder(bt.right)
  }
}

中序遍历算法口诀(左根右)

function midorder(bt) {
  if (bt.left) {
    midorder(bt.left)
  }

  console.log(bt.val)

  if (bt.right) {
    midorder(bt.right)
  }
}

后序遍历算法口诀(左右根)

function postorder(bt) {
  if (bt.left) {
    postorder(bt.left)
  }
  if (bt.right) {
    postorder(bt.right)
  }
  console.log(bt.val)
}

二叉树

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: {
        left: null,
        right: null
      }
    },
    right: {
      val: 7,
      left: null,
      right: null
    }
  }
}

二叉树的先中后序遍历(非递归版本)

  • 在面试中,递归当时的前中后序遍历太简单了,面试官会筛选高水平的选手要求非递归的算法去实现前中后序遍历
  • 非递归的前中后序遍历是用的函数调用栈的思想,要用到栈
  • 倒着写push 因为是栈,后进先出pop while 比如 ”左右根“,
// 先序遍历二叉树
// 根左右
// 利用函数调用栈思想
function preorder(tree) {
  let stack = []
  stack.push(tree)

  while (stack.length > 0) {
    const node = stack.pop()
    console.log(node.val)

    if (node.right) {
      stack.push(node.right)
    }

    if (node.left) {
      stack.push(node.left)
    }
  }
}

// 后序遍历二叉树 [左右根],利用函数调用栈思想
// 其实后序遍历就是前序遍历,利用前序遍历二叉树的算法,前序是【根左右】, 后序是【左右根】,那么我们把后序遍历倒置一下就是【根右左】
// 【根右左】就很像我们的前序遍历 【根左右】,只不过在访问根的时候我们给记录下来outStack,然后依次pop就是我们要的答案
// 我们通过前序的算法,稍作改动
function postorder(tree) {
  let stack = []
  let outStack = []
  stack.push(tree)

  while (stack.length > 0) {
    const node = stack.pop()

    // 1、根
    outStack.push(node.val)

    // 3、左
    if (node.left) {
      stack.push(node.left)
    }

    // 2、右
    if (node.right) {
      stack.push(node.right)
    }

    // 我们的顺序是 1 3 2 原因是我们利用栈的特性,后进先出
  }

  while (outStack.length > 0) {
    console.log(outStack.pop())
  }
}

中序遍历(非递归版本)

// 中序遍历二叉树 [左根右]
// 利用指针 p
// 先把左子树全部push到栈,然后输出,然后指针改成右子树
function inorder(tree) {
  let stack = []
  let p = tree

  while (stack.length > 0 || p) {
    while (p) {
      stack.push(p)
      p = p.left
    }
    const n = stack.pop()
    console.log(n.val)
    p = n.right
  }
}

inorder(bt)

遍历JSON的所有节点值

实际运用场景:处理后端返回的json数据

渲染Antd中的树组件

总结

二叉树前中序遍历分为2个版本,一个是递归版本比较简单,按照 “根左右” ”左根右“ ”左右根“ 依次进行递归就可以,第二个版本是利用函数堆栈的思想,就是利用栈的概念,前序与后序其实很类似,向stack内push,只不过要注意的是后序遍历其实就是用的前序遍历的思想,唯独比较难的是中序遍历(非递归版本),因为要用到指针。

树是一种分层数据的抽象模型,在前端广泛应用

编码

349.两个数组的交集 LeetCode

leetcode-cn.com/problems/in…

时间复杂度 O(n^2) filter * has

空间复杂度 O(n)

总结:需要用到集合的has方法与数组的filter方法

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 * 交集-就要使用Set集合
 */
var intersection = function(nums1, nums2) {
    // [1, 2, 2, 1] [2, 2]
    let set = new Set(nums1)
    return [... new Set( nums2.filter((item) => set.has(item)) )]
};

通过字典Map的方式来实现两个数组的交集

时间复杂度 O(n)

空间复杂度 O(n)

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function(nums1, nums2) {
    // 通过字典来实现
    let map = new Map()
    nums1.forEach((item) => { // 空间复杂度 O(n)
        map.set(item, true)
    })

    let res = []
    nums2.forEach((item) => {
        if(map.get(item)) {
            res.push(item)
            map.delete(item)
        }
    })
    return res
    
};

3.无重复字符的最长子串 LeetCode

leetcode-cn.com/problems/lo…

解题步骤

  1. 双指针维护一个滑动窗口(就好像切换音频)
  2. 不断的移动右指针,遇到重复的字符,就把左指针移动到重复字符的下一位
  3. 过程中,记录所有窗口的长度,并返回最大值

解题知识点:双指针、滑动窗口、字典

边界:获取的重复字符必须在滑动窗口内 map.get(s[r]) ≥ l)

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    // 双指针、滑动窗口、字典、边界(map.get(s[r]) >= l)
    let l = 0;
    // "abcabcbb"
    //   l
    //    r

    let res = 0;
    let map = new Map()
    // 右指针向右移动
    for(let r = 0; r < s.length; r++)  {
        if(map.has(s[r]) &&  map.get(s[r]) >= l) {
            l = map.get(s[r]) + 1
        }
        res = Math.max(res, r - l + 1) // 1 2 3
        map.set(s[r], r) // a:0 b:1 c:2
    }

    return res
    
};

// 边界
// "abba"
// 第一次
// r              l                res           map
// 0              0                 1            a:0
// 1              2                 2            a:0 b:1
// 2              2                 1            a:0 b:1 b:2
// 3            如果没有 map.get(s[r]) >= l 限制,就会取滑动窗口之外取值

76.最小覆盖子串 LeetCode

104.二叉树的最大深度 LeetCode

leetcode-cn.com/problems/ma…

时间复杂度 O(n)

空间复杂度O(nlogn)最好的情况,最坏的情况是O(n) //它的空间复杂度看似是O(1)实则不是,因为在函数内调用函数会形成函数调用堆栈,我们这里的dfs深度优先遍历也不例外,它也形成了堆栈,这样的空间复杂度我们值需要嵌套了多少层就可以,我们dfs嵌套的层数就是二叉树的最大深度

dfs Math.max

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    // dfs
    let res = 0;
    function dfs(n, l) {
        if(!n){return false}
        res = Math.max(res, l)
        if(n.left) {
            dfs(n.left, l+1)
        }
        if(n.right) {
            dfs(n.right, l+1)
        }
    }
    dfs(root, 1)
    return res
    
};

// 优化版本,只有在叶子结点的时候才进行最深层数的比较
var maxDepth = function(root) {
    // dfs
    let res = 0;
    function dfs(n, l) {
        if(!n){return false}
        // 优化,只有在叶子结点的时候才计算
        if(!n.left && !n.right) {
            res = Math.max(res, l)
        }
        if(n.left) {
            dfs(n.left, l+1)
        }
        if(n.right) {
            dfs(n.right, l+1)
        }
    }
    dfs(root, 1)
    return res
    
}

101.二叉树的最小深度 LeetCode

leetcode-cn.com/problems/mi…

最佳算法:广度优先遍历

/**
 * 第一次做这道题的时候,首先参考的是二叉树最大深度的思路,直接将max改成min,但是不行,因为每一次dfs都会 l从小到大 如果直接比较min,不准确
 * 最小深度的时机应该是没有左右子树的时候,因此这道题的关键点是 if(!root.left && !root.right) minArr.push(l)
 */
var minDepth = function(root) {
    if(!root) {
        return 0
    }

    let minArr = [];
    // dfs
    function dfs(root, l) {
        // 访问跟节点
        console.log(root.val)
        
        if(!root.left && !root.right) {
            minArr.push(l)
        }
        if(root.left) {
            dfs(root.left, l + 1)
        }
        if(root.right) {
            dfs(root.right, l + 1)
        }
    }

    dfs(root, 1)
    return Math.min(...minArr)

}

// 这道题是求最小深度,那么我们最佳的使用算法是应该使用广度优先遍历
// 第一次尝试做的时候bfs遍历还是很顺利的,但是在实现【记录每个节点的层级】卡壳了 那么我们该怎么做?
// 很神奇的一种操作,我们在通过递归的时候是通过 dfs(root.left, l + 1) 传递了 l + 1作为了层次,如果在队列过程中模拟该方法的实现?
// 点睛之笔: let queue = [[root]] 改写为 let queue = [[root, 1], [root, l+1]] 记录一下层级

// 时间复杂度O(n) 空间复杂度O(n)
var minDepth = function(root) {
   // 广度优先遍历
    if(!root) {
        return 0
    }
   let queue = [[root, 1]] // 点睛之笔,正常我们bfs广度遍历的时候只是 let queue = [root]

   while(queue.length > 0) {
        let [n, l] = queue.shift()
        if(!n.left && !n.right) {
            return l
        }
        if(n.left) {   
            queue.push([n.left, l + 1])
        }
        if(n.right) {
            queue.push([n.right, l + 1])
        }
    }
}

102.二叉树的层序遍历 LeetCode

leetcode-cn.com/problems/bi…

当时在做这道题的时候遇见一个逻辑实现问题:依次输出的 3,0 9,1 20,1 15,2 7,2 怎么转换为 [[3],[9,20],[15,7]]

/**
 * 队列 bfs 【记录每层-点睛之笔 queue = [[root, 1]]】
 */
var levelOrder = function(root) {
    // bfs queue = [root, l]
    if(!root) {
        return []
    }
    let arr = []
    let map = new Map()

    let queue = [[root, 1]] // 点睛之笔
    while(queue.length > 0) {
        let [n, l] = queue.shift();

				 // 依次输出的 3,0  9,1 20,1 15,2 7,2 怎么转换为 [[3],[9,20],[15,7]]
        if(!arr[l-1]) {
            arr.push([n.val])
        } else {
            arr[l-1].push(n.val)
        }
        
        n.left && queue.push([n.left, l + 1])
        n.right && queue.push([n.right,l + 1])
    }

    return arr
    
};

112.路径总和 LeetCode

leetcode-cn.com/problems/pa…


/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 * 这道题与二叉树的层序遍历比较像,那个是要记录每一层,这个要记录每个节点相加值
 */
var hasPathSum = function(root, targetSum) {
    // dfs遍历每一个节点时候附带当前节点与之前路径节点的和
    // 时间复杂度O(n) 空间复杂度O(n)
    if(!root) {
        return false
    }
    let flag = false
    // 第一步 实现dfs
    function dfs(n, s) {
        console.log(n.val, s)
        if(s === targetSum && !n.left && !n.right) {
            flag = true
        }
        n.left && dfs(n.left, s + n.left.val)
        n.right && dfs(n.right, s + n.right.val)
    }

    dfs(root, root.val)

    return flag
    

};