数据结构--堆和堆排序以及堆排序的优化

堆的结构,我们可以分为物理中的真实结构和脑海中的逻辑结构,换句话来说,堆其实在实现上是一种结构,但是我们分析的时候是另一种结构。

好了,不卖关子了。

堆在实现的时候其实是数组结构,但是我们在分析的时候在脑海中应该是完全二叉树结构。

那我们先说一下什么是完全二叉树。

完全二叉树

对于完全二叉树,就是二叉树的节点要么是满的,要么二叉树是不满的,但是它也是从左到右依次变满的状态。

举个例子来说:

  • 如果它是孤零零的一个节点,那它也是完全二叉树,因为它只有一层,并且在这一层中是满的。
  • 如果它是两个节点,那么第二层的节点处在第一层节点的左节点上,右子树为空(从左到右依次变满),这样的也叫作完全二叉树
  • 如果它是两个节点,那么第二层的节点处在第一层节点的右节点上,左子树为空,这样的就不能叫做完全二叉树结构。
    在这里插入图片描述

堆在实现的时候,并没有用二叉树的结构,这只是我们脑海中堆应该有的一个概念。

那堆到底是怎么实现的呢?我们脑海中的树是如何具体实现的呢?

堆其实可以用一个从0出发的连续数组来描述的。
在这里插入图片描述

那问题又来了,从一段连续的数组到二叉树它又有什么对应的关系呢?

  • 首先,二叉树中某个节点下标为 i ,它的左子节点下标为 2i+1 ,右子节点下标是 2i + 2 ,它的父节点下标为( i - 1 )/ 2
  • 那当然,数组默认是从下标 0 开始使用的,上面的对应关系也只是适用于从下标0 开始。
  • 如果数组从下标1 开始使用呢?二叉树中某个节点下标为 i ,它的左子节点下标就变成了 2i ,右子节点下标变成 2i +1 ,它的父节点下标变为 i / 2
  • 举个例子
    在这里插入图片描述

堆的分类

  • 堆分为大根堆和小根堆
  • 大根堆:在这个完全二叉树中,以某个节点起始的整棵树,树中的最大值就是该节点
  • 小根堆:在这个完全二叉树中,以某个节点起始的整棵树,树中的最小值就是该节点
    在这里插入图片描述

heapInsert操作(插入)和heapify操作(删除)

大根堆为例

两个思考问题

1. 当用户给了一个数组时,用户逐渐向数组中添加数字,我们该如何调整使其变成大根堆呢?

  • 逐渐向数组添加数字,当添加的数字( i 位置),小于它的父节点(( i - 1 )/ 2)时,直接添加就可以了。
  • 当添加的数字( i 位置),大于它的父节点(( i - 1 )/ 2)时,就需要进行交换了。将 i 位置的新添加的数字与它的父节点(( i - 1 )/ 2)的数字进行交换。
    在这里插入图片描述
  • 通过上述方法,就能保证在数组的成长过程中,是成长出的每一步都是大根堆。
  • 这个向数组中逐步添加数字调整形成大根堆的过程就叫做heapInsert
  • 代码如下:
	public void heapInsert(int [] arr ,int index){
		while(arr[index] > arr[(index-1)/2]){
			swap(arr,index,(index-1)/2);//交换index和(index-1)/2
			index=(index-1)/2;
		}
	}

2. 调整为大根堆后,进行popmax()操作(减堆操作,减去最大值)后,我们又该如何调整使其变成大根堆呢?

  • 开辟一个临时变量max,将该大根堆中的最大值赋值给max,交换最后一位与最大值的位置(将最末尾的数字写入下标为0 的位置)
  • 注意:这里最末尾的数字不一定是最小值。只是最后一个数字在这里插入图片描述
  • 代码如下:
	public void heapify(int [] arr,int index ,int heapsize){
		int left = index * 2 + 1;//左孩子下标
		while(left<heapsize){//下方还有孩子时
		//找到index节点两个孩子中较大的一个,赋值给largest
			int largest = left+1 < heapsize && arr[left+1] >arr[left] ? left+1 : left;  
		//较大的孩子与父节点index进行比较
			largest = arr[largest] > arr[index] ? largest : index;
		//两个孩子均没有父节点大
			if(largest==index){
				break;
			}
			swap(arr,largest,index);
			index=largest;
			left=index * 2 + 1;
		}
		
	}

关于堆,关键的也就是这两个操作heapInsert和heapify。清楚堆的这两个操作,对于堆排序就很容易理解了

堆排序

堆排序,简单来说,就是重复的heapify操作。再简单点,就是popmax操作。

举个例子来说。

假如用户给了一个数组[ 3 4 0 2 7 ],怎么进行堆排序呢?

我们上面讲到,当用户一个一个向数组中添加新的数字,我们可以使用heapInsert操作将其排成大根堆

现在用户给了我们数组中全部的数字,让我们排成大根堆,怎么做呢?

其实方法是一样的。我们自己一个一个加进堆中不就可以了!

对,这就是堆排序的第一步。

1. 先将整个数组变成大根堆结构

  • 有效size = 1。先看第 0 位 - 第 0 位上是不是大根堆?[ 3 4 0 2 7 ]

    • 此时 i = 0;只有 3 自己,当然,是大根堆。
  • 有效size = 2。再看第 0 位到第 1 位呢?

    • 不是大根堆,此时 i = 1;因为4的父节点(i-1)/2 =0 ,第 0 位为3 < 4
    • 小了怎么办? 我们就用 heapify 调整不就可以了
    • 经过调整之后,数组就变为了[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 3。再往下,第 0 位到第 2 位呢?

    • 是大根堆,此时 i = 2;因为 0 的父节点(i-1)/2 =0 ,第 0 位为 4 > 0
    • 此时,数组为[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 4。第 0 位到第 3 位呢?

    • 是大根堆,此时 i = 3;因为 2 的父节点(i-1)/2 =1 ,第 1 位为 3 > 2
    • 此时,数组为[ 4 3 0 2 7 ]
      在这里插入图片描述
  • 有效size = 5。第 0 位到第 4 位呢?

    • 是大根堆,此时 i = 4;因为 7 的父节点(i-1)/2 =1 ,第 1 位为 3 < 7
    • 用 heapify 调整,
    • 子节点与父节点交换,交换完之后,数组为[ 4 7 0 2 3 ],此时是大根堆了吗?
    • 当然还不是,我们在进行调整,子节点与父节点交换,交换完之后,数组为[ 7 4 0 2 3 ],OK,此时调整为了大根堆
      在这里插入图片描述

2. 到这里,我们的堆排序第一步就完成了。我们的第二步就相当于做popmax操作

  1. 将最后一位与第 0 位进行交换,有效size=5-1=4 。 即切断树的最后一个分支
    交换后数组为[ 3 4 0 2 7 ],这里7并没有消失,仍在这里,只是7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整为[ 4 3 0 2 7 ]
    在这里插入图片描述
  2. 继续让最后一位与0位置的数进行交换,有效size=4-1=3 。 即切断树的最后一个分支
    交换后数组为[ 2 3 0 4 7 ],这里4 7并没有消失,仍在这里,只是4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整为[ 3 2 0 4 7 ]
    在这里插入图片描述
  3. 继续让最后一位与0位置的数进行交换,有效size=3-1=2 。 即切断树的最后一个分支
    交换后数组为[ 0 2 3 4 7 ],这里3 4 7并没有消失,仍在这里,只是3 4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整[ 2 0 3 4 7 ]
    在这里插入图片描述
  4. 继续让最后一位与0位置的数进行交换,有效size=2-1=1 。 即切断树的最后一个分支
    交换后数组为[ 0 2 3 4 7 ],这里2 3 4 7并没有消失,仍在这里,只是2 3 4 7处于无效区中。
    当然,交换完之后仍要保持大根堆的状态,此时就需要进行调整[ 0 2 3 4 7 ]
    在这里插入图片描述
  5. 继续让最后一位与0位置的数进行交换,有效size=1-1=0 。此时停止,再看此时数组为[ 0 2 3 4 7 ],这就排好序了,结束。

这整个过程就是堆排序,先排成大根堆,然后popmax,当有效区为0时,顺序就排好了,这整个过程就叫做heapSort()

代码如下:

	public void heapSort(){
		if(arr==null||arr.length<2){
			//提示为空或只有一个数
		}
		//把列出的数组中的数一个一个heapInsert,排成大根堆
		for(int i=0;i<arr.length;i++){
			heapInsert(arr,i);
		}
		int heapSize=arr.length;
		//交换第0位和最后一位
		swap(arr,0,--heapSize);
		//当有效区等于0时,结束
		while(heapSize>0){
			heapify(arr,0,heapSize);
			swap(arr,0,--heapSize);
		}
	}

堆排序的优化

上述代码中,我们使用heapInsert()操作,将用户所提供的数组排成大根堆。
这种依次将数加进来的方法可行,但是我们分析它的时间复杂度为O( N * logN )
因此,就有了另外一种做法。
既然用户已经给了数组中所有的数,那我为什么不直接把这个二叉树排成大根堆呢?
举个例子吧。
还是那个数组[ 3 4 0 2 7 ]
在这里插入图片描述

  • 这个时候,我先看最后一层以最后一层节点为头结点的树是不是大根堆?

    因为只有一个数,所以,是大根堆。这一步,时间复杂度为O(N/2)

  • 倒数第二层,以倒数第二层节点为头结点的树是不是大根堆?

    这个时候,最糟糕的情况,我需要看的同时进行一步调整,这一步,时间复杂度为O(N/4)*2

  • 倒数第三层,以倒数第三层节点为头结点的树是不是大根堆?

    这个时候,最糟糕的情况,我需要看的同时进行两步调整,这一步,时间复杂度为O(N/8)*3

    根据这个,我们可以判断这样的时间复杂度应该为T(N)=O(N/2)+O(N/4)*2+O(N/8)*3+。。。。
    等比数列求和T(N)=C*O(N)。 C=2

所以,当用户给出我们数组中全部的数时,我们完完全全的从底开始进行heapify操作要比一个一个的heapInsert操作要快
修改代码如下:

	public void heapSort(){
		if(arr==null||arr.length<2){
			//提示为空或只有一个数
		}
		//把列出的数组中的数一个一个heapInsert,排成大根堆
		for(int i=arr.length;i>=0;i--){
			heapify(arr,i,arr.length);
		}
		int heapSize=arr.length;
		//交换第0位和最后一位
		swap(arr,0,--heapSize);
		//当有效区等于0时,结束
		while(heapSize>0){
			heapify(arr,0,heapSize);
			swap(arr,0,--heapSize);
		}
	}

综上

堆排序的整个过程为:
	- 现将整个数组变成大根堆结构
		- 一个一个heapInsert,时间复杂度O( N * logN )
		- 整体heapify,时间复杂度O( N  )
	- 将堆的最后一位与第 0 位进行交换,在调整为大根堆,当堆的大小为 0 时,排序完成
  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值