小黄的刷题之路(六)——码题集OJ赛-堆排序求最小范围

题目

在这里插入图片描述


分析和思路

2.1 审题

看完题目,我们可以总结出以下重要的几点:

  • 给定的数组本身是已经升序排列
  • 要求给出一个能包括所有数组至少一个元素的范围,这个范围的长度要尽可能小,同时区间的左端点尽可能小
  • 不超过5000组,每组元素不超过20个,元素取值范围在 [ − 120 , 120 ] [-120,120] [120,120]

2.2 思路

题目要求:【包括所有数组至少一个元素的最小范围

(为了方便说明和理解,以样例1举例)

(1)根据范围之间大小关系的定义,我们首先给出一个很容易能得到的满足条件的初始范围:[0 , 5],所有升序数组的首元素(即所有数组的最小值)中的最小值min作为左端点,最大值max作为右端点,这个范围很明显包括了所有数组的首元素,满足条件

(2)拿到这个初始范围[0 , 5]之后,我们怎么逐渐向最终的最小范围靠拢呢?——我的想法是从升序数组的左边向右边开始试探(保证了范围的左端点尽可能小),以范围长度尽可能小为目标去找

(3)为此我们利用堆排序算法来实现,堆排序的详细介绍和算法实现在下一part会介绍,这里先把堆排序简单理解为实现了升序排序,堆顶元素就是这个堆中的最小值

(4)这样子我们得到初始范围的过程就变成了:把上面三个数组(下面简称a b c)的首元素4,0,5放进堆中,堆排序之后堆顶元素就是0,得到一个满足条件的范围[0 , 5],这个范围的长度是 5 − 0 = 5 5-0=5 50=5把这个初始范围逐渐向右挪,看看有没有区间长度比5小的范围,如果有的话,把它作为新的最小范围,然后继续找继续比较,以此寻找最小范围。

(5)怎么向右挪,比较并更新最小范围呢?——把堆顶元素0弹出来,注意这个0其实是 b [ 0 ] b[0] b[0],接着我们把 b [ 1 ] b[1] b[1]加入堆中,堆排序之后堆变成[4 , 5 , 9],新的范围就是[4 , 9],因为[0,5] < [4,9],所以不做更新,当前的最小范围还是[0,5]。然后重复上述操作,直到加入堆的元素是某个数组的末尾元素,试探就结束了,此时的最小范围就是我们想要的最终答案。


代码实现

python标准库——heapq

heapq模块提供了堆队列算法的实现,这个API与教材的堆算法实现有所不同,具体区别有两方面:(a)我们使用了从零开始的索引。这使得节点和其孩子节点索引之间的关系不太直观但更加适合,因为 Python 使用从零开始的索引。 (b)我们的 pop 方法返回最小的项而不是最大的项

详见官方API文档的介绍:heapq — 堆队列算法 — Python 3.10.7 文档

python代码
import heapq
nums = []
in_nums = [] # 可以理解为 n 行的二维数组
def get_in():
    n = int(input()) # 升序数组的个数
    for _ in range(n): # _是占位符,表示不在意变量的值,只是用于循环遍历n次
        a = input()
        a = a.split(' ')
        a = list(map(int,a)) # 每个数组存储为列表,并作为元素放到nums里
        nums.append(a)
def main():
    get_in()
    rangeLeft, rangeRight = -120, 120
    maxValue = max(vec[0] for vec in nums) #所有升序数组首元素中最大的那个
    # 枚举函数 i是nums的下标,vec是对应的值(数组)
    # 元组的后两个数是为了定位nums里某个数组里的某个元素,而元组的第一个数就是这个元素
    # 元组 = (元素值,一级索引(哪一行/哪一个数组),二级索引(数组下标))
    priorityQueue = [(vec[0], i, 0) for i, vec in enumerate(nums)]
    # 为了方便理解,对应到题目给的例子:priorityQueue=[(4,0,0),(0,1,0),(5,2,0)]
    heapq.heapify(priorityQueue) #转换成堆,初始化堆	

    while True:
        # 从升序数组的左边开始向右找(暗中已经保证了最小范围的左边尽可能小),只需以区间长度最短为目标找到尽头就可以了
        # 弹出并返回priorityQueue的最小的元素
        minValue, row, idx = heapq.heappop(priorityQueue)
        if maxValue - minValue < rangeRight - rangeLeft: # 找出最短的区间长度
            rangeLeft, rangeRight = minValue, maxValue
        if idx == len(nums[row]) - 1:# 找到数组的末尾就结束了
            break
        maxValue = max(maxValue, nums[row][idx + 1])
        # 把弹出元素的下一个加入堆中,保持堆的不变性
        heapq.heappush(priorityQueue, (nums[row][idx + 1], row, idx + 1))
        
    print(str(rangeLeft)+' '+str(rangeRight))

if __name__ == '__main__':
    main();
代码逻辑的说明

为什么要heappop弹出堆顶的最小值,又把这个最小值的下一个元素加入堆中,堆排序保持小根堆的不变性,然后重复这样的操作呢?——为了逐渐调整最小范围的位置,使其整体范围逐渐向右移动,试探这个过程有没有比当前的最小范围的长度更小的[minValue , maxValue]。一步步逐渐试探并更新最小范围。

为了逐渐向右试探且又要满足条件【包括所有数组至少一个元素的范围】,所以弹出哪个数组的元素就必须将同一个数组的元素加入堆中,才能满足题目要求。至于要加入这个数组的哪一个元素,我们可以这样考虑,因为是升序,所以弹出的minValue的下一个元素比minValue大,同时又是这个数组剩余元素中最小的。相比其他元素,加入minValue的下一个元素到堆中,以此得到的新的范围是最小的,而这也是我们想要的。

加入以后更新堆中元素的最大值maxValue,同时堆的不变性保证了堆顶一定是堆中的最小值,得到一个新的范围[maxValue , minValue]与当前的最小范围做比较,如果更小,那非常好,我们找到了一个更小的范围,我们马上做更新。否则就得继续试探继续找,直到加入堆中的元素是某个数组的尾部元素,那么我们的试探和搜寻就结束了


下面是题目所给例子求解的详细过程,方便大家理解:

初始的priorityQueue = [(4, 0, 0), (0, 1, 0), (5, 2, 0)]
堆中的最小值:0,它在第1行,下标是0 | 堆中的最大值:5
最小范围:[0 , 5]([0,5] < [-120,120],更新!)
弹出堆中的最小值0
把弹出的最小值的下一个元素nums[1][1]=9加入堆中
--------------------------------------------
堆中的最小值:4,它在第0行,下标是0 | 堆中的最大值:9
最小范围:[0 , 5]([0,5] < [4,9],不更新!)
弹出堆中的最小值4
把弹出的最小值的下一个元素nums[0][1]=10加入堆中
---------------------------------------------
堆中的最小值:5,它在第2行,下标是0 | 堆中的最大值:10
最小范围:[0 , 5]([0,5] < [5,10],不更新!)
弹出堆中的最小值5
把弹出的最小值的下一个元素nums[2][1]=18加入堆中
---------------------------------------------
堆中的最小值:9,它在第1行,下标是1 | 堆中的最大值:18
最小范围:[0 , 5]([0,5] < [9,18],不更新!)
弹出堆中的最小值9
把弹出的最小值的下一个元素nums[1][2]=12加入堆中
---------------------------------------------
堆中的最小值:10,它在第0行,下标是1 | 堆中的最大值:18
最小范围:[0 , 5]([0,5] < [10,18],不更新!)
弹出堆中的最小值10
把弹出的最小值的下一个元素nums[0][2]=15加入堆中
----------------------------------------------
堆中的最小值:12,它在第1行,下标是2 | 堆中的最大值:18
最小范围:[0 , 5]([0,5] < [12,18],不更新!)
弹出堆中的最小值12
把弹出的最小值的下一个元素nums[1][3]=20加入堆中
---------------------------------------------
堆中的最小值:15,它在第0行,下标是2 | 堆中的最大值:20
最小范围:[0 , 5]([0,5] < [15,20],不更新!)
弹出堆中的最小值15
把弹出的最小值的下一个元素nums[0][3]=24加入堆中
---------------------------------------------
堆中的最小值:18,它在第2行,下标是1 | 堆中的最大值:24
最小范围:[0 , 5]([0,5] < [18,24],不更新!)
弹出堆中的最小值18
把弹出的最小值的下一个元素nums[2][2]=22加入堆中
---------------------------------------------
堆中的最小值:20,它在第1行,下标是3 | 堆中的最大值:24
最小范围:[20 , 24]([20,24] < [0,5],更新!)
idx走到数组末尾,结束while循环
---------------------------------------------
最终答案:20 24

拓展

堆排序算法
  • 堆的定义

堆是一棵完全二叉树,分为小根堆和大根堆。

大根堆:双亲结点的值都大于孩子结点的值;小根堆:双亲结点的值都小于孩子结点的值

现有一个序列 R[1…n],关键字为 k 1 , k 2 , . . . k n k_1,k_2,...k_n k1,k2,...kn(序列号可以理解为结点的下标或者索引,而关键字可以理解为对应的值,比如R[i].key就是R[i]的值)

R [ 2 i ] 是 R [ i ] 的左孩子结点, R [ 2 i + 1 ] 是 R [ i ] 的右孩子结点 R[2i]是R[i]的左孩子结点,R[2i+1]是R[i]的右孩子结点 R[2i]R[i]的左孩子结点,R[2i+1]R[i]的右孩子结点

  • 筛选算法

堆排序的关键是构造堆,这里采用==筛选算法建堆==。

所谓“筛选”指的是,对一棵左/右子树均为堆的完全二叉树,**“调整”**根结点使整个二叉树也成为一个堆。

在这里插入图片描述

void sift(RecType R[]int low,int high) 	//调整堆的算法
{  int i=low,j=2*i;    	//R[j]是R[i]的左孩子
   RecType tmp=R[i];	//暂存R[low]
   while (j<=high) 
   {
     if (j<high && R[j].key<R[j+1].key) j++;//j指向大孩子
     if (tmp.key<R[j].key)  	//双亲小
     {  R[i]=R[j];   	     	//将R[j]调整到双亲结点位置
        i=j;         	     	//修改i和j值,以便继续向下筛选
        j=2*i;
     }
     else break;     	      	//双亲大:不再调整
   }
   R[i]=tmp;
}
  • 循环建立初始堆

R [ n / 2 ] R[n/2] R[n/2]是一棵完全二叉树最右下角的最小子树的根节点,从最小的子树开始逐渐向上调整,循环建立初始堆

在这里插入图片描述

  • 最大记录归位

在这里插入图片描述

  • 堆排序算法
void HeapSort(RecType R[]int n)
{  int i;
   for (i=n/2;i>=1;i--) 	//循环建立初始堆
       sift(R,i,n); 
   for (i=n; i>=2; i--)	//进行n-1次循环,完成推排序
   {//R[1]是最大记录
       swap(R[1],R[i]);      	//R[1] <--> R[i]
       sift(R,1,i-1);   	//筛选R[1]结点,得到i-1个结点的堆
   }
}
  • 堆排序算法分析
    • 对高度为 h h h的堆,一次“筛选”所需进行的关键字比较的次数至多为 2 ( h − 1 ) 2(h-1) 2(h1)
    • 对 n 个关键字,建成高度为 h = ( ⌊ l o g 2 n ⌋ + 1 ) h=(\lfloor log_2n \rfloor+1) h=(⌊log2n+1)的堆,所需进行的关键字比较的次数不超过4n
    • 调整“堆顶” n − 1 n-1 n1次,总共进行的关键字比较的次数不超过 2 n ( ⌊ l o g 2 n ⌋ ) 2n(\lfloor log_2n \rfloor) 2n(⌊log2n⌋)
    • 因此,堆排序算法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

⭐感谢您能看到这里,我会好好努力继续分享好的文章的!⭐

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值