从小白开始刷算法 Heap 堆篇 最小堆排序 leetcode.692

序言

虽然算法很难,但不应该就放弃。这是一个学习笔记,希望你们喜欢~

先自己尝试写,大概十几分钟仍然写不出来
看思路,再尝试跟着思路写
仍然写不出来,再看视频
b站up视频推荐:爱学习的饲养员

leetcode其他文章:

数组篇:
从小白开始刷算法 数组篇 leetcode.485
从小白开始刷算法 数组篇 leetcode.283
从小白开始刷算法 数组篇 leetcode.27

链表篇:
从小白开始刷算法 ListNode 链表篇 leetcode.203
从小白开始刷算法 ListNode 链表篇 leetcode.206

队列篇
从小白开始刷算法 ListNode 链表篇 leetcode.933

栈篇
从小白开始刷算法 Stack 栈篇 leetcode.20
从小白开始刷算法 Stack 栈篇 leetcode.496

哈希篇
从小白开始刷算法 Hash 哈希篇 leetcode.217
从小白开始刷算法 Hash 哈希篇 leetcode.705

树篇

从小白开始刷算法 Tree 树篇 先序遍历 leetcode.144
从小白开始刷算法 Tree 树篇 中序遍历 leetcode.94
从小白开始刷算法 Tree 树篇 后序遍历 leetcode.94
堆篇

从小白开始刷算法 Heap 堆篇 最大堆排序 leetcode.215

堆篇

难度:中等

题目:

692.前K个高频单词

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

示例 1:

输入: words = [“i”, “love”, “leetcode”, “i”, “love”, “coding”], k = 2
输出: [“i”, “love”]
解析: “i” 和 “love” 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 “i” 在 “love” 之前。
示例 2:

输入: [“the”, “day”, “is”, “sunny”, “the”, “the”, “the”, “sunny”, “is”, “is”], k = 4
输出: [“the”, “is”, “sunny”, “day”]
解析: “the”, “is”, “sunny” 和 “day” 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。

题目来源:力扣(LeetCode)

最下堆排序+哈希表思路

能否写出:不能写出。

时间:1个多小时

思路:

第一次学习最小堆堆排序,前一篇是最大堆排序

从小白开始刷算法 Heap 堆篇 最大堆排序 leetcode.215

堆排序介绍

堆排序是一种基于最小堆(Min Heap)数据结构的排序算法,它利用最小堆的性质进行排序。最小堆是一种特殊的二叉树,它满足以下两个性质:

最小堆性质:对于堆中任意节点 i,其父节点的值小于等于它的值,即 heap[parent(i)] <= heap[i]。
完全二叉树性质:堆是一棵完全二叉树,除了最底层外,其他层都是满的,且最底层从左到右填充。
堆排序的基本思想是先构建一个最小堆,然后将堆顶元素(最小值)与堆中的末尾元素交换,然后将剩余元素重新调整为最小堆,重复这个过程直到所有元素都有序。

堆排序的具体步骤如下:

构建最小堆:从数组的中间位置开始,从右到左对每个非叶子节点进行调整,使其满足最小堆性质。这一步将整个数组调整为一个最小堆。
堆排序:从数组的末尾开始,依次将堆顶元素与当前末尾元素交换,然后调整剩余元素为最小堆。每次交换后,将堆的大小减1,排除已排序的元素。重复这个过程,直到堆中只剩下一个元素。
排序完成:经过堆排序后,数组中的元素按照从小到大的顺序排列。
堆排序的时间复杂度为 O(nlogn),其中 n 是数组的长度。它是一种原地排序算法,不需要额外的空间,但由于涉及到交换操作,所以是一种不稳定的排序算法。

堆排序在实际应用中具有一定的局限性,因为它对数据的随机访问较多,对于较大规模的数据或者外部存储的数据,堆排序可能会导致较差的缓存性能。但在某些特定场景下,堆排序的性能相对较好,例如需要找出前 k 个最大或最小的元素时,堆排序可以通过部分排序来实现高效的解决方案。

最小堆也是使用数组来实现的,与最大堆类似。不同之处在于,最小堆中父节点的值小于等于其子节点的值,而最大堆中父节点的值大于等于其子节点的值。最小堆的构建和调整过程与最大堆类似,只是在比较和交换的时候使用相反的逻辑。

最小堆是使用树结构?

最小堆是一种基于数组实现的完全二叉树,而不是树结构。尽管最大堆的表示形式在视觉上看起来像是一棵树,但实际上它是通过数组来实现的。

最小堆的性质是父节点的值小于等于其子节点的值。通过使用数组来表示最小堆,可以方便地满足这一性质,并且具有一些优势:

  1. 数组是一种连续存储的数据结构,相比树结构,它具有更好的缓存性能。在访问数组元素时,可以利用 CPU 缓存的特性,提高访问效率。
  2. 数组可以通过索引直接访问元素,而不需要像树结构一样需要遍历搜索。这使得对最大堆的插入、删除等操作更加高效。
  3. 数组实现的最大堆相对于树结构来说,占用的内存空间更小。树结构需要额外的指针和连接操作,而数组只需要连续的内存空间即可。

综上所述,使用数组来实现最小堆既能满足最小堆的性质,又能提供更好的性能和节省内存空间。因此,最小堆通常采用数组来表示,而不是树结构。

我们来一行行看代码,我最喜欢debug式看代码。

首先,记住这张图,关键点在于根节点(num[0]),节点位置(num[7]之前),和叶子节点位置(num[7]之后)。

在这里插入图片描述

  • 节点位置与叶子节点位置,他们之间是可以通过计算得出来的
    • 所有节点位置计算:nums.length/2-1 例:13/2-1 = 5; num[5]之前的节点位置
    • 节点位置与叶子节点位置计算:拿num[1]和num[5]举例
      • 左节点计算:int left =2 * index + 1 例:num[1]左节点 2 * 1+1= 3 num[5]子节点 2 * 5+1= 11
      • 右节点计算:int right =2 * index + 2 例:num[1]右节点 2 * 1+2= 4 num[5]子节点 2 * 5+2= 12
// 仅是我的思路代码,leetcode上大神更厉害
class MinHeap {
    public static void heapSort(int[] nums) {
    int n = nums.length;

    // 构建最小堆
    buildMinHeap(nums);

    // 进行堆排序
    for (int i = n - 1; i >= 0; i--) {
        // 将堆顶元素(最小值)与当前末尾元素交换
        swap(nums, 0, i);

        // 调整堆,保持最小堆性质
        minHeapify(nums, i, 0);
    	}
	}
    
    public static void buildMinHeap(int[] nums) {
        int n = nums.length;
        for (int i = n / 2 - 1; i >= 0; i--) {
            minHeapify(nums, n, i);
        }
    }

    public static void minHeapify(int[] nums, int heapSize, int index) {
        int smallest = index;
        int left = 2 * index + 1;
        int right = 2 * index + 2;

        if (left < heapSize && nums[left] < nums[smallest]) {
            smallest = left;
        }

        if (right < heapSize && nums[right] < nums[smallest]) {
            smallest = right;
        }

        if (smallest != index) {
            swap(nums, index, smallest);
            minHeapify(nums, heapSize, smallest);
        }
    }

    public static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

思路:

首先,使用HashMap统计每个单词的出现频率,其中单词作为键,频率作为值。

然后,我们使用最小堆(PriorityQueue)来保存频率前k高的单词。在最小堆中,我们定义了一个自定义的比较器Lambda表达式来进行排序。比较器的逻辑如下:

  • 首先,我们比较两个单词的频率是否相等。

  • 如果频率相等,则按照字典序降序排列,即b.compareTo(a)

  • 如果频率不相等,则按照频率升序排列,即frequencyMap.get(a) - frequencyMap.get(b)

接下来,我们遍历频率Map,将单词依次加入最小堆中。如果堆的大小超过了k,则移除堆顶的元素,保持堆的大小为k。

在调用minHeap.poll()方法时,会调用比较器(Comparator)来进行元素的比较。在这段代码中,我们使用的比较器是通过Lambda表达式定义的,它会根据单词的频率和字典序进行比较。

最后,我们将堆中的单词依次弹出,并将其添加到结果列表中。由于最小堆中的单词是按照频率升序排列的,因此我们需要将结果列表进行反转,使其按照频率降序排列。

最终,返回结果列表即为出现频率前k高的单词。

// 仅是我的思路代码,leetcode上大神更厉害
class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        // 统计每个单词的出现频率
        Map<String, Integer> frequencyMap = new HashMap<>();
        for (String word : words) {
            frequencyMap.put(word, frequencyMap.getOrDefault(word, 0) + 1);
        }

        // 使用最小堆来保存频率前k高的单词
        PriorityQueue<String> minHeap = new PriorityQueue<>(
                (a, b) -> frequencyMap.get(a).equals(frequencyMap.get(b))
                        ? b.compareTo(a) : frequencyMap.get(a) - frequencyMap.get(b));

        // 遍历频率Map,维护最小堆的大小为k
        for (String word : frequencyMap.keySet()) {
            minHeap.offer(word);
            if (minHeap.size() > k) {
                minHeap.poll();
            }
        }

        // 构建结果列表
        List<String> result = new ArrayList<>();
        while (!minHeap.isEmpty()) {
            result.add(minHeap.poll());
        }
        Collections.reverse(result);

        return result;
    }
}

时间复杂度:O(nlogk)

  • n是单词的总数,k是要求的频率前k高的单词数。我们需要遍历整个单词数组来统计频率,同时最小堆的插入和弹出操作的时间复杂度为O(logk)

空间复杂度:O(n)

  • 用于存储频率Map和最小堆
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小小大凳子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值