堆排序(大顶推)

堆排序

图片和描述转载于https://www.cnblogs.com/lanhaicode/p/10546257.html,代码是根据该描述以自己的理解自己实现的。

1、什么是堆?

堆是一种非线性结构,(本篇随笔主要分析堆的数组实现)可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组

按照堆的特点可以把堆分为大顶堆小顶堆

大顶堆:每个结点的值都大于等于其左右孩子结点的值

小顶堆:每个结点的值都小于等于其左右孩子结点的值

(堆的这种特性非常的有用,堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素)

 

2、堆的特点(数组实现)

 

(图片来源:https://www.cnblogs.com/chengxiao/p/6129630.html)

我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

(图片来源:https://www.cnblogs.com/chengxiao/p/6129630.html)

 

我们用简单的公式来描述一下堆的定义就是:(读者可以对照上图的数组来理解下面两个公式)

大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2] 

小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2] 

 

3、堆和普通树的区别

内存占用:

普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额外的内存。堆仅仅使用数组,且不使用指针

(可以使用普通树来模拟堆,但空间浪费比较大,不太建议这么做)

 

平衡

二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(nlog2n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(nlog2n) 的性能


搜索:

在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入、删除操作

 

 

4、堆排序的过程

先了解下堆排序的基本思想:

将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值,

如此反复执行,便能得到一个有序序列了,建立最大堆时是从最后一个非叶子节点开始从下往上调整的(这句话可能不好太理解),下面会举一个例子来理解堆排序的基本思想

 

给一个无序序列如下

int a[6] = {7, 3, 8, 5, 1, 2};

现在可以根据数组将完全二叉树还原出来

好了,现在我们要做的事情就是要把7,3,8,5,1,2变成一个有序的序列,如果想要升序就是1,2,3,5,7,8   如果想要降序就是8,7,5,3,2,1 ,这两种就是我们要的最终结果,然后我们就可以根据我们想要的结果来选择

适合类型的堆来进行排序

升序----使用大顶堆

降序----使用小顶堆

5、为什么升序要用大顶堆呢

上面提到过大顶堆的特点:每个结点的值都大于等于其左右孩子结点的值,我们把大顶堆构建完毕后根节点的值一定是最大的,然后把根节点的和最后一个元素(也可以说最后一个节点)交换位置,那么末尾元素此时就是最大元素了(理解这点很重要)

知道了堆排序的原理下面就可以来操作了,在进行操作前先理清一下步骤

 

(假设我们想要升序的排列)

第一步:先n个元素的无序序列,构建成大顶堆

第二步:将根节点与最后一个元素交换位置,(将最大元素"沉"到数组末端

第三步:交换过后可能不再满足大顶堆的条件,所以需要将剩下的n-1个元素重新构建成大顶堆

第四步:重复第二步、第三步直到整个数组排序完成

 

6、图解交换过程(得到升序序列,使用大顶堆来调整)

这里以int a[6] = {7, 3, 8, 5, 1, 2}为例子

先要找到最后一个非叶子节点,数组的长度为6,那么最后一个非叶子节点就是:长度/2-1,也就是6/2-1=2,然后下一步就是比较该节点值和它的子树值,如果该节点小于其左\右子树的值就交换(意思就是将最大的值放到该节点)

8只有一个左子树,左子树的值为2,8>2不需要调整

 

 

下一步,继续找到下一个非叶子节点(其实就是当前坐标-1就行了),该节点的值为3小于其左子树的值,交换值,交换后该节点值为5,大于其右子树的值,不需要交换

下一步,继续找到下一个非叶子节点,该节点的值为7,大于其左子树的值,不需要交换,再看右子树,该节点的值小于右子树的值,需要交换值

下一步,检查调整后的子树,是否满足大顶堆性质,如果不满足则继续调整(这里因为只将右子树的值与根节点互换,只需要检查右子树是否满足,而8>2刚好满足大顶堆的性质,就不需要调整了,

如果运气不好整个数的根节点的值是1,那么就还需要调整右子树)

 

到这里大顶堆的构建就算完成了,然后下一步交换根节点(8)与最后一个元素(2)交换位置(将最大元素"沉"到数组末端),此时最大的元素就归位了,然后对剩下的5个元素重复上面的操作

(这里用粉红色来表示已经归位的元素)

剩下只有5个元素,最后一个非叶子节点是5/2-1=1,该节点的值(5)大于左子树的值(3)也大于右子树的值(1),满足大顶堆性质不需要交换

找到下一个非叶子节点,该节点的值(2)小于左子树的值(5),交换值,交换后左子树不再满足大顶堆的性质再调整左子树,左子树满足要求后再返回去看根节点,根节点的值(5)小于右子树的值(7),再次交换值

 

得到新的大顶堆,如下图,再把根节点的值(7)与当前数组最后一个元素值(1)交换,再重构大顶堆->交换值->重构大顶堆->交换值····,直到整个数组都变成有序序列

 

最后得到的升序序列如下图

7、大顶堆排序的代码实现

#include <stdio.h>

void Swap(int *heap, int i,int j);        /* 交换元素的值 */
void BuildMaxHeap(int *heap, int len);/* 构建大顶堆 */

int main()
{
    int a[] = {7, 3, 8, 5, 1, 2};
    int len = sizeof(a)/sizeof(int);    /* 数组长度 */
    int i,j;

    for (i = len; i > 0; i--)// 需要构建len次。
    {   
        BuildMaxHeap(a, i); 
        Swap(a, 0,i-1);//构建完成一次后,将,堆顶和最后一个元素交换位置,此时,最后一个元素为最大值,下一次不需要处理最后一个元素了。
    }   
    for (i = 0; i < len; i++)
    {   
        printf("%d ", a[i]);
    }   
    printf("\n");

    return 0;
}
int HeapAdjust(int *heap,int i,int len)
{
    int l,r,max;
    max =i; 
    l=2*i+1;
    r=2*i+2;
    if (l < len && heap[max] < heap[l])    /* 根节点大于左子树,构建小顶堆只需修改比较条件 */
    {   
        min =l; 
    }   
    if (r < len && heap[max] < heap[r])    /* 根节点大于右子树,构建小顶堆只需修改比较条件 */
    {
        max =r;
    }
    if(max !=i)
    {
        Swap(heap,max,i);
        HeapAdjust(heap,max,len);//交换完以后,二叉树下面的节点可能又变成无序的了,因此递归向下调整一下。
    }
}


/* Function: 构建大顶堆 */
void BuildMaxHeap(int *heap,int len)
{
    int i;
    for (i = len/2-1; i >= 0; i--)//从最后一个非叶子节点开始。进行堆调整。
    {
        HeapAdjust(heap,i,len);
    }
}

/* Function: 交换交换根节点和数组末尾元素的值*/
void Swap(int *heap, int i,int j)
{
    int temp;

    temp = heap[i];
    heap[i] = heap[j];
    heap[j] = temp;
}

 

<think> 首先,用户的问题是关于堆排序中的建堆操作,具体是堆排序的建堆操作原理及实现。用户提到“排序”,但根据上下文,应该是“堆排序”的笔误,因为引用中都是关于堆排序的内容。 我需要根据系统级指令来构建回答: - 所有行内数学表达式使用 `$...$` 格式。 - 独立公式使用 `$$...$$` 格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如 `[^1]`。 - 回答结构清晰,帮助用户逐步解决问题,保证真实可靠。 参考提供的引用: - [^1] 描述了堆的基本概念和建堆操作:堆是完全二叉树,每个节点值大于等于或小于等于子节点(大顶堆或小顶堆),建堆时从最后一个非叶子节点开始调用调堆函数。 - [^2] 提到了删除操作通常针对堆顶元素,删除后调整。 - [^3] 定义了堆的基本概念:完全二叉树,节点值满足堆序性质。 - [^4] 给出了建堆的代码:从 `length/2-1` 开始,调用下沉操作。 - [^5] 提供了堆排序的Java实现,包括建堆和堆化操作。 用户的具体问题是:堆排序的建堆操作原理及实现。 回答结构: 1. **介绍堆排序和建堆**:简要解释堆排序是什么,以及建堆在其中的作用。 2. **建堆的原理**:解释建堆的基本原理,包括为什么需要建堆、建堆的过程(从最后一个非叶子节点开始下沉调整)。 3. **建堆的实现**:提供代码示例,基于引用中的代码,可能用Python或其他语言(引用中有C和Java),但要确保真实可靠。既然用户提供了引用,我可以参考那些代码。 4. **分析复杂度**:简要讨论建堆的时间复杂度。 5. **生成相关问题**:在末尾添加相关问题。 6. **引用标识**:在回答中引用相关段落时添加引用标识。 代码实现:用户引用中有C和Java代码,我可以改写为Python,因为之前的示例是Python,但引用中主要是C和Java。为了保持一致性,我可以用伪代码或Python实现。或者直接使用引用中的语言,但为了清晰,我可以用Python重写。 建堆操作的关键点(基于引用): - 建堆的目的是将无序数组调整成一个堆(通常是大顶堆或小顶堆)。 - 步骤:从最后一个非叶子节点开始(索引为 `len/2 - 1`),到根节点(索引0),对每个节点调用下沉操作(adjust heap, heapify)。 - 下沉操作:比较节点与其子节点,如果不满足堆序,交换并递归调整。 复杂度:建堆的时间复杂度是 O(n),因为下沉操作在树的高度上,但平均起来是线性时间。 回答步骤: 1. **引言**:堆排序包括建堆和排序阶段。 2. **建堆原理**: - 堆是完全二叉树,可以用数组表示。 - 大顶堆:父节点值 >= 子节点值。 - 建堆:从最后一个非叶子节点开始,向上调整每个子树为堆。 3. **实现**:用代码展示,参考引用。 4. **复杂度**:建堆 O(n)。 5. **相关问题**:生成几个相关问题。 生成相关问题: 1. 堆排序的整体过程是怎样的? 2. 建堆操作的时间复杂度为什么是 O(n)? 3. 堆排序的优化方法有哪些? 引用标识:在描述原理时,引用 [^3] 定义堆的概念;在描述建堆过程时,引用 [^1] 和 [^4]。 用中文回答。 代码实现:我可以写一个Python版本的建堆函数,基于引用中的逻辑。 Python代码示例: ```python def heapify(arr, n, i): # 下沉操作,i是当前节点索引 largest = i left = 2 * i + 1 right = 2 * i + 2 if left < n and arr[left] > arr[largest]: # 对于大顶堆 largest = left if right < n and arr[right] > arr[largest]: largest = right if largest != i: arr[i], arr[largest] = arr[largest], arr[i] heapify(arr, n, largest) def build_heap(arr): n = len(arr) start_index = n // 2 - 1 # 最后一个非叶子节点 for i in range(start_index, -1, -1): heapify(arr, n, i) ``` 在回答中解释这个代码。 数学表达式:如果有数学公式,比如索引计算,用行内 `$...$`,如 `$ \text{last non-leaf index} = \lfloor n/2 \rfloor - 1 $`。 回答结构: - 标题:堆排序的建堆操作原理及实现 - 正文:分点解释原理和实现 - 代码块 - 复杂度分析 - 相关问题 引用标识:在相关段落后添加,如根据引用 [^3],堆是一种完全二叉树... [^3] 最终回答框架: 1. **堆排序简介** 2. **建堆操作原理** - 堆的定义 [^3] - 建堆的必要性 - 建堆过程:自底向上调整 3. **建堆实现** - 代码示例(Python) 4. **时间复杂度分析** 5. **相关问题** </think> ### 堆排序的建堆操作原理及实现 堆排序是一种高效的排序算法,其核心步骤是**建堆(Heapify)**,即将无序数组调整为一个堆结构(通常是大顶堆或小顶堆)。建堆操作是堆排序的基础,确保数组满足堆性质:对于大顶堆,父节点值 ≥ 子节点值;对于小顶堆,父节点值 ≤ 子节点值。下面我将逐步解释建堆的原理及实现方法,确保内容真实可靠(基于计算机科学共识和参考引用)。 --- ### 1. 建堆操作原理 堆是一种完全二叉树,可以用数组表示(索引从0开始),满足以下性质: - 父节点索引为 $i$ 时,左子节点索引为 $2i+1$,右子节点索引为 $2i+2$。 - 建堆的目标是将无序数组转换为堆结构,例如大顶堆(根节点为最大值)[^3]。 **建堆的核心原理是自底向上调整**: - **起点**:从**最后一个非叶子节点**开始调整(索引为 $\lfloor n/2 \rfloor - 1$,其中 $n$ 是数组长度)。这是因为叶子节点本身已满足堆性质,无需调整[^1][^4]。 - **调整过程**:对每个非叶子节点执行**下沉操作(Sink或Heapify)**: - 比较当前节点与其子节点的值。 - 如果不满足堆序(例如大顶堆中父节点值 < 子节点值),则交换节点。 - 递归调整被交换的子节点,直到整个子树满足堆性质。 - **方向**:从最后一个非叶子节点向根节点(索引0)遍历,确保每次调整后子树成为堆[^4][^5]。 **为什么这样有效?** - 自底向上方式减少了调整次数,平均时间复杂度为 $O(n)$(详细分析见下文)。 - 通过下沉操作维护堆序性质,避免不必要的交换[^1][^5]。 --- ### 2. 建堆操作实现 下面以**大顶堆**为例,提供Python代码实现(基于引用中的逻辑)。代码包括两个函数: - `heapify(arr, n, i)`:下沉操作,调整以 $i$ 为根的子树。 - `build_heap(arr)`:建堆入口,从最后一个非叶子节点开始遍历。 ```python def heapify(arr, n, i): """下沉操作:调整以索引i为根的子树为大顶堆 arr: 输入数组 n: 堆的大小(数组长度) i: 当前节点索引 """ largest = i # 初始化最大值索引 left = 2 * i + 1 # 左子节点索引 right = 2 * i + 2 # 右子节点索引 # 如果左子节点存在且大于当前节点,更新最大值索引 if left < n and arr[left] > arr[largest]: largest = left # 如果右子节点存在且大于当前最大值,更新最大值索引 if right < n and arr[right] > arr[largest]: largest = right # 如果最大值不是当前节点,交换并递归调整 if largest != i: arr[i], arr[largest] = arr[largest], arr[i] # 交换 heapify(arr, n, largest) # 递归调整被交换的子节点 def build_heap(arr): """建堆函数:将无序数组转换为大顶堆""" n = len(arr) start_index = n // 2 - 1 # 最后一个非叶子节点索引(公式:⌊n/2⌋ - 1) # 从最后一个非叶子节点向根节点遍历 for i in range(start_index, -1, -1): heapify(arr, n, i) ``` **代码说明**: - **输入**:无序数组(如 `[4, 10, 3, 5, 1]`)。 - **输出**:建堆后的数组满足大顶堆性质(如 `[10, 5, 3, 4, 1]`)。 - **关键点**: - 时间复杂度:建堆操作平均为 $O(n)$,最坏情况为 $O(n \log n)$,但实际中接近线性(因为大多数节点高度较低)[^1][^5]。 - 空间复杂度:$O(1)$(原地操作,无需额外空间)。 **示例运行**: ```python arr = [4, 10, 3, 5, 1] build_heap(arr) print(arr) # 输出:[10, 5, 3, 4, 1](满足大顶堆) ``` --- ### 3. 时间复杂度分析 建堆的时间复杂度为 $O(n)$,其导基于堆的结构: - 完全二叉树高度为 $h = \lfloor \log_2 n \rfloor$。 - 下沉操作的成本与节点高度相关:高度为 $h$ 的节点最多调整 $h$ 次。 - 总调整次数上限为 $\sum_{h=0}^{\log n} \frac{n}{2^{h+1}} \cdot h = O(n)$,因为大多数节点在低高度层[^1][^5]。 此分析表明,建堆比逐元素插入($O(n \log n)$)更高效。 --- ### 4. 相关问题 1. 堆排序的整体过程是怎样的?建堆后如何完成排序[^2][^5]? 2. 建堆操作的时间复杂度为什么是 $O(n)$?能否给出详细数学证明? 3. 堆排序的优化方法有哪些?例如如何避免递归以提升性能[^4]? 4. 堆排序在哪些应用场景中表现优异?例如优先队列或Top-K问题[^3]。 [^1]: 堆的各种操作实现及分析,包括调堆函数和建堆逻辑。 [^2]: 堆的实现和堆排序,强调删除操作的优化。 [^3]: 堆的基本概念,定义堆的性质和类型。 [^4]: 堆排序之堆的概念,提供建堆和下沉操作代码。 [^5]: 堆排序的构建和heapify操作,包含Java实现和复杂度分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值