算法 - 数据结构

·  阅读 49

一、栈 stack

栈的概念

  1. 一个后进先出的数据结构
  2. JavaScript 没有栈,但可以用 Array 实现栈的所有功能
const stack = []
// 入栈操作 push()添加至数组最后一项
stack.push(1);
stack.push(2);
// 出栈操作 pop()移除数组最后一项,并返回
const item1 = stack.pop()
const item2 = stack.pop()
  1. 栈的应用场景
1.需要后进先出的场景
2.比如:十进制转二进制、判断字符串的括号是否有效、函数调用堆栈.......

LeetCode:20.有效的括号

题目描述:

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

解题步骤:

1.新建一个栈
2.然后扫描字符串,遇到左括号入栈,遇到和栈顶括号类型匹配的右括号就出栈,类型不匹配就直接判定不合法
3.字符串扫描完了,最后栈为空就合法,否则不合法
var isValid = function (s) {
    if (s.length % 2 === 1) { return false }
    const stack = []
    for (let i = 0; i < s.length; i++) {
        const c = s[i];
        const isLeft = c === '(' || c === '{' || c === '['
        if (isLeft) { 
            stack.push(c) 
        }else {
            const t = stack[stack.length - 1]
            const isMatch = (t === '(' && c === ')') || (t === '{' && c === '}') || (t === '[' && c === ']')
            if (isMatch) { 
                stack.pop() 
            }
            else { 
                return false 
            }
        }
    }
    return stack.length === 0
}

LeetCode:1047. 删除字符串中的所有相邻重复项

题目描述:

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,
这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",
其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"

解题步骤:

1.新建一个栈
2.然后扫描字符串,判断栈顶字符和扫描到的字符是否相等,如果相等,就出栈,否则扫描到的字符入栈
3.字符串扫描完了,在把栈转成字符串
var removeDuplicates = function (s) {
    let stack = []
    for (let i = 0; i < s.length; i++) {
        if (stack[stack.length - 1] === s[i]) {
            stack.pop()
        }
        else{
            stack.push(s[i])
        }
    }
    return stack.join('')
};

LeetCode:150. 逆波兰表达式求值

题目描述:

示例 1:

输入: ["2", "1", "+", "3", " * "]
输出: 9
解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
示例 2:

输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6
示例 3
var evalRPN = function (tokens) {
    let stack = []
    for (const item of tokens) {
        if (["+", "-", "*", "/"].includes(item) && stack.length >= 2) {
            switch (item) {
                case '+':
                    stack.push(stack.pop() + stack.pop())
                    break
                case '-':
                    stack.push(stack[stack.length - 2] - stack[stack.length - 1])
                    stack.splice(stack.length - 3, 2)
                    break
                case '*':
                    stack.push(stack.pop() * stack.pop())
                    break
                case '/':
                    stack.push(parseInt(stack[stack.length - 2] / stack[stack.length - 1]))
                    stack.splice(stack.length - 3, 2)
                    break
            }
        } else {
            stack.push(parseInt(item))
        }
    }
    return stack.pop()
};

二、队列 queue

队列概念

  1. 队列是一个先进先出的数据结构
  2. JavaScript没有队列,但可以用Array实现队列的所有功能
const queue = [];
// 入队
queue.push(1)
queue.push(2)
// 出队 shift()移除数组第一个元素并返回
const item1 = queue.shift()
const item2 = queue.shift()
// 队头元素
queue[0]
  1. 应用场景:需要先进先出的场景,食堂排队打饭、js异步中任务队列、计算最近请求次数

LeetCode:933. 最近的请求次数

题目描述:

写一个 `RecentCounter` 类来计算特定时间范围内最近的请求。
越早发出的请求就越早不在最近3000ms内的请求里,满足先进先出,用队列

输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]

解释:
RecentCounter recentCounter = new RecentCounter();
recentCounter.ping(1);     // requests = [1],范围是 [-2999,1],返回 1
recentCounter.ping(100);   // requests = [1, 100],范围是 [-2900,100],返回 2
recentCounter.ping(3001);  // requests = [1, 100, 3001],范围是 [1,3001],返回 3
recentCounter.ping(3002);  // requests = [1, 100, 3001, 3002],范围是 [2,3002],返回 3

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

LeetCode:239. 滑动窗口最大值

题目描述

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。
你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7
var maxSlidingWindow = function (nums, k) {
    let queue = [] // 存放单调队列的下标
    let result = []
    for (let i = 0; i < nums.length; i++) {
        // 在滑动窗口之外的直接从队头删掉
        if (i - queue[0] >= k) {
            queue.shift()
        }
        // 如果新加进来的数比单调队列中原来的数都要大,则直接弹出队列中的其他数
        while (nums[queue[queue.length - 1]] <= nums[i]) {
            queue.pop()
        }
        queue.push(i)
        if (i >= k - 1) {
            result.push(nums[queue[0]])
        }
    }
    return result
};

三、链表 linkedList

链表概念

  1. 多元素组成的列表
  2. 元素存储是不连续的,用next指针连在一起
{ val: 1, next: { val: 2, next: { val: 3, next: null } } }
  1. 链表和数组的区别
数组:增删非收尾元素时往往需要移动元素
链表:增删非首尾元素,不需要移动元素,只需要更改next指向即可
  1. JavaScript中没有链表这个数据结构,但是可以用object来模拟
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
}

// 插入 (将e插入c和d之间)
const e = { val: 'e' }
c.next = e
e.next = d

// 删除 (删除e)
c.next = d
console.log(a)

LeetCode:237.删除链表中的节点

题目描述

输入:head = [4,5,1,9], node = 5
输出:[4,1,9]
解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,
该链表应变为 4 -> 1 -> 9
// 解题思路:无法直接获取被删除节点的上一个节点,那就将被删除节点转移到下一个节点
// 解题步骤:将被删除节点的值改成下一个节点的值,删除下一个节点
var deleteNode = function (node) {
    node.val = node.next.val
    node.next = node.next.next
};

LeetCode:206.反转链表

题目描述:

输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
// 解题思路:如果反转两个节点:将n+1的next指向n,反转多个节点,则需要双指针遍历,重复上述操作
// 解题步骤:第一步:新建两个指针,一前一后遍历链表,第二步:反转双指针
var reverseList = function (head) {
    let p1 = head
    let p2 = null
    while (p1) {
        const tmp = p1.next
        p1.next = p2
        p2 = p1
        p1 = tmp
    }
    return p2
};

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

题目描述

输入: head = [1,1,2]
输出: [1,2]
// 解题思路:因为输入的链表是有序的,所以重复元素一定相邻;
// 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素

// 解题步骤:
// 第一步:遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素
// 第二步:遍历结束后,返回原链表的头部
var deleteDuplicates = function (head) {
    let p = head
    while (p && p.next) {
        if (p.val === p.next.val) {
            p.next = p.next.next
        }
        else {
            p = p.next
        }
    }
    return head
};

LeetCode:141. 环形链表

// 解题思路:用一快一慢两个指针遍历链表,如果指针能够相逢,那么链表有环
var hasCycle = function (head) {
    let p1 = head
    let p2 = head
    while (p1 && p2 && p2.next) {
        p1 = p1.next
        p2 = p2.next.next
        if (p1 === p2) {
            return true
        }
    }
    return false
};

LeetCode:1290.二进制链表转整数

题目描述

输入: head = [1,0,1]
输出: 5
解释: 二进制数 (101) 转化为十进制数 (5)
var getDecimalValue = function (head) {
    let p = head
    let arr = []
    // 链表转数组
    while (p) {
        arr.push(p.val)
        p = p.next
    }
    // 数组转字符串,再转十进制
    return parseInt(arr.join(''),2)
};

LeetCode:21. 合并两个有序链表

题目描述

输入: l1 = [1,2,4], l2 = [1,3,4]
输出: [1,1,2,3,4,4]
var mergeTwoLists = function (l1, l2) {
    let p1 = l1
    let p2 = l2
    let l3 = new ListNode(0)
    let p3 = l3
    if (l1 == null) { return l2 }
    if (l2 == null) { return l1 }
    while (p1 || p2) {
        if (p1 == null) {
            p3.next = p2
            return l3.next
        }
        if (p2 == null) {
            p3.next = p1
            return l3.next
        }
        if (p1.val <= p2.val) {
            let p = new ListNode(p1.val)
            p3.next = p
            p1 = p1.next
            p3 = p3.next
        } else {
            let p = new ListNode(p2.val)
            p3.next = p
            p2 = p2.next
            p3 = p3.next
        }
    }
    return l3.next
};

LeetCode:203. 移除链表元素

题目描述:

输入: head = [1,2,6,3,4,5,6], val = 6
输出: [1,2,3,4,5]
var removeElements = function (head, val) {
    let l = new ListNode(0)
    let p = head
    let p1 = l
    while (p) {
        if (p.val !== val) {
            let p2 = new ListNode(p.val)
            p1.next = p2
            p1 = p1.next
        }
        p = p.next
    }
    return l.next
};

四、集合 set

集合概念

  1. 无序唯一的数据结构
  2. ES6中有集合,名为Set
  3. 集合的常用操作:去重、判断某元素是否在集合中、求交集
// 去重
const arr = [1, 1, 2, 2]
const arr2 = [...new Set(arr)]

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

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

LeetCode:349. 两个数组的交集

var intersection = function (nums1, nums2) {
    return [...new Set(nums1)].filter(item => nums2.includes(item))
};

五、字典 map

字典概念

  1. 与集合类似,字典也是一种存储唯一值的数据结构,但是它是以键值对的形式来存储的
  2. ES6有字典,名为Map
  3. 字典的常规操作,键值对的增删改查
const m = new Map()

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

// 删
m.delete('b')
//删除所有的键值对
// m.clear()   

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

// 查
m.get('a') //true

LeetCode:20.有效的括号

var isValid = function (s) {
    if (s.length % 2 === 1) {
        return false
    }
    const stack = []
    const map = new Map()
    map.set('(', ')')
    map.set('{', '}')
    map.set('[', ']')
    for (let i = 0; i < s.length; i++) {
        const c = s[i];
        if (map.has(c)) {
            stack.push(c)
        } else {
            const t = stack[stack.length - 1]
            if (map.get(t) === c) {
                stack.pop()
            } else {
                return false
            }
        }
    }
    return stack.length === 0
}

LeetCode:1. 两数之和

var twoSum = function(nums, target) {
     const map = new Map()
    for (let i = 0; i < nums.length; i++) {
        const n = target - nums[i]
        if (map.has(n)) {
            return [map.get(n), i]
        } else {
            map.set(nums[i], i)
        }
    }
};

LeetCode:13. 罗马数字转整数

var romanToInt = function(s) {
 let num = 0
    let map = new Map([
        ['I', 1], ['V', 5], ['X', 10], ['L', 50], ['C', 100], ['D', 500], ['M', 1000],
        ['IV', 4], ['IX', 9], ['XL', 40], ['XC', 90], ['CD', 400], ['CM', 900]
    ])
    for (let i = 0; i < s.length; i++) {
        if ([...map.keys()].includes(s.slice(i, i + 2))) {
            num = num + map.get(s.slice(i, i + 2))
            i++
        }
        else {
            num = num + map.get(s[i])
        }
    }
    return num
};

六、树 tree

树概念

  1. 一种分层数据的抽象模型
  2. 前端工作中常见的树包括:DOM树、级联选择、树形控件......
  3. JS中没有树,但是可以用 Object 和 Array 构建树
  4. 树的常用操作:深度/广度优先遍历,先中后序遍历
  5. 二叉树中每个节点最多只能有两个子节点
// 二叉树
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,
        },
    },
};

module.exports = bt;
  1. 深度优先遍历:尽可能深的搜索树的分支
  2. 广度优先遍历:先访问离根节点最近的节点

深度优先遍历

// 深度优先遍历算法口诀(尽可能深的搜索树的分支)
// 1.访问根节点
// 2.对根节点的 children 挨个进行深度优先遍历
const dfs = (tree) => {
  console.log(tree.var);
  tree.children.forEach(dfs);
  // 等价于 tree.children.forEach((child) => dfs(child));
};
dfs(tree);

广度优先遍历

// 广度优先遍历算法口诀 (先访问离根节点最近的节点)
// 1.新建一个队列,把根节点入队
// 2.把队头入队并访问
// 3.把队头的children挨个入队
// 4.重复第二第三步,直到队列为空

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

二叉树先序遍历

const bt = require("./bt");

// 二叉树先序遍历算法口诀
// 1.访问根节点
// 2.对根结点的左子树进行先序遍历
// 3.对根节点的右子树进行先序遍历

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

// 非递归版

const preorder2 = (root) => {
  if (!root) {
    return;
  }
  // 模拟 js 的函数执行栈
  const stack = [root];
  while (stack.length) {
    const n = stack.pop();
    console.log(n.val);
    // 因为栈是后进先出的数据结构,所以先放right节点再放left节点
    if (n.right) stack.push(n.right);
    if (n.left) stack.push(n.left);
  }
};
preorder2(bt);

二叉树中序遍历

const bt = require("./bt");

// 二叉树中序遍历算法口诀
// 1.对根节点的左子树进行中序遍历
// 2.访问根节点
// 3.对根节点的右子树进行中序遍历

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

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

二叉树后序遍历

const bt = require("./bt");

// 二叉树后序遍历算法口诀
// 1.对根节点的左子树进行后序遍历
// 2.对根节点的右子树进行后序遍历
// 3.访问根节点

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

// 非递归版
const postordder2 = (root) => {
  if (!root) return;
  // 模拟 js 的函数执行栈
  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);
  }
};
postordder2(bt);

LeetCode:226.翻转二叉树

const flipTree = (root) => {
    const inverNode = (left, right) => {
        let temp = left;
        left = right;
        right = temp;
        //需要重新给root赋值一下
        root.left = left;
        root.right = right;
    }
    if (root === null) return root
    //确定节点处理逻辑 交换
    inverNode(root.left, root.right);
    flipTree(root.left)
    flipTree(root.right)
    return root
}

LeetCode:100. 相同的树

题目描述:

给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
var isSameTree = function (p, q) {
    if (p === null && q === null) return true
    if (p === null || q === null) return false
    return p.val === q.val && isSameTree(p.left, q.left) && isSameTree(p.right, q.right)
};

LeetCode:101. 对称二叉树

const symmetric = (queue) => {
    let arr = queue.map(item => {
        if (!item) { return null }
        else { return item.val }
    });
    for (let i = 0; i < arr.length / 2; i++) {
        if (arr[i] !== arr[arr.length - 1 - i]) {
            return false
        }
    }
    return true
}

var isSymmetric = function (root) {
    if (!root) return false
    let queue = [root]
    while (queue.length) {
        let len = queue.length
        for (let i = 0; i < len; i++) {
            let n = queue.shift()
            if (n) {
                queue.push(n.left)
                queue.push(n.right)
            }
        }
        if (!symmetric(queue)) { return false }
    }
    return true
};

LeetCode:102. 二叉树的层序遍历

题目描述:

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]] 
var levelOrder = function (root) {
    let queue = []
    let result = []
    queue.push(root)
    if (root === null) return result
    while (queue.length) {
        let len = queue.length // 当前层级点数
        let curLevel = [] // 每一层节点
        for (let i = 0; i < len; i++) {
            let n = queue.shift()
            curLevel.push(n.val)
            n.left && queue.push(n.left)
            n.right && queue.push(n.right)
        }
        result.push(curLevel)
    }
    return result
};

LeetCode:104. 二叉树的最大深度

题目描述:

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3
var maxDepth = function (root) {
    if (root === null) return 0
    let queue = [root]
    let count = 0 // 二叉树深度
    while (queue.length) {
        let len = queue.length // 当前层级节点个数
        for (let i = 0; i < len; i++) {
            let n = queue.shift()
            n.left && queue.push(n.left)
            n.right && queue.push(n.right)
        }
        count++
    }
    return count
};

LeetCode:111. 二叉树的最小深度

var minDepth = function (root) {
    if (root === null) return 0
    let queue = [root]
    let count = 1 // 二叉树的最小深度
    while (queue.length) {
        let len = queue.length // 当前层级节点个数
        for (let i = 0; i < len; i++) {
            let n = queue.shift()
            n.left && queue.push(n.left)
            n.right && queue.push(n.right)
            if (!n.left && !n.right) {
                return count
            }
        }
        count++
    }
};

LeetCode:515. 在每个树行中找最大值

题目描述:

给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。
输入: root = [1,3,2,5,3,null,9]
输出: [1,3,9]
var largestValues = function (root) {
    let queue = []
    let result = []
    if (root === null) return result
    queue.push(root)
    while (queue.length) {
        let len = queue.length // 当前层级节点个数
        let curLevelMax = null // 当前层级节点最大值
        for (let i = 0; i < len; i++) {
            let n = queue.shift()
            if (curLevelMax === null) {
                curLevelMax = n.val
            }
            else if (n.val > curLevelMax) {
                curLevelMax = n.val
            }
            n.left && queue.push(n.left)
            n.right && queue.push(n.right)
        }
        result.push(curLevelMax)
    }
    return result
};
分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改