LeetCode347 前 K 个高频元素

LeetCode347 前 K 个高频元素

  • 题目
    • 解题一:基于桶排序
    • 解题二:基于堆排序
      • 小顶堆
      • 大顶堆
    • 解题三:基于快速排序

题目

LeetCode347 前 K 个高频元素_第1张图片
在这里插入图片描述
这题其实只是要先计数,剩下的只是排序,与 LeetCode215 数组中的第K个最大元素 类似。

解题一:基于桶排序

LeetCode347 前 K 个高频元素_第2张图片

// javascript
var topKFrequent = function(nums, k) {
    let res = new Array();
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    // 桶排序
    // 将频率作为数组下标,对于出现频率不同的数字集合,存入对应的数组下标
    const list = new Array(nums.length + 1);
    for (const [num, count] of occurrences.entries()) {
    	// 获取出现的次数作为下标
        if (list[count] === undefined) list[count] = new Array();
        list[count].push(num);
    }
    // 倒序遍历数组获取出现顺序从大到小的排列
    for (let i = list.length - 1; i >= 0 && res.length < k; i--) {
        if (list[i] !== undefined) {
            res = res.concat(list[i]);  // res 有被改动,不能声明为 const
        }
    }
    return res;
};

LeetCode347 前 K 个高频元素_第3张图片

解题二:基于堆排序

小顶堆

LeetCode347 前 K 个高频元素_第4张图片

// javascript
var topKFrequent = function(nums, k) {
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    const queue = new MinPriorityQueue();
    for (let [currNum, currPriority] of occurrences.entries()) {
        if (queue.size() < k) {
            queue.enqueue(currNum, currPriority);
        }
        else {
            let frontEntry = queue.front();
            if (frontEntry.priority < currPriority) {
                queue.dequeue();
                queue.enqueue(currNum, currPriority);
            }
        }
    }
    return queue.toArray().map(el => el.element);
};

调用了 JS 的小顶堆 API,详细用法见:priority-queue/README.md
LeetCode347 前 K 个高频元素_第5张图片
手写小顶堆:

// javascript
var topKFrequent = function(nums, k) {
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    const heap = Array.from(occurrences);
    // 构建大小为k的小顶堆
    buildMinHeap(heap, k);
    for (let i = k; i < heap.length; i++) {
        if (heap[i][1] > heap[0][1]) {
            swap(heap, i, 0);
            minHeapify(heap, 0, k);
        }
    }
    return heap.slice(0, k).map(el => el[0]);
};

// 原地建堆,从后往前,自上而下式建小顶堆
const buildMinHeap = (heap, size) => {
    // 从最后一个非叶子节点开始,自下而上堆化
    for(let i = (size >> 1) - 1; i >= 0; i--) {
        minHeapify(heap, i, size);
    }
}

// 堆化
const minHeapify = (heap, i, size) => {
    let minIndex = i;
    const leftIndex = (i << 1) + 1, rightIndex = (i << 1) + 2;
    if (leftIndex < size && heap[leftIndex][1] < heap[minIndex][1]) minIndex = leftIndex;
    if (rightIndex < size && heap[rightIndex][1] < heap[minIndex][1]) minIndex = rightIndex;
    if (minIndex !== i) {
        swap(heap, minIndex, i);
        minHeapify(heap, minIndex, size);
    }
};

// 交换
const swap = (heap, i , j) => {
    [heap[i], heap[j]] = [heap[j], heap[i]];
}

或者封装成一个类:

// javascript
class MinHeap {
    constructor() {this.heap = [];}
    swap(i, j) {[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];}  // 交换节点
    getParentIndex(i) {return ((i - 1) >> 1);}  // 获取父节点, 2进制操作,取商: Math.floor((i - 1) / 2)
    getLeftIndex(i)   {return (i << 1) + 1;}
    getRightIndex(i)  {return (i << 1) + 2;}
    shiftUp(index) {     // 上移
        if (index == 0) return;
        const parentIndex = this.getParentIndex(index);
        // 父节点大于当前节点
        if (this.heap[parentIndex] && this.heap[parentIndex].priority > this.heap[index].priority) {
            this.swap(parentIndex, index);
            this.shiftUp(parentIndex);
        }
    }
    shiftDown(index) {   // 下移
        let minIndex = index;
        const leftIndex = this.getLeftIndex(index);
        const rightIndex = this.getRightIndex(index);
        if (this.heap[leftIndex] && this.heap[leftIndex].priority < this.heap[minIndex].priority) {
            minIndex = leftIndex;
        }
        if (this.heap[rightIndex] && this.heap[rightIndex].priority < this.heap[minIndex].priority) {
            minIndex = rightIndex;
        }
        if (minIndex !== index) {
            this.swap(minIndex, index);
            this.shiftDown(minIndex);
        }
    }
    // 将值插入堆的底部,即数组的尾部。
    // 然后_上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
    // 大小为k的堆中插入元素的时间复杂度为O(logK)
    enqueue(value) {
        this.heap.push(value);
        this.shiftUp(this.size() - 1);
    }
    // 删除堆顶
    // 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)。
    // 然后下移: 将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶。
    // 大小为k的堆中删除堆顶的时间复杂度为O(logk)。
    dequeue() {
        // 因为下面的操作会先 enqueue 再 dequeue 所以不会出现 heap 大小为 1 的情况
        this.heap[0] = this.heap.pop();
        this.shiftDown(0);
    }
    front() {return this.heap[0];}
    size() {return this.heap.length;}
}
var topKFrequent = function(nums, k) {
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    const h = new MinHeap();
    for (const [element, priority] of occurrences.entries()) {
        h.enqueue({priority, element});
        if (h.size() > k) {
            h.dequeue();
        }
    }
    return h.heap.map(el => el.element);
};

大顶堆

// javascript
var topKFrequent = function(nums, k) {
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    const list = Array.from(occurrences);
    buildMaxHeap(list);
    for (let i = list.length - 1; i >= list.length - k; i--) {
        swap(list, 0, i);
        maxHeapify(list, 0, i);
    }
    return list.slice(list.length - k).map(element => element[0]);
};

const buildMaxHeap = (list) => {
    for (let i = (list.length >> 1) - 1; i >= 0; i--) {
        maxHeapify(list, i, list.length);
    }
};

const maxHeapify = (list, i, size) => {
    let maxIndex = i, l = (i << 1) + 1, r = (i << 1) + 2;
    if (l < size && list[l][1] > list[maxIndex][1]) maxIndex = l;
    if (r < size && list[r][1] > list[maxIndex][1]) maxIndex = r;
    if (maxIndex !== i) {
        swap(list, i, maxIndex);
        maxHeapify(list, maxIndex, size);
    }
};

const swap = (list, i, j) => {
    [list[i], list[j]] = [list[j], list[i]];
};

时间复杂度: O ( k l o g n ) O(klogn) O(klogn),n 是数组中不同数字的数量,堆中有 n 个元素,移除堆顶的时间是 logn,重复操作了 k 次。
空间复杂度: O ( n ) O(n) O(n),n 是数组中不同数字的数量,哈希表和堆的空间。

解题三:基于快速排序

这题求前 K 个高频元素,排序顺序是从大到小进行排列。
LeetCode347 前 K 个高频元素_第6张图片

// javascript
var topKFrequent = function (nums, k) {
    // 统计出现次数
    const occurrences = new Map();
    for (const n of nums) {
        occurrences.set(n, (occurrences.get(n) || 0) + 1);
    }
    const list = Array.from(occurrences);
    quickSelect(list, 0, list.length - 1, k);
    return list.slice(0, k).map(element => element[0]);
};

var partition = function(list, left, right) {
    // Math.random() * (right - left + 1) => [0, right - left + 1)
    // Math.floor([0, right - left + 1)) => 0, 1, 2, ..., right - left
    // left + Math.floor([0, right - left + 1)) => left, left + 1, ..., right
    const pivot = list[left];
    let i = left, j = right;
    while (i < j) {
        while (i < j && list[j][1] <= pivot[1]) j--; // 从后往前,找到比pivot更大的数
        list[i] = list[j];                           // 将更大的数放入左边
        while (i < j && list[i][1] >= pivot[1]) i++; // 从前往后,找到比pivot更小的数
        list[j] = list[i];                           // 将更小的数放入右边
    }
    list[i] = pivot;                                 // 将pivot放入最终位置 
    return i;
};

var quickSelect = function(list, left, right, k) {
    if (left >= right) return;
    let index = partition(list, left, right);
    // index - left = k - 1 时,因为题目说可以按任意顺序返回答案,所以不需要再排序
    // index - left > k - 1 时,前 k 个高频元素在左边
    if (index - left > k - 1) quickSelect(list, left, index - 1, k);
    // index - left < k - 1 时,前 index - left + 1 个高频元素在左边
    // 要去右边寻找剩下的前 k - (index - left + 1) 个高频元素
    if (index - left < k - 1) quickSelect(list, index + 1, right, k - (index - left + 1));
};

LeetCode347 前 K 个高频元素_第7张图片

你可能感兴趣的:(刷题笔记,堆(优先队列),快速排序,桶排序,哈希表,堆排序)