数据结构-堆

537 阅读3分钟

堆,是一种特殊的完全二叉树。 他要求他的子节点必须大于等于或小于等于父节点。且每个二叉树都必须填满,或者最后的右子树可以为空。同时,堆又分为最小堆和最大堆。下图就是典型的最大堆。

image.png

堆在js中的表示方式

image.png

在js中,我们通常使用数组去表示一个堆。 数组中的元素顺序,遵循广度优先遍历分别推入数组。于是,我们得出几个数组表示堆的规律。

  • 左侧节点的位置是 2 * parentIndex + 1
  • 右侧节点的位置是 2 * (parentIndex + 1)
  • 父节点的位置是 (parentIndex - 1) / 2

构建最小堆类

因为js没有直接提供堆这个数据结构,所以我们可以使用一个构造函数去生成堆。我们需要去实现

  • insert插入
  • remove移除堆顶
  • pop移除堆尾
  • size获取堆的大小

insert实现

这里,我们要实现insert,可以先将元素插入到堆底,然后跟父节点进行不断的比较,同时交换元素,直到元素满足最小堆的条件,跳出递归。

class MinHeap{
    constructor() {
        this.heap = [];
    }
    
    getParentIndex(childIndex) {
        return Math.floor((childIndex  - 1) / 2);
    }
    
    swap(idx1, idx2) {
        const temp = this.heap[idx1];
        this.heap[idx1] = this.heap[idx2];
        this.heap[idx2] = temp;
    }
    
    shiftUp(childIndex) {
        if (childIndex === 0) { // 当元素处于堆顶时,已经不能再往上移动了
            return ;
        }
        const parentIndex = this.getParentIndex(childIndex);// 获取父节点的index
        const parent = this.heap[parentIndex];
        const child = this.heap[childIndex];
        if (parent > child) {
            this.swap(childIndex, parentIndex);// 比较交换
            this.shiftUp(parentIndex); // 递归交换,直到插入元素满足最小堆的条件
        }
    }
    
    insert(item) {
        this.heap.push(item); // 先在堆底推入一个元素
        this.shiftUp(this.heap.length - 1); // 根据大小进行交换,使堆满足最小堆的条件
    }

}

remove实现

这里要移除堆顶,如果直接移除堆顶元素,会导致堆结构混乱,所以,我们采取的思路是将堆底的元素移除,同时替换掉堆顶。然后根据父节点与子节点的大小关系去递归的调整父子节点的index。

class MinHeap{
    constructor() {
        this.heap = [];
    }
    
    swap(idx1, idx2) {
        const temp = this.heap[idx1];
        this.heap[idx1] = this.heap[idx2];
        this.heap[idx2] = temp;
    }
    
    getLeftChildIndex(parentIndex) {
        return 2 * parentIndex + 1;
    }
    
    getRightChildIndex() {
        return 2 * (parentIndex + 1);
    }
     
    shiftDown(parentIndex) {
        if (parentIndex === this.heap.length) {return;} // 等于堆底时已经无法下移
        const parent = this.heap[parentIndex];
        const leftChildIndex = this.getLeftChildIndex(parentIndex);
        const rightChildIndex = this.getRightChildIndex(parentIndex);
        const leftChild = this.heap[leftChildIndex];
        const rightChild = this.heap[rightChildIndex];
        
        if (leftChild < parent) {
            this.swap(parentIndex, leftChildIndex);
            this.shiftDown(leftChildIndex);
        }
        if (rightChild < parent) {
            this.swap(parentIndex, rightChildIndex);
            this.shiftDown(rightChildIndex);
        }
    }
    
    remove() {
        this.heap[0] = this.heap.pop(); 
        this.shiftDown(0);
    }

}

pop移除堆尾

这里可以直接移除堆尾即可。

class MinHeap{
    constructor() {
        this.heap = [];
    }
    
    pop() {
        return this.heap.pop();
    }

}

size 获取堆的大小

class MinHeap{
    constructor() {
        this.heap = [];
    }
    
    get size() {
        return this.heap.length;
    }

}

堆的应用

堆能高效的找出最大值和最小值。因为只要堆构建完成,最大值或者最小值就是堆顶。或者找出数组中第k个大的元素。

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

image.png

解题思路:

通常,这种第k个最大元素我们都可以考虑使用堆这个数据结构去完成。因为第k个最大元素或者最小元素。实际上就是我们构建一个堆。 然后不断的删除堆顶。当堆大小大于k时我们就删除,直至堆大小等于k。这时,堆顶元素就是第k个大的元素。

class MinHeap {
  constructor() {
    this.heap = [];
  }

  get size() {
      return this.heap.length;
  }

  getParentIndex(index) {
    return Math.floor((index - 1) / 2);
  }

  swap(index1, index2) {
    const temp = this.heap[index1];
    this.heap[index1] = this.heap[index2];
    this.heap[index2] = temp;
  }

  shiftUp(index) {
    // 拿到父节点
    if (index === 0) {
      return;
    }
    const parentIndex = this.getParentIndex(index);
    if (this.heap[parentIndex] > this.heap[index]) {
      this.swap(index, parentIndex);
      this.shiftUp(parentIndex);
    }
  }

  insert(item) {
    this.heap.push(item);
    this.shiftUp(this.heap.length - 1);
  }

  /**
   * @description 获取左侧子节点的index
   */
  getLeftIndex(parentIndex) {
    return 2 * parentIndex + 1;
  }

  getRightIndex(parentIndex) {
    return 2 * (parentIndex + 1);
  }


  shiftDown(index) {
    if (index === this.heap.length) {
      // 已经到堆底了,没有向下交换的意义了
      return;
    }
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);

    if (this.heap[index] > this.heap[leftIndex]) {
      this.swap(index, leftIndex);
      this.shiftDown(leftIndex);
    }

    if (this.heap[index] > this.heap[rightIndex]) {
      this.swap(index, rightIndex);
      this.shiftDown(rightIndex);
    }
  }

  /**
   * @description 移除堆顶元素
   */
  remove() {
    this.heap[0] = this.heap.pop(); // 移除尾部并用尾部元素替换堆顶元素。
    this.shiftDown(0);
  }

  
}

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    const heap = new MinHeap();
    nums.forEach(item => {
        heap.insert(item);
        if (heap.size > k) {
            heap.remove();
        }
    })
    
    return heap.heap[0];
};


当然,我们也可以走走捷径 - - 。

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    return nums.sort((prev, curr) => curr - prev)[k - 1];
};

347. 前 K 个高频元素

image.png

代码思路:

  1. 用字典记录每个元素的出现次数。
  2. 将记录的字典值逐个推入到一个最小堆中, 堆的比较数据使用字典记录的出现次数。
  3. 不断移除堆顶元素,使得最小堆的长度等于k。这时剩下的就是前k个大小的了。

代码实现:

class MinHeap {
  constructor() {
    this.heap = [];
  }

  getParentIndex(index) {
    return Math.floor((index - 1) / 2);
  }

  swap(index1, index2) {
    const temp = this.heap[index1];
    this.heap[index1] = this.heap[index2];
    this.heap[index2] = temp;
  }

  shiftUp(index) {
    // 拿到父节点
    if (index === 0) {
      return;
    }
    const parentIndex = this.getParentIndex(index);
    if (this.heap[parentIndex].value > this.heap[index].value) {
      this.swap(index, parentIndex);
      this.shiftUp(parentIndex);
    }
  }

  insert(item) {
    this.heap.push(item);
    this.shiftUp(this.heap.length - 1);
  }

  /**
   * @description 获取左侧子节点的index
   */
  getLeftIndex(parentIndex) {
    return 2 * parentIndex + 1;
  }

  getRightIndex(parentIndex) {
    return 2 * (parentIndex + 1);
  }

  shiftDown(index) {
    if (index === this.heap.length) {
      // 已经到堆底了,没有向下交换的意义了
      return;
    }
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);

    if (this.heap[index].value > this.heap[leftIndex].value) {
      this.swap(index, leftIndex);
      this.shiftDown(leftIndex);
    }

    if (this.heap[index].value > this.heap[rightIndex].value) {
      this.swap(index, rightIndex);
      this.shiftDown(rightIndex);
    }
  }

  get size() {
    return this.heap.length;
  }

  /**
   * @description 移除堆顶元素
   */
  remove() {
    this.heap[0] = this.heap.pop(); // 移除尾部并用尾部元素替换堆顶元素。
    this.shiftDown(0);
  }
}

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var topKFrequent = function (nums, k) {
  const m = new Map();
  nums.forEach((item) => {
    m.set(item, m.has(item) ? m.get(item) + 1 : 1);
  });
  const h = new MinHeap();
  m.forEach((value, key) => {
    h.insert({ value, key });
    if (h.size > k) {
      h.remove();
    }
  });
  return h.heap.map(item => item.key);
};