【前端算法系列】堆

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

看到“第K个最大元素”,就用最小堆
思路:构建一个最小堆,并依次把数组的值插入堆中。当堆的容量超过k,就删除堆顶。插入结束后,堆顶就是第K个最大元素
可以想象是一个小公司,堆顶是最弱的员工,当发现超出人数了,就要裁员,经过优胜劣汰,不断去淘汰堆顶

/** 时间复杂度:O(N*logK)  循环嵌套循环   k为堆的大小 N为nums的长度(循环次数)
 *  空间复杂度:O(k) k为堆的长度
 */
var findKthLargest = function(nums, k) {
    const h = new MinHeap()
    nums.forEach(n=>{
        h.insert(n)
        if(h.size()>k){ // 超过人数,开始裁员
            h.pop()
        }
    })
    return h.peek()
};

347. 前 K 个高频元素

// 时间复杂度:for循环O(n)、sort=>排序算法最快也是O(n*logN)),取较大的就是O(n*logN))
var topKFrequent = function(nums, k) {
    // 设置映射关系
    const map = new Map()
    nums.forEach(n=>{
        map.set(n, map.has(n) ? map.get(n)+1 : 1) // 如果上次已经出现过这个频率,就拿它的频率数+1
    })
    console.log()
    // 对频率进行排序,将map转数组,降序排序(b的频率-a的频率)
   const list = Array.from(map).sort((a, b)=> b[1] - a[1] )
    return list.slice(0, k).map(n=>n[0])
};

优化:数组中没必要对所有元素进行排序,可以利用堆拿到前K个高频元素再排序
建一个最小堆,把元素和频率插入到最小堆,并按频率进行排序,而且最小堆大小永远维持在K

// 最小堆:堆顶最小,都是大于等于当前值
class MinHeap{
  constructor(){
    this.heap = []
  }
  // 父结点索引
  getParentIndex(i){
    // return Math.floor((i-1)/2)
    return (i-1) >> 1 // 二进制往右边移一位
  }
  // 左侧子节点
  getLeftIndex(i){
    return i * 2 + 1
  }
  // 右侧子节点
  getRightIndex(i){
    return i * 2 + 2
  }
  // 上浮:父结点值大于子节点值就要进行交换,直到父节点小于等于插入的值
  swim(index){
    if(index===0) return // 如果堆顶,就不上浮
    const parentIndex = this.getParentIndex(index)
    if(this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value){
      this.swap(parentIndex, index)
      this.swim(parentIndex) // 不断上浮
    }
  }
  // 下沉:将最后一个元素跟堆顶交换,并不断拿当前节点a[k]和它的子节点a[2k]、a[2k+1]中较小者交换位置
  sink(index){
    const leftIndex = this.getLeftIndex(index)
    const rightIndex = this.getRightIndex(index)
    // 左侧子节点值小于当前节点值,进行交换
    if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value){
      this.swap(leftIndex, index) 
      this.sink(leftIndex) // 继续执行下沉,直到它找到适合位置
    }
    // 右侧子节点值小于当前节点值,进行交换
    if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value){
      this.swap(rightIndex, index) 
      this.sink(rightIndex) // 继续执行下沉,直到它找到适合位置
    }
  }
  // 交换
  swap(i1, i2){
    const temp = this.heap[i1]
    this.heap[i1] = this.heap[i2]
    this.heap[i2]  = temp
  }
  // 插入:堆也是数组,所以是数组尾部
  insert(value){
    this.heap.push(value)
    this.swim(this.heap.length-1)
  }
  // 删除堆顶
  pop(){
    // 把数组的最后一位转移到头部
    this.heap[0] = this.heap.pop()
    this.sink(0)
  }
  // 获取堆顶:返回数组头部
  peek(){
    return this.heap[0]
  }
  // 获取堆大小:返回数组长度
  size(){
    return this.heap.length
  }
}

/** 时间复杂度:O(n*logK)) 最小堆的操作为log  n为map的长度  k小于等于数组的长度
 *  空间复杂度:O(n) map长度为数组长度 
 */
var topKFrequent = function(nums, k) {
    // 设置映射关系
    const map = new Map()
    nums.forEach(n=>{
        map.set(n, map.has(n) ? map.get(n)+1 : 1) // 如果上次已经出现过这个频率,就拿它的频率数+1
    })
    const h = new MinHeap()
    map.forEach((value, key)=>{
        h.insert({value, key})
        // 超过K个就删出栈顶,不断剔除最小的元素,保障堆只有前k个高频元素
        if(h.size()>k){
            h.pop()
        }
    })
    return h.heap.map(a=>a.key)
};

23. 合并K个升序链表

想拿到最小值的,可以考虑用最小堆(数组的话要遍历全部,不适合)

新链表的下一个节点一定是k个链表头中的最小节点

1)构建一个最小堆,并依次把链表头插入堆中
2)弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
3)等堆元素全部弹出,合并工作就完成了

// 最小堆:堆顶最小,都是大于等于当前值
class MinHeap{
  constructor(){
    this.heap = []
  }
  // 父结点索引
  getParentIndex(i){
    // return Math.floor((i-1)/2)
    return (i-1) >> 1 // 二进制往右边移一位
  }
  // 左侧子节点
  getLeftIndex(i){
    return i * 2 + 1
  }
  // 右侧子节点
  getRightIndex(i){
    return i * 2 + 2
  }
  // 上浮:父结点值大于子节点值就要进行交换,直到父节点小于等于插入的值
  swim(index){
    if(index===0) return // 如果堆顶,就不上浮
    const parentIndex = this.getParentIndex(index)
    if(this.heap[parentIndex] && this.heap[parentIndex].val > this.heap[index].val){
      this.swap(parentIndex, index)
      this.swim(parentIndex) // 不断上浮
    }
  }
  // 下沉:将最后一个元素跟堆顶交换,并不断拿当前节点a[k]和它的子节点a[2k]、a[2k+1]中较小者交换位置
  sink(index){
    const leftIndex = this.getLeftIndex(index)
    const rightIndex = this.getRightIndex(index)
    // 左侧子节点值小于当前节点值,进行交换
    if(this.heap[leftIndex] && this.heap[leftIndex].val < this.heap[index].val){
      this.swap(leftIndex, index) 
      this.sink(leftIndex) // 继续执行下沉,直到它找到适合位置
    }
    // 右侧子节点值小于当前节点值,进行交换
    if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val){
      this.swap(rightIndex, index) 
      this.sink(rightIndex) // 继续执行下沉,直到它找到适合位置
    }
  }
  // 交换
  swap(i1, i2){
    const temp = this.heap[i1]
    this.heap[i1] = this.heap[i2]
    this.heap[i2]  = temp
  }
  // 插入:堆也是数组,所以是数组尾部
  insert(value){
    this.heap.push(value)
    this.swim(this.heap.length-1)
  }
  // 删除堆顶
  pop(){
    if(this.size()===1) return this.heap.shift()
    const top = this.heap[0] // 记录堆顶
    this.heap[0] = this.heap.pop() // 把数组的最后一位转移到头部
    this.sink(0)
    return top
  }
  // 获取堆顶:返回数组头部
  peek(){
    return this.heap[0]
  }
  // 获取堆大小:返回数组长度
  size(){
    return this.heap.length
  }
}


/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode[]} lists
 * @return {ListNode}
 */
/** 时间复杂度:O(n*logK) n是while循环中所有链表的所有节点,
 *  每一轮循环也有时间复杂度,pop/insert操作中时间复杂度是logK,堆的大大小是k
 *  空间复杂度:O(k) 堆的大小是k
 */
var mergeKLists = function(lists) {
    // 创建一个新链表,用于把最小元素插入新链表的next
    const res = new ListNode(0) 
    // 指针
    let p = res 
    // 新建最小堆,并把链表的头部节点都放到堆里
    const h = new MinHeap()
    lists.forEach(l=>{
       // 把头部节点都插入队里
       if(l) h.insert(l)
    }) 
    while(h.size()){
        // 把最小值弹出来,接到新链表上,指针往下走一步
        const n = h.pop() 
        p.next = n
        p = p.next 
        // 最小值已经被接入链表中,那就拿它后面的链表进入插入堆里,跟其他值pk,拿到最小值继续
        if(n.next) h.insert(n.next)
    }
    return res.next
};

根据字符出现频率排序

给定一个字符串,请将字符串里的字符按照出现的频率降序排列。

输入:“tree”
输出:“eert”
解释:
'e’出现两次,'r’和’t’都只出现一次。
因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。

堆排序,复杂度是最低的
es6 map 方便移除与添加
思路:1)统计出现次数 2)排序 3)输出

超级丑数

编写一段程序来查找第 n 个超级丑数。
超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。

输入: n = 12, primes = [2,7,13,19]
输出: 32
解释: 给定长度为 4 的质数列表 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32]

质因数是约数(能被2整除 n%i ===0 )和质数(大于1的自然数中,除了1和它本身整除,不再有其他约数)
质因数也是质数,只不过任意一个数的因素
丑数:只包含因子2、3、5的正整数,比如4、10、12都是丑数,另外1也是丑数

思路:先找到任意一个正整数的质因数,再去这个质数列表primes中去查,如果在就是超级丑数
怎么算正整数的质因数是什么?

步骤:
1)先找到任意一个正整数的质因数(计算)
2)质因数是都在指定质因数范围内(查)
用最大堆、最小堆也好,堆具备快速查找,当找一个数不存在的时候特别容易,比如找100,最大堆的已经是19,就不可能找到
3)是否达到指定个数n(验证)

扩展

判断是否质数(素数)

function isPrime(num) {
    if (validate(num)) {
        for (let i = 2; i < num; i++) {
            if (num % i == 0) {
                return false
            }
        }
        return true
    }
    return false
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值