【算法13天:Day13】第五章栈和队列 LeetCode 前 K 个高频元素(347)

106 阅读6分钟

第三题:

image.png

解法一:(Map+排序)

解题思路:先使用Map将每个整数频率统计出来,然后将Map根据value进行排序,再将排序后的key按顺序push到temp中,然后根据k,将前频率前k个数push到result中,输出result即可。

var topKFrequent = function (nums, k) {
    let map = new Map()
    let temp = []
    let result = []
    for (let num of nums) {
        if (!map.get(num)) {
        map.set(num, 1)
        } else {
        map.set(num, map.get(num) + 1)
        }
    }
    const sortNumAsc = new Map([...map].sort((a, b) => b[1] - a[1]))
    for (let key of sortNumAsc.keys()) {
        temp.push(key)
    }
    while (k--) {
        result.push(temp.shift())
    }
    return result
}

// 或者

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    // //法一:哈希表+sort
    let map = new Map();
    for(let num of nums) {
        map.set(num, map.has(num) ? map.get(num) + 1 : 1);//初始化出现次数为1,之后累加
    }
    if(k === map.size) return [...map.keys()];//k如果等于map.size,直接返回全部key
    let arr = Array.from(map).sort((a, b) => {return b[1] - a[1]});//从大到小排序
    return arr.slice(0, k).map(n => n[0])//截取前k个key
};

解法二:(Map+桶排序)

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    //法二:哈希表+桶排序
    let map = new Map();
    for(let num of nums) {
        map.set(num, map.has(num) ? map.get(num) + 1 : 1);//初始化出现次数为1,之后累加
    }
    if(k === map.size) return [...map.keys()];//k如果等于map.size,直接返回全部key
    const bucketSort = () => {
        let arr = [];
        let res = [];
        map.forEach((value, key) => {//arr[i]存放频率为i的key数组
            if(!arr[value]) arr[value] = [key];
            else arr[value].push(key);
        });
        for(let i = arr.length - 1; i >= 0 && res.length < k; i--) {
            if(arr[i]) {
                res.push(...arr[i]);//将数组转换为用逗号分割的参数序列
            }
        }
        return res;
    }
    return bucketSort();
};

解法三:(Map+小顶堆)

遍历一遍数组统计每个元素的频率,并将元素值( key )与出现的频率( value )保存到 map 中

通过 map 数据构建一个前 k 个高频元素小顶堆,小顶堆上的任意节点值都必须小于等于其左右子节点值,即堆顶是最小值。

具体步骤如下:

  • 遍历数据,统计每个元素的频率,并将元素值( key )与出现的频率( value )保存到 map 中
  • 遍历 map ,将前 k 个数,构造一个小顶堆
  • 从 k 位开始,继续遍历 map ,每一个数据出现频率都和小顶堆的堆顶元素出现频率进行比较,如果小于堆顶元素,则不做任何处理,继续遍历下一元素;如果大于堆顶元素,则将这个元素替换掉堆顶元素,然后再堆化成一个小顶堆。
  • 遍历完成后,堆中的数据就是前 k 大的数据
let topKFrequent = function(nums, k) {
    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(2*i <= k && map.get(heap[2*i]) < map.get(heap[i])) {
            minIndex = 2*i
        }
        if(2*i+1 <= k && map.get(heap[2*i+1]) < map.get(heap[minIndex])) {
            minIndex = 2*i+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
}

// 或者

// js 没有堆 需要自己构造
class Heap {
    constructor(compareFn) {
        this.compareFn = compareFn;
        this.queue = [];
    }

    // 添加
    push(item) {
        // 推入元素
        this.queue.push(item);

        // 上浮
        let index = this.size() - 1; // 记录推入元素下标
        let parent = Math.floor((index - 1) / 2); // 记录父节点下标

        while (parent >= 0 && this.compare(parent, index) > 0) { // 注意compare参数顺序
            [this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]];

            // 更新下标
            index = parent;
            parent = Math.floor((index - 1) / 2);
        }
    }

    // 获取堆顶元素并移除
    pop() {
        // 堆顶元素
        const out = this.queue[0];

        // 移除堆顶元素 填入最后一个元素
        this.queue[0] = this.queue.pop();

        // 下沉
        let index = 0; // 记录下沉元素下标
        let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标
        let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;

        while (searchChild !== undefined && this.compare(index, searchChild) > 0) { // 注意compare参数顺序
            [this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]];

            // 更新下标
            index = searchChild;
            left = 2 * index + 1;
            searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
        }

        return out;
    }

    size() {
        return this.queue.length;
    }

    // 使用传入的 compareFn 比较两个位置的元素
    compare(index1, index2) {
        // 处理下标越界问题
        if (this.queue[index1] === undefined) return 1;
        if (this.queue[index2] === undefined) return -1;

        return this.compareFn(this.queue[index1], this.queue[index2]);
    }

}

const topKFrequent = function (nums, k) {
    const map = new Map();

    for (const num of nums) {
        map.set(num, (map.get(num) || 0) + 1);
    }

    // 创建小顶堆
    const heap= new Heap((a, b) => a[1] - b[1]);

    // entry 是一个长度为2的数组,0位置存储key,1位置存储value
    for (const entry of map.entries()) {
        heap.push(entry);

        if (heap.size() > k) {
            heap.pop();
        }
    }

    // return heap.queue.map(e => e[0]);

    const res = [];

    for (let i = heap.size() - 1; i >= 0; i--) {
        res[i] = heap.pop()[0];
    }

    return res;
};
// js 没有堆 需要自己构造
class Heap {
    constructor(compareFn) {
        this.compareFn = compareFn;
        this.queue = [];
    }

    // 添加
    push(item) {
        // 推入元素
        this.queue.push(item);

        // 上浮
        let index = this.size() - 1; // 记录推入元素下标
        let parent = Math.floor((index - 1) / 2); // 记录父节点下标

        while (parent >= 0 && this.compare(parent, index) > 0) { // 注意compare参数顺序
            [this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]];

            // 更新下标
            index = parent;
            parent = Math.floor((index - 1) / 2);
        }
    }

    // 获取堆顶元素并移除
    pop() {
        // 堆顶元素
        const out = this.queue[0];

        // 移除堆顶元素 填入最后一个元素
        this.queue[0] = this.queue.pop();

        // 下沉
        let index = 0; // 记录下沉元素下标
        let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标
        let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;

        while (searchChild !== undefined && this.compare(index, searchChild) > 0) { // 注意compare参数顺序
            [this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]];

            // 更新下标
            index = searchChild;
            left = 2 * index + 1;
            searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left;
        }

        return out;
    }

    size() {
        return this.queue.length;
    }

    // 使用传入的 compareFn 比较两个位置的元素
    compare(index1, index2) {
        // 处理下标越界问题
        if (this.queue[index1] === undefined) return 1;
        if (this.queue[index2] === undefined) return -1;

        return this.compareFn(this.queue[index1], this.queue[index2]);
    }
}
const topKFrequent = function (nums, k) {
    const map = new Map();

    for (const num of nums) {
        map.set(num, (map.get(num) || 0) + 1);
    }

    // 创建小顶堆
    const heap= new Heap((a, b) => a[1] - b[1]);

    // entry 是一个长度为2的数组,0位置存储key,1位置存储value
    for (const entry of map.entries()) {
        heap.push(entry);

        if (heap.size() > k) {
            heap.pop();
        }
    }

    // return heap.queue.map(e => e[0]);

    const res = [];

    for (let i = heap.size() - 1; i >= 0; i--) {
        res[i] = heap.pop()[0];
    }

    return res;
};

总结:在 JavaScript 中按值对 Map 进行排序

按值对 Map 进行排序:

  • 使用展开语法 (...) 获取 Map 条目的数组
  • 在数组上调用 sort() 方法
  • 将结果传递给 Map() 构造函数
// ✅ 当 VALUES 是 NUMBERS
const map2 = new Map([
  ['three', 3],
  ['one', 1],
  ['two', 2],
]);

// ✅ 按值升序排序(从低到高)
const sortNumAsc = new Map([...map2].sort((a, b) => a[1] - b[1]));

// 👇️ {'one' => 1, 'two' => 2, 'three' => 3}
console.log(sortNumAsc);

// ✅ 按值降序排序(从高到低)
const sortedNumDesc = new Map([...map2].sort((a, b) => b[1] - a[1]));

// 👇️ {'three' => 3, 'two' => 2, 'one' => 1}
console.log(sortedNumDesc);

// ✅ 当值是字符串时
const map1 = new Map([
  ['three', 'c'],
  ['one', 'a'],
  ['two', 'b'],
]);

// ✅ 按值升序排序(从低到高)
const sortedAsc = new Map([...map1].sort((a, b) => (a[1] > b[1] ? 1 : -1)));

// 👇️ {'one' => 'a', 'two' => 'b', 'three' => 'c'}
console.log(sortedAsc);

// ✅ 按值降序排序(从高到低)
const sortedDesc = new Map([...map1].sort((a, b) => (a[1] > b[1] ? -1 : 1)));

// 👇️ {'three' => 'c', 'two' => 'b', 'one' => 'a'}
console.log(sortedDesc);

该代码片段显示了如何按值对字符串和数字值按升序和降序对 Map 进行排序。

Map 对象会记住键的插入顺序。

这就是我们使用 Map() 构造函数以正确顺序创建新 Map 的原因。

扩展语法 (...) 允许我们获取包含 Map 条目的数组。

const map2 = new Map([  ['three', 3],
  ['one', 1],
  ['two', 2],
]);

// 👇️ [['three', 3], ['one', 1], ['two', 2]]
console.log([...map2]);

下一步是调用数组的 sort() 方法,并传递给它一个函数。

我们传递给 sort() 方法的函数定义了排序顺序。

该函数使用 2 个参数调用。 在我们的示例中 – 2 个包含键值对的数组。

/ ✅ 当 VALUES 是 NUMBERS
const map2 = new Map([
  ['three', 3],
  ['one', 1],
  ['two', 2],
]);

// ✅ 按值升序排序(从低到高)
const sortNumAsc = new Map([...map2].sort((a, b) => a[1] - b[1]));

// 👇️ {'one' => 1, 'two' => 2, 'three' => 3}
console.log(sortNumAsc);

sort() 方法使用以下逻辑对数组中的元素进行排序:

  • 如果比较函数的返回值大于0,则将b排在a之前。
  • 如果比较函数的返回值小于0,则将a排在b之前。
  • 如果比较函数的返回值等于 0,则保持 a 和 b 的原始顺序。

在上面的代码片段中:

  • 如果 a 数组中的第二个元素(值)减去 b 数组中的第二个元素返回的值大于 0,我们将 b 排在 a 之前
  • 如果减法返回负数,我们将 a 排序在 b 之前。
  • 如果减法返回 0,我们保持 a 和 b 的原始顺序。

如果需要按值降序对 Map 进行降序排序,只需将 b 的值减去 a 的值即可。

// ✅ 当 VALUES 是 NUMBERS
const map2 = new Map([
  ['three', 3],
  ['one', 1],
  ['two', 2],
]);

// ✅ 按值降序排序(从高到低)
const sortedNumDesc = new Map([...map2].sort((a, b) => b[1] - a[1]));

// 👇️ {'three' => 3, 'two' => 2, 'one' => 1}
console.log(sortedNumDesc);

我们使用相同的方法按字符串值对 Map 进行排序。

// ✅ 当 VALUES 是 字符串
const map1 = new Map([
  ['three', 'c'],
  ['one', 'a'],
  ['two', 'b'],
]);

// ✅ 按值升序排序(从低到高)
const sortedAsc = new Map([...map1].sort((a, b) => (a[1] > b[1] ? 1 : -1)));

// 👇️ {'one' => 'a', 'two' => 'b', 'three' => 'c'}
console.log(sortedAsc);

这次我们检查 a 数组中值的 UTF-16 编码单元值是否大于 b 数组中的值,如果大于则返回 1,否则返回 -1。

如果我们返回 1(大于 0),那么我们将 b 排序在 a 之前。

我们可以通过更改 1 和 -1 的位置来将排序更改为降序。

// ✅ 当 VALUES 是 字符串
const map1 = new Map([
  ['three', 'c'],
  ['one', 'a'],
  ['two', 'b'],
]);

// ✅ 按值降序排序(从高到低)
const sortedDesc = new Map([...map1].sort((a, b) => (a[1] > b[1] ? -1 : 1)));

// 👇️ {'three' => 'c', 'two' => 'b', 'one' => 'a'}
console.log(sortedDesc);