因为需精确控制内存布局、避免动态分配或理解堆底层逻辑,手写数组可访问内部、复用缓冲区、支持原地建堆;其核心是维护最大堆,插入时sift_up、弹出时sift_down以保持堆序性。

为什么不用 std::priority_queue 而要手写数组实现
因为需要精确控制内存布局、避免动态分配(比如嵌入式或实时系统),或者想理解堆结构底层逻辑。标准库的 std::priority_queue 底层就是用 std::vector + 堆算法实现的,但封装后无法直接访问内部数组、无法复用已有缓冲区、也不支持原地建堆(in-place heapify)等操作。
手写数组实现的核心是维护一个**最大堆**(或最小堆)结构:父节点索引为 (i-1)/2,左子为 2*i+1,右子为 2*i+2。所有操作必须严格满足堆序性。
如何用普通数组完成插入和弹出
插入新元素后从底向上 sift_up,弹出堆顶后用末尾元素补位再从顶向下 sift_down。关键不是“放进去”,而是“放完立刻恢复堆序”。
- 插入时:把元素加到数组末尾,然后不断和父节点比较交换,直到父节点 ≥ 当前值(最大堆)
- 弹出时:取
arr[0]作为返回值,将arr[size-1]移到arr[0],然后从根开始sift_down——比较当前节点与两个子节点,选较大者交换,重复直到无更大子节点 - 注意边界:
sift_down中必须检查子索引是否,否则越界访问;<code>sift_up中父索引为(i-1)/2,整数除法天然向下取整,无需额外处理
示例插入逻辑片段:
立即学习“C++免费学习笔记(深入)”;
void push(int val) {
arr[size] = val;
int i = size++;
while (i > 0 && arr[(i-1)/2] < arr[i]) {
std::swap(arr[(i-1)/2], arr[i]);
i = (i-1)/2;
}
}数组大小固定时怎么安全处理溢出和空队列
固定大小数组没有扩容机制,必须在接口层显式拒绝非法操作,不能靠异常或静默失败。
- 插入前检查
size == capacity,直接返回错误码或 assert,不尝试覆盖或丢弃 - 弹出前检查
size == 0,此时访问arr[0]是未定义行为;可返回特殊值(如INT_MIN)并设valid=false,或要求调用方先调用empty() - 不要重用
size变量做双重用途(比如同时表示长度和“是否初始化”),容易在清空后误判非空
常见错误现象:push 后 size 超过 capacity,后续 sift_up 访问 arr[capacity] —— 这是栈溢出或踩内存,编译器不会报错,但运行时崩溃或数据错乱。
性能和兼容性上哪些细节容易被忽略
数组实现优先级队列的常数因子比 std::priority_queue 更低,但易错点集中在索引计算和循环终止条件。
-
sift_down循环中,若只比较左子而忽略右子存在性,会导致选错最大值;正确做法是先确认右子存在,再决定是否参与比较 - 使用
int索引时,当size接近INT_MAX,2*i+2可能溢出;实际项目中若容量超 10⁶,建议用size_t并加溢出检查 - 若需支持自定义类型,运算符重载必须提供
operator<或传入比较函数对象;但数组实现里没法像模板容器那样自动推导,得手动改arr[i] < arr[j]的比较逻辑
最常被跳过的一步:建堆阶段不用逐个 push,而应从最后一个非叶子节点(size/2 - 1)开始倒序 sift_down,这样整体是 O(n) 而非 O(n log n) —— 但前提是数组初始数据已全部就位且无序。


















