面试一个大厂,让我实现最小堆的的两个功能:
- insert:插入元素,保持最小堆特性
- pop:推出第一个最小的元素
首先,堆是一种二叉树结构,但经过了排序,最小的元素永远是根元素,且对所有子树都成立。
但是对于每一个树,其左右子节点的大小则并不一定是排序的,也就是说左子节点和右子节点谁大谁小不一定。且左右子节点的值可能等于根节点。
根据 [1] 和观摩其他大神写的最小堆,实现最小堆不需要显式的实现 Node,因为堆总是向左填充(维持排序),其数据结构一直被完好的维持。若使用显式节点实现会破坏结构也会影响性能。
实现如下
MInheapSource.h:
1 #pragma once 2 #ifndef MINHEAPSOURCE_H 3 #define MINHEAPSOURCE_H 4 5 template<class T> // 可使用多种数据类型作为模板 6 class MinHeap { 7 public: 8 MinHeap(int maxSize); // 构造空最小堆 9 MinHeap(T* arr, int size); // 从数组中构造堆 10 MinHeap(MinHeap& m); // 从其他最小堆构造 11 ~MinHeap(); 12 13 // 节点位置计算 14 inline int left(int index) { return 2 * index + 1; } 15 inline int right(int index) { return 2 * index + 2; } 16 inline int parent(int index) { return (index - 1) / 2; } 17 18 // 返回目前MinHeap大小 19 inline int size(){ return _currentSize + 1 ;} 20 bool isEmpty(); 21 bool isFull(); 22 void insert(const T& t); 23 bool pop(T& result); // 推出第一个最小元素 24 25 T* getMinHeap(); 26 // 显式数组 27 void print(); 28 private: 29 T* _minheap = nullptr; // 堆本体 30 int _currentSize; // 目前大小 31 int _maxSize; // 最大容量 32 void shiftDown(const int index, const int end); // 向下调整堆以维持堆的层级 33 void shiftUp(int index); // 向上调整堆以维持堆的层级 34 }; 35 36 #endif
MInheapSource.cpp:
1 #include "MinHeapSource.h" 2 #include <iostream> 3 4 template<class T> 5 MinHeap<T>::MinHeap(int maxSize) { 6 _maxSize = maxSize; 7 _minheap = new T[_maxSize]; 8 if (_minheap == nullptr) { 9 std::cerr << "MinHeap Memory allocation failed" << std::endl; 10 exit(-1); 11 } 12 _currentSize = 0; 13 } 14 15 template<class T> 16 MinHeap<T>::MinHeap(T* arr, int size) { 17 _maxSize = size; 18 _minheap = new T[_maxSize]; 19 if (_minheap == nullptr) { 20 std::cerr << "MinHeap Memory allocation failed" << std::endl; 21 exit(-1); 22 } 23 // 逐个赋值 24 _currentSize = size; 25 for (int i = 0; i < _currentSize; ++i) { 26 _minheap[i] = arr[i]; 27 } 28 29 // 利用下滑算法形成最小堆 30 // NOTE: 以为curPos指向的时下标,所以要减2 31 int curPos = (_currentSize - 2) / 2; 32 while (curPos >= 0) { 33 shiftDown(curPos, _currentSize - 1); 34 --curPos; 35 } 36 } 37 38 template<class T> 39 MinHeap<T>::MinHeap(MinHeap& m) { 40 _minheap = new T[_maxSize]; // 申请新的空间 41 if (_minheap == nullptr) { 42 std::cerr << "MinHeap Memory allocation failed" << std::endl; 43 exit(-1); 44 } 45 for (int i = 0; i < _maxSize; ++i) 46 *_minheap[i] = *m._minheap[i]; 47 _currentSize = m._currentSize; 48 _maxSize = m._maxSize; 49 } 50 51 template<class T> 52 MinHeap<T>::~MinHeap() { 53 if(_minheap != nullptr) 54 delete []_minheap; 55 } 56 57 template<class T> 58 bool MinHeap<T>::isEmpty() { 59 return _currentSize == 0; 60 } 61 62 template<class T> 63 bool MinHeap<T>::isFull() { 64 return _currentSize == _maxSize; 65 } 66 67 template<class T> 68 T* MinHeap<T>::getMinHeap() { 69 return _minheap; 70 } 71 72 template<class T> 73 void MinHeap<T>::insert(const T& t) { 74 if (isFull()) { 75 std::cerr << "the MinHeap is full, can't insert more element" << std::endl; 76 return; 77 } 78 _minheap[_currentSize] = t; 79 shiftUp(_currentSize); // 自下而上调整最小堆 80 ++_currentSize; 81 } 82 83 template<class T> 84 bool MinHeap<T>::pop(T& result) { 85 if (_currentSize > _maxSize) { 86 std::cerr << "the MinHeap is empty, no element to pop" << std::endl; 87 return false; 88 } 89 result = _minheap[0]; 90 // 将当前的第一个元素替换为最后一个元素,然后自上而下调整为最小堆 91 _minheap[0] = _minheap[_currentSize - 1]; 92 _currentSize--; 93 shiftDown(0, _currentSize - 1); // 自上而下调整为最小堆 94 return true; 95 } 96 97 // 自下而上调整最小堆(修复 insert) 98 template<class T> 99 void MinHeap<T>::shiftUp(int index) { 100 if (isEmpty()) return; 101 102 int current = index; 103 int parentC = parent(current); 104 // 若抵达顶点,返回 105 if (current <= 0) return; 106 // 若父节点大于当前节点时,递归交换 107 if (_minheap[parentC] > _minheap[current]) { 108 int temp = _minheap[parentC]; 109 _minheap[parentC] = _minheap[current]; 110 _minheap[current] = temp; 111 shiftUp(parentC); 112 } 113 } 114 115 // 自上而下调整最小堆(修复 pop) 116 template<class T> 117 void MinHeap<T>::shiftDown(const int index, const int end) { 118 if (isEmpty()) return; 119 120 int current = index; // 保留当前索引 121 int leftC = left(current); 122 123 // 若左子节点或右子节点超出范围,返回 124 if (leftC > end) 125 return; 126 127 // 比较左右子节点(主要和左子节点作比较) 128 if (leftC < end && _minheap[leftC] > _minheap[leftC + 1]) 129 ++leftC; 130 131 // 若当现结点大于左子节点时,递归交换 132 if (_minheap[current] > _minheap[leftC]) { 133 int temp = _minheap[current]; 134 _minheap[index] = _minheap[leftC]; 135 _minheap[leftC] = temp; 136 shiftDown(leftC, end); 137 } 138 } 139 140 template<class T> 141 void MinHeap<T>::print() { 142 for (int i = 0; i < _currentSize; ++i) { 143 std::cout << _minheap[i] << " "; 144 } 145 std::cout << std::endl; 146 }
主程序:
1 // MinHeap.cpp : This file contains the 'main' function. Program execution begins and ends there. 2 // 3 #include "MinHeapSource.h" 4 #include "MinHeapSource.cpp" // include source file because the template class has to include definition explict in the main 5 #include <iostream> 6 7 using namespace std; 8 9 int main() { 10 11 int a[] = { 9, 6, 5, 4, 3, 2, 1 }; 12 MinHeap<int> heap(a, 7); 13 14 cout << "Init the heap" << endl; 15 heap.print(); 16 17 int p; 18 heap.pop(p); 19 cout << "Pop smallest element: " << p << endl; 20 heap.print(); 21 22 cout << "Insert 10" << endl; 23 heap.insert(10); 24 heap.print(); 25 26 return 0; 27 } 28 29 /* 30 运行结果: 31 Init the heap 32 1 3 2 4 6 9 5 33 Pop smallest element: 1 34 2 3 5 4 6 9 35 Insert 10 36 2 3 5 4 6 9 10 37 */
注:
1)为什么要下滑和上滑(ShiftDown & ShiftUp)?
这主要是因为堆结构在插入(insert)和删除最小元素(pop)操作时会破坏堆的结构,所以需要下滑和上滑两个操作。下滑用于修复插入后堆元素的破坏,上滑用于修复删除最小元素时对堆元素的破坏。[3]
参考资料:
[1] 实现指引:https://www.coder.work/article/2752123
[2] 实现参考:https://blog.csdn.net/qq_37623612/article/details/88696924
[3] 为什么要ShiftDown & ShiftUp:https://mingshan.fun/2019/05/14/heap/