javascript 算法之 堆 必出精品

156 阅读4分钟

1、介绍

2、js实现最小堆类

  • 首先新建一个堆类
// 新建一个最小堆类
class MinHeap {
  constructor() {
    this.heap = [];
  }
}

  • 插入堆的方法
  • 也需要理清楚 思路先
1、想写insert()方法 先把插入值value 放入到heap中
2、写上移方法  shiftUp   拿到父节点 考虑堆顶 index==0情况
3、需要拿到当前父节点位置 getParentIndex
4、需要写一个 swrap()方法 父节点 > 当前节点 两个值交换

  • 需要特别注意 构造函数 this.heap = [] 不能写成 heap()

  • 数组索引方式使用数组 应该用 heap[xxx] 中括号

  • 插入方法 需要注意 this.heap.push(value);

  • this.shiftUp(this.heap.length - 1); 这两个不能少

// 新建一个最小堆类
class MinHeap {
  constructor() {
    this.heap = [];  //特别注意
  }

  //交换方法
  swrap(i1, i2) {
    var temp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = temp;
  }
  // 拿到父节点
  getParentIndex(i) {
    // 父节点的位置 (i-1)/2
    // return Math.floor((i-1)/2)
    // 二进制右移 操作 一位操作  取商  看起来更加简洁
    return (i - 1) >> 1;
  }

  shiftUp(index) {
    // 当前位置在堆顶   不再操作
    if (index == 0) {
      return;
    }
    const parentIndex = this.getParentIndex(index);
    //对比当前节点和上一节点的值 如果当前节点小于上一节点 交换
    if (this.heap[parentIndex] > this.heap[index]) {
      //执行swrap 交换函数
      this.swrap(parentIndex, index);
      //持续执行上移操作   由于当前已经交换过了 所以传入 parentIndex
      this.shiftUp(parentIndex);
    }
  }

  // 插入方法
  insert(value) {
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1);
    console.log(this.heap);
  }
}

// 测试
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);

  • 我们可以使用 nodejs查看测试结果

  • 这样 后一个 确保了 顶点节点是最小的节点

  • 插入节点操作完成

4、删除堆顶

  • 先理清一下 思路 避免比较懵逼
1、新建pop()方法
2、新建shiftDown 下移方法
3、得到左右节点 getLeftIndex  getRightIndex
4、比较左节点和当前节点值及右节点与当前节点值
// 新建一个最小堆类
class MinHeap {
  constructor() {
    this.heap = [];
  }

  //交换方法
  swrap(i1, i2) {
    var temp = this.heap[i1];
    this.heap[i1] = this.heap[i2];
    this.heap[i2] = temp;
  }
  // 拿到父节点
  getParentIndex(i) {
    // 父节点的位置 (i-1)/2
    // return Math.floor((i-1)/2)
    // 二进制右移 操作 一位操作  取商  看起来更加简洁
    return (i - 1) >> 1;
  }

  // 获取左节点
  getLeftIndex(i) {
    return 2 * i + 1;
  }
  //获取右节点
  getRightIndex(i) {
    return 2 * i + 2;
  }
  shiftUp(index) {
    // 当前位置在堆顶   不再操作
    if (index == 0) {
      return;
    }
    const parentIndex = this.getParentIndex(index);
    //对比当前节点和上一节点的值 如果当前节点小于上一节点 交换
    if (this.heap[parentIndex] > this.heap[index]) {
      //执行swrap 交换函数
      this.swrap(parentIndex, index);
      //持续执行上移操作   由于当前已经交换过了 所以传入 parentIndex
      this.shiftUp(parentIndex);
    }
  }

  //定义 下移函数
  shiftDown(index) {
    // 获取左右节点
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    //比较左节点和当前节点值及右节点与当前节点值
    if (this.heap[leftIndex] < this.heap[index]) {
      this.swrap(leftIndex, index);
      this.shiftDown(leftIndex);
    }
    if (this.heap[rightIndex] < this.heap[index]) {
      this.swrap(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); //执行下移 函数
    console.log(this.heap);
  }
}

// test insert()
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1);

//test pop()
h.pop();

  • 结果

5、获取 堆顶和 堆的大小

  • 这个地方很清楚明确 只展示 部分代码
// 获取堆顶 获取数组头部 
  peek(){
    return this.heap[0]

  }

  //获取堆的大小 返回数组的长度
  size(){
    return this.heap.length

  }
  • 结果 比较详细的展示出来

6、leetcode 215 数组中第K个最大元素

  • 来来来 上面的逻辑比较简单 我们理清楚后实战一下吧
//当然 这前面省略了 最小堆构建的过程
// 时间复杂度 O(nlogk)  空间复杂度主要是堆 O(k)
var findKthLargest = function(nums, k) {
    // 实例化最小堆
    const h = new MinHeap()
    //循环遍历并将数组值插入堆中
    nums.forEach(n=>{
        h.insert(n)
        //如果容量大于k 删除堆顶
        if(h.size() > k){
            h.pop()
        }
    })
    // 返回堆顶
    return h.peek()
};

7 、leetcode 317 前k个高频元素

  • 当然 此处代码 省略了 构建 MinHeap
/*
 * 时间复杂度 O(nlogk) 空间复杂度 O(n)  因为字典可能是O(n) 堆是O(k) 两者相加为O(n)
 */
var topKFrequent = function(nums, k) {
    // 1、新建字典
    const map =new Map()
    // 2、统计每个元素出现的频率
    nums.forEach(n=>{
        map.set(n,map.has(n) ? map.get(n)+1:1)
    })
    console.log(map)  //Map(3) { 1 => 3, 2 => 2, 3 => 1 }

    // 3、实例化最小堆
    const h = new MinHeap()
    //遍历堆数值和次数
    map.forEach((value,key)=>{
        h.insert({value,key})
        //当容量大于k 删除堆顶
        if(h.size()>k){
            h.pop()
        }
    })
   return h.heap.map(a=>a.key)
};
  • 需要注意 由于 下面 插入堆中的结构发生了 变化 相应的 堆也要变化
  • 也要确保 类型 这样的this.heap[parentIndex] 有值

8、LeetCode:23. 合并K个升序链表

  • 改造一下最小堆
  • 当然 此处代码 省略了 构建 MinHeap
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 * 时间复杂度 O(nlogk) 循环有内层循环 pop()和insert()
 * 空间复杂度 O(k) k表示链表
 */
var mergeKLists = function(lists) {
    // 1、新建链表
    const res = new ListNode(0)
    // 2、实例化最小堆
    const h =  new MinHeap()
    // 3、新建一个指针 指向链表
    let p = res 
    //  循环遍历 将链表放入堆中
    lists.forEach(l =>{
        if(l) h.insert(l)
    })
    // 堆里有值时  循环
    while(h.size()){
        // 弹出堆顶 
        const n = h.pop()
        // 将弹出的堆顶接到 输出链表
        p.next = n
        //指针向下移动
        p = p.next
        // 放入堆中
    if(n.next) h.insert(n.next)
    }

    return res.next
};

9、总结一下