Leetcode刷题笔记——堆篇

Leetcode刷题笔记——堆篇

一、堆的相关概念

1.堆的特性

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

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

方案1: 在堆中一般将数组的第一个位置(即数组下标为 0 的位置)元素置为空(即从索引1开始编号)

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

方案2: 勤俭持家小能手,数组的第一个位置不置空(即从索引0开始编号)

使其满足父结点与左右孩子结点的编号关系为 : 父结点: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

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

def min_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 heap[child] < heap[parent]:
            heap[child], heap[parent] = heap[parent], heap[child]   # 交换位置
            parent = child      # 父节点指向子节点的位置
        else:
            break


# 构建堆
nums_1 = [6, 5, 1, 2, 8, 3, 4, 9]
nums_2 = [6, 5, 1, 2, 8, 3, 4, 9]
# 从最后一个非叶子节点遍历到根节点,遍历结束后 nums 即为一个最小堆
for i in range(len(nums_1) // 2 - 1, -1, -1):
    min_heapify(nums_1, i, len(nums_1))

print(f"手动调整最小堆nums_1:{nums_1}")  # [9, 8, 4, 5, 6, 3, 1, 2]

# 方案2:使用heapq.heapify
heapq.heapify(nums_2)
print(f"调用模块调整为最小堆nums_2: {nums_2}")

将数组调整为最大堆

def max_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 heap[child] > heap[parent]:
            heap[child], heap[parent] = heap[parent], heap[child]   # 交换位置
            parent = child      # 父节点指向子节点的位置
        else:
            break


# 构建堆
nums_1 = [6, 5, 1, 2, 8, 3, 4, 9]
# 从最后一个非叶子节点遍历到根节点,遍历结束后 nums 即为一个最大堆
for i in range(len(nums_1) // 2 - 1, -1, -1):
    max_heapify(nums_1, i, len(nums_1))

print(f"手动调整最大堆nums_1:{nums_1}")  # [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()操作的逻辑是一样的,只是写法上稍微有点差异,为了方便大家理解不同博主的代码风格,其实构建一个最大堆的本质就是从最后一个非叶子节点开始,依次下沉调整

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

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

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

小结

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

·

二、TOPK问题(壳子题:能套很多题)

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

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

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

第一题:前K个高频元素

leetcode347-前K个高频元素:中等题 (详情点击链接见原题)

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案

解题思路

  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
  1. 构造规模为 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小顶堆

  1. 维护最小堆-在遍历序列剩余元素过程中
    接下来我们需要遍历规模 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)

python代码解法(手动实现堆):

from collections import Counter


class Solution:

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        def insert_heap(min_heap):  # 在堆的末尾插入元素需要进行上浮操作
	        """上浮,如果新加入的节点 < 父节点,新节点一直上浮"""
            child_index = len(min_heap) - 1
            parent_index = (child_index - 1) // 2
            value = min_heap[child_index]   # 先将最后一个叶子节点保存,因为还不确定该节点的最终位置
            
             # 统计前K个高频元素,所以这里比较的是频率,迭代比较确定新节点最终的位置
            while parent_index >= 0 and value[1] < min_heap[parent_index][1]:
                min_heap[child_index] = min_heap[parent_index]
                child_index = parent_index
                parent_index = (child_index - 1) // 2
            min_heap[child_index] = value    # 将新节点放入它该在的位置

        def sift_down(parent_index, heap, heap_size):
	        """下沉,如果新的根节点>子节点就一直下沉"""
            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][1] < heap[child_index][1]:  # 左右子节点进行比较,选取左右孩子中小的与父节点交换
                    child_index = child_index + 1
                if heap[child_index][1] < temp[1]:   # 如果子节点 < 新节点,交换
                    heap[parent_index] = heap[child_index]
                    parent_index = child_index
                    child_index = 2 * parent_index + 1
                else:    # 否则说明已经有序 break
                    break
            heap[parent_index] = temp

        frequency = Counter(nums)
        stat = list(frequency.items())
        # [(1, 3), (2, 2), (3, 15), (4, 4), (6, 8), (8, 11)] (数字,该数字出现的频率)
        min_heap = []
        for i in range(k):
            min_heap.append(stat[i])    # 新元素加入堆尾
            insert_heap(min_heap)  # 构造含k个元素的最小堆

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

python代码解法(利用内置模块heapq):

import heapq
from collections import Counter


class Item:
    def __init__(self, key, value):
        self.key = key
        self.value = value

    def __lt__(self, other):
        if self.value != other.value:
            return self.value < other.value
        return self.key > other.key   # 保证堆中元素在value相同的情况下,按key的大小从大到小排序


class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        min_heap = []
        fre_cnt = list(Counter(nums).items())

        for key, value in fre_cnt:
            heapq.heappush(min_heap, Item(key, value))
            while len(min_heap) > k:
                heapq.heappop(min_heap)
        return [item.key for item in min_heap]

第二题:前K个高频元素

Leetcode692. 前K个高频单词:中等题 (详情点击链接见原题)

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序

python代码解法(利用内置模块heapq):

import heapq
from collections import Counter

class Word:
    def __init__(self, key, value):
        self.key = key
        self.value = value

    def __lt__(self, other):
        if self.value != other.value:
            return self.value < other.value
        return self.key > other.key  # 从小到大


class Solution:
    def topKFrequent(self, words: List[str], k: int) -> List[str]:
        cnt = Counter(words)
        min_heap = []
        for key, value in cnt.items():
            heapq.heappush(min_heap, Word(key, value))
            while len(min_heap) > k:
                heapq.heappop(min_heap)
        min_heap.sort(reverse=True)
        return [x.key for x in min_heap]

第三题:最小K个数

面试题 17.14. 最小K个数:中等题 (详情点击链接见原题)

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可

python代码解法:

class Solution:
    def smallestK(self, arr: List[int], k: int) -> List[int]:
        max_heap = []
        for i in range(len(arr)):
            heapq.heappush(max_heap, -arr[i])
            # print(max_heap)
            while len(max_heap) > k:
                heapq.heappop(max_heap)
                # print(max_heap)
        return [-i for i in max_heap]

三、第 K 大 or 第 K 小问题

第一题:数组中的第K个最大的元素

leetcode215-数组中的第K个最大的元素:中等题 (详情点击链接见原题)

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

class Solution:
    def max_heapify(self, heap, heap_size, root):
        left = 2 * root + 1
        while left <= heap_size:
            if left + 1 <= heap_size and heap[left + 1] > heap[left]:
                left += 1  # 取两者之间的较大者
            if heap[root] < heap[left]:  # 如果父结点中的元素小于子节点中的较大者
                heap[root], heap[left] = heap[left], heap[root]  # 交换父结点和较大子节点
                root = left  # 将子节点作为父结点继续往下调整
                left = 2 * root + 1
            else:
                break

    def findKthLargest(self, nums: List[int], k: int) -> int:
        n = len(nums)
        for i in range(n // 2 - 1, -1, -1):  # 注意建立堆时的取值
            self.max_heapify(nums, n - 1, i)  # 建立大根堆

        for i in range(n - 1, n - 1 - k, -1):  # 堆排序的本质
            nums[i], nums[0] = nums[0], nums[i]  # 将第一个元素和最后一个元素互换
            self.max_heapify(nums, i - 1, 0)  # 然后排除最后一个元素重新按照大根堆进行调整
        
        # for i in range(1, k + 1):
        #    print(f"第{i}次下沉,下称的元素为{nums[0]}")
        #    nums[0], nums[n - i] = nums[n - i], nums[0]
        #    max_heapify(nums, 0, n - i)
        # return nums[-k]
        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)

解题思路
维护一个规模为 k 的最小堆,遍历完 nums 中所有元素后,min_heap 中保存的是前 K 个最大的元素,堆顶元素则为第 K 个最大的元素

python代码解法(利用内置模块heapq):

import heapq


class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        min_heap = []
        for i in range(len(nums)):
            heapq.heappush(min_heap, nums[i])
            if len(min_heap) > k:
                heapq.heappop(min_heap)
        return min_heap[0]

第二题: 数据流的中位数

Leetcode295. 数据流的中位数:困难题 (详情点击链接见原题)

中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值

解题思路:
将数据流保存在一个列表中,并在添加元素时保持数组有序
建立一个小顶堆 A 和 大顶堆 B,各保存列表一半的元素,且规定

  • A(小顶堆) 保存较大的一半,长度为 N // 2 (N 为偶数)(N + 1) // 2 (N 为奇数)
  • B(大顶堆) 保存较小的一半,长度为 N //2 (N 为偶数)(N - 1) // 2 (N 为奇数)

则中位数可仅根据 A, B 的堆顶元素计算得到

addNum(num)

  1. m = n(即 N 为偶数时):需向 A 添加一个元素【将新元素num插至 B,再将 B 堆顶元素插入至 A
  2. m ≠ n(即 N 为奇数时):需向 B 添加一个元素【将新元素 num 插至 A,再将 A 堆顶元素插入至 B

findMedian()
3. 当 m = n(N 为偶数):则中位数为 (A的堆顶元素 + B的堆顶元素) // 2
4. 当 m ≠ n(N 为奇数) :则中位数为 A的对应元素

注意:一个最小堆 min_heap,一个最大堆 max_heap,要根据两个堆的大小先选择一个堆插入元素,再把第一个堆的堆顶插入到第二个堆,以确保最小堆的全部元素都比最大堆要小

python完整题解代码

import heapq


class MedianFinder:

    def __init__(self):
        self.A = []  # 小顶堆: 保存较大的一半
        self.B = []  # 大顶堆: 保存较小的一半

    def addNum(self, num: int) -> None:
        if len(self.A) != len(self.B):  # 两个堆中元素不等时往 B 中插
            heapq.heappush(self.A, num)
            heapq.heappush(self.B, -heapq.heappop(self.A))
        else:  # 两个堆中元素相等时往 A 中插
            heapq.heappush(self.B, -num)
            heapq.heappush(self.A, -heapq.heappop(self.B))

    def findMedian(self) -> float:
        if len(self.A) != len(self.B):  # 两个堆中元素不等时取两个堆中的堆顶元素的平均值
            return self.A[0]
        else:
            return (self.A[0] - self.B[0]) / 2

第三题:查找和最小的 K 对数字

Leetcode373. 查找和最小的 K 对数字:中等题 (详情点击链接见原题)

定两个以 非递减顺序排列 的整数数组 nums1nums2 , 以及一个整数 k
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2

解题思路:
初始把 (0, 0) 入堆,每次出堆时,可能成为下一个数对的是 (i + 1, j)(i, j + 1),但这会导致一个问题,当 (1, 0) 出堆时,会把 (1,1) 出堆,当 (0, 1) 出堆时,也会把 (1, 1) 出堆,为了避免重复元素,需要一个哈希表记录在堆中的下标对,只有当下标不在堆中时,才能入堆

如果要把 (i, j) 入堆,那么出堆的下标对是什么?根据上面的讨论,出堆的下标只能是 (i - 1, j), (i, j - 1),只要保证 (i - 1, j)(i, j - 1) 其中一个会将 (i, j) 入堆,而另一个什么也不做就不会出现重复了

python完整题解代码

class Solution:
    def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
        ans = []
        heap = []
        for i in range(min(len(nums1), k)):
            heapq.heappush(heap, [nums1[i] + nums2[0], i, 0])
        while heap and len(ans) < k:
            _, i, j = heapq.heappop(heap)
            ans.append([nums1[i], nums2[j]])
            if j + 1 < len(nums2):
                heapq.heappush(heap, [nums1[i] + nums2[j + 1], i, j + 1])
        return ans

四、堆的其他应用

第一题:合并K个升序链表

Leetcode23:合并K个升序链表困难题

给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表

方法1:最小堆(需要具备堆的基础概念)

方法1需要大家对堆的基础知识有一定的了解,如果不熟悉的同学请先去了解一下最大堆最小堆的概念以及如何进行堆调整,堆中节点的下沉上浮,没搞清楚这些概念之前这个方法会很难理解

分析

  • 合并后的第一个节点first一定是某个链表的头结点(因为链表已经升序排列),
  • 合并后的第二个节点可能是某个链表的头结点,也可能是first的下一个节点,那么每当我们找到一个节点值最小的节点x,我们就把节点 x.next 加入【可能是最小节点的集合】中因此我们可以利用最小堆来充当这个可能是最小节点的集合

在了解了上述概念之后其实并不用大家动手写关于堆的代码,python中有个标准库heapq里面封装好了关于堆的操作方法,现在让我来给大家介绍一下

heapq.heappush(heap, num):先创建一个空堆,然后将数据一个一个地添加到堆中。每添加一个数据后,heap都满足小顶堆的特性
heapq.heappop(heap):将堆顶的数据出堆,并将堆中剩余的数据构造成新的小顶堆

python题解代码

import heapq


class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:

        dummy = ListNode(0)
        p = dummy
        heap = []
        for i in range(len(lists)):
            if lists[i]:
                heapq.heappush(heap, (lists[i].val, i))  # 将所有链表的头结点入堆(这里根据lists[i].val构建最小堆)
                lists[i] = lists[i].next  # 指针后移
        while heap:
            val, idx = heapq.heappop(heap)  # 将堆顶的最小元素弹出
            p.next = ListNode(val)
            p = p.next
            if lists[idx]:  # 如果弹出的最小节点的下个节点不为空(则有可能为最小)
                heapq.heappush(heap, (lists[idx].val, idx))  # 继续入堆(重新自动构建最小堆)
                lists[idx] = lists[idx].next
        return dummy.next


def create_list(linklist):
    """
    :param linklist:拿来构建链表的数组
    :return: head-返回链表的头结点
    """
    node_list = []
    for i in range(len(linklist)):
        node = ListNode()
        node.val = linklist[i]
        node_list.append(node)
    head = node_list[0]
    p = head
    for node in node_list[1:]:
        p.next = node
        p = p.next
    return head


if __name__ == '__main__':
    s = Solution()
    res = []
    lists = [[1, 4, 5], [1, 3, 4], [2, 6]]
    for lis in lists:
        res.append(create_list(lis))
    after_head = s.mergeKLists(res)
    while after_head:
        print(after_head.val)
        after_head = after_head.next

总结

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

code_lover_forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值