算法通关村第十四关——堆高效解决的经典问题(白银)

算法通关村第十四关——堆高效解决的经典问题(白银)

1 在数组中找到第K大的元素

leetcode 215. 数组中的第K个最大元素

这道题是一个非常经典和重要的题目,解决方法主要有三种:

  1. 选择法(先遍历一遍找到最大的元素,然后再遍历找到第二大,直到找到第K大的元素)
  2. 快速排序法(前面讲解过了,这里略)
  3. 堆排序法(找最大用小堆,找最小用大堆,找中间用两个堆)

这个专题那我们肯定是用堆排序法,那我们使用java自带的一个结构PriorityQueue(优先队列)

PriorityQueue 实现的是 Queue 接口 ,可以使用 Queue 提供的方法,以及自带的方法。

在这里插入图片描述

PriorityQueue概述
Java PriorityQueue 实现了 Queue 接口,不允许放入 null 元素;其通过堆实现,具体说是通过完全二叉树(complete binary tree)实现的小顶堆(任意一个非叶子节点的权值,都不大于其左右子节点的权值),也就意味着可以通过数组来作为PriorityQueue 的底层实现,数组初始大小为11;也可以用一棵完全二叉树表示。

常用方法总结

public boolean add(E e); //在队尾插入元素,插入失败时抛出异常,并调整堆结构
public boolean offer(E e); //在队尾插入元素,插入失败时抛出false,并调整堆结构

public E remove(); //获取队头元素并删除,并返回,失败时前者抛出异常,再调整堆结构
public E poll(); //获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构

public E element(); //返回队头元素(不删除),失败时前者抛出异常
public E peek();//返回队头元素(不删除),失败时前者抛出null

public boolean isEmpty(); //判断队列是否为空
public int size(); //获取队列中元素个数
public void clear(); //清空队列
public boolean contains(Object o); //判断队列中是否包含指定元素(从队头到队尾遍历)
public Iterator iterator(); //迭代器

所以这道题的算法代码:

class Solution {
    public int findKthLargest(int[] nums, int k) {
        // 创建一个最小堆
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();
        
        // 将数组中的前k个元素加入最小堆
        for (int i = 0; i < k; i++) {
            minHeap.offer(nums[i]);
        }
        
        // 遍历数组剩余元素,如果比堆顶元素大则替换堆顶元素,并重新调整堆
        for (int i = k; i < nums.length; i++) {
            if (nums[i] > minHeap.peek()) {
                minHeap.poll();
                minHeap.offer(nums[i]);
            }
        }
        
        // 返回最小堆的堆顶元素,即第k个最大元素
        return minHeap.peek();
    }
}

2 堆排序原理

查找:找小用大,找大用小

排序:升序用小,降序用大。

前面介绍了如何用堆来进行特殊情况的查找,堆的另一个很重要的作用是可以进行排序,那怎么排的呢?

其实非常简单,我们知道在大顶堆中,根节点是整个结构最大的元素,我先将其拿走,剩下的重排,此时根节点就是第二大的元素,我再将其拿走,再排,依次类推。最后堆只剩一个元素的时候,是不是拿走的数据也就排好序了?具体来说,建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。

看一个例子,我们对上面第一章的序列 [12 23 54 2 65 45 92 47 204 31]进行排序,首先构建一个大顶堆,然后每次我们都让根元素出堆,剩下的继续调整为大顶堆:

image.png

这时候会发现出堆的序列刚好是:204、92、65、54、47、45…。也就是刚好是从大到小的顺序排列的。

所以我们可以明白 ,如果是一个小顶堆,那自然是升序的。所以在排序的时候:

排序:升序用小,降序用大。

这个与前面的查找是相反的。

3 合并K个排序链表

leetcode 23. 合并 K 个升序链表

这道题也是很经典的题目,主要的方法有三种:

  1. 顺序合并(两个链表合并,然后一路按照顺序合并)
  2. 分治合并(两个两个合并,那么第一遍合并之后剩下2/K个,再进行剩下4/K个,最后完成)
  3. 大小堆(优先队列合并)

因为每个队列都是从小到大排序的,我们每次都要找最小的元素,所以我们要用小根堆,构建方法和操作与大顶堆完全一样,不同的是每次比较谁更小。 使用堆合并的策略是不管几个链表,最终都是按照顺序来的。每次都将剩余节点的最小值加到输出链表尾部,然后进行堆调整,最后堆空的时候,合并也就完成了。

第一步:创建优先队列,将链表添加到队列中

PriorityQueue<ListNode> q = new PriorityQueue<>(Comparator.comparing(node -> node.val));
for(int i=0; i<lists.length; i++){
    if(lists[i] != null){
        q.add(lists[i]);
    }
}

这里使用了Comparator.comparing()方法来创建一个比较器(Comparator),该比较器定义了如何比较两个节点的值。通过Lambda表达式,您指定了节点的值作为比较的依据。

这个优先队列将会根据链表节点的值来确定节点之间的顺序。当插入新节点时,优先队列会根据节点值的大小自动调整节点的位置。

第二步:创建返回的链表,用虚拟节点

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists == null || lists.length == 0) return null;

        PriorityQueue<ListNode> q = new PriorityQueue<>(Comparator.comparing(node -> node.val));
        for(int i=0; i<lists.length; i++){
            if(lists[i] != null){
                q.add(lists[i]);
            }
        }
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

    }
}

第三步:将链表从优先队列取出来,添加,每次添加最小的那个数

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists == null || lists.length == 0) return null;

        PriorityQueue<ListNode> q = new PriorityQueue<>(Comparator.comparing(node -> node.val));
        for(int i=0; i<lists.length; i++){
            if(lists[i] != null){
                q.add(lists[i]);
            }
        }
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

        while(!q.isEmpty()){
            tail.next = q.poll();    // 取出最小堆的顶,也就是最小的值,添加进去
            tail = tail.next;		 // 移动指针
            if(tail.next != null){   // 判断这个链表的节点是否全部添加完
                q.add(tail.next);    // 如果未添加完,把剩余的链表丢进优先队列,
                                     // 优先队列会进行排序,使最小的值依然在最上面
            }
        }

        return dummy.next;

    }
}

over~~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
TSP问题(Traveling Salesman Problem,旅行商问题)是一个经典的组合优化问题,它要求在给定的城市之间找到一条最短路径,使得每个城市只被经过一次,并且最终回到起点。 在本文中,我们将介绍如何使用Python解决TSP问题的动态规划算法。 动态规划算法 动态规划算法是一种解决复杂问题的有效方法,它通常用于优化问题。TSP问题的动态规划算法的思路是:将问题分解为子问题,然后通过计算子问题的最优解来逐步构建整个问题的最优解。 具体来说,我们可以使用以下步骤来解决TSP问题: 1. 定义状态:将TSP问题定义为一个二元组$(S,i)$,其中$S$表示已经经过的城市集合,$i$表示当前所在的城市。 2. 定义状态转移方程:我们定义$dp(S,i)$表示从城市$i$出发,经过集合$S$中所有城市的最短路径长度。状态转移方程为: $$ dp(S,i) = \begin{cases} 0 & \text{if } S=\{i\} \\ \min\limits_{j\in S,j\ne i}\{dp(S-\{i\},j)+dist[j][i]\} & \text{otherwise} \end{cases} $$ 其中$dist[i][j]$表示城市$i$到城市$j$之间的距离。 3. 初始状态:$dp(\{i\},i)=0$。 4. 最终状态:$dp(\{1,2,\cdots,n\},1)$即为所求的最短路径长度。 代码实现 下面是使用Python实现TSP问题动态规划算法的代码: ```python import math def tsp_dp(dist): n = len(dist) # 记录子问题的最优解 dp = [[math.inf] * n for _ in range(1 << n)] # 初始状态 for i in range(n): dp[1 << i][i] = 0 # 构建状态转移方程 for s in range(1, 1 << n): for i in range(n): if s & (1 << i) == 0: continue for j in range(n): if i == j or s & (1 << j) == 0: continue dp[s][i] = min(dp[s][i], dp[s ^ (1 << i)][j] + dist[j][i]) # 返回最终状态 return min(dp[(1 << n) - 1][i] + dist[i][0] for i in range(n)) # 示例 dist = [ [0, 2, 9, 10], [1, 0, 6, 4], [15, 7, 0, 8], [6, 3, 12, 0] ] print(tsp_dp(dist)) # 输出:21 ``` 在上面的代码中,我们首先使用$dp$数组记录子问题的最优解,然后通过状态转移方程逐步构建整个问题的最优解。 最后,我们通过计算$dp(\{1,2,\cdots,n\},1)$和从最后一个城市回到起点的距离之和的最小值来得到TSP问题的最优解。 总结 通过本文,我们学习了如何使用Python解决TSP问题的动态规划算法。TSP问题是一个经典的组合优化问题,它的解决方法还有很多其他的算法,例如分支定界算法、遗传算法等。如果你对这些算法感兴趣,可以进一步学习相的知识。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值