前端算法入门之路(五)(堆与优先队列)

166 阅读7分钟

船长表情包镇楼

图片名称

堆:通常是一个可以被看做一棵完全二叉树的数组对象。

  • 大顶堆:根节点值最大,任意子节点均小于父节点
  • 大顶堆:根节点值最小,任意子节点均大于父节点
  • 而完全二叉树可以使用一段连续的存储空间表示(数组)

尾部插入调整

  • 大顶堆:插入的值会放在完全二叉树中最后一层最左侧,或者放在数组的末尾,然后跟父节点比较大小,如果比父节点大,则跟父节点交换位置
  • 小顶堆:插入的值会放在完全二叉树中最后一层最左侧,或者放在数组的末尾,然后跟父节点比较大小,如果比父节点小,则跟父节点交换位置

头部弹出调整

  • 大顶堆:弹出根节点,将尾节点放入根节点位置上,该节点将与较大的子节点交换位置直到符合大顶堆的性质
  • 大顶堆:弹出根节点,将尾节点放入根节点位置上,该节点将与较小的子节点交换位置直到符合小顶堆的性质

堆排序

使用大顶堆,堆顶元素弹出与堆尾元素互换,第二次弹出与倒数第二个堆尾元素互换,依次类推,最终可以获得一个从小到大的有序数组

堆与优先队列

堆是优先队列的一种实现方式

图片名称

核心应用

  • 求集合的最值

船长金句:数据结构分为两部分,一部分是结构定义,另外一部分是结构操作。数据结构就是定义一种性质,然后维护这种性质

使用js手撕一个堆

class Heap {
    constructor(data, type, compartor) {
        this.data = data || []
        // 比较规则,不传比较规则会有一个默认规则,可以指定类型,传入less是大顶堆,
        // 传入自定义比较规则则type失效
        this.compartor = compartor || function(a, b) {
            if (type == 'less') return a - b
            else return b - a
        }
        this.heapify()
    }
    size() {
        return this.data.length
    }
    // 处理元素顺序
    heapify() {
        if (this.size() < 2) {
        return
        }
        for (let i = 1; i < this.size(); i++) {
        this.bubbleUp(i)
        }
    }
    // 获取堆顶元素
    top() {
        if (!this.size()) return null
        return this.data[0]
    }
    // 添加元素操作
    push(val) {
        this.data.push(val)
        this.bubbleUp(this.size() - 1)
    }
    // 弹出堆顶元素
    pop() {
        if (!this.size()) return null
        if (this.size() == 1) return this.data.pop()
        let res = this.data[0]
        this.data[0] = this.data.pop()
        if (this.size()) {
            this.bubbleDown(0)
        }
        return res
    }
    // 交换元素
    swap(i, j) {
        if (i === j) return
        [this.data[i], this.data[j]] = [this.data[j], this.data[i]]
    }
    // 向上调整,最⾼调整到0号位置
    bubbleUp(index) {
        while (index) {
            //获取到当前节点的⽗节点,
            const parenIndex = (index - 1) >> 1;
            // const parenIndex = Math.floor((index - 1) / 2);
            // const parenIndex = (index - 1) / 2 | 0;
            //⽐较⽗节点的值和当前的值哪个⼩。
            if (this.compartor(this.data[index], this.data[parenIndex]) > 0) {
                // 交换⽗节点和⼦节点
                this.swap(index, parenIndex);
                // 交换⽗节点和⼦节点下标
                index = parenIndex;
            } else {
                //防⽌死循环。
                break;
            }
        }
    }
    //获取到最⼤的下标,保证不会交换出界。
    bubbleDown(index) {
        let lastIndex = this.size() - 1;
        while (index < lastIndex) {
            //获取左右⼉⼦的下标
            let leftIndex = index * 2 + 1;
            let rightIndex = index * 2 + 2;
            // 待交换节点
            let findIndex = index;
            if (leftIndex <= lastIndex
                && this.compartor(this.data[leftIndex], this.data[findIndex]) > 0) {
                findIndex = leftIndex;
            }
            if (rightIndex <= lastIndex && this.compartor(this.data[rightIndex], this.data[findIndex]) > 0) {
                findIndex = rightIndex;
            }
            if (index !== findIndex) {
                this.swap(index, findIndex);
                index = findIndex;
            } else {
                break;
            }
        }
    }
}

LeetCode肝题(以训练为目的)

  1. 剑指 Offer 40. 最小的k个数
// 创建一个大顶堆, 如果堆的长度大于k则弹出,剩余的就是最小的k个数
var getLeastNumbers = function(arr, k) {
    if (k == 0) return []
    let h = new Heap(arr, 'less')
    while(h.size() > k) h.pop()
    return h.data;
};
    1. 最后一块石头的重量
// 创建一个大顶堆, 每次弹出俩堆顶元素相减,大于0再放入堆中,直到只剩一个元素
var lastStoneWeight = function(stones) {
    let h = new Heap(stones, 'less')
    while(h.size() > 1) {
      let big = h.pop(), small = h.pop()
      if (big - small) h.push(big - small)
    }
    return h.top()
  };
    1. 数据流中的第 K 大元素
// 创建一个小顶堆, 堆的长度大于k则弹出,堆顶元素就是第k大的元素
var KthLargest = function(k, nums) {
    this.h = new Heap(nums, 'greator')
    this.k = k
};

KthLargest.prototype.add = function(val) {
    this.h.push(val)
    while(this.h.size() > this.k) this.h.pop()
    return this.h.top()
};
    1. 查找和最小的K对数字
// 自定义一个大顶堆的比较规则, 比较的是a数组的两项之和与b数组的两项之和
var compartor = function(a, b) {
    return a[0] + a[1] - b[0] - b[1]
}
var kSmallestPairs = function(nums1, nums2, k) {
    let h = new Heap([], 'less', compartor)
    for(let i in nums1) {
        for(let j in nums2) {
            // 将所有的数字排列放进大顶堆中,比较任意两数之和,和较小的k组会留到大顶堆中
            h.push([nums1[i], nums2[j]])
            if(h.size() > k) h.pop()
        }
    }
    return h.data
};
    1. 数组中的第K个最大元素
// 创建一个小顶堆, 堆的长度大于k则弹出,堆顶元素就是第k大的元素
var findKthLargest = function(nums, k) {
    let h = new Heap(nums, 'greator')
    while(h.size() > k) h.pop()
    return h.top()
};
    1. 前K个高频单词
// 自定义一个小顶堆的比较规则, 比较的是节点的数值和单词的优先
var topKFrequent = function(words, k) {
    let obj = {}
    // 先统计所有单词出现的次数
    for(let item of words) {
        obj[item] = obj[item] + 1 || 1
    }
    // 自定义比较规则, 当次数不相等时次数小的往上走
    // 当单词次数相等时,比较单词的大小即单词的先后顺序,排在后面的单词优先弹出
    // 当sort排序时,a>b的情况又是升序排序,符合题意
    let compartor = function(a, b) {
        if(obj[a] == obj[b]) return a > b ? 1:-1
        return obj[b] - obj[a]
    }
    let h = new Heap(Object.keys(obj), 'greator', compartor)
    while(h.size() > k) h.pop()
    h.data.sort(compartor)
    return h.data
};
    1. 数据流的中位数
// 维护两个堆,数据的前半段用大顶堆表示,数据的后半段用小顶堆表示
// 设定第一个堆最多比第二个堆多一个元素,否则调整数量
// 当堆的元素数量相等时,中位数就是两个堆的堆顶元素/2,不相等时就是第一个堆的堆顶元素
var MedianFinder = function() {
    this.lessH = new Heap([], 'less')
    this.greatorH = new Heap([], 'greator')
};

/** 
 * @param {number} num
 * @return {void}
 */
MedianFinder.prototype.addNum = function(num) {
    if (this.lessH.size() == 0 || num <= this.lessH.top()) {
        this.lessH.push(num)
    } else {
        this.greatorH.push(num)
    }
    if(this.lessH.size() < this.greatorH.size()) {
        this.lessH.push(this.greatorH.pop())
    } 
    if (this.lessH.size() - this.greatorH.size() > 1) {
        this.greatorH.push(this.lessH.pop())
    }
};

/**
 * @return {number}
 */
MedianFinder.prototype.findMedian = function() {
    if (this.lessH.size() == this.greatorH.size()) {
        return (this.lessH.top() + this.greatorH.top())/2
    }
    return this.lessH.top()
};
    1. 丑数 II
// 维护一个小顶堆
var nthUglyNumber = function(n) {
    let h = new Heap([1], 'greator')
    while(--n) {
        // 取出堆顶元素top
        let top = h.pop()
        // 如果是5的整数倍则只push top*5
        if (top % 5 === 0) {
            h.push(top * 5)
        // 如果是3的整数倍则push top*5和top*3
        } else if (top % 3 === 0) {
            h.push(top * 5)
            h.push(top * 3)
        } else {// 否则三个都push进堆里,这是为了防止添加重复元素
            h.push(top * 5)
            h.push(top * 3)
            h.push(top * 2)
        }
    }
    return h.top()
};
    1. 超级丑数
// 创建一个数组p记录乘数的位置,创建一个数组data记录所有丑数
var nthSuperUglyNumber = function(n, primes) {
    let p = new Array(primes.length).fill(0)
    let data = [1], ans = 1
    while(data.length < n) {
        ans = primes[0] * data[p[0]]
        // 得出所有丑数取最小值赋值给ans
        for(let i = 1; i< primes.length; i++) {
            ans = Math.min(ans, primes[i] * data[p[i]])
        }
        // 将所有能得出ans的丑数下标+1,更新坐标的同时也能去除重复值
        for(let i = 0; i< primes.length; i++) {
            if(primes[i] * data[p[i]] === ans) p[i]++
        }
        data.push(ans)
    }
    return ans
};
    1. 移除石子的最大得分
// 先排个序, a最小,c最大
var maximumScore = function(a, b, c) {
    let ans = 0
    if (a > b) [a, b] = [b, a]
    if (a > c) [a, c] = [c, a]
    if (b > c) [b, c] = [c, b]
    if (c - b > a) { // 如果b和c的差大于a,则最终得分为b+c
        ans = b + a
    } else { // 否则先将bc的差抵消掉a,再将a的一半分别抵消掉b和c,最终再加上b或者c
        ans = c - b
        a -= ans
        let i = a >> 1
        b -= i
        ans += i * 2
        ans += b
    }
    return ans
};
    1. 设计推特
// 设计一个大顶堆,比较的是第三项时间
// 设计一个用户集合,key为用户id,value为关注列表
// 这题用排序集合更加简答,后期再优化
var cmp = function(a, b) {
    return a[2] - b[2]
}
var Twitter = function() {
    this.user = {}
    this.twitter = new Heap([], 'less', cmp)
    this.time = 0
};
// 发送推特,放进堆中
Twitter.prototype.postTweet = function(userId, tweetId) {
    this.twitter.push([userId, tweetId, this.time++])
};
// 关注,往用户的关注列表push被关注的用户id
Twitter.prototype.follow = function(followerId, followeeId) {
    this.user[followerId] ? this.user[followerId].push(followeeId) : this.user[followerId] = [followeeId]
};
// 取关,往用户的关注列表删除被关注的用户id
Twitter.prototype.unfollow = function(followerId, followeeId) {
    if (!this.user[followerId]) return
    this.user[followerId].splice(this.user[followerId].findIndex(item => item == followeeId), 1)
};
// 获取推特,如果堆为空返回空数组,设计一个Set集合存入用户和他的关注列表,如果堆顶推特的id在Set里则push该推特进ans里,最后返回ans
Twitter.prototype.getNewsFeed = function(userId) {
    if(this.twitter.size() == 0) return []
    let list = [...(this.user[userId]||[]), userId], ans = [], n = 10, data = JSON.parse(JSON.stringify(this.twitter.data))
    let userSet = new Set(list)
    while(n && this.twitter.size()) {
        let top = this.twitter.pop()
        if (userSet.has(top[0])) {
            ans.push(top[1])
            n--
        }
    }
    this.twitter = new Heap(data, 'less', cmp)
    return ans
};
    1. 积压订单中的订单总数
// 阅读理解题,给一个订单数组,每个元素的第一项是价格,第二项是数量,第三项是类别,0是购买订单,1是销售订单
// 维护两个堆,购买堆是大顶堆,销售堆是小顶堆,堆元素是一个数组,第一项为价格,第二项为数量
// 用最大的购买价格匹配最低的销售价格(奸商就完事了)
// 自定义两个堆的比较规则
var compartor1 = function(a, b) {
    return a[0] - b[0]
}
var compartor2 = function(a, b) {
    return b[0] - a[0]
}
var getNumberOfBacklogOrders = function(orders) {
    let buy = new Heap([], 'less', compartor1),  sell = new Heap([], 'greator', compartor2)
    for(let x of orders) {
        if (x[2] == 0) {
            while (x[1] != 0 && sell.size() && sell.data[0][0] <= x[0]) {
                let cnt = Math.min(x[1], sell.data[0][1])
                x[1] -= cnt
                sell.data[0][1] -= cnt
                if (sell.data[0][1] == 0) sell.pop()
            }
            if (x[1] != 0) buy.push(x)
        } else {
            while (x[1] != 0 && buy.size() && buy.data[0][0] >= x[0]) {
                let cnt = Math.min(x[1], buy.data[0][1])
                x[1] -= cnt
                buy.data[0][1] -= cnt
                if (buy.data[0][1] == 0) buy.pop()
            }
            if (x[1] != 0) sell.push(x)
        }
    }
    let mod = 1000000007, sum = 0
    for(let item of buy.data) {
        sum = (sum + item[1]) % mod
    }
    for(let item of sell.data) {
        sum = (sum + item[1]) % mod
    }
    return sum
};