堆(Heap)

注意本文说的堆是数据结构中的堆,而不是java内存模型中的堆。

一、定义

n个元素的序列{k1, k2, …, kn}当且仅当满足以下关系时,称之为。若堆顶元素最小,则称之为小顶堆或小根堆。若堆顶元素最大,则称之为大顶堆或大根堆。如下图所示。
在这里插入图片描述
在这里插入图片描述

二、性质

若以一维数组作为堆的存储结构,并将该一维数组看成是一个完全二叉树,则完全二叉树中所有非终端结点的值均不大于(或不小于)其左、右孩子结点的值。

堆顶元素(或完全二叉树的根)是堆中最小值(或最大值)。

最后一个非终端结点是第⌊n2⌋\lfloor \frac{n}{2} \rfloor2n个元素。

三、操作

  • 向上移动
    在这里插入图片描述
    向上移动又有人称其为上浮,是将一个元素与其父结点比较大小,不符合堆的条件就交换位置,交换后继续与新的父结点比较,如此循环,直到符合堆的条件为止。如上图所示,如果是小根堆,而该元素却小于父结点,那么就需要将其向上移动,移动后再与新的父结点比较,以此类推,直到找到某个位置,它不再小于父结点,则该满足堆的条件,移动结束。

参考代码:

	private void shiftUp(int k) {
		while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
			swap(k, parent(k));
			k = parent(k);
		}
	}
  • 向下移动
    在这里插入图片描述
    向下移动又有人称其为下沉,是将一个元素与其孩子结点进行比较与调整。如上图所示,如果是小根堆,用该结点与其左右孩子中较小的一个结点比较,如果大于那个孩子,则与其交换,交换后继续与新的孩子结点比较,以此类堆,直到找到其合适位置为止。

参考代码:

	private void shiftDown(int k) {
		while(left(k) < _heapSize) {
			int j = left(k);
			if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
			if(_heap[k] < _heap[j]) break;
			swap(k,j);
			k = j;
		}
	}
  • 插入

插入也就是向堆中加入新成员的操作。那么新成员放在哪里呢?放在最后。那放在最后是不是可能破坏堆的结构啊?没错。怎么办?将其向上移动。

参考代码:

	public void insert(int v) {
		if(_heapSize >= _maxSize) return; //如超出容量,这里只简单地返回,实际中请根据需求进行处理
		_heap[_heapSize] = v;
		shiftUp(_heapSize);
		_heapSize++;
	}
  • 删除

删除也就是堆顶元素被拿走了。群龙无首这下怎么办?别急我们要选出新的堆顶。下面我来告诉你怎么办,首先把堆中最后一个元素搬到堆顶,然后将其向下移动。对,就这么简单。

参考代码:

	public int delMin() throws Exception {
		if(_heapSize == 0) {
			throw new Exception("The heap is already empty!");
		}
		int max = _heap[0];
		_heapSize--;
		swap(0, _heapSize);
		shiftDown(0);
		return max;
	}
  • 创建

个人理解堆的创建其实就是将一个序例通过某些操作,使其满足堆的条件从而转化为堆的过程。也就是你给我一个序列,我还你一个堆!

那么如何去搞呢? 有两种方式:

1)逐个插入。插入操作会自觉保证插入后该序列仍然是堆。

参考代码

	public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = new int[maxSize];

		//请看关键代码,逐个插入
		for(int i = 0; i < initialNums.length; i++) {
			insert(initialNums[i]);
		}
	}

2)逐个调整。从最后一个非终端结点开始,向前,逐个调整以各个非终端结点为根的子树,使每棵子树都变成堆,等最后一个非终端结点调整完毕,整个序列就变成了堆。

参考代码

	public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = Arrays.copyOf(initialNums, initialNums.length);
		_heapSize = initialNums.length;
		
		//从最后一个非终端结点开始逐棵子树调整
		for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
			shiftDown(i);
		}
	}

四、完整代码

该代码简单实现了小顶堆的创建、插入、删除等操作。希望能够辅助读者理解。为简单起见,这里只接收int类型数据。

完整代码:

package just.doit;
import java.lang.Exception;

public class MinHeap {
	private  int _heapSize = 0;
	private  int _maxSize;
	private  int[] _heap = null;

//	public MinHeap(int[] initialNums, int maxSize) {
//		_maxSize = maxSize;
//		_heap = Arrays.copyOf(initialNums, initialNums.length);
//		_heapSize = initialNums.length;
//		
//		//从最后一个非终端结点开始逐棵子树调整
//		for(int i = ((_heapSize - 1) / 2); i >= 0; i--) {
//			shiftDown(i);
//		}
//	}

	public MinHeap(int[] initialNums, int maxSize) {
		_maxSize = maxSize;
		_heap = new int[maxSize];

		//逐个插入
		for(int i = 0; i < initialNums.length; i++) {
			insert(initialNums[i]);
		}
	}

	public void insert(int v) {
		if(_heapSize >= _maxSize) return; //如超出容量,这里只简单地返回,实际中请根据需求进行处理
		_heap[_heapSize] = v;
		shiftUp(_heapSize);
		_heapSize++;
	}

	public int delMin() throws Exception {
		if(_heapSize == 0) {
			throw new Exception("The heap is already empty!");
		}
		int max = _heap[0];
		_heapSize--;
		swap(0, _heapSize);
		shiftDown(0);
		return max;
	}

	public void printMinHeap() {
		for(int i = 0; i < _heapSize; i++) {
			System.out.print(_heap[i]+" ");
		}
		System.out.println();
	}

	private void shiftUp(int k) {
		while(parent(k) >= 0 && _heap[k] < _heap[parent(k)]) {
			swap(k, parent(k));
			k = parent(k);
		}
	}

	private void shiftDown(int k) {
		while(left(k) < _heapSize) {
			int j = left(k);
			if(right(k) < _heapSize && _heap[right(k)] < _heap[left(k)]) j++;
			if(_heap[k] < _heap[j]) break;
			swap(k,j);
			k = j;
		}
	}

	private void swap(int i, int j) {
		int temp = _heap[i];
		_heap[i] = _heap[j];
		_heap[j] = temp;
	}
	//本代码从0开始存储,所以left为2 * k + 1,若从1开始存储则left为2 * k
	private int left(int k) {
		return 2 * k + 1;
	}
	//本代码从0开始存储,所以right为2 * k + 2,若从1开始存储则right为2 * k + 1
	private int right(int k) {
		return 2 * k + 2;
	}
	//本代码从0开始存储,所以parent为(k - 1) / 2,若从1开始存储则parent为k / 2
	private int parent(int k) {
		return (k - 1) / 2;
	}

	public static void main(String[] args) throws Exception {
		int[] a = {10,33,1,4,3,29,5,8};

		MinHeap maxHeap = new MinHeap(a, 20); //使用逐个插入的方式构建堆

		maxHeap.printMinHeap(); // 1 3 5 8 4 29 10 33

		System.out.println(maxHeap.delMin()); // 取出堆顶元素 1

		maxHeap.printMinHeap(); //取出堆顶元素后的新堆 3 4 5 8 33 29 10

		maxHeap.insert(6); // 插入 6

		maxHeap.printMinHeap(); // 插入后的新堆 3 4 5 6 33 29 10 8
	}

}

五、使用场景

堆的使用场景包括但不限于一下三种。

  • 堆排序

有了上面的基础,堆排序的思路很简单,给一个序列,先将其构建成堆,堆顶元素肯定是最大(或最小值),将堆顶元素放到序列末尾,并把末尾元素补充到堆顶,并对其进行向下调整,调整到n-1位置为止,这样前n-1个元素又是一个堆,又可以取到第二大(或第二小)的值,以此类推,直到堆只剩下一个元素,将得到一个有序序列。

如下代码是通过构建小根堆,将int数组从大到小排序:

	public static void heapSort(int[] initialNums) {
		int[] heap = buildMinHeap(initialNums);

		for(int i = heap.length - 1; i > 0; i--) {
			swap(heap, 0, i);
			shiftDown(heap, 0, i);
		}
	}

完整代码:


package just.doit;

import java.util.Arrays;

public class Sort {
	public static void heapSort(int[] initialNums) {
		int[] heap = buildMinHeap(initialNums);

		for(int i = heap.length - 1; i > 0; i--) {
			swap(heap, 0, i);
			shiftDown(heap, 0, i);
		}
	}

	private static int[] buildMinHeap(int[] initialNums) {
		for(int i = ((initialNums.length - 1) / 2); i >= 0; i--) {
			shiftDown(initialNums, i, initialNums.length);
		}
		return initialNums;
	}

	private static void shiftDown(int[] heap, int k, int heapSize) {
		while(left(k) < heapSize) {
			int j = left(k);
			if(right(k) < heapSize && heap[right(k)] < heap[left(k)]) j++;
			if(heap[k] < heap[j]) break;
			swap(heap,k,j);
			k = j;
		}
	}

	private static void swap(int[] heap, int i, int j) {
		int temp = heap[i];
		heap[i] = heap[j];
		heap[j] = temp;
	}

	private static int left(int k) {
		return 2 * k + 1;
	}

	private static int right(int k) {
		return 2 * k + 2;
	}

	public static void main(String[] args) {
		int[] a = {5,23,7,33,2,1,16,9};
		System.out.println("排序前:" + Arrays.toString(a));
		Sort.heapSort(a);
		System.out.println("堆排序后:" + Arrays.toString(a));
	}

}

输出:
排序前:[5, 23, 7, 33, 2, 1, 16, 9]
堆排序后:[33, 23, 16, 9, 7, 5, 2, 1]

  • 优先队列

堆可以用来实现优先队列(Priority Queue)。说到队列,大家立刻会想到先进先出。根据名字来看,优先队列似乎不一样。没错,它根据元素的优先级来决定取出顺序。关于优先队列这里不过多讲述。

  • 海量数据中找TopK

例如给了一百万个数据,我想找到最大的100个数据。那么我可以先拿100个元素建一个小根堆,然后一个一个取剩下的元素与堆顶比较,如果大于堆顶,则把堆顶删除,再把这个元素放入堆中。如果小于堆顶,则不做处理。最后堆中100个元素则为最大的100元素。

六、总结

以上则为作者对堆的一些认识与总结,希望能给读者一些启发。如有不妥之处,希望能得到批评指正!

结尾与君共同赏古诗一首,愿君更上一层楼!

      登鹳雀楼  
    [唐] 王之涣  
白日依山尽,黄河入海流。  
欲穷千里目,更上一层楼。  

七、参考文献

《数据结构》 严蔚敏 吴伟民 编著
《算法导论》 殷建平 徐云 等译
《算法》 谢路云 译

扫码关注微信公众号:
在这里插入图片描述

非常好的问题!你提到了 `malloc` 中提到的“heap)”,这是 C 语言内存管理中的一个**核心概念**。下面我们来彻底讲清楚:**什么是heap)?它和栈(stack)有什么区别?它是如何工作的?** --- ### ✅ 什么是Heap)? > **Heap)是程序运行时用于动态分配内存的一块区域**,程序员可以手动申请和释放内存。 - 它由操作系统提供,通过标准库函数如 `malloc`、`calloc`、`realloc` 和 `free` 来管理。 - 和“栈”不同,的生命周期完全由程序员控制 —— 你申请,你就得负责释放。 #### 📌 简单类比: | 类比 | 栈(Stack) | Heap) | |------|-------------|-----------| | 比喻 | 自助餐厅的托盘架(后进先出) | 图书馆借书(任意顺序借还) | | 管理方式 | 自动管理(进入函数分配,离开自动回收) | 手动管理(调用 `malloc` 分配,`free` 释放) | | 使用场景 | 局部变量、函数调用 | 动态数组、链表节点、大对象等 | --- ### 🔍 内存布局总览 一个典型的 C 程序在运行时的内存布局如下: ``` +------------------+ | 栈 (Stack) | ← 向下增长(高地址 → 低地址) | (局部变量) | +------------------+ | | | (Heap) | ← 向上增长(低地址 → 高地址) | (malloc 分配) | +------------------+ | 未初始化数据 | (如 global 变量未初始化) +------------------+ | 已初始化数据 | (如 int x = 10;) +------------------+ | 代码段 | (程序指令) +------------------+ ``` - **栈**:从高地址向低地址增长 - ****:从低地址向高地址增长 - 它们中间是空闲区域,谁需要就往中间“挤” --- ### ✅ 的特点 | 特性 | 说明 | |------|------| | 🟢 动态分配 | 大小可以在运行时决定(比如用户输入 n,再 `malloc(n * sizeof(int))`) | | 🔴 手动管理 | 必须显式调用 `malloc` 和 `free`,否则会导致内存泄漏或崩溃 | | ⏱️ 分配较慢 | 因为涉及系统调用和内存管理算法 | | 🧩 空间碎片化 | 频繁分配/释放可能导致内存不连续,影响性能 | | 🌐 生命周期长 | 分配的内存不会随着函数结束而消失,直到你 `free` 它 | --- ### ✅ 示例:对比栈与上的内存分配 ```c #include <stdio.h> #include <stdlib.h> void stack_example() { int a[1000]; // 在栈上分配 // 函数返回后自动释放 } void heap_example() { int *p = (int*)malloc(1000 * sizeof(int)); // 在上分配 if (p != NULL) { p[0] = 123; printf("p[0] = %d\n", p[0]); free(p); // ❗必须手动释放 p = NULL; } } int main() { stack_example(); heap_example(); return 0; } ``` - `a[1000]`:放在栈上,函数退出自动销毁 - `p` 指向的空间:放在上,除非调用 `free()`,否则一直存在! --- ### ⚠️ 常见错误(相关) #### 1. **忘记 `free()` → 内存泄漏** ```c while (1) { int *p = malloc(1000); // 每次循环都申请,但从不释放 } // 内存很快耗尽 ``` #### 2. **访问已释放的内存(悬空指针)** ```c int *p = malloc(sizeof(int)); *p = 42; free(p); *p = 10; // ❌ 危险!行为未定义 ``` #### 3. **重复释放** ```c free(p); free(p); // ❌ 错误!未定义行为 ``` 建议:`free(p); p = NULL;` 以防止误操作。 #### 4. **越界写入** ```c int *p = malloc(5 * sizeof(int)); for (int i = 0; i < 10; i++) { p[i] = i; // 写到第6个以后就是非法访问 } ``` 这可能会破坏管理结构,导致程序崩溃。 --- ### 💡 什么时候该用? | 场景 | 建议使用的原因 | |------|----------------| | 数组大小在运行时才知道 | 如 `scanf("%d", &n); int *arr = malloc(n * sizeof(int));` | | 数据结构需要动态增删 | 如链表、树、图的节点 | | 要跨函数共享数据 | 栈变量函数返回后就没了,上的还能用 | | 对象太大,怕栈溢出 | 栈空间有限(通常几MB),大数组放更安全 | --- ### 🛠️ 如何调试问题? 推荐工具: - **Valgrind**(Linux):检测内存泄漏、越界、非法访问 - **AddressSanitizer**(GCC/Clang):编译时加 `-fsanitize=address` - **Visual Studio 调试器**(Windows):自带内存诊断功能 示例使用 AddressSanitizer: ```bash gcc -fsanitize=address -g program.c ./a.out ``` 如果发生越界或内存泄漏,会直接报错。 --- ### ✅ 总结 > **Heap)是 C 语言中用于动态内存分配的关键区域**: - 用 `malloc` / `calloc` / `realloc` 分配 - 用 `free` 释放 - 不自动回收,必须手动管理 - 灵活但危险,容易出错 - 是实现高级数据结构的基础 记住一句话: > **栈适合小而生命周期短的数据,适合大而需要长期存在的数据。** ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值