Leetcode刷题笔记——堆

Leetcode刷题笔记——堆

堆的相关概念

1.堆的特性

① 必须是完全二叉树
② 用数组实现
③ 任意结点的值是其子树所有结点的最大值或最小值

2.使用数组构建堆的两种方式

方案1: 在堆中一般将数组的第一个位置(即数组下标为 0 的位置)元素置为空

使其满足父结点与左右孩子结点的编号关系为 : 父结点:n左子结点:2 × n右子结点: 2 × n + 1

方案2: 勤俭持家小能手,数组的第一个位置不置空

使其满足父结点与左右孩子结点的编号关系为 : 父结点:n左子结点:2 × n + 1右子结点: 2 × n + 2

基于两种方案在编码中需要注意的地方
注1:已知任一节点编号求父节点的编号

使用方案1当已知任一节点编号为x时,可推出它的父节点编号为 x // 2
使用方案2当已知任一节点编号为x时,它的父节点编号为 (x - 1) // 2

3.大顶堆和小顶堆的概念

大顶堆: 每个结点的值都大于或者等于其左右孩子结点的值,堆顶元素是堆中最大的元素
小顶堆:每个结点的值都小于或等于其左右孩子结点的值,堆顶元素是堆中最小的元素

4.堆调整/构建堆(将给定的数组构建为最大堆or最小堆)

对于堆的调整,我们一般从最后一个非叶子结点开始调整

使用方案1,最后一个非叶结点的编号为: len(heap) // 2
使用方案2,最后一个非叶结点的编号为: (len(heap) - 1) // 2

堆的调整本质上是一个全局范畴,对于给定的整个数组,需要我们将该数组调整为一个最大堆或者最小堆(也可以说构建一个最大堆/最小堆)
堆调整(最大堆)的python完整代码(方案2):

def heapify(heap, parent, heap_size):
    """
    :param heap: 传进来的拿来构建堆的数组
    :param parent: 父节点(一般从最后一个非叶子节点开始调整)
    :param heap_size: 数组的规模
    :return: 
    """
    while parent * 2 + 1 < heap_size:
        child = 2 * parent + 1  # 先定位左子节点
        if child + 1 < heap_size and heap[child + 1] > heap[child]:
            child = child + 1  # 找出左右子节点中较大的那个
        if child < heap_size and heap[child] > heap[parent]:
            heap[child], heap[parent] = heap[parent], heap[child]
            parent = child 
        else:
            break
            
# 构建堆
nums = [6, 5, 1, 2, 8, 3, 4, 9]
# 从最后一个非叶子节点遍历到根节点,遍历结束后 nums 即为一个最大堆
for i in range(len(nums) // 2 - 1, -1, -1):
    heapify(nums, i, len(nums))

print(nums)   # [9, 8, 4, 5, 6, 3, 1, 2]

5.节点的上浮和下沉

上浮:节点的上浮与堆的插入操作相关

在堆中插入一个节点一般放在最后一个位置(即将要插入的结点放在最底层的最右边),插入后如果破坏了最大堆或者最小堆的性质则需要把新节点到合适位置满足堆的性质

这里以最大堆举例,对于最大堆而言元素上浮的终止条件

  • 已经走到根节点
  • 已经走到比父节点小的位置

(最大堆中)元素上浮的python完整代码(方案2):

def sift_up(heap):
    """
    上浮,在最大堆中如果新加入的节点的值 > 父节点的值
    尾结点就一直上浮
    """
    child_index = len(heap) - 1
    parent_index = (child_index - 1) // 2
    temp = heap[child_index]  # temp保存插入的叶子节点的值用于最后的赋值

    while child_index > 0 and temp > heap[parent_index]:
        # 无需真正交换,单向赋值即可
        heap[child_index] = heap[parent_index]
        child_index = parent_index
        parent_index = (child_index - 1) // 2
    heap[child_index] = temp

小根堆的上浮大同小异,这里不做举例

下沉:下沉操作与堆的删除操作相关

堆的删除操作一般是将堆顶元素(最大值 or 最小值)和堆尾元素互换【用最后一个结点代替根结点】,并将堆的实际规模减1,最后对堆顶元素进行下沉操作(取根结点两孩子的较大的一个,若较大值大于此时的根节点,将该较大值移动至根结点,不断重复,直到找到最后一个结点该插入的位置),使其继续保持大根堆 or 小根堆的性质

(最大堆中)元素下沉的python完整代码(方案2):

def sift_down(parent_index, heap_size, heap):
    """
    堆的节点下沉操作,大根堆
    :param parent_index: 待下沉的节点下标
    :param heap_size: 堆的长度范围
    :param heap: 原数组
    :return:
    """
    # temp暂存要调整的结点值,在最后找到被筛选结点的最终位置时,直接放入最终位置
    temp = heap[parent_index]
    child_index = 2 * parent_index + 1
    while child_index < heap_size:
        """如果有右孩子,且右孩子小于左孩子的值,则定位到右孩子"""
        if child_index + 1 < heap_size and heap[child_index + 1] > heap[child_index]:
            child_index += 1
        # 如果父节点小于任何一个孩子的值,直接跳出
        if temp >= heap[child_index]:
            break
        # 否则将父节点的值赋值为子节点的值,父节点继续下沉
        heap[parent_index] = heap[child_index]
        parent_index = child_index
        child_index = 2 * parent_index + 1
    heap[parent_index] = temp

细心一点的同学会发现,这里的下沉操作sift_down()与我们上面堆调整的heapify()操作的逻辑是一样的,只是写法上稍微有点差异,为了方便大家理解不同博主的代码风格,其实构建一个最大堆的本质就是从最后一个非叶子节点开始,依次下沉调整

在实际刷题中其实利用插入(上浮)与删除(下沉)操作与堆的调整操作可以互相取代,都可以完成对方所能完成的任何操作,但是一个是全局操作,一个是局部操作(针对某个元素的调整)

对于已经是一个最大堆/最小堆,只是因为插入/删除某个节点破坏了最大堆/最小堆的性质,则应使用上浮或下沉操作
对于一个给定的数组,需要将整个数组构建成一个最大堆/最小堆则应是一个全局性的操作,应使用堆调整操作构建一个最大堆/最小堆

注:在某些题目中如果对本可以使用上浮或下沉完成的,使用堆调整去完成虽然在本地调试没有问题,但在力扣上提交代码会超时

小结

千万不要觉得博主啰嗦,其实关于堆的核心就是这些细节,细节没搞清楚刷再多的题都是云里雾里,你会被循环和分支条件里面的各种条件给绕晕,这些细节决定了你在手动实现代码的时候对于循环和分支判断中是否应该写上等号,细节不清楚才是导致你头脑混乱的根本

·

面试中的高频考题

leetcode347-前K个高频元素:中等题

前情摘要:Top-K问题(壳子题:能套很多题)

场景: 从序列中选择最大的K个数
构造含K个数的小顶堆,每次取数组中剩余数与堆顶元素【K个数中的最小值】进行比较,如果新加入的数比堆顶元素大则用新元素替换堆顶元素,并对新堆重新进行堆调整【对新元素向下调整为一个新的最小堆】,如果新元素比堆顶元素小则新元素小于最小堆中的任意一个元素则不用进行操作
此时能保证最小堆中的k个元素即前k个最大的元素

场景: 从序列中选择最小的K个数
同理我们应构造含K个元素的大顶堆,每次取数组中剩余数与堆顶元素【K个数中的最大值】进行比较,如果新加入的数比堆顶元素小则用新元素替换堆顶元素,并对新堆重新进行堆调整【对新元素向下调整为一个新的最大堆】,如果新元素比堆顶元素大则新元素大于最大堆中的任意一个元素则不用进行操作

1.构造含K个元素的堆(最大堆 or 最小堆)Top-K大建小顶堆,Top-K小建大顶堆
2.遍历序列中剩余元素与堆顶元素进行比较,若互换则需对新堆进行堆调整
3.最后堆中剩余元素即我们需要的元素

接下来我们正式来看一下这道题

1.统计元素出现的频率(这里的操作与堆无关)

测试数据-瞎造的,方便演示

nums = [1, 1, 1, 2, 2, 3, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 6, 6, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8]
- 使用python自带的Counter方法
frequecy = Counter(nums)
  • 使用哈希表
frequecy = {}
for i in range(len(nums)):
	frequecy[nums[i]] = frequecy.get(nums[i], 0) + 1
2.构造规模为 k + 1 的最小堆 minHeap (采用方案1)

关于sift_up()函数-堆调整-上浮操作
在堆的末尾添加一个元素,与父节点比较, 如果大于父节点, 与父结点交换位置后(上浮节点继续垂直上浮直到成为根节点或者小于父节点)

def sift_up(min_heap, child):
    """上浮,如果新加入的节点 < 父节点就一直上浮"""
    val = min_heap[child]    # 先将最后一个叶子节点保存,因为还不确定该节点的最终位置

    # 统计前K个高频元素,所以这里比较的是频率,迭代比较确定新节点最终的位置
    while child // 2 > 0 and min_heap[child // 2][1] > val[1]:
        min_heap[child] = min_heap[child // 2]
        child //= 2 
    min_heap[child] = val	# 将新节点放入它该在的位置
stat = list(frequecy.items())
# [(1, 3), (2, 2), (3, 15), (4, 4), (6, 8), (8, 11)] (数字,该数字出现的频率)

# 构建规模为 k+1 的堆
min_heap = [(0, 0)]			# 将第一个位置占位
for i in range(k):
    min_heap.append(stat[i])	# 新元素加入堆尾
    sift_up(min_heap, len(min_heap) - 1)	# 堆调整-上浮

至此我们就构建了一个规模为 K + 1小顶堆

3.维护最小堆-在遍历序列剩余元素过程中

接下来我们需要遍历规模 k 之外的数据,大于堆顶元素则用新元素替换堆顶结点,从堆顶开始对新元素进行下沉操作

关于sift-down()函数-下沉操作

def sift_down(min_heap, root, k):
    """下沉,如果新的根节点>子节点就一直下沉"""
    while root * 2 < k:
        child = root * 2  # 左子节点
        # 左右子节点进行比较,选取左右孩子中小的与父节点交换
        if child + 1 < k and min_heap[child + 1][1] < min_heap[child][1]:
            child = child + 1
            
        # 如果子节点 < 新节点,交换,如果已经有序 break
        if min_heap[child][1] < min_heap[root][1]:
                min_heap[root], min_heap[child] = min_heap[child], min_heap[root]	# 交换父节点与子节点
                root = child	# 父节点重新赋值
            else:
                break
for i in range(k, len(stat)):
    if stat[i][1] > min_heap[1][1]:		# 大于堆顶元素则将新元素入堆
        min_heap[1] = stat[i]		# 用新节点替换堆顶结点
        sift_down(min_heap, 1, k + 1)
Leetcode347-前K个高频元素完整代码
from collections import Counter


class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
    	def sift_up(min_heap, child):
		    """上浮,如果新加入的节点 < 父节点就一直上浮"""
		    val = min_heap[child]    # 先将最后一个叶子节点保存,因为还不确定该节点的最终位置
		
		    # 统计前K个高频元素,所以这里比较的是频率,迭代比较确定新节点最终的位置
		    while child // 2 > 0 and min_heap[child // 2][1] > val[1]:
		        min_heap[child] = min_heap[child // 2]
		        child //= 2 
		    min_heap[child] = val	# 将新节点放入它该在的位置

		def sift_down(min_heap, root, k):
		    """下沉,如果新的根节点>子节点就一直下沉"""
		    while root * 2 < k:
		        child = root * 2  # 左子节点
		        # 左右子节点进行比较,选取左右孩子中小的与父节点交换
		        if child + 1 < k and min_heap[child + 1][1] < min_heap[child][1]:
		            child = child + 1
		            
		        # 如果子节点 < 新节点,交换,如果已经有序 break
		        if min_heap[child][1] < min_heap[root][1]:
		                min_heap[root], min_heap[child] = min_heap[child], min_heap[root]	# 交换父节点与子节点
		                root = child	# 父节点重新赋值
		            else:
		                break
	
		min_heap = [(0, 0)]			# 将第一个位置占位
		stat = list(frequecy.items())
		# [(1, 3), (2, 2), (3, 15), (4, 4), (6, 8), (8, 11)] (数字,该数字出现的频率)
		
		# 构建规模为 k+1 的堆
		for i in range(k):
		    heap.append(stat[i])	# 新元素加入堆尾
		    sift_up(heap, len(heap) - 1)	# 上浮

		for i in range(k, len(stat)):
		    if stat[i][1] > min_heap[1][1]:		# 大于堆顶元素则将新元素入堆
		        min_heap[1] = stat[i]		# 用新节点替换堆顶结点
		        sift_down(min_heap, 1, k + 1)

Leetcode215-数组中的第K个最大的元素

leetcode215-数组中的第K个最大的元素

求第K个最大元素无非就是按从小到大排好序之后的倒数第K个元素,就是排序的变种
python完整题解代码

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        def heapify(heap, root, heap_size):
            # 先对比左右子节点的大小
            while root * 2 < heap_size:
                child = 2 * root + 1  # 先定位左子节点
                if child + 1 < heap_size and heap[child + 1] > heap[child]:
                    child = child + 1  # 找出左右子节点中较大的那个
                if child < heap_size and heap[child] > heap[root]:
                    heap[child], heap[root] = heap[root], heap[child]
                    root = child
                else:
                    break

        n = len(nums)
        # 对于堆的调整,我们一般从最后一个非叶子结点开始调整
        for i in range(n // 2 - 1, -1, -1):  # 构建一个最大堆
            heapify(nums, i, n)
        for j in range(n - 1, n - k - 1, -1):
            nums[j], nums[0] = nums[0], nums[j]
            heapify(nums, 0, j - 1)
        return nums[-k]

如果说上题我们采用循序渐进先讲原理再讲方法再讲实现思路,那么本题我选择直接对代码分析
本题我们采用的建堆方案是方案2,即勤俭持家式建堆,数组下标为0的位置不浪费,
1.我们在将该数组调整为一个最大堆,在进行堆调整的时候,从最后一个非叶节点开始调整
最后一个非叶节点下标的取值为 n // 2 - 1,第一个元素即数组下标为0的位置也要参与进来,所以第二个参数为 -1

n = len(nums)
# 对于堆的调整,我们一般从最后一个非叶子结点开始调整
for i in range(n // 2 - 1, -1, -1):  # 构建一个最大堆
	heapify(nums, i, n)

2.建大顶堆的方式大同小异,我们的上题和堆排序中的建堆方式都是如此,注意一下细节
- 传进去的n是堆的规模,分支和循环里面的条件不要取等号
- 我们采用的是方案2,父节点的左子节点的编号表示方式为 2 * root + 1
- 在进行节点之间值的比较的时候要先判断一下节点编号是否超出了堆的规模

def heapify(heap, root, heap_size):
    # 先对比左右子节点的大小
    while root * 2 < heap_size:
        child = 2 * root + 1  # 先定位左子节点
        if child + 1 < heap_size and heap[child + 1] > heap[child]:
            child = child + 1  # 找出左右子节点中较大的那个
        if child < heap_size and heap[child] > heap[root]:
            heap[child], heap[root] = heap[root], heap[child]
            root = child
        else:
            break

3.将该数组调整为一个最大堆,因为每次堆调整都能确定一个最大元素的最终位置,所以我们只需进行k次调整就能确定第k大的元素的在堆中的最终位置

倒数第 1 大: n - 1
倒数第 2 大: n - 2
倒数第 3 大: n - 3

倒数第 k 大: n - k
要保证能对倒数第K个元素即 n - k 的位置进行调整,第二个参数必须为 n - k -1

for j in range(n - 1, n - k - 1, -1):
    nums[j], nums[0] = nums[0], nums[j]
    heapify(nums, 0, j - 1)

相关类似题型

Leetcode692-前K个高频单词
Leetcode703-数据流中的第K大元素

总结

本文主要给大家介绍了力扣上关于堆的面试中的常见高频考题,关于堆的概念博主看了很多题解以及相关的博文,很多博主只是简单介绍了,堆调整,堆排序,最大堆最小堆的相关概念,却没有讲在自己动手写代码的时候需要注意的细节,导致大家自己动手的时候,一看就会,一动就废,博主写的这篇博文只是帮大家聚焦真正让你棘手的地方,搞清楚细节才好动手实现!!!

你可能感兴趣的:(Leetcode刷题笔记,leetcode,笔记,算法,排序算法)