堆与堆排序

目录

一.堆的定义

二.堆的性质

2.1堆的性质:

2.2堆的分类

2.3 数组表示的堆的性质

三.堆排序的过程

3.1图解过程

3.2代码实现(C++)

四.堆排序的时间复杂度


堆是一种数据结构,与堆内存是不同的概念,不要混淆。

一.堆的定义

堆是一个可以被看做一棵 完全二叉树数组对象

二.堆的性质

2.1堆的性质:

        1. 堆中某个结点的值总是 不大于不小于 其父结点的值

​        2. 堆总是一棵完全二叉树

2.2堆的分类

按照根结点是 最大值 还是 最小值 分为 大根堆 和 小根堆

大根堆:

        根结点是最大值 的堆,用于维护和查询 max

        大根堆的任意结点的值 >= 它所有子结点的值(父 >= 子)

小根堆:

        根结点是最小值 的堆,用于维护和查询 min

        小根堆的任意结点的值 <= 它所有子结点的值(父 <= 子)

示例图如下:

2.3 数组表示的堆的性质

以下性质主要是由完全二叉树的性质得到的结论。  在数组表示的堆中,数组下标0 为父节点,父结点与子结点的性质如下:

        第 i 个结点的 父结点 下标 为 (i-1)/2 ;

        第 i 个结点的 左子结点 下标 为 2i+1 ;

        第 i 个结点的 右子结点 下标 为 2i+2 ; 

        最后一个非叶子结点 下标为:n/2 -1

        叶子结点是 下标从 n/2 开始,后面所有的都是 叶结点。

三.堆排序的过程

实现堆排序的思想:

 将一个长为n的序列构造成一个大顶堆,则整个序列的最大值就是堆顶的根结点。  将最大值结点末尾结点的值互换,此时末尾结点的值就是最大值。(即数组的最后一个元素为最大值) 

然后将剩余的 n-1个序列重新构造成一个大顶堆,再将n-1序列的最大值与末尾结点的值互换,就会得到 次最大值。 如此重复执行,就可以得到一个有序序列了。

3.1图解过程

假设 输入的长度为7的序列为  [7,10,15,30,35,23,40], 图示如下:

建立最大堆的时候是从 最后一个非叶子结点 开始 从下往上调整。下面为序列建堆的过程:

        1.先从最后一个非叶子结点 结点2,开始调整:

 

        结点2和结点6的值进行互换后,结点6的值被更改了,但是它不再有叶子结点,因此不需要继续对结点6建大根堆。

         2.再往结点2的兄弟结点1,进行调整:

         结点1和结点4的值进行互换后,结点4的值被更改了,但是它不再有叶子结点,因此不需要继续对结点4建大根堆。

         3.再往结点1的上一个父结点,进行调整:

         结点0和结点2的值进行互换后,结点2的值被更改了,结点2有叶子结点,因此需要继续对结点2建大根堆。

 这个过程就实现了对长为7的序列 构造大顶堆。

 堆排序就是基于构建的堆,将堆顶的(最大值)根结点的值 与 末尾结点的值进行互换:

然后再对剩下的6个结点,再进行构造成一个大顶堆,重复执行下去。

3.2代码实现(C++)

两种写法:

非递归写法:

#include <iostream>
#include <algorithm>
using namespace std;

//构建大顶堆
void max_heapify(int arr[], int start, int end)
{
    //建立父节点指标和子节点指标
    int dad = start;
    //左子节点 下标
    int son = dad * 2 + 1;
    while (son <= end)  //若子节点指标在范围内才做比较
    {
        //右子结点  && 左右子结点对比    先比较两个子节点大小,选择最大的
        if (son + 1 <= end && arr[son] < arr[son + 1]) 
            son++; //使用右子节点
        if (arr[dad] > arr[son]) //如果父节点大于子节点代表调整完毕,直接跳出函数
            return;
        else  //否则交换父子内容再继续子节点和孙节点比较
        {
            swap(arr[dad], arr[son]);
            
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void heap_sort(int arr[], int len)
{
    //初始化,i从最后一个父节点开始调整, 往前都是父节点
    for (int i = len / 2 - 1; i >= 0; i--)
        max_heapify(arr, i, len - 1);

    //先将第一个元素和已经排好的元素前一位做交换,
    //再从新调整(刚调整的元素之前的元素),直到排序完毕
    for (int i = len - 1; i > 0; i--)
    {
        swap(arr[0], arr[i]);
        max_heapify(arr, 0, i - 1);
    }
}

int main()
{
    int arr[] = { 7,10,15,30,35,23,40 };
    int len = (int)sizeof(arr) / sizeof(*arr);
    heap_sort(arr, len);
    for (int i = 0; i < len; i++)
        cout << arr[i] << ' ';
    cout << endl;
    system("pause");
    return 0;
}

递归写法:

#include<iostream>
#include <algorithm>
using namespace std;

void heapify(int* tree, int n, int i)
{
	if (i >= n) return;
	//左子结点
	int c1 = i * 2 + 1;
	//右子结点
	int c2 = i * 2 + 2;
	//假设最大值坐标是根结点,获取左右子结点的最大值
	int max = i;
	if (c1<n && tree[c1]>tree[max])
	{
		max = c1;
	}
	if (c2<n && tree[c2]>tree[max])
	{
		max = c2;
	}
	if (max != i)
	{
		//将左右子树的最大值赋给父结点
		swap(tree[max],tree[i]);
		//较小的值,被赋给左子树或右子树,则左子树或右子树 需要重新建堆 
		heapify(tree,n,max);
	}

}

void build_heap(int *tree, int n)
{
	int last_node = n - 1;
	//最后一个结点的父节点 下标,即最后一个非叶子结点
	int parent = (last_node - 1) / 2;
	
	//针对最后一个父节点的 及其前面的父节点进行建堆
	for (int i =parent; i>=0; i--)
	{
		heapify(tree,n,i);
	}
}

void heap_sort(int *tree, int n)
{
	build_heap(tree,n);

	for (int i = n-1; i>=0; i--)
	{
		//堆顶与末尾结点值交换
		swap(tree[i], tree[0]);
		//i不断在砍断
		heapify(tree,i,0);
	}
}

int main()
{
	int tree[] = { 7,10,15,30,35,23,40 };
	int n = 7;
	heap_sort(tree,n);

	for (int i=0;i<n;i++)
	{
		cout << tree[i]<<" ";
	}
	cout<< endl;

	return 0;
}

四.堆排序的时间复杂度

堆排序的时间复杂度是O(n logn),是一个不稳定的排序算法。

(稳定的定义是:如果排序前后它们的相对次序一定保持不变,就称排序算法是稳定的
否则就称排序算法是不稳定的)

C++中的  priority_queue容器, 其内部实现就是通过二叉堆(默认大根堆)实现,在工作中可以直接使用容器内置算法来做排序。详情见优先队列的用法:

C++中优先队列的priority_queue<int,vector<int>,greater<int>>与priority<int>的用法_Appleeatingboy的博客-CSDN博客

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值