作者: sweetBoy_9126 | 来源:发表于2021-12-01 20:09 被阅读0次

1. 是什么

  • 一种特殊的完全二叉树。(完全二叉树:每层节点完全填满,最后一层若不完全填满,则只缺少右侧部分的节点)
    比如:


  • 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点。

  • js 中通常用数组表示堆。



    上面红色标记的012345就是它的索引值

  • 左侧子节点的位置是 2 * index + 1
    比如:根节点index是0, 2 * 0 + 1 是1,所以左侧子节点位置是1,3对应的左侧子节点的位置是 2 * 1 + 1 是3 ;6这个元素对应左侧子节点的位置是 2*2+1 是 5

  • 右侧子节点的位置是 2 * index + 2。
    比如:1的右侧节点 2 * 0 + 2 = 2;3的右侧节点是 2 * 1 + 2 = 4

  • 父节点位置是 (index - 1) / 2的商
    比如:5的父节点位置是 (3-1)/2=1;9的父节点是(4-1)/2 = 1

1.1 二叉堆的操作

1.1.1 插入节点

当二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。 例如插入一个新节点,值是 0。


这时,新节点的父节点5比0大,显然不符合最小堆的性质。于是让新节点“上浮”,和父节点交换位置。


继续用节点0和父节点3做比较,因为0小于3,则让新节点继续“上浮”。



继续比较,最终新节点0“上浮”到了堆顶位置。


1.1.2 删除节点

二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点。例如删除最小堆的堆顶节点1。

这时,为了继续维持完全二叉树的结构,我们把堆的最后一个节点10临时补到原本堆顶的位置。



接下来,让暂处堆顶位置的节点10和它的左、右孩子进行比较, 如果左、右孩子节点中最小的一个(显然是节点2)比节点10小,那么让节点10“下沉”。



继续让节点10和它的左、右孩子做比较,左、右孩子中最小的是 节点7,由于10大于7,让节点10继续“下沉”。

2. 堆的应用

  • 堆能高效、快速的找出最大值和最小值,时间复杂度:O(1)。
  • 找出第 K 个最大(小)元素。

2.1. 最小堆类

实现步骤

  • 在类里,声明一个数组,用来装元素。
  • 主要方法:插入、删除堆顶、获取堆顶、获取堆大小。

插入

  • 将值插入堆的底部,即数组的尾部。
  • 然后上移:将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值。
  • 大小为 k 的堆中插入元素的时间复杂度为 O(logk)
class MinHeap {
  constructor() {
    this.heap = []
  }
  getParentIndex(i) {
    return Math.floor((i-1)/2);
  }
  swap(parentIndex, index) {
    const temp = this.heap[parentIndex];
    this.heap[parentIndex] = this.heap[index];
    this.heap[index] = temp;
  }
  shiftUp(index) {
    if (index === 0) return;
    const parentIndex = this.getParentIndex(index);
    if (this.heap[parentIndex] > this.heap[index]) {
      // 交换顺序
      this.swap(parentIndex, index)
      // 递归往上查找
      this.shiftUp(parentIndex)
    }
  }
  insert(value) {
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1)
  }
}
const h = new MinHeap();
h.insert(3);
h.insert(2);
h.insert(1)

删除堆顶

  • 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)。
  • 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶。
  • 大小为 k 的堆中删除堆顶的时间复杂度为 O(logk)。
shiftDown(index) {
    const leftChildIndex = this.getLeftChildrenIndex(index);
    const rightChildIndex = this.getRightChildrenIndex(index);
    if (this.heap[leftChildIndex] < this.heap[index]) {
      this.swap(index, leftChildIndex);
      this.shiftDown(leftChildIndex);
    }
    if (this.heap[rightChildIndex] < this.heap[index]) {
      this.swap(index, rightChildIndex);
      this.shiftDown(rightChildIndex);
    }
  }
pop() {
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
  }

获取堆顶和堆的大小
获取堆顶:返回数组的头部。
获取堆的大小:返回数组的长度。

peek() {
    return this.heap[0];
  }
  size() {
    return this.heap.length;
  }

2.2. 数组中的第 K 个最大元素 leetCode 215

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

解题思路

  • 看到“第 K 个最大元素”
  • 考虑使用最小堆。

解题步骤

  • 构建一个最小堆,并依次把数组的值插入堆中。
  • 当堆的容量超过 K,就删除堆项。
var findKthLargest = function(nums, k) {
    const minHeap = new MinHeap();
    nums.forEach(num => {
        minHeap.insert(num);
        if (minHeap.size() > k) {
            minHeap.pop();
        }
    })
    return minHeap.peek();
};

时间复杂度 O(n * logK) 因为for循环里嵌套了 insert ,insert 的复杂度是 logk,空间复杂度数组里维护了一个k的长度的堆,所以是 O(k)

2.3. 前 K 个高频元素 leetCode 347

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

示例 1:

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

方法1:用字典

var topKFrequent = function(nums, k) {
    const map = new Map();
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1);
    })
    return Array.from(map).sort((a, b) => b[1] - a[1]).slice(0, k).map(n => n[0])
};

时间复杂度:使用 sort 排序的时间复杂度最优的情况下是 O(n log n),所以我们的时间复杂度是 O(n log n)
方法2: 最小堆

class MinHeap {
  constructor() {
    this.heap = []
  }
  getParentIndex(i) {
    return Math.floor((i-1)/2);
  }
  getLeftChildrenIndex(i) {
    return 2 * i + 1;
  }
  getRightChildrenIndex(i) {
    return 2 * i + 2;
  }
  swap(parentIndex, index) {
    const temp = this.heap[parentIndex];
    this.heap[parentIndex] = this.heap[index];
    this.heap[index] = temp;
  }
  shiftUp(index) {
    if (index === 0) return;
    const parentIndex = this.getParentIndex(index);
    if (this.heap[parentIndex]?.value > this.heap[index].value) {
      // 交换顺序
      this.swap(parentIndex, index)
      // 递归往上查找
      this.shiftUp(parentIndex)
    }
  }
  shiftDown(index) {
    const leftChildIndex = this.getLeftChildrenIndex(index);
    const rightChildIndex = this.getRightChildrenIndex(index);
    if (this.heap[leftChildIndex] && this.heap[leftChildIndex].value < this.heap[index].value) {
      this.swap(index, leftChildIndex);
      this.shiftDown(leftChildIndex);
    }
    if (this.heap[rightChildIndex] && this.heap[rightChildIndex].value < this.heap[index].value) {
      this.swap(index, rightChildIndex);
      this.shiftDown(rightChildIndex);
    }
  }
  insert(value) {
    this.heap.push(value);
    this.shiftUp(this.heap.length - 1)
  }
  pop() {
    this.heap[0] = this.heap.pop();
    this.shiftDown(0);
  }
  peek() {
    return this.heap[0];
  }
  size() {
    return this.heap.length;
  }
}
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function(nums, k) {
    const map = new Map();
    nums.forEach(n => {
        map.set(n, map.has(n) ? map.get(n) + 1 : 1);
    })
    const heap = new MinHeap();
    map.forEach((value, key) => {
        heap.insert({ value, key });
        if (heap.size() > k) {
            heap.pop();
        }
    })
    return heap.heap.map(h => h.key)
};

时间复杂度:map是 O(n)里面嵌套了 insert 和 pop 的复杂度是 O(logk),所以时间复杂度是 O(n log k) 因为 k 小于 o所以时间复杂度比第一种低
空间复杂度有一个map最差情况下是 O(n)

2.4. 合并 K 个升序链表 leetCode 23

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 1:

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
  1->4->5,
  1->3->4,
  2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6

解题思路:

解题步骤:


/**
 * 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}
 */
 class MinHeap {
  constructor() {
    this.heap = []
  }
  getParentIndex(i) {
    return Math.floor((i-1)/2);
  }
  getLeftChildrenIndex(i) {
    return 2 * i + 1;
  }
  getRightChildrenIndex(i) {
    return 2 * i + 2;
  }
  swap(parentIndex, index) {
    const temp = this.heap[parentIndex];
    this.heap[parentIndex] = this.heap[index];
    this.heap[index] = temp;
  }
  shiftUp(index) {
    if (index === 0) return;
    const parentIndex = this.getParentIndex(index);
    if (this.heap[parentIndex]?.val > this.heap[index]?.val) {
      // 交换顺序
      this.swap(parentIndex, index)
      // 递归往上查找
      this.shiftUp(parentIndex)
    }
  }
  shiftDown(index) {
    const leftChildIndex = this.getLeftChildrenIndex(index);
    const rightChildIndex = this.getRightChildrenIndex(index);
    if (this.heap[leftChildIndex]?.val < this.heap[index]?.val) {
      this.swap(index, leftChildIndex);
      this.shiftDown(leftChildIndex);
    }
    if (this.heap[rightChildIndex]?.val < this.heap[index]?.val) {
      this.swap(index, rightChildIndex);
      this.shiftDown(rightChildIndex);
    }
  }
  insert(value) {
    this.heap.push(value);
    this.shiftUp(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.shiftDown(0);
    return top;
  }
  peek() {
    return this.heap[0];
  }
  size() {
    return this.heap.length;
  }
}
var mergeKLists = function(lists) {
    const res = new ListNode(0);
    let p = res;
    const h = new MinHeap();
    lists.forEach(l => {
        if (l) {
            h.insert(l)
        }
    });
    console.log(h.heap)
    while(h.size()) {
        const n = h.pop();
        // 把新的链表的头的下一项放到堆里继续比较
        if (n.next) h.insert(n.next)
        p.next = n;
        p = p.next
    }
    return res.next;
};

相关文章

  • 堆 - 堆的应用

    堆有三个典型的应用场景:实现优先队列、求 Top K 、求中位数 实现优先队列 优先队列:队列的性质是先进先出,但...

  • 二叉堆是一棵满二叉树,父节点的值大于子节点。 用数组存储二叉堆,假如一个节点的下标为k,那么这个节点的左孩子就是2...

  • 应用: 排序,从小到大用最大堆,从大到小用最小堆 选出元素中的 top k 个top k 个最小数:数组前k个元素...

  • 完全二叉树 二叉堆 二叉堆有最大堆和最小堆的区别,最大堆只保证每个节点的父节点大于当前节点,但不保证上一层节点的值...

  • 堆的定义: n个元素序列{k1,k2,...,ki,...,kn},当且仅当满足下列关系时称之为堆: (ki...

  • http://bubkoo.com/2014/01/14/sort-algorithm/heap-sort/ 1 ...

  • 堆 …

    南山南,北山北,南山有谷堆,北山有花蕾,山坡下,大道中,野树停在石堆,秋风送,冷雪飘,旅途空旷叶儿飞,时间漫,皱纹...

  • 题目:100w个数中找出最大的100个。 维护一个100个元素的小根堆即可。 或者直接维护一个用来存储当前最大的1...

网友评论

      本文标题:

      本文链接:https://www.haomeiwen.com/subject/fpfexrtx.html