[路飞]_前K个高频单词

357 阅读3分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

leetcode-692 前K个高频单词

题目介绍

给一非空的单词列表,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

示例1

输入: ["i", "love", "leetcode", "i", "love", "coding"], k = 2
输出: ["i", "love"]
解析: "i""love" 为出现次数最多的两个单词,均为2次。
    注意,按字母顺序 "i""love" 之前。

示例2

输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny""day" 是出现次数最多的四个单词,
    出现次数依次为 4, 3, 21 次。

注意:

  1. 假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
  2. 输入的单词均由小写字母组成。

解题思路

解决这道题的方法不难,首先遍历所有的单词,然后统计每个单词出现的频率,按顺序进行排序,然后返回次数最多的 k 个单词就行了

需要注意的是,当单词出现的次数一样多时,需要根据字母的顺序排序。在 js 中对单词进行比较是将每个字母都转换成 ASCII 码进行比较,相同位置的字母依次比较,直到出现不同字母的位置为止,而且字母之间不能直接通过相减的方式比较两个字母的大小关系,但是可以通过 “>”、 “<” 来比较两个字母的大小关系,也可以使用 js 中自带的 localeCompare 方法来比较两个单词的大小关系。

思路一:哈希表 + 直接排序

解题步骤:

  1. 遍历所有的单词,将单词出现的次数保存到哈希表 wordsMap
  2. 将哈希表中的所有键加入到数组 ans
  3. 将数组 ans 根据题目的排序要求进行从大到小排序
  4. ans 数组的前 k 项即为当前题目的结果
var topKFrequent = function(words, k) {
    // 创建一个保存单词出现频次的哈希表
    const wordsMap = new Map()
    // 遍历所有单词,保存单词出现频次
    words.forEach(v => {
        // 如果第一次出现该单词,次数设置为 0
        if (!wordsMap.has(v)) wordsMap.set(v, 0)
        // 将单词出现的次数 +1
        wordsMap.set(v, wordsMap.get(v) + 1)
    })
    // 创建一个存放哈希表的键
    const ans = []
    for(const key of wordsMap.keys()) {
        ans.push(key)
    }
    // 按题目要求的顺序从大到小进行排序
    ans.sort((a, b) => {
        if (wordsMap.get(a) !== wordsMap.get(b)) return wordsMap.get(b) - wordsMap.get(a)
        return a.localeCompare(b)
    })
    // 返回排序后数组的前 k 项
    return ans.slice(0, k)
};

思路二:哈希表 + 小顶堆

对于这种求最大 k 个元素的问题,最好的方式是使用堆来做,因为题目是要求前 k 个频次最高的单词,因此我们可以用一个大小为 k 的小顶堆来维护频次最高的 k 个单词,最后将堆中的单词依次弹出,即可得到最终结果

解题步骤:

  1. 遍历所有的单词,将单词出现的次数保存到哈希表 wordsMap
  2. 创建一个大小为 k 的小顶堆,当小顶堆的大小小于 k 时,直接向小顶堆插入数据,向上调整,否则,当待插入数据大于堆顶元素时,将对顶元素替换为待插入的元素,向下调整
  3. 遍历完整个哈希表的数据,依次弹出堆顶元素
  4. 从结果数组头部插入弹出元素
  5. 返回结果数组
var topKFrequent = function(words, k) {
    // 统计单词的频次插入到哈希表中
    const wordsCount = new Map()
    words.forEach(v => {
        if (!wordsCount.has(v)) wordsCount.set(v, 0)
        wordsCount.set(v, wordsCount.get(v) + 1)
    })
    // 将哈希表中的键值对依次插入到小顶堆中
    const heap = new Heap(k)
    for(const [key, value] of wordsCount.entries()) {
        heap.push([key, value])
    }
    // 依次弹出小顶堆的堆顶元素,从头部插入结果数组中
    const ans = []
    while(heap.size()) {
        ans.unshift(heap.pop()[0])
    }
    return ans
};

class Heap{
    constructor(k) {
        this.heap = []
        this.k = k
    }

    // 返回当前堆的大小
    size() {
        return this.heap.length
    }

    // 插入元素
    // 如果当前堆的大小小于 k 值,从堆尾插入,向上调整
    // 如果当前堆的大小等于 k 值,如果待插入元素大于堆顶元素,替换堆顶元素的位置,向下调整
    push(val) {
        if (this.size() < this.k) {
            this.heap.push(val)
            this.sortBack()
        } else if (this.compare(val, this.heap[0])) {
            this.heap[0] = val
            this.sortFront()
        }
    }

    // 从堆顶弹出元素,然后将尾部元素移动到堆顶,向下调整
    pop() {
        const val = this.heap[0]
        const back = this.heap.pop()
        if (this.heap.length) {
            this.heap[0] = back
            this.sortFront()
        }
        return val
    }

    // 向上调整堆结构
    sortBack() {
        let i = this.size() - 1
        while (i > 0 && this.compare(this.heap[Math.floor((i - 1) / 2)], this.heap[i])) {
            [this.heap[i], this.heap[Math.floor((i - 1) /2)]] = [this.heap[Math.floor((i - 1) /2)], this.heap[i]]
            i = Math.floor((i - 1) / 2)
        }
    }

    // 向下调整堆结构
    sortFront() {
        let i = 0
        while (i * 2 + 1 < this.size()) {
            let temp = i
            if (this.compare(this.heap[temp], this.heap[i * 2 + 1])) temp = i * 2 + 1
            if (i * 2 + 2 < this.size() && this.compare(this.heap[temp], this.heap[i * 2 + 2])) temp = i * 2 + 2
            if (temp === i) break
            [this.heap[i], this.heap[temp]] = [this.heap[temp], this.heap[i]]
            i = temp
        }
    }

    // 根据题目规则比较元素之间的大小
    compare(a, b) {
        if (a[1] !== b[1]) return a[1] > b[1]
        return a[0] < b[0]
    }
}