数据结构与算法之堆(八)

949 阅读2分钟

  • 堆是一种特殊的完全二叉树
  • 所有的节点都大于等于(最大堆),或者小于等于(最小堆)它的子节点
  • JS 中通常用数组表示堆
  • 左侧子节点的位置是 2 * ndex + 1
  • 右侧子节点的位置是 2 * index + 2
  • 父节点位置是 (index -1) / 2

堆的应用

  • 堆能高效、快速的找出最大值和最小值,因为时间复杂度是 O(1)

  • 找出第 K 个最大(小)元素

JS实现最小堆

class MinHeap {
  constructor() {
    this.heap = [];
  }
  // 交换节点的值
  swap(i1, i2) {
    [this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]];
  }
  // 获取父节点
  getParentIndex(i) {
    return Math.floor((i - 1) / 2);
    // 等价于
    // return (index - 1) >> 1;
  }
  // 获取左侧节点索引
  getLeftIndex(i) {
    return i * 2 + 1;
  }
  // 获取右侧节点索引
  getRightIndex(i) {
    return i * 2 + 2;
  }
  // 上移
  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);
    }
  }
  // 下移
  shiftDown(index) {
    const leftIndex = this.getLeftIndex(index);
    const rightIndex = this.getRightIndex(index);
    
    // 如果左子节点小于当前的值时交换并下移左节点
    if(this.heap[leftIndex] < this.heap[index]) {
      this.swap(leftIndex, index);
      this.shiftDown(leftIndex);
    }
    // 如果右子节点小于当前的值时交换并下移右节点
    if(this.heap[rightIndex] < this.heap[index]) {
      this.swap(rightIndex, index);
      this.shiftDown(rightIndex);
    }
  }
  // 插入节点
  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;
  }
}

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

解题思路

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

解题步骤

  • 构建一个最小堆,并依次把数组的值插入堆中
  • 当堆的容量超过K,就删除堆顶
  • 插入结束后,堆顶就是第 K 个最大元素
// 时间复杂度 O(n * logK) 
// 空间复杂度 O(K) K就是堆的大小

const findKthLargest = function (nums, k) {
  // 使用上面js实现的最小堆类,来构建一个最小堆
  const h = new MinHeap();
  
  nums.forEach(item => {
    h.insert(item);
    // 当堆的大小超过k,进行优胜劣汰
    if (h.size() > k) {
      h.pop();
    }
  })
	// 返回堆顶
  return h.peek();
};

LeetCode:347.前 K 个高频元素

// 时间复杂度 O(n * logn)
// 空间复杂度 O(K)

const topKFrequent = (nums, k) => {
  const map = new Map();
  // 记录元素出现的次数
  nums.forEach(n => {
    map.set(n, map.has(n) ? map.get(n) + 1 : 1);
  })
  // 依据出现次数降序排序
  const list = Array.from(map).sort((a, b) => b[1] - a[1]);
  // 截取前 k 个元素
  return list.slice(0, k).map(n => n[0]) 
}

使用堆解:

// 时间复杂度 O(n * logK)
// 空间复杂度 O(K)

const topKFrequent = (nums, k) => {
  const map = new Map();
  nums.forEach(n => {
    map.set(n, map.has(n) ? map.get(n) + 1 : 1);
  })

  const h = new MinHeap();
  map.forEach((value, key) => {
    // 由于插入的元素结构发生了变化,所以需要对最小堆的类 进行改造一下,改造上移和下移操作即可,详见后面注释内容
    h.insert({ key, value });
    if(h.size() > k) {
      h.pop();
    }
  })

  return h.heap.map(item => item.key);
}

// shiftUp(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.shiftUp(parentIndex);
//   }
// }
// shiftDown(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.shiftDown(leftIndex)
//   }

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

LeetCode:23.合并K个排序链表

解题思路:

  • 新链表的下一个节点一定是 k 个链表头中的最小节点
  • 考虑使用最小堆

解题步骤:

  • 构建一个最小堆,并依次把链表头插入堆中
  • 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
  • 等堆元素全部弹出,合并工作就完成了
// 时间复杂度 O(n * logK) K是链表,n为所有链表的节点数之和
// 空间复杂度 O(K) K是堆的大小

const mergeKLists = function(lists) {
  // 创建新链表及指针和最小堆
  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;
    // 弹出节点下一个节点插入到堆中
    if(n.next) h.insert(n.next);
  }
  // 返回新链表
  return res.next;
};

// 改造下最小堆的上移下移和删除的操作
// shiftUp(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.shiftUp(parentIndex);
//   }
// }
// shiftDown(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.shiftDown(leftIndex);
//   }
//   if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val) {
//     this.swap(rightIndex, index);
//     this.shiftDown(rightIndex);
//   }
// }

// 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;
// }