[路飞]_前端算法第十二弹-347. 前 K 个高频元素(数组,小顶堆,桶排序)

178 阅读3分钟

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

看到这道题我们首先想到的就是要先算出每个数字出现的频率,然后根据这个频率排序,再找出我们所需要的前K个高频元素。

  1. 统计元素出现频率
  2. 对频率排序
  3. 找出前K个高频元素

map+数组

我们首先想到的第一种解法,应该是利用map遍历一次数组,统计每个元素的频率,将值(key)和频率(value)保存到map中。再通过数组进行排序。

let topKFrequent = function(nums, k) {
   // 创建一个map,再通过set确定一共有多少不相等的个元素
    let map = new Map(), arr = [...new Set(nums)]
    // 遍历nums将num的个数加入到map中
    nums.map((num) => {
        if(map.has(num)) map.set(num, map.get(num)+1)
        else map.set(num, 1)
    })
    // 对所有元素进行通过map数量进行排序后,取前K项
    return arr.sort((a, b) => map.get(b) - map.get(a)).slice(0, k);
};

这种解法由于我们需要一次map,和一次sort,所以时间复杂度为O(nlogn)。

map+小顶堆

同样我们遍历一次数组,统计每个元素的频率,将值(key)和频率(value)保存到map中。通过map数组构建一个前K个高频元素的小顶堆。小顶堆的任意节点的值都必须小于等于其左右子节点的值,堆顶则为最小值。

  • 遍历nums,统计map

  • 遍历map,将前K个值,构造最小堆

  • 从K开始,继续遍历,并进行比较

  • 结束返回

    let topKFrequent = function(nums, k) { // 构造map数组 let map = new Map(), heap = [,] nums.map((num) => { if(map.has(num)) map.set(num, map.get(num)+1) else map.set(num, 1) })

    // 如果元素数量小于等于 k
    if(map.size <= k) {
        return [...map.keys()]
    }
    
    // 如果元素数量大于 k,遍历map,构建小顶堆
    let i = 0
    map.forEach((value, key) => {
        if(i < k) {
            // 取前k个建堆, 插入堆
            heap.push(key)
            // 原地建立前 k 堆
            if(i === k-1) buildHeap(heap, map, k)
        } else if(map.get(heap[1]) < value) {
            // 替换并堆化
            heap[1] = key
            // 自上而下式堆化第一个元素
            heapify(heap, map, k, 1)
        }
        i++
    })
    // 删除heap中第一个元素
    heap.shift()
    return heap
    

    };

    // 原地建堆,从后往前,自上而下式建小顶堆 let buildHeap = (heap, map, k) => { if(k === 1) return // 从最后一个非叶子节点开始,自上而下式堆化 for(let i = Math.floor(k/2); i>=1 ; i--) { heapify(heap, map, k, i) } }

    // 堆化 let heapify = (heap, map, k, i) => { // 自上而下式堆化 while(true) { let minIndex = i if(2i <= k && map.get(heap[2i]) < map.get(heap[i])) { minIndex = 2i } if(2i+1 <= k && map.get(heap[2i+1]) < map.get(heap[minIndex])) { minIndex = 2i+1 } if(minIndex !== i) { swap(heap, i, minIndex) i = minIndex } else { break } } }

    // 交换 let swap = (arr, i , j) => { let temp = arr[i] arr[i] = arr[j] arr[j] = temp }

遍历数组需要 O(n) 的时间复杂度,一次堆化需要 O(logk) 时间复杂度,所以利用堆求 Top k 问题的时间复杂度为 O(nlogk)。

map+桶排序

这里取前k个高频元素,使用计数排序不再适合,在上题目中使用计数排序,将 i 元素出现的次数存储在 bucket[i] ,但这种存储不能保证 bucket 数组上值是有序的,例如 bucket=[0,3,1,2] ,即元素 0 未出现,元素 1 出现 3 次,元素 2 出现 1 次,元素 3 出现 2 次,所以计数排序不适用于取前k个高频元素,不过,不用怕,计数排序不行,还有桶排序。

桶排序是计数排序的升级版。它也是利用函数的映射关系。

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

  • 首先使用 map 来存储频率
  • 然后创建一个数组(有数量的桶),将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标(桶内)即可。
let topKFrequent = function(nums, k) {
    let map = new Map(), arr = [...new Set(nums)]
    nums.map((num) => {
        if(map.has(num)) map.set(num, map.get(num)+1)
        else map.set(num, 1)
    })
    
    // 如果元素数量小于等于 k
    if(map.size <= k) {
        return [...map.keys()]
    }
    
    return bucketSort(map, k)
};

// 桶排序
let bucketSort = (map, k) => {
    let arr = [], res = []
    map.forEach((value, key) => {
        // 利用映射关系(出现频率作为下标)将数据分配到各个桶中
        if(!arr[value]) {
            arr[value] = [key]
        } else {
            arr[value].push(key)
        }
    })
    // 倒序遍历获取出现频率最大的前k个数
    for(let i = arr.length - 1;i >= 0 && res.length < k;i--){
        if(arr[i]) {
            res.push(...arr[i])
        }
	}
	return res
}

此时的时间复杂度为O(n)