(二)数据结构-堆及其应用

166 阅读2分钟

堆分为大顶堆和小顶堆,对数组中topk的问题进行求解,以及堆排序,通过堆广的应用等

1.求数组的topk问题(堆解决)

  • 求最大的topk,建立小顶堆( 每次和小顶堆比较,大于小顶堆替换堆顶元素,并调整堆 ,堆的大小始终k,是所有topk中要返回的数据)
  • 求最小的topk,建立大顶堆

解决思路(求数组中的最小的前k个数)

方案1:直接遍历在排序去前k个(时间复杂度nlogn)

方案2:构建堆排序

  • 把前k个数构建一个大顶堆
  • 从第k个数开始,和大顶堆的最大值进行比较,若比最大值小,交换两个数的位置,重新构建大顶堆
  • 一次遍历之后大顶堆里的数就是整个数据里最小的k个数。

时间复杂度为

O(n)+O(nlogk) = O(nlogk)

这里只设计堆相关的实现

/**
 * 求数组中topk
 */
function swap(arr, i, j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp
}
//插入元素向上调整
function moveUp(arr, index) {
  let parent = parseInt((index - 1) / 2);
  while (parent > -1 && arr[parent] < arr[index]) {
    swap(arr, parent, index);
    index = parent;
    parent = parseInt((index - 1) / 2)
  }
}
// 向下调整
function moveDown(arr, index, k) {
  let left = 2 * index + 1;
  //保证不会越界
  while (left < k) {
    //取左右孩子的最大值
    let larget = (left + 1) < k && arr[left] < arr[left + 1] ? left + 1 : left;
    //求父元素和子元素的最大值
    larget = arr[index] > larget ? index : larget;
    if (larget == index) {
      break
    }
    swap(arr, index, larget);
    index = larget;
    left = 2 * index + 1

  }
}

function topMink(arr, k) {
  if (!arr || k > arr.length) return null;
  for (let i = 0; i < arr.length; i++) {
    if (i < k) {
      //构建大顶堆
      moveUp(arr, i);
    } else {
      //当大于arr[0],交换并向下调整
      if (arr[0] < arr[i]) {
        swap(arr, 0, i);
        moveDown(arr, 0, k)
      }
    }
  }
  return arr.slice(0, k).reverse()
}

var arr = [5, 3, 1, 4, 7]
console.log(topMink(arr, 3));

扩展(nlogK)

当求解字符串中,出现频率最高的topk,依旧可以使用该方法

  • 先整体遍历保存在map中(key:value),key表示的是字符,value表示出现的次数,
  • 遍历map根据value的值构建小顶堆,(这里通过Object.entries(map)可以获取每一项,在通过item[0],item[1]分别获取键和值)
  • 当前的value>小顶堆的元素,就覆盖小顶堆,调整小顶堆,首次遍历(i<k)直接进堆(注意堆的大小始终是k,存放的是当前topk)
  • 最后返回堆中的所有的key就是词频最高的topk 这里主要是leetcode中通过reduce的解法
function topKFrequent(words, k) {
  return Array.from(words.reduce((map, word) => map.set(word, (map.get(word) || 0) + 1), new Map()))
    .sort((a, b) => b[1] - a[1] === 0 ? a[0].localeCompare(b[0]) : b[1] - a[1])
    .slice(0, k)
    .map(a => a[0])

}

2.堆排序(这里的堆从0开始计算)

  • 堆:将数组看成是一个完全二叉树,堆分为大顶堆和小顶堆.

    父子元素之间的关系

    父元素:(i-1)/2

    左孩子:2i+1<n

    右孩子:2i-1<n,

    这里的i是数组的下标值,当他们小于最后数组的长度表示子节点存在没有越界

  • 堆结构非常重要

    • 堆结构的插入heapInsert和调整heapify
    • 堆结构的增大和减小
    • 建立堆的过程,时间复杂度为O(N)
    • 优先级队列结构就是堆结构

思路

  • 通过遍历,边插入边调整堆
  • 交换堆的最大值和最后一个元素,将调整范围-1,调整堆,
  • 每次将最大值放在数组的末尾,直到调整范围变为0,此时数组中所有的元素都有序

复杂度

  • 时间:O(nlogn)
  • 空间:O(1)
  • 不稳定

注意

  • 堆在调整的过程中的时间复杂度就是logN,在不断的调整二叉树结构;插入N
  • 通过堆就可以实现一个优先级队列

image.png

function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}
//注意这里采用的是大顶堆
//向上调整,插入操作
function insert(arr, index) {
  let parent = Math.floor((index - 1) / 2);
  while (parent > -1 && arr[index] > arr[parent]) {
    swap(arr, index, parent);
    index = parent;
    parent = Math.floor((index - 1) / 2);
  }
}
//向下调整
function heapfy(arr, index, heapsize) {
  let left = 2 * index + 1;
  while (left < heapsize) {
    let largest = (left + 1) < heapsize && arr[left + 1] > arr[left] ? left + 1 : left;
    largest = arr[largest] > arr[index] ? largest : index;
    if (largest == index) break;
    swap(arr, largest, index);
    index = largest;
    left = 2 * index + 1;
  }
}

function heapSort(arr) {
  if (!arr || arr.length < 1) return;
  for (let i = 0; i < arr.length; i++) {
    insert(arr, i)
  }
  let heapsize = arr.length;
  swap(arr, 0, --heapsize);
  while (heapsize) {
    heapfy(arr, 0, heapsize);
    swap(arr, 0, --heapsize)
  }
}

let arr = [4, 3, 6, 7, 2, 1, 8]
heapSort(arr);
console.log(arr)

3.补充:通过堆实现优先级队列

  • 这里通过堆来实现优先级队列(可以通过数组实现也可以通过链表实现)

  • 堆在实现的过程中都是通过数组来实现

    • 优先队列插入都是先插到最后的新结点位置,数组的话直接插到最后就行,不需要记录最后结点的位置。
    • 因为完全二叉树的结构,我们可以通过当前结点的索引来计算父结点的索引位置
  • 主要的应用场景

    • 数据的topk

在调整过程的时间复杂度O(logn)树的高度

/**
 * 在堆的基础上进行修改
 */
function PriorityQueue(type = 'max') {
  this.queue = [];
  this.type = type;//min||max
}
function swap(arr, i, j) {
  let temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp
}
//入队列,也就是向上调整
PriorityQueue.prototype.moveUp = function () {
  let index = this.queue.length - 1;
  let parent = parseInt((index - 1) / 2);
  if (this.type == 'max') {
    while (parent > -1 && this.queue[index] > this.queue[parent]) {
      swap(this.queue, index, parent);
      index = parent;
      parent = parseInt((index - 1) / 2)
    }
  } else if (this.queue == 'min') {
    while (parent > -1 && this.queue[index] < this.queue[parent]) {
      swap(this.queue, index, parent);
      index = parent;
      parent = parseInt((index - 1) / 2)
    }
  }
}

//向下调整
PriorityQueue.prototype.moveDown = function () {
  let index = 0;
  let length = this.queue.length;
  let left = 2 * index + 1;
  if (this.type == 'max') {
    while (left < length) {
      let largest = (left + 1) < length && this.queue[left + 1] > this.queue[left] ? left + 1 : left;
      largest = this.queue[index] > this.queue[largest] ? index : largest;
      if (largest == index) break;
      swap(this.queue, index, largest)
      index = largest;
      left = 2 * index + 1;
    }
  } else if (this.type == 'min') {
    while (left < length) {
      let min = (left + 1) < length && this.queue[left + 1] > this.queue[left] ? left : left + 1;
      min = this.queue[min] > this.queue[index] ? index : min;
      if (min == index) break;
      swap(this.queue, min, index);
      index = min;
      left = 2 * index + 1
    }
  }
}

PriorityQueue.prototype.enqueue = function (data) {
  this.queue.push(data);
  this.moveUp();
}

PriorityQueue.prototype.dequeue = function () {
  if (!this.queue.length) return null;
  let res = this.queue.shift();
  //用最后一个元素插入头部
  this.queue.unshift(this.queue.pop())
  //向下调整
  this.moveDown();
  return res
}

const p = new PriorityQueue(type = 'max');
p.enqueue(5);
p.enqueue(10);
console.log(p)
p.dequeue();
console.log(p)