前端数据结构总结

224 阅读11分钟

前端数据结构总结

栈是什么

  • 一种后进先出的数据结构
  • js 没有栈数据结构,但是可以用 Array 实现
  • push(入栈)、pop(出栈)、stack[stack.length - 1](栈顶元素)
const stack = [];
stack.push(1);
stack.push(2);
const item1 = stack.pop();
const item2 = stack.pop();

栈使用场景

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

一、十进制转二进制

  • 后出的余数反而要排到前面
  • 把余数依次入栈再出栈,就可以得到二进制结果

十进制转二进制.png

二、括号闭合

  • 左括号入栈,遇到右括号出栈
  • 最后栈空则合法
(((((()))))) // Valid
()()()() // Valid
((((((()) // Invalid
(()((()))) // Valid

三、函数调用堆栈

  • 最后调用的函数最先执行完
  • JS 解释器用栈控制函数的调用顺序
function foo1() {
  ...
  foo2();
  ...
}
function foo2() {
  console.log(1);
}

foo1();

leetcode 相关题目

leetcode-20.有效括号

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
    // 长度奇数直接返回false
    if (s.length % 2 !== 0) return false;
    const stack = [];
    const m = new Map();
    m.set('(', ')');
    m.set('[', ']');
    m.set('{', '}');
    // 开头右括号直接返回false
    if (!m.has(s[0])) return false;
    // 循环
    for (let i = 0; i < s.length; i++) {
        if (m.has(s[i])) {
            stack.push(s[i]);
        } else {
            const top = stack[stack.length - 1];
            if (m.get(top) === s[i]) {
                stack.pop();
            } else {
                return false;
            }
        }
    }
    return stack.length === 0;
};

leetcode-144.二叉树前序遍历

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function (root) {
    const res = [];
    const stack = [];
    if (root) stack.push(root);
    while (stack.length) {
        const top = stack.pop();
        res.push(top.val);
        if (top.right) stack.push(top.right);
        if (top.left) stack.push(top.left);
    }
    return res;
};

队列

队列是什么

  • 一种先进先出的数据结构
  • js 没有栈数据结构,但是可以用 Array 实现
  • push(入队)、shift(出队)、queue[0](队列第一个顶元素)
const queue = [];
queue.push(1);
queue.push(2);
const item1 = queue.shift();
const item2 = queue.shift();

队列使用场景

  • 需要先进先出的场景
  • 比如:食堂排队打饭、JS 异步中的任务队列、计算最近请求次数

一、JS 异步中的任务队列

  • JS 单线程,无法处理异步中的并发任务
  • 使用任务队列先后处理异步任务

leetcode 相关题目

leetcode-933.最近请求次数

/** 
 * @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;
};

链表

链表是什么

  • 多个元素组成的列表
  • 元素储存不连续,用 next 指针连接在一起
  • js 中没有链表,可以用 Object 实现

数组 vs 链表

  • 数组:增删非首尾元素时往往需要移动元素
  • 比如:增删非首尾元素时,只需修改 next 指向即可,不需要移动元素
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);
  p = p.next;
}
// 插入
const e = { val: 'e' };
c.next = e;
e.next = d;
// 删除
c.next = d; // e 在链表中被删除

JS 中的原型链与链表

  • 原型链本质是链表结构
  • 原型链上的节点是各种原型对象,如 Object.prototype 等
  • 原型链通过 __proto__ 属性连接各原型对象
// 手写 instanceof
const instanceof = (a, b) => {
  let p = a;
  while (p) {
    if (p.__proto__ === b.prototype) {
      return true;
    }
    p = p.__proto__;
  }
  return false;
}
const o = {};
const foo = () => {};
Object.prototype.a = 'a';
Function.prototype.b = 'b';

console.log(o.a); // 'a'
console.log(o.b); // undefined

console.log(foo.a); // 'a'
console.log(foo.b); // 'b'

JS 使用链表指针获取 json 节点值

const json = {
  a: { b: { c: 1 } },
  d: { e: 2 }
};
const path = ['a', 'b', 'c'];

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

leetcode 相关题目

leetcode-237.删除链表中的节点

/**
 * @param {ListNode} node
 * @return {void} Do not return anything, modify node in-place instead.
 */
var deleteNode = function (node) {
    node.val = node.next.val;
    node.next = node.next.next;
};

leetcode-206.反转链表

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
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-2.两数相加

/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function (l1, l2) {
    const l3 = new ListNode();
    let p1 = l1;
    let p2 = l2;
    let p3 = l3;
    let carry = 0;
    while (p1 || p2) {
        const v1 = p1 ? p1.val : 0;
        const v2 = p2 ? p2.val : 0;
        const sum = v1 + v2 + carry;
        carry = Math.floor(sum / 10);
        p3.next = new ListNode(sum % 10);
        if (p1) p1 = p1.next;
        if (p2) p2 = p2.next;
        p3 = p3.next;
    }
    if (carry) {
        p3.next = new ListNode(carry);
    }
    return l3.next
};

leetcode-83.删除排序链表中的重复元素

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
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.环形链表

/**
 * @param {ListNode} head
 * @return {boolean}
 */
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-234.回文链表

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var isPalindrome = function (head) {
    let fp = head;
    let sp = head;
    let reverse = null;
    while (fp && fp.next && sp) {
        // 快慢指针 + 翻转链表
        fp = fp.next.next;
        const tmp = sp.next;
        sp.next = reverse;
        reverse = sp;
        sp = tmp;
    }
    if (fp) {
        // 节点个数为奇数,sp 需再走一步
        sp = sp.next;
    }
    while (sp && reverse) {
        if (sp.val !== reverse.val) {
            return false;
        }
        sp = sp.next;
        reverse = reverse.next;
    }
    return true;
};

集合

集合是什么

  • 一种无序且唯一的数据结构
  • ES6 中有集合,Set
  • 常用操作:去重、判断是否在集合中、求交集
// 去重
const arr = [1, 1, 2, 2];
const arr2 = [...new Set(arr)];
// 判断是否在集合中
const set = new Set(arr);
set.has(2); // true
// 求交集
const set2 = new Set([2, 3]);
const set3 = new Set([...set1].filter(item => set2.has(item)));

// add、delete、has、迭代
set.add(2);
set.delete(2);
set.has(2);
set.forEach(val => console.log(val));

// set -> array 
const arr1 = [...set1];
const arr2 = Array.from(set2);
// array -> set
const set1 = new Set(arr1);

leetcode 相关题目

leetcode-349.两个数组的交集

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    // 求交集,无序且唯一,使用集合
    // 时间复杂度 O(n2)
    // const set1 = new Set(nums1);
    // const set2 = new Set(nums2);
    // const set3 = new Set([...set1].filter(item => set2.has(item)));
    // return [...set3];

    // 用字典
    // 时间复杂度 O(2n)
    const m = new Map();
    const res = [];
    nums1.forEach(item => {
        m.set(item, true);
    })
    nums2.forEach(item => {
        if (m.get(item)) {
            res.push(item);
            m.delete(item);
        }
    })
    return res;
};

字典

字典是什么

  • 与集合类似,字典也是存储唯一值的数据结构,但他是以键值对的形式来存储
  • ES6 中有字典,Map
  • 常用操作:键值对增删查改
const m = new Map();
m.set('key', 'value');
m.get('key');
m.has('key');
m.delete('key');
m.clear(); // 删除所有键值对

leetcode 相关题目

leetcode-349.两个数组的交集(用字典)

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number[]}
 */
var intersection = function (nums1, nums2) {
    // 求交集,无序且唯一,使用集合
    // 时间复杂度 O(n2)
    // const set1 = new Set(nums1);
    // const set2 = new Set(nums2);
    // const set3 = new Set([...set1].filter(item => set2.has(item)));
    // return [...set3];

    // 用字典
    // 时间复杂度 O(2n)
    const m = new Map();
    const res = [];
    nums1.forEach(item => {
        m.set(item, true);
    })
    nums2.forEach(item => {
        if (m.get(item)) {
            res.push(item);
            m.delete(item);
        }
    })
    return res;
};

leetcode-20.有效括号(用栈 + 字典)

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
    // 长度奇数直接返回false
    if (s.length % 2 !== 0) return false;
    const stack = [];
    const m = new Map();
    m.set('(', ')');
    m.set('[', ']');
    m.set('{', '}');
    // 开头右括号直接返回false
    if (!m.has(s[0])) return false;
    // 循环
    for (let i = 0; i < s.length; i++) {
        if (m.has(s[i])) {
            stack.push(s[i]);
        } else {
            const top = stack[stack.length - 1];
            if (m.get(top) === s[i]) {
                stack.pop();
            } else {
                return false;
            }
        }
    }
    return stack.length === 0;
};

leetcode-1.两数之和

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var twoSum = function (nums, target) {
    const m = new Map();
    for (let i = 0; i < nums.length; i++) {
        const n = nums[i];
        const t = target - nums[i];
        if (m.has(t)) {
            return [m.get(t), i];
        }
        m.set(n, i);
    }
    return [];
};

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

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function (s) {
    // 左右指针
    let l = 0;
    let maxLen = 0;
    const m = new Map();
    for (let r = 0; r < s.length; r++) {
        if (m.has(s[r]) && m.get(s[r]) >= l) {
            l = m.get(s[r]) + 1;
        }
        maxLen = Math.max(maxLen, r - l + 1);
        m.set(s[r], r);
    }
    return maxLen;
};

leetcode-76.最小覆盖子串

/**
 * @param {string} s
 * @param {string} t
 * @return {string}
 */
var minWindow = function (s, t) {
    let l = 0;
    let r = 0;
    const need = new Map();
    let res = '';
    for (let i = 0; i < t.length; i++) {
        need.set(t[i], need.has(t[i]) ? need.get(t[i]) + 1 : 1);
    }
    let needType = need.size;
    while (r < s.length) {
        if (need.has(s[r])) {
            need.set(s[r], need.get(s[r]) - 1);
            if (need.get(s[r]) === 0) needType--;
        }
        while (needType === 0) {
            const newRes = s.substr(l, r - l + 1);
            if (!res || res.length > newRes.length) res = newRes;
            if (need.has(s[l])) {
                need.set(s[l], need.get(s[l]) + 1);
                if (need.get(s[l]) === 1) needType++;
            }
            l++;
        }
        r++;
    }
    return res;
};

树是什么

  • 一种分层数据的抽象模型
  • js 中相关的树:DOM 树、树形组件
  • js 中没有树数据结构,可以用 Array 和 Object 实现
  • 树的常用操作:深度、广度优先遍历,先中后序遍历

深度、广度优先遍历

一、深度优先遍历

  • 尽可能深地搜索树的分支
  • 算法步骤:
    1. 访问根节点
    2. 对根节点的 children 依次深度优先遍历

深度优先遍历.png

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

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

  dfs(tree);

二、广度优先遍历

  • 先访问离根节点最近的节点
  • 算法步骤:
    1. 新建一个队列,把根节点入队
    2. 把队头出队,并访问
    3. 把队头的 children 依次入队
    4. 重复 2、3,直到队列为空

广度优先遍历.png

const bfs = (root) => {
  const q = [root];
  while (q.length) {
    const top = q.shift();
    console.log(top);
    top.children.forEach(item => q.push(item))
  }
}

bfs(tree);

二叉树先中后序遍历

一、先序遍历

  • 算法步骤:
    1. 访问根节点
    2. 对根节点的左子树先序遍历
    3. 对根节点的右子树先序遍历

二叉树先序遍历.png

const binaryTree = {
  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 },
  }
}

// 递归实现
const preorder = (root) => {
  if (!root) return;
  console.log(root);
  preorder(root.left);
  preorder(root.right);
}

preorder(binaryTree);

// 栈实现
const stackPreorder = (root) => {
  if (!root) return;
  const stack = [root];
  while (stack.length) {
    const top = stack.pop();
    console.log(top);
    if (top.right) stack.push(top.right);
    if (top.left) stack.push(top.left);
  }
}

stackPreorder(binaryTree);

二、中序遍历

  • 算法步骤:
    1. 对根节点的左子树中序遍历
    2. 访问根节点
    3. 对根节点的右子树中序遍历

二叉树中序遍历.png

// 递归实现
const inorder = (root) => {
  if (!root) return;
  inorder(root.left);
  console.log(root);
  inorder(root.right);
}

inorder(binaryTree);

// 栈实现
const stackInorder = (root) => {
  if (!root) return;
  const stack = [];
  let p = root;
  while (stack.length || p) {
    while (p) {
      stack.push(p);
      p = p.left;
    }
    const top = stack.pop();
    console.log(top);
    p = top.right;
  }
}

stackInorder(binaryTree);

三、后序遍历

  • 算法步骤:
    1. 对根节点的左子树后序遍历
    2. 对根节点的右子树后序遍历
    3. 访问根节点

二叉树后序遍历.png

// 递归实现
const postorder = (root) => {
  if (!root) return;
  postorder(root.left);
  postorder(root.right);
  console.log(root);
}

postorder(binaryTree);

// 栈实现
const stackPostorder = (root) => {
  if (!root) return;
  const stack = [root];
  const outputStack = [];
  while (stack.length) {
    const top = stack.pop();
    outputStack.push(top);
    if (top.left) stack.push(top.left);
    if (top.right) stack.push(top.right);
  }
  while (outputStack.length) {
    const output = outputStack.pop();
    console.log(output);
  }
}

stackPostorder(binaryTree);

前端遍历 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, []);

leetcode 相关题目

leetcode-104.二叉树最大深度

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function (root) {
    // 深度优先遍历
    let maxLevel = 0;
    const dfs = (n, l) => {
        if (!n) return;
        if (!n.left && !n.right) {
            maxLevel = Math.max(maxLevel, l);
        }
        dfs(n.left, l + 1);
        dfs(n.right, l + 1);
    }
    dfs(root, 1);
    return maxLevel;
};

leetcode-111.二叉树最小深度

/**
 * @param {TreeNode} root
 * @return {number}
 */
var minDepth = function (root) {
    // 广度优先遍历更快
    if (!root) return 0;
    const q = [[root, 1]];
    while (q.length) {
        const [top, l] = q.shift();
        if (!top.left && !top.right) {
            return l;
        }
        if (top.left) q.push([top.left, l + 1]);
        if (top.right) q.push([top.right, l + 1]);
    }
};

leetcode-102.二叉树的层序遍历

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
    // 广度优先遍历
    if (!root) return [];
    const q = [root];
    const res = [];
    while (q.length) {
        let len = q.length;
        res.push([]);
        while (len--) {
            const top = q.shift();
            res[res.length - 1].push(top.val);
            if (top.left) q.push(top.left);
            if (top.right) q.push(top.right);
        }
    }
    return res;
};

leetcode-94.二叉树的中序遍历

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
    if (!root) return [];
    const res = [];
    // 栈实现
    const stack = [];
    let p = root;
    while (stack.length || p) {
        while (p) {
            stack.push(p);
            p = p.left;
        }
        const top = stack.pop();
        res.push(top.val);
        p = top.right;
    }
    return res;
};

leetcode-112.路径总和

/**
 * @param {TreeNode} root
 * @param {number} targetSum
 * @return {boolean}
 */
var hasPathSum = function (root, targetSum) {
    // 深度优先遍历
    if (!root) return false;
    let res = false;
    const dfs = (n, sum) => {
        if (!n) return;
        sum += n.val;
        if (!n.left && !n.right && sum === targetSum) {
            // 叶子节点
            res = true;
        }
        dfs(n.left, sum);
        dfs(n.right, sum);
    }
    dfs(root, 0);
    return res;
};

图是什么

  • 图是网络结构的抽象模型,是一组由边连接的节点
  • 图可以表示任意二元关系,如路线、航班
  • js 中没有图数据结构,可以用 Array 和 Object 构建
  • 图的表示法:领接矩阵、领接表等

图的深度、广度优先遍历

一、图的深度优先遍历

  • 算法步骤:
    1. 访问根节点
    2. 对根节点的没有访问过的相邻节点挨个进行深度优先遍历
const graph = {
  0: [1, 2],
  1: [2],
  2: [0, 3],
  3: [3]
}

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

二、图的广度优先遍历

  • 新建队列,根节点入队
  • 把队头出队并访问
  • 把队头没访问过的相邻节点入队
  • 重复 2、3 步骤,直到队列为空
const visited = new Set();
const q = [2];
visited.add(2);
while (q.length) {
  const top = q.shift();
  console.log(top);
  graph[top].forEach(item => {
    if (!visited.has(item)) {
      q.push(item);
      visited.add(top);
    }
  })
}

leetcode 相关题目

leetcode-65.有效数字

有效数字图构建:

有效数字图构建.png

/**
 * @param {string} s
 * @return {boolean}
 */
var isNumber = function (s) {
    // 构建图(领接表)
    // blank 表示空格
    // sign 表示 ‘+-’
    // e 表示 e
    // digit 表示数字
    // . 表示 .
    // 最终状态为 3、5、6 时,返回 true
    const graph = {
        0: { 'digit': 6, 'blank': 0, 'sign': 1, '.': 2 },
        1: { 'digit': 6, '.': 2 },
        2: { 'digit': 3 },
        3: { 'digit': 3, 'e': 4 },
        4: { 'digit': 5, 'sign': 7 },
        5: { 'digit': 5 },
        6: { 'digit': 6, '.': 3, 'e': 4 },
        7: { 'digit': 5 }
    };
    let state = 0;
    for (let i = 0; i < s.length; i++) {
        let c = s[i];
        if (c <= '9' && c >= '0') {
            c = 'digit';
        } else if (c === '+' || c === '-') {
            c = 'sign';
        } else if (c === ' ') {
            c = 'blank';
        } else {
            c = c.toLocaleLowerCase();
        }
        if (!graph[state][c]) return false;
        state = graph[state][c];
    }
    return state === 3 || state === 5 || state === 6
};

leetcode-417.太平洋大西洋水流问题

/**
 * @param {number[][]} heights
 * @return {number[][]}
 */
var pacificAtlantic = function (heights) {
    // 图深度优先遍历
    // 分别记录从海岸线开始逆流而上能访问到的节点
    // 输出既能两个矩阵中都为 true 的节点
    if (!heights || !heights[0]) return false
    const m = heights.length;
    const n = heights[0].length;
    // 构建二维数组
    const flow1 = Array.from({ length: m }, () => new Array(n).fill(false));
    const flow2 = Array.from({ length: m }, () => new Array(n).fill(false));
    // 深度优先遍历
    const dfs = (r, c, flow) => {
        flow[r][c] = true;
        // 对满足条件的上下左右节点递归遍历
        [[r - 1, c], [r + 1, c], [r, c - 1], [r, c + 1]].forEach(([nr, nc]) => {
            if (
                // 保证这个节点在矩阵内
                nr < m && nc < n && nr >= 0 && nc >= 0 &&
                // 保证逆流而上
                heights[nr][nc] >= heights[r][c] &&
                // 保证这个节点没有被访问过
                !flow[nr][nc]
            ) {
                dfs(nr, nc, flow);
            }
        })
    }
    for (let r = 0; r < m; r++) {
        // 第一列(太平洋海岸线)
        dfs(r, 0, flow1);
        // 最后一列(大西洋海岸线)
        dfs(r, n - 1, flow2);
    }
    for (let c = 0; c < n; c++) {
        // 第一行(太平洋海岸线)
        dfs(0, c, flow1);
        // 最后一行(大西洋海岸线)
        dfs(m - 1, c, flow2);
    }
    // 输出
    const res = [];
    for (let r = 0; r < m; r++) {
        for (let c = 0; c < n; c++) {
            if (flow1[r][c] && flow2[r][c]) {
                res.push([r, c]);
            }
        }
    }
    return res;
};

leetcode-133.克隆图

/**
 * @param {Node} node
 * @return {Node}
 */
var cloneGraph = function (node) {
    // 拷贝所有节点
    // 拷贝所有边
    if (!node) return;
    // 深度优先遍历
    // const visited = new Map();
    // const dfs = (n) => {
    //     const nCopy = new Node(n.val);
    //     visited.set(n, nCopy);
    //     (n.neighbors || []).forEach(ne => {
    //         if (!visited.has(ne)) {
    //             dfs(ne);
    //         }
    //         nCopy.neighbors.push(visited.get(ne));
    //     })
    // }
    // dfs(node);
    // return visited.get(node);

    // 广度优先遍历
    const visited = new Map();
    visited.set(node, new Node(node.val));
    const q = [node];
    while (q.length) {
        const top = q.shift();
        (top.neighbors || []).forEach(ne => {
            if (!visited.has(ne)) {
                q.push(ne);
                visited.set(ne, new Node(ne.val));
            }
            visited.get(top).neighbors.push(visited.get(ne));
        })
    }
    return visited.get(node);
};

堆是什么

  • 堆是一种特殊的完全二叉树
  • 所有节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
  • js 中通常用数组表示堆
  • 左侧子节点的位置是 2*index+1
  • 右侧子节点的位置是 2*index+2
  • 父节点的位置是 (index-1)/2

js用数组表示堆.png

堆的应用场景

  • 高效、快速找出最大值或最小值 时间复杂度:O(1)
  • 找出第 K 个最大(小)元素
  • 找出第 K 个最大元素算法步骤:
    1. 构建一个最小堆(堆顶为最小元素)
    2. 当容量超过 K,删除堆顶元素
    3. 插入结束后,堆顶就是第 K 个最大元素

js 实现最小堆类

class MinHeap {
  constructor() {
    this.heap = [];
  }

  // 插入
  // 时间复杂度:O(logK)
  insert(value) {
    // 1. 插入节点到堆尾
    // 2. 上移,将该节点与父节点交换,直到该节点大于父节点
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
  }

  // 删除堆顶
  // 时间复杂度:O(logK)
  pop() {
    // 1. 用数组尾部节点替换堆顶(直接删除第一个节点会导致每个节点前移,破坏了整个堆结构)
    // 2. 下移,将新堆顶与子节点交换,直到子节点大于这个新的堆顶
    if (this.size === 1) return this.heap.shift();
    const top = this.heap[0];
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
    return top;
  }

  // 上移
  shiftUp(index) {
    if (index === 0) return;
    const parentIndex = this.getParentIndex(index);
    if (this.heap[index] < this.heap[parentIndex]) {
      // 若当前节点值小于父节点的值,则需要交换该节点于父节点的位置
      this.swap(index, parentIndex);
      // 递归调用,直到当前节点值大于等于父节点的值
      // 因当前节点于父节点下标已完成交换,所以参数为 parentIndex
      this.shiftUp(parentIndex);
    }
  }
  
  // 下移
  shiftDown(index) {
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    if (this.heap[leftIndex] < this.heap[index]) {
      this.swap(index, leftIndex);
      this.shiftDown(leftIndex);
    }
    if (this.heap[rightIndex] < this.heap[index]) {
      this.swap(index, rightIndex);
      this.shiftDown(rightIndex);
    }
  }

  // 获取父节点的 index
  getParentIndex(index) {
    // 父节点 index 为当前节点 (index - 1) / 2
    // >> 1,表示二进制运算:右移一位
    return (index - 1) >> 1;
    // 完全等价于:
    // return Math.floor((index - 1) / 2);
  }

  // 获取左侧子节点
  getLeftIndex(index) {
    return 2 * index + 1;
  }

  // 获取右侧子节点
  getRightIndex(index) {
    return 2 * index + 2;
  }

  // 交换节点位置
  swap(i1, i2) {
    const tmp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = tmp;
  }

  // 获取堆顶
  peek() {
    return this.heap[0];
  }

  // 获取堆大小
  size() {
    return this.heap.length;
  }
}

const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);
h.heap // [1, 3, 2]
h.pop();
h.heap // [2, 3]
h.peek(); // 3
h.size(); // 2

leetcode 相关题目

leetcode-215.数组中第K个最大元素

class MinHeap {
  ...
  // 实现最小堆类
  ...
}
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function (nums, k) {
    const h = new MinHeap();
    nums.forEach(item => {
        h.insert(item);
        if (h.size() > k) {
            h.pop();
        }
    })
    return h.peek();
};

leetcode-347.前K个高频元素

class MinHeap {
  ...
  // 实现最小堆类
  ...
}
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function (nums, k) {
    const m = new Map();
    nums.forEach(item => {
        m.set(item, m.has(item) ? m.get(item) + 1 : 1);
    })
    // js 原生排序,时间复杂度O(n log n)
    // const list = Array.from(m).sort((a, b) => b[1] - a[1]).slice(0, k);
    // return list.map(item => item[0]);

    // 最小堆实现,时间复杂度O(log n)
    const h = new MinHeap();
    m.forEach((value, key) => {
        h.insert({ value, key });
        if (h.size() > k) {
            h.pop();
        }
    })
    return h.heap.map(item => item.key)
};

leetcode-23.合并K个升序链表

class MinHeap {
  ...
  // 实现最小堆类
  ...
}
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
var mergeKLists = function (lists) {
    // 1. 构建最小堆,并把每个链表头插入堆中
    // 2. 弹出堆顶,接入输出链表,并将弹出堆顶所在链表的新链表头插入堆中
    // 3. 堆中元素弹出完,则完成合并
    let res = new ListNode();
    let p = res;
    const h = new MinHeap();
    lists.forEach(list => {
        if (!list) return;
        h.insert(list);
    })
    while (h.size()) {
        const top = h.pop();
        p.next = top;
        p = p.next;
        if (top.next) h.insert(top.next);
    }
    return res.next;
};