215. 数组中的第K个最大元素
看到“第K个最大元素”,就用最小堆
思路:构建一个最小堆,并依次把数组的值插入堆中。当堆的容量超过k,就删除堆顶。插入结束后,堆顶就是第K个最大元素
可以想象是一个小公司,堆顶是最弱的员工,当发现超出人数了,就要裁员,经过优胜劣汰,不断去淘汰堆顶
/** 时间复杂度:O(N*logK) 循环嵌套循环 k为堆的大小 N为nums的长度(循环次数)
* 空间复杂度:O(k) k为堆的长度
*/
var findKthLargest = function(nums, k) {
const h = new MinHeap()
nums.forEach(n=>{
h.insert(n)
if(h.size()>k){ // 超过人数,开始裁员
h.pop()
}
})
return h.peek()
};
347. 前 K 个高频元素
// 时间复杂度:for循环O(n)、sort=>排序算法最快也是O(n*logN)),取较大的就是O(n*logN))
var topKFrequent = function(nums, k) {
// 设置映射关系
const map = new Map()
nums.forEach(n=>{
map.set(n, map.has(n) ? map.get(n)+1 : 1) // 如果上次已经出现过这个频率,就拿它的频率数+1
})
console.log()
// 对频率进行排序,将map转数组,降序排序(b的频率-a的频率)
const list = Array.from(map).sort((a, b)=> b[1] - a[1] )
return list.slice(0, k).map(n=>n[0])
};
优化:数组中没必要对所有元素进行排序,可以利用堆拿到前K个高频元素再排序
建一个最小堆,把元素和频率插入到最小堆,并按频率进行排序,而且最小堆大小永远维持在K
// 最小堆:堆顶最小,都是大于等于当前值
class MinHeap{
constructor(){
this.heap = []
}
// 父结点索引
getParentIndex(i){
// return Math.floor((i-1)/2)
return (i-1) >> 1 // 二进制往右边移一位
}
// 左侧子节点
getLeftIndex(i){
return i * 2 + 1
}
// 右侧子节点
getRightIndex(i){
return i * 2 + 2
}
// 上浮:父结点值大于子节点值就要进行交换,直到父节点小于等于插入的值
swim(index){
if(index===0) return // 如果堆顶,就不上浮
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value){
this.swap(parentIndex, index)
this.swim(parentIndex) // 不断上浮
}
}
// 下沉:将最后一个元素跟堆顶交换,并不断拿当前节点a[k]和它的子节点a[2k]、a[2k+1]中较小者交换位置
sink(index){
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
// 左侧子节点值小于当前节点值,进行交换
if(this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value){
this.swap(leftIndex, index)
this.sink(leftIndex) // 继续执行下沉,直到它找到适合位置
}
// 右侧子节点值小于当前节点值,进行交换
if(this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value){
this.swap(rightIndex, index)
this.sink(rightIndex) // 继续执行下沉,直到它找到适合位置
}
}
// 交换
swap(i1, i2){
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
// 插入:堆也是数组,所以是数组尾部
insert(value){
this.heap.push(value)
this.swim(this.heap.length-1)
}
// 删除堆顶
pop(){
// 把数组的最后一位转移到头部
this.heap[0] = this.heap.pop()
this.sink(0)
}
// 获取堆顶:返回数组头部
peek(){
return this.heap[0]
}
// 获取堆大小:返回数组长度
size(){
return this.heap.length
}
}
/** 时间复杂度:O(n*logK)) 最小堆的操作为log n为map的长度 k小于等于数组的长度
* 空间复杂度:O(n) map长度为数组长度
*/
var topKFrequent = function(nums, k) {
// 设置映射关系
const map = new Map()
nums.forEach(n=>{
map.set(n, map.has(n) ? map.get(n)+1 : 1) // 如果上次已经出现过这个频率,就拿它的频率数+1
})
const h = new MinHeap()
map.forEach((value, key)=>{
h.insert({value, key})
// 超过K个就删出栈顶,不断剔除最小的元素,保障堆只有前k个高频元素
if(h.size()>k){
h.pop()
}
})
return h.heap.map(a=>a.key)
};
23. 合并K个升序链表
想拿到最小值的,可以考虑用最小堆(数组的话要遍历全部,不适合)
新链表的下一个节点一定是k个链表头中的最小节点
1)构建一个最小堆,并依次把链表头插入堆中
2)弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
3)等堆元素全部弹出,合并工作就完成了
// 最小堆:堆顶最小,都是大于等于当前值
class MinHeap{
constructor(){
this.heap = []
}
// 父结点索引
getParentIndex(i){
// return Math.floor((i-1)/2)
return (i-1) >> 1 // 二进制往右边移一位
}
// 左侧子节点
getLeftIndex(i){
return i * 2 + 1
}
// 右侧子节点
getRightIndex(i){
return i * 2 + 2
}
// 上浮:父结点值大于子节点值就要进行交换,直到父节点小于等于插入的值
swim(index){
if(index===0) return // 如果堆顶,就不上浮
const parentIndex = this.getParentIndex(index)
if(this.heap[parentIndex] && this.heap[parentIndex].val > this.heap[index].val){
this.swap(parentIndex, index)
this.swim(parentIndex) // 不断上浮
}
}
// 下沉:将最后一个元素跟堆顶交换,并不断拿当前节点a[k]和它的子节点a[2k]、a[2k+1]中较小者交换位置
sink(index){
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
// 左侧子节点值小于当前节点值,进行交换
if(this.heap[leftIndex] && this.heap[leftIndex].val < this.heap[index].val){
this.swap(leftIndex, index)
this.sink(leftIndex) // 继续执行下沉,直到它找到适合位置
}
// 右侧子节点值小于当前节点值,进行交换
if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val){
this.swap(rightIndex, index)
this.sink(rightIndex) // 继续执行下沉,直到它找到适合位置
}
}
// 交换
swap(i1, i2){
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
// 插入:堆也是数组,所以是数组尾部
insert(value){
this.heap.push(value)
this.swim(this.heap.length-1)
}
// 删除堆顶
pop(){
if(this.size()===1) return this.heap.shift()
const top = this.heap[0] // 记录堆顶
this.heap[0] = this.heap.pop() // 把数组的最后一位转移到头部
this.sink(0)
return top
}
// 获取堆顶:返回数组头部
peek(){
return this.heap[0]
}
// 获取堆大小:返回数组长度
size(){
return this.heap.length
}
}
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode[]} lists
* @return {ListNode}
*/
/** 时间复杂度:O(n*logK) n是while循环中所有链表的所有节点,
* 每一轮循环也有时间复杂度,pop/insert操作中时间复杂度是logK,堆的大大小是k
* 空间复杂度:O(k) 堆的大小是k
*/
var mergeKLists = function(lists) {
// 创建一个新链表,用于把最小元素插入新链表的next
const res = new ListNode(0)
// 指针
let p = res
// 新建最小堆,并把链表的头部节点都放到堆里
const h = new MinHeap()
lists.forEach(l=>{
// 把头部节点都插入队里
if(l) h.insert(l)
})
while(h.size()){
// 把最小值弹出来,接到新链表上,指针往下走一步
const n = h.pop()
p.next = n
p = p.next
// 最小值已经被接入链表中,那就拿它后面的链表进入插入堆里,跟其他值pk,拿到最小值继续
if(n.next) h.insert(n.next)
}
return res.next
};
根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
输入:“tree”
输出:“eert”
解释:
'e’出现两次,'r’和’t’都只出现一次。
因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。
堆排序,复杂度是最低的
es6 map 方便移除与添加
思路:1)统计出现次数 2)排序 3)输出
超级丑数
编写一段程序来查找第 n 个超级丑数。
超级丑数是指其所有质因数都是长度为 k 的质数列表 primes 中的正整数。
输入: n = 12, primes = [2,7,13,19]
输出: 32
解释: 给定长度为 4 的质数列表 primes = [2,7,13,19],前 12 个超级丑数序列为:[1,2,4,7,8,13,14,16,19,26,28,32]
质因数是约数(能被2整除 n%i ===0 )和质数(大于1的自然数中,除了1和它本身整除,不再有其他约数)
质因数也是质数,只不过任意一个数的因素
丑数:只包含因子2、3、5的正整数,比如4、10、12都是丑数,另外1也是丑数
思路:先找到任意一个正整数的质因数,再去这个质数列表primes中去查,如果在就是超级丑数
怎么算正整数的质因数是什么?
步骤:
1)先找到任意一个正整数的质因数(计算)
2)质因数是都在指定质因数范围内(查)
用最大堆、最小堆也好,堆具备快速查找,当找一个数不存在的时候特别容易,比如找100,最大堆的已经是19,就不可能找到
3)是否达到指定个数n(验证)
扩展
判断是否质数(素数)
function isPrime(num) {
if (validate(num)) {
for (let i = 2; i < num; i++) {
if (num % i == 0) {
return false
}
}
return true
}
return false
}