大顶堆 小顶堆 堆排序

基础知识

大顶(根)堆与小顶(根)堆都是一种数据结构,它们都是完全二叉树,但不是用指针链表表示的二叉树,而是使用顺序存储的数组存储元素值。大顶堆即堆顶元素为整个堆中的最大值(小顶堆类似),它可以在O(1)时间内获取最大值。下面以大顶堆讲解一些堆的相关知识,并给出大、小顶堆的代码。
首先,既然是顺序存储,且又是完全二叉树的结构,其节点如何存储呢?此处使用二叉树的层次遍历方式存储节点值,本文使用的节点索引如下:

父节点:(root-1) / 2
左子节点:root * 2 + 1
右子节点:root * 2 + 2

即:

def left(root):
    return 2 * root + 1
    
def right(root):
    return 2 * root + 2
        
def parent(root):
    return (root - 1) // 2

这种索引方式下根节点在数组的0位置,也有将数组0位置不用,而将根节点放在1位置的方式,那种方式的索引和本文使用的稍有不同,此处也记录下:

父节点:root / 2
左节点:root * 2
右节点:root * 2 + 1

对于两种方式,完全可以分别使用[0,1,2][1,2,3]分别代表父节点、左右子节点来尝试。
在介绍具体如何构建大顶堆之前,先介绍两种操作:sinkswim

sink: 将当前节点的值往下沉至合适的位置,比如当前值小于其某个子节点,则需要做sink操作
swim: 将当前节点的值往上移动至合适的位置,比如当前值大于其父节点,则需要做swim操作

首先,swim操作比较简单,只要当前节点值大于其父节点值,就一直将当前节点的值与父节点交换,代码如下:

def swim(nums,root):
    while root > 0 and nums[parent[root]]<nums[root]:
        nums[root],nums[parent(root)] = nums[parent(root)],nums[root]
        root = parent(root)

注意条件中的root>0,因为对于根节点来说,显然是没有父节点的。
swim操作比较简单,只需比较当前节点与父节点的值,但是对于sink操作来说,它需要比较当前节点与左右子节点的值。首先,我们需要清楚什么时候需要执行sink操作,因为是大顶堆,父节点必须大于等于左右子节点,因此只要当前节点不同时大于其左右子节点,那么就需要交换。那么,怎么交换?和谁交换呢?我们可以先获得左右子节点的最大值older,如果当前节点的值大于等于older,说明它无需再往下做sink操作了;如果小于older,将当前节点的值与older对应的节点的值交换,即可在完成当前点的下沉的同时,满足大顶堆的性质(父节点大于等于子节点)。那么,下沉到何时结束呢?下沉至当前节点没有左右子节点即可。

def sink(nums,root):
    length = len(nums)
	while left(root) < length:
	    # 先假设左节点是较大的子节点,注意此处older是最大值的索引
	    older = left(root)
	    # 若右子节点大于左子节点,更新older
	    while right(root) < length and nums[right(root)] > older:
	        older = right(root)
        # 若当前节点大于等于older,说明下沉结束
        if nums[older] <= nums[root] :
            break
        # 交换
        nums[root],nums[older] = nums[older],nums[root]
        root = older

以上,sinkswim操作就完成了,在具体将如何构建大顶堆之前,需要指出一点,在sinkswim中,频繁地使用了交换操作,当树的结构很大时,其实这些交换操作会很耗时。实际上,我们可以无需每次进行交换,而是保存一开始需要操作的节点值,直到找到它该放的位置再赋值,期间只需不断更新中间节点的值即可。
对于sink操作来说,首先保存当前节点的值为temp,在while循环中,判断older的值与temp的值,若older大于temp,将当前节点的值设置为older,继续往下搜索,否则,将当前节点的值赋值为temp。这个过程拿纸笔画一下,就很清晰了。
修改后代码如下所示:

def sink(nums,root):
    length = len(nums)
    # 记录需要下沉的点的值
    temp = nums[root]
	while left(root) < length:
	    # 先假设左节点是较大的子节点,注意此处older是最大值的索引
	    older = left(root)
	    # 若右子节点大于左子节点,更新older
	    while right(root) < length and nums[right(root)] > older:
	        older = right(root)
        # 若temp大于等于older,说明下沉结束
        if nums[older] <= temp:
            break
        # 直接赋值即可
        nums[root] = nums[older]
        root = older
    # 最后把temp放在该放的位置
    nums[root] = temp

对于swim操作来说,同样可以记录欲上升的点为temp,然后每次判断父节点与temp的关系,代码如下:

def swim(nums,root):
	temp = nums[root]
	while root > 0 and nums[parent(root)]  < temp:
	    # 若父节点的值小于temp,将当前值赋值为父节点的值
	    nums[root] = nums[parent(root)]
	    root = parent(root)
	# 将temp放在合适的位置
	nums[root] = temp

上述两个操作,对于单个节点来说,时间复杂度都为 O ( l o g N ) O(logN) O(logN)
至此,swimsink操作就讲完了,在讲构建之前,再讲一下大顶堆的插入和删除操作。
首先,对于插入操作来说,可以直接将插入的值放在堆底,然后对其进行swim操作即可。
对于删除操作(大顶堆的删除指删除堆顶元素)来说,先将堆底的值赋值给堆顶,然后将堆底的值弹出,再将此时的堆顶进行sink操作即可。
可见,插入和删除操作复杂度都是 O ( l o g N ) O(logN) O(logN)

大顶堆

下面,开始讲大顶堆的创建。
创建最大堆有两种方法:

  1. 先创建一个空堆,然后每次往其中插入一个点,故复杂度为 O ( N l o g N ) O(NlogN) O(NlogN)
  2. 先将N个元素顺序放入堆中,然后从最后一个非叶子节点开始,依次进行sink操作,这个复杂度肯定是小于 O ( N l o g N ) O(NlogN) O(NlogN)。因为它没有对N个点进行操作。

此处使用第二种方法,从最后一个非叶子节点开始依次往上进行sink操作。那么,首先得定位最后一个非叶子节点,最后一个非叶子节点即是堆底节点的父节点,即它的索引是parent(length-1)
以下,贴出大顶堆的python代码。

class MaxHeap:
    def __init__(self,nums):
        self.nums = nums
        self.length = len(nums)
        # 此处加1是因为,python的range无法取到最后一个元素
        for i in reversed(range(self.parent(self.length-1)+1)):
            self.sink(i)
            
    def left(self,root):
        return 2 * root + 1
        
    def right(self,root):
        return 2 * root + 2
        
    def parent(self,root):
        return (root - 1) // 2
        
    def sink(self,root):
        temp = self.nums[root]
        while self.left(root) < self.length:
            # 先假设左孩子较大
            older = self.left(root)
            # 若右孩子较大,更新older
            if self.right(root) < self.length and self.nums[self.right(root)] > self.nums[older]:
                older = self.right(root)
            # 若最大值小于等于temp,停止下沉
            if self.nums[older] <= temp:
                break
            # 更新
            self.nums[root] = self.nums[older]
            root = older
        # 将temp放在该放的位置上
        self.nums[root] = temp
        
    def swim(self,root):
        temp = self.nums[root]
        while root > 0 and self.nums[self.parent(root)] < temp:
            self.nums[root] = self.nums[self.parent(root)]
            root = self.parent(root)
        self.nums[root] = temp
            
    def insert(self,value):
        self.nums.append(value)
        self.length += 1
        self.swim(self.length-1)
        
    def delMax(self,):
        self.nums[0] = self.nums[-1]
        self.nums.pop()
        self.length -= 1
        self.sink(0)
        
    @property
    def max(self,):
        return self.nums[0]
 
# 测试代码
print('\n')
print('原始数组为:')
nums = [21,25,49,25,16,8]
print(nums)
a = MaxHeap(nums)
print('构建好的大顶堆:')
print(a.nums)
print('最大值为:')
print(a.max)
print('插入元素50: ')
a.insert(50)
print(a.nums)
print('最大值为:')
print(a.max)
print('删除最大值后:')
a.delMax()
print(a.nums)
print('最大值为:')
print(a.max)

测试:
在这里插入图片描述

小顶堆

小顶堆其实和大顶堆很类似,只需要修改sinkswim操作的比较规则即可,此处直接贴代码。

class MinHeap:
    def __init__(self,nums):
        self.nums = nums
        self.length = len(self.nums)
        for i in reversed(range(self.parent(self.length-1)+1)):
            self.sink(i)

    def left(self,root):
        return 2 * root + 1
        
    def right(self,root):
        return 2 * root + 2
        
    def parent(self,root):
        return (root - 1) // 2
        
    def sink(self,root):
        temp = self.nums[root]
        while self.left(root) < self.length:
            smaller = self.left(root)
            if self.right(root) < self.length and self.nums[self.right(root)] < self.nums[smaller]:
                smaller = self.right(root)
            if temp <= self.nums[smaller]:
                break
            self.nums[root] = self.nums[smaller]
            root = smaller
        self.nums[root] = temp

    def swim(self,root):
        temp = self.nums[root]
        while root > 0 and self.nums[self.parent(root)] > temp:
            self.nums[root] = self.nums[self.parent(root)]
            root = self.parent(root)
        self.nums[root] = temp

    def insert(self,value):
        self.nums.append(value)
        self.length += 1
        self.swim(self.length-1)

    def delMin(self,):
        self.nums[0] = self.nums[-1]
        self.nums.pop()
        self.length -= 1
        self.sink(0)
    
    @property
    def min(self,):
        return self.nums[0]        
  
# 测试代码
nums = [49,25,21,25,16,8]
print('\n')
print('原始数组为:')
print(nums)
a = MinHeap(nums)
print('构建好的小顶堆:')
print(a.nums)
print('最小值为:')
print(a.min)
print('插入元素2: ')
a.insert(2)
print(a.nums)
print('最小值为:')
print(a.min)
print('删除最小值后:')
a.delMin()
print(a.nums)
print('最小值为:')
print(a.min) 

在这里插入图片描述

堆排序

此处讲升序排序,堆排序是基于最大堆进行的排序方法,其时间复杂度为 O ( N l o g N ) O(NlogN) O(NlogN),额外空间复杂度为 O ( 1 ) O(1) O(1),同时兼具了归并排序和插入排序列的优点(分别是时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)和额外空间复杂度为 O ( 1 ) O(1) O(1))。
其思想为:

先构建一个长度为N的大顶堆,然后交换堆顶和堆底的元素,将前N-1个元素看作一个大小为N-1的大顶堆重新调整(即对堆顶进行sink),然后继续交换堆顶和堆底,以此类推,最后即完成了升序排序。

class HeapSort:
    def __init__(self,nums):
        self.nums = nums
        self.length = len(nums)
        for i in reversed(range(self.parent(self.length-1)+1)):
            self.sink(i)

    def left(self,root):
        return 2 * root + 1
    
    def right(self,root):
        return 2 * root + 2

    def parent(self,root):
        return (root - 1) // 2

    def sink(self,root):
        temp = self.nums[root]
        while self.left(root) < self.length:
            older = self.left(root)
            if self.right(root) < self.length and self.nums[self.right(root)] > self.nums[older]:
                older = self.right(root)
            if self.nums[older] <= temp:
                break
            self.nums[root] = self.nums[older]
            root = older
        self.nums[root] = temp

    def heap_sort(self,):
        # 获取长度
        length = self.length
        for i in reversed(range(1,length)):
            self.nums[0],self.nums[i] = self.nums[i],self.nums[0]
            # 堆的大小减一
            self.length -= 1
            self.sink(0)
        # 恢复堆的真实长度
        self.length = length

# 测试代码
nums = [21,25,49,25,16,8]
print('\n')
print('原始数组为:')
a = HeapSort11(nums)
print('构建好的大顶堆:')
print(a.nums)
a.heap_sort()
print('堆排序后:')
print(a.nums)

在这里插入图片描述

如果想要降序排序,只需修改大顶堆为小顶堆即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值