一.堆的概念及实现
1.1堆的概念
在数据结构中,堆是一种特殊的树形数据结构。堆可以分为最大堆和最小堆两种类型。
最大堆:对于堆中的任意节点,其父节点的值都不小于它的值。换句话说,最大堆中的根节点是堆中的最大值。并且,最大堆的任意子树也都是最大堆。
最小堆:对于堆中的任意节点,其父节点的值都不大于它的值。最小堆中的根节点是堆中的最小值,且任意子树也都是最小堆。
堆通常用一个数组来表示,其中每个元素对应堆的一个节点。堆的性质保证了数组中的元素满足特定的顺序关系。
在堆中,通常可以进行以下操作:
- 插入:将一个元素插入到堆中的合适位置,保持堆的性质。
- 删除根节点:删除堆中的根节点,并保持堆的性质。对于最大堆,删除的是最大值;对于最小堆,删除的是最小值。
- 堆化:将一个无序的数组转换为堆的形式。
- 取最值 :获取堆中的最大值或最小值,即根节点的值。
堆广泛应用于各种算法和数据结构中,例如堆排序、优先队列、图算法(如最短路径算法中的Dijkstra算法)等。堆的特性使得这些算法具有高效的时间复杂度。
举个简单的例子解释堆的概念:
假设有一堆学生的成绩数据,每个学生都有一个分数,表示他们的学术表现。最大堆代表这种情况:每个学生的分数都比他们的孩子(下面的学生)更高。
现在,你想根据学生成绩来组织这些数据。你将最高分的学生放在最前面,其次是次高分的学生,以此类推。这样,你会得到一个最大堆,其中根节点是分数最高的学生,而任何一个学生的分数都不会超过他的父节点。
当你要添加新的学生成绩时,你需要将其放置在正确的位置,以保持最大堆的性质。如果新的学生的分数比他的父节点更高,你可能需要将他与父节点交换位置,以确保最大堆的性质。
最小堆的情况则相反。假设你有一堆学生的成绩数据,每个学生的分数都比他们的孩子更低。这意味着你将最低分的学生放置在最前面,而任何一个学生的分数都不会低于他的父节点。
1.2堆的性质
堆是一种特殊的数据结构,具有以下性质:
1. 堆是一个完全二叉树:堆是由完全二叉树组成的,意味着除了最后一层外,其它层都必须填满,且最后一层的节点都靠左排列。
2. 最大堆性质:对于最大堆,父节点的值大于或等于其子节点的值。换句话说,堆中的最大元素位于根节点。
3. 最小堆性质:对于最小堆,父节点的值小于或等于其子节点的值。换句话说,堆中的最小元素位于根节点。
4. 堆序性质:堆中的每个节点都必须满足堆的性质,即父节点的值要么大于等于(最大堆)或小于等于(最小堆)子节点的值。这意味着在堆中,无论是最大堆还是最小堆,根节点都是堆中的最大或最小元素。
5. 堆的高度:堆的高度是指从根节点到叶子节点的最长路径的长度。对于一个有 n 个节点的堆,其高度通常为 O(log n)。
这些性质使得堆成为一种非常有用的数据结构,尤其在优先队列、堆排序和图算法中经常被使用。堆的特点使得我们能够高效地访问和操作具有最大或最小优先级的元素,从而提高算法的效率和性能。
1.3堆的存储方式
堆可以使用数组来进行存储。在数组表示中,每个元素对应堆中的一个节点,通过数组的索引来确定节点之间的关系。
对于一个堆,我们使用以下规则来存储节点:
- 根节点存储在数组的索引位置 0 处。
- 对于任意节点 i,其父节点存储在索引位置 (i-1)/2 处。
- 对于任意节点 i,其左子节点存储在索引位置 2i+1 处。
- 对于任意节点 i,其右子节点存储在索引位置 2i+2 处。
比如如下存储最大堆
数组的存储结构是这样:
数组表示: [90, 80, 75, 60, 55, 40, 30, 20, 10, 25]
索引表示: 0 1 2 3 4 5 6 7 8 9
数组的逻辑结构是这样:
简单解释索引位置的关系:
这样的关系是由完全二叉树的性质决定的。在完全二叉树中,每个节点都有可能存在左子节点和右子节点,且它们的位置是固定的。通过将完全二叉树的节点按照一定顺序存储在数组中,我们可以利用数组的索引来表示节点之间的关系。
根节点存储在数组的索引位置 0 处:因为完全二叉树的根节点始终位于最上层,所以它在数组中的位置是固定的,即索引位置 0 处。
对于任意节点 i,其父节点存储在索引位置 (i-1)/2 处:通过简单的数学计算,我们可以确定父节点在数组中的位置。对于任意节点 i,我们将节点索引减去 1,然后除以 2,可以得到父节点在数组中的索引位置。
对于任意节点 i,其左子节点存储在索引位置 2i+1 处:根据完全二叉树的性质,左子节点总是在父节点的左侧,所以我们可以通过将节点索引乘以 2,再加上 1,得到左子节点在数组中的索引位置。
对于任意节点 i,其右子节点存储在索引位置 2i+2 处:类似地,根据完全二叉树的性质,右子节点总是在父节点的右侧,所以我们可以通过将节点索引乘以 2,再加上 2,得到右子节点在数组中的索引位置
例子:
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
- 比如:
二.堆的创建及时间复杂度
2.1堆向下过程(以小堆为例)
堆的创建是指将一个无序的数组或数据集转换为堆的过程。创建堆的常见方法是使用向下过程
以下是使用向下过程创建堆的一般步骤:
- 从最后一个非叶子节点开始,依次向上迭代直到根节点。最后一个非叶子节点的索引为数组长度的一半减一(n/2 - 1)。
- 对于每个节点,执行向下过程:
- 比较当前节点与其子节点的值,并找到值最大(或最小)的子节点。如果有左子节点,其索引为2 * i + 1;如果有右子节点,则索引为2 * i + 2。
- 如果当前节点的值小于其最大(或最小)子节点的值,则交换当前节点与最大(或最小)子节点的值。
- 更新当前节点的索引为最大(或最小)子节点的索引,即`i`的值更新为最大(或最小)子节点的索引。
- 重复步骤3至5,直到节点`i`不再有子节点或其值大于(或小于)其子节点的值。
- 重复步骤2,直到根节点。
通过以上步骤,将数组或数据集中的元素逐个进行向下过程,最终可以创建一个满足堆的性质的堆。在最大堆中,每个节点的值都大于或等于其子节点的值;在最小堆中,每个节点的值都小于或等于其子节点的值。
需要注意的是,堆的创建过程的时间复杂度为O(n),其中n是数组或数据集的大小。
参考图示如下:
- 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
- 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在 parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标识。
- 将parent与较小的孩子child比较,如果parent小于较小的孩子child,调整结束
- 否则交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1。
参考代码如下:
public void shiftDown(int[] array, int parent) {
// child先标记parent的左孩子,因为parent可能右左没有右
int child = 2 * parent + 1;
int size = array.length;
while (child < size) {
// 如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
if(child+1 < size && array[child+1] < array[child]){
child += 1;
}
// 如果双亲比其最小的孩子还小,说明该结构已经满足堆的特性了
if (array[parent] <= array[child]) {
break;
}else{
// 将双亲与较小的孩子交换
int t = array[parent];
array[parent] = array[child];
array[child] = t;
// parent中大的元素往下移动,可能会造成子树不满足堆的性质,