IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> 堆排序(HeapSort) -> 正文阅读

[数据结构与算法]堆排序(HeapSort)



1. 堆的概念

堆的数据结构也即是个顺序存储的完全二叉树。在STL容器当中的实现为 p r i o r i t y _ q u e u e priority\_queue priority_queue,即优先队列。
之前左神的视频中已经讲过,昨日腾讯音乐一面要求写堆排序,时间比较久远且当时的代码也比较复杂。因此,在这里重新整理一下。之前的实现方法见:排序归纳总结(插入排序、归并排序、堆排序、快速排序、桶排序)——3. 堆排序

其中每个结点的关键字都不大于其孩子结点的关键字,这样的堆称为小根堆
其中每个结点的关键字都不小于其孩子结点的关键字,这样的堆称为大根堆



2. 堆的实例

举例来说,对于 n n n个元素的序列 n u m s 0 , n u m s 1 , . . . , n u m s n {nums_0, nums_1, ... , nums_n} nums0?,nums1?,...,numsn?当且仅当满足下列关系之一时,称之为堆:
(1) n u m s i < = n u m 2 i + 1 nums_i <= num_{2i+1} numsi?<=num2i+1? n u m s i < = n u m 2 i + 2 nums_i <= num_{2i+2} numsi?<=num2i+2? (小根堆)
(2) n u m s i > = n u m 2 i + 1 nums_i >= num_{2i+1} numsi?>=num2i+1? n u m s i > = n u m 2 i + 2 nums_i >= num_{2i+2} numsi?>=num2i+2? (大根堆)
其中 i = 1 , 2 , … , n 2 i=1,2,…,\frac{n}{2} i=1,2,,2n?向下取整;

如上图所示,序列 n u m s { 3 , 8 , 15 , 31 , 25 } nums\{3, 8, 15, 31, 25\} nums{3,8,15,31,25}是一个典型的小根堆。
堆中有两个父结点( n u m s . s i z e ( ) > > 1 = f l o o r ( 5 2 ) = 2 nums.size() >> 1 = floor(\frac{5}{2}) = 2 nums.size()>>1=floor(25?)=2,这里父节点的个数可以理解为子树的个数),元素 3 3 3和元素 8 8 8
元素 3 3 3在数组中以 n u m s [ 0 ] nums[0] nums[0]表示,它的左孩子结点是 n u m s [ 1 ] nums[1] nums[1],右孩子结点是 n u m s [ 2 ] nums[2] nums[2]
元素 8 8 8在数组中以 n u m s [ 1 ] nums[1] nums[1]表示,它的左孩子结点是 n u m s [ 3 ] nums[3] nums[3],右孩子结点是 n u m s [ 4 ] nums[4] nums[4],它的父结点是 n u m s [ 0 ] nums[0] nums[0]

可以看出,它们满足以下规律:
设当前元素在数组中以 n u m s [ i ] nums[i] nums[i]表示,那么:

  • (1) 它的左孩子结点是: n u m s [ 2 i + 1 ] nums[2i+1] nums[2i+1];
  • (2) 它的右孩子结点是: n u m s [ 2 i + 2 ] nums[2i+2] nums[2i+2];
  • (3) 它的父结点是: n u m s [ i ? 1 2 ] nums[\frac{i-1}{2}] nums[2i?1?];
  • (4) n u m s [ i ] < = n u m s [ 2 i + 1 ] nums[i] <= nums[2i+1] nums[i]<=nums[2i+1] n u m s [ i ] < = n u m s [ 2 i + 2 ] nums[i] <= nums[2i+2] nums[i]<=nums[2i+2](小根堆特征)。


3. 堆排过程


3.1 堆排思想

堆排序基本思想:将待排序序列(元素数目为 n n n)构造为一个大/小顶堆,此时,整个序列的最大/小值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大/小值。然后将剩余 n ? 1 n-1 n?1 个元素重新构造成一个堆,这样会得到 n n n 个元素的次小值。如此反复执行,便可得到一有序序列了。

具体操作层面上为:

  • (1) 根据初始数组去构造初始堆(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大/小,构建过程只需从非叶子节点进行逆序调整即可,次第对父节点沿着深搜路径(从根节点到叶子节点)进行顺序调整);
  • (2) 每次交换第一个和最后一个元素,输出最后一个元素(最大/小值),然后把剩下元素重新调整为大/小根堆;

3.2 堆排过程示意

以无序序列 { 1 , 3 , 4 , 5 , 2 , 6 , 9 , 7 , 8 , 0 } \{ 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 \} {1,3,4,5,2,6,9,7,8,0}的升序排序为例:

(1) 构建初始大根堆:

(2) 取出最大值放置到序列末尾,除开序列末尾将剩余元素重新调整为大根堆,重复该操作直至剩余元素的个数为1:


3.3 堆排代码实现

代码实现,使用LeetCode中的 912. 排序数组 进行测试,这里所求的是升序序列,也即是大根堆实现对无序数组的排序,实现的代码如下:

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
    	//由完全二叉树性质,从非叶子节点/内部节点进行逆序遍历,
    	//右边界下标为nums.size() >> 1;
    	//构建初始的大根堆数据结构
        for(int i = nums.size() >> 1; i >= 0; --i) 
            heapAdjust(nums, i, nums.size());
        //进行首尾元素交换,并重新调整数组元素位置确保剩余元素是大根堆结构
        for(int i = nums.size() - 1; i > 0; --i) {
        	//首位元素交换
            int tmp = nums[i];
            nums[i] = nums[0];
            nums[0] = tmp;
            //调整剩余元素为大根堆
            heapAdjust(nums, 0, i); 
        }
        return nums;
    }

private:
    void heapAdjust(vector<int>& nums, int parent, int size) {
        int tmp = nums[parent]; //记录父节点数值
        int child = parent * 2 + 1; //左节点的下标
        while(child < size) { //当未到达叶子节点
        	//更新child下标,使其等于parent的左右子节点的较大的节点下标
            child = child + 1 < size && nums[child] < nums[child+1] ? child + 1 : child;
            //如果父节点数值大于子节点较大值下标,满足大根堆,跳出
            if(tmp >= nums[child]) break;
            //如果小于,则不满足,进行替换,并继续按照最大子节点的路径进行下沉操作,
            //直至找到父节点数值的合适位置,最终合适位置是parent下标;
            nums[parent] = nums[child]; 
            parent = child; //更新parent
            child = child * 2 + 1; //更新左节点下标
        }
        //可理解为环形数组的移位过程,可以注意到上图构建初始化大根堆中的第(4)图,
        //{3, 8, 7}路径替换为了{8, 7, 3}
        nums[parent] = tmp;
    }
};


4. 复杂度分析

总体情况如下:


时间复杂度:

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。

其中构建初始堆经推导复杂度为 O ( n ) O(n) O(n),在交换并重建堆的过程中,需交换 n ? 1 n-1 n?1次,而重建堆的过程中,根据完全二叉树的性质, [ l o g 2 ( n ? 1 ) , l o g 2 ( n ? 2 ) . . . 1 ] [log_2(n-1),log_2(n-2)...1] [log2?(n?1),log2?(n?2)...1]逐步递减,近似为 n l o g n nlogn nlogn。所以堆排序时间复杂度一般认为就是 O ( n l o g n ) O(nlogn) O(nlogn)级。
在这里插入图片描述
在这里插入图片描述

  • 堆的存储表示是顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式;
  • 当想得到一个序列中第 k k k个最小的元素之前的部分排序序列,最好采用堆排序,堆排序的时间复杂度是 O ( n + k l o g 2 n ) O(n+klog_2n) O(n+klog2?n),若 k ≤ n l o g 2 n k≤\frac{n}{log_2n} klog2?nn?,则可得到的时间复杂度为 O ( n ) O(n) O(n)

算法稳定性:

堆排序是一种不稳定的排序方法。因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-08-21 15:43:11  更:2021-08-21 15:43:31 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 22:31:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码