数据结构与算法(Dart)之堆(六)

825 阅读6分钟

堆(heap)是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:

  • 堆中某个结点的值总是不大于不小于父结点的值;
  • 堆总是一棵完全二叉树

根结点最大的堆叫做最大堆大根堆根结点最小的堆叫做最小堆小根堆

截屏2024-01-24 下午7.35.55.png

数组实现堆

对于一个完全二叉树来说,用数组来存储是非常节省存储空间的。因为我们不需要存储指向左右子节点的指针,单纯的通过数组下标就可以找到一个节点的左右子节点和父节点。如下图所示。

截屏2024-01-24 下午7.52.32.png

对于数组中下标为 n 的节点来说, 这里的 n 从 1 开始算, 那么:

  • 左子节点的索引是 2n
  • 右子节点的索引是 2n+1
  • 父节点的索引满足 n/2

建堆

建一个大顶堆

我们从最后一个非叶子节点开始,从上往下进行调整、堆化:

插入数据

  • 让新插入的节点(也就是当前数组插入的最后一个节点)和其父节点对比大小。如果满足子节点大于等于父节点,就互换两个节点。一直重复这个过程,直到满足要求。

删除堆顶元素

  • 首先把堆的最后一个元素和堆顶元素互换位置,然后删除最后一个元素。
  • 然后从堆顶开始, 从上往下调整,直到父子节点满足大小关系为止

插入数据删除堆顶数据的主要逻辑就是堆的调整,所以往堆里插入一个元素删除堆顶元素的时间复杂度是O(logN)

class Heap {
  /// 堆: 实现大根堆
  int _capacity = 0; // 堆的最大容量

  late List _list; // 存储元素的数组

  int _length = 0; // 长度

  Heap({required int capacity}) {
    _capacity = capacity;
    _list = List.filled(capacity, null);
  }

  /// 添加元素, 并堆化
  /// 为了最大性能的存储堆, 并保证堆中节点与数组下标的关系, 从下标1开始存储数据, 所以这里是 ++_length, 0位置不存储数据
  /// 时间复杂度: O(logN)
  add(item) {
    if (isFull()) throw StackOverFlowException();

    /// 从数组末尾插入, 然后开始堆化(heapify), 这里是最大堆,我们从下往上堆化
    _list[++_length] = item;
    _bubbleUp(_length);
  }

  /// 上浮算法, 使索引k处的元素处于一个正确的位置
  void _bubbleUp(int index) {
    /// 从下往上堆化
    /// 循环, 如果当前节点元素 < 两个子节点元素中的较大值, 那么交换
    /// 堆化的过程就是:用当前节点元素与父节点元素做比较, 大于父节点元素就交换,直到小于父节点元素为止
    /// 到根节点截止, 根节点的索引是1
    while (index > 1) {
      /// 父节点的索引
      var parentIndex = index ~/ 2;

      /// 当前节点元素与父节点元素做比较, 大于父节点元素就交换
      if (moreThan(index, parentIndex)) {
        /// 交换
        exchange(index, parentIndex);
        index = parentIndex;
      } else {
        break;
      }
    }
  }

  /// 删除、返回堆顶元素, 并重新排列堆
  /// 时间复杂度: O(logN)
  removeFirst() {
    if (isEmpty()) throw StackEmptyException();

    /// 获取堆顶元素
    var result = _list[1];

    /// 交换索引1处元素和最大索引处的元素, 让完全二叉树最右侧的元素变为临时根结点
    /// 交换后, 数组最后一个元素, 就是交换前的堆顶元素
    exchange(1, _length);

    /// 删除交换后的最大索引处的元素
    _removeLast();

    /// 通过下沉算法,重新排列堆
    _bubbleDown(1);
    return result;
  }

  /// 下沉算法: 从上往下调整
  _bubbleDown(int index) {
    /// 循环比较当前k节点和其 (左子节点2*k 以及 右子节点2*k+1处的较大值的) 元素大小
    /// 当前节点元素小,则交换与子节点元素中最大值的位置
    var leftChildIndex = index * 2;

    /// 左子节点索引: k * 2
    /// 右子节点索引: k * 2 + 1

    while (leftChildIndex <= _length) {
      /// 记录当前节点的左右子节点元素中的最大值索引
      int maxChildIndex = 0;

      int rightChildIndex = 2 * index + 1;

      if (rightChildIndex <= _length) {
        /// 当前k节点有右叶子节点, 取左右子节点元素中的较大者索引
        maxChildIndex = moreThan(leftChildIndex, rightChildIndex)
            ? leftChildIndex
            : rightChildIndex;
      } else {
        /// 当前k节点只有左子节点
        maxChildIndex = leftChildIndex;
      }

      // 当前k节点的值大于 (左右子节点中最大节点的值), 则不需要交换, 循环结束
      if (moreThan(index, maxChildIndex)) {
        break;
      }

      /// k节点的值小于maxChildIndex的值,则需要继续交换
      exchange(index, maxChildIndex);

      /// 交换k的值, 重新标记左子节点
      index = maxChildIndex;
      leftChildIndex = index * 2;
    }
  }

  /// 移除、返回最后一个元素/节点
  _removeLast() {
    if (isEmpty()) throw StackEmptyException();
    var last = _list[_length];
    _list[_length] = null;

    /// 索引减1
    _length--;
    return last;
  }

  /// 排序算法: 从小到大排序
  /// 时间复杂度: O(NlogN)
  sort() {
    /// 循环交换1索引处和排序的元素中最大的索引处的元素

    while (_length != 1) {
      /// 交换元素
      exchange(1, _length);

      /// 索引减1
      _length--;

      /// 堆索引1处的元素继续下沉
      _bubbleDown(1);
    }

    return _list;
  }

  /// 判断索引i处的元素 < 索引j处的元素
  bool moreThan(int i, int j) {
    /// print(1.compareTo(2)); // => -1
    /// print(2.compareTo(1)); // => 1
    /// print(1.compareTo(1)); // => 0
    return _list[i].compareTo(_list[j]) > 0;
  }

  /// 交换数组, 下标为 i 和 j 的元素
  void exchange(int i, int j) {
    var temp = _list[i];
    _list[i] = _list[j];
    _list[j] = temp;
  }

  toList() {
    return _list;
  }

  bool isEmpty() {
    /// 判断队列是否为空
    return _length == 0;
  }

  bool isFull() {
    /// 判断队列是否满
    return _length == _capacity;
  }
}

class StackOverFlowException implements Exception {
  const StackOverFlowException();
  String toString() => 'StackOverFlowException';
}

class StackEmptyException implements Exception {
  const StackEmptyException();
  String toString() => 'StackEmptyException';
}

void main() {
  Heap _queue = Heap(capacity: 18);

  /// 添加数据
  _queue.add(17);
  _queue.add(21);
  _queue.add(13);
  _queue.add(15);
  _queue.add(33);
  _queue.add(9);
  _queue.add(2);
  _queue.add(16);
  _queue.add(5);
  _queue.add(7);
  _queue.add(3);
  _queue.add(6);
  _queue.add(8);
  _queue.add(1);

  // print('HeapQueue, 堆顶, first: ${_queue.first}');

  // print('HeapQueue, _queue: $_queue');

  _queue.add(35);
  _queue.add(12);
  // print('HeapQueue, _queue: $_queue');
  _queue.add(3);

  print('HeapQueue, toList: ${_queue.toList()}\n');

  print('HeapQueue, go remove first');

  /// 移除堆顶元素, 重新堆化
  var first = _queue.removeFirst();
  print('HeapQueue, first: $first');
  print('HeapQueue, toList: ${_queue.toList()}\n');

  first = _queue.removeFirst();
  print('HeapQueue, first: $first');
  print('HeapQueue, toList: ${_queue.toList()}\n');

  /// 堆排序
  var sortList = _queue.sort();
  print('HeapQueue, sortList: ${sortList}\n');
}

堆排序

  • 堆顶的数据与堆尾数据进行交换,这样就达到排好最大值的效果
  • 将排好的数字不再纳入建堆中【即建堆的目的就是选出最大值,所以已经选出来的,就无需要再参加】
  • 并将交换后产生新的树进行重新建堆【因为此时只有根节点发生变化,而左、右子树的关系并没有改变,所以可以利用向下调整算法进行建堆】
  • 重复上述步骤即可,直至里只剩下一个元素为止

时间复杂度: O(NlogN)

  /// 排序算法: 从小到大排序
  sort() {
    /// 循环交换1索引处和排序的元素中最大的索引处的元素

    while (_length != 1) {
      /// 交换元素
      exchange(1, _length);

      /// 索引减1
      _length--;

      /// 堆索引1处的元素继续下沉
      _bubbleDown(1);
    }

    return _list;
  }

参考资料

堆排序原理及其应用场景

“堆”还能这样用_堆的应用

数据结构与算法之堆

深入数据结构:“堆”

数据结构与算法(七)堆

数据结构与算法(java):堆

使用 Swift 实现基于堆的优先级队列

Priority Queue using Binary Heap