查找类问题(3)_LC:215、692

1.2 图

图中的度(degree):

每个节点与别的节点相连的边的条数,就是这个节点的度(每条边就是一个度)

图的分类:

无向图,有向图,权重图

有向图中的两个概念:

入度:多少边指向该节点

出度:多少边以这个点为起点,指向别的节点

注意:一个节点的入度和出度之和等于该节点的度

图示如下:

 

1.3 堆(LC:215 + 692)

堆(Heap):

首先,是一种完全二叉树

然后,分为两种情况:

  • 最大堆:树中每个节点的值均大于等于该节点的孩子节点
  • 最小堆:树中每个节点的值均小于等于该节点的孩子节点
  • 最大堆的特点:堆顶元素是所有元素中的最大值
  • 最小堆的特点:堆顶元素是所有元素中的最小值

 

堆相关操作的时间复杂度:

访问Access:在堆中不通过索引去访问元素

搜索Search:搜索堆顶元素时的复杂度是O(1),搜索其它元素时是O(N)

添加Insert:O(logN)。假设是最大堆,将添加的元素放在堆的最后一位,然后开始与其父节点比较大小,比父节点大则和父节点交换位置,一直交换到堆顶元素或者小于等于父节点时,不再进行交换,交换的次数的最大值是堆的层数,即logN次,下图是添加节点元素值为7的过程:

删除Delete:O(logN)。假设是最小堆,将堆中最后一个元素填充到所删除的元素的位置上(为了保持完全二叉树的结构),然后将该元素和其孩子节点比较大小(可以是左孩子,可以右孩子),比该元素小,则和孩子节点交换位置,一直交换到没有孩子节点或者大于等于其孩子节点时,不再进行交换,交换的次数的最大值是堆的层数,即logN次,下图是删除节点元素值为1的过程:

 

堆化操作:

把一组无序的数添加到堆中去,即把一个无序数组,转化成堆

堆化操作的时间复杂度是O(n),和堆排序的时间复杂度O(nlogn)不同

将一个长度为n的数组转换成堆,需要两步:

  1. 将这个数组转换成完全二叉树,时间复杂度为O(n):因为只需要从头到尾遍历一遍数组即可,如下图所示:
  2. 将完全二叉树转换成堆,时间复杂度为O(n):假设为最小堆,需要从最下面一层开始,将每个节点和其父节点进行比较,将较小的放在父节点的位置,直到所有的父节点都比孩子节点小,这个操作也需要O(n)的时间复杂度

由于步骤1和2是级联的,所以最终总的时间复杂度为O(n)

 

注意 堆化 和 向堆中添加元素 的区别:

堆化不是向已有的堆中添加一个一个无序数组的元素,即堆化不是前面所说的添加Insert(时间复杂度为O(logN))这个操作

堆化过程:无序数组--完全二叉树--堆,这个过程的时间复杂度为O(n)

 

堆相关操作的代码:

//创建堆结构
PriorityQueue<Integer> minheap = new PriorityQueue<>();
PriorityQueue<Integer> maxheap = new PriorityQueue<>(Collections.reverseOrder()); //在最小堆的基础上进行逆序排列

//添加元素,O(logN)
minheap.add(10);
minheap.add(11);
minheap.add(8);
minheap.add(9);
minheap.add(2);
minheap.add(5);

maxheap.add(10);
maxheap.add(11);
maxheap.add(8);
maxheap.add(9);
maxheap.add(2);
maxheap.add(5);

//打印
System.out.println(minheap.toString()); //[2, 8, 5, 11, 9, 10]
System.out.println(maxheap); //[11, 10, 8, 9, 2, 5]

//获取堆顶元素(不删除),O(1)
minheap.peek(); //2
maxheap.peek(); //11

//删除堆顶元素,并返回该元素,此时堆会将剩下的节点元素重新交换位置,得到最小堆或者最大堆,O(logN)
minheap.poll(); //2
maxheap.poll(); //11

//获取堆的大小,O(1)
minheap.size(); //5
maxheap.size(); //5

//遍历堆,O(N)
while(!minheap.isEmpty()){
    System.out.println(minheap.poll()); //5 8 9 10 11
}

 

和堆有关题目的关键词:前k个/第k个 + 最大/最小 的元素

 

215. 数组中的第K个最大元素 - 力扣(LeetCode) (leetcode-cn.com)

难度:中等

在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4

说明:

你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。

 

思路:

求第k个最大的元素,只需要将最大堆中的前k-1个堆顶元素删除,此时的堆顶元素就是第k个最大的元素

同理,求第k个最小的元素,使用最小堆即可

代码:

class Solution {
    public int findKthLargest(int[] nums, int k) {
        //创建最大堆
        PriorityQueue<Integer> maxheap = new PriorityQueue<>(Collections.reverseOrder());

        int res = 0;

        for(int item : nums){
            maxheap.add(item);
        }

        //将前k-1个大的元素删掉后,就可以得到第k个最大的元素
        while(k > 1){
            maxheap.poll();
            k--;
        }

        res = maxheap.peek();
        return res;
    }
}

复杂度分析:

时间复杂度:O(NlogN)

  • 第二个while循环:删除k-1次堆顶元素,时间复杂度为O(klogN)。总的时间复杂度为两者之和,因为k
  • 第一个for循环:遍历数组的时间复杂度是O(N),其中每一次循环都需要将数组中的元素添加到堆中,即堆的添加操作,该操作的时间复杂度为O(logN),因此该循环的总时间复杂度为O(NlogN)

空间复杂度:O(N),需要额外存放N个节点元素的堆空间,大小为O(N)

 

692. 前K个高频单词 - 力扣(LeetCode) (leetcode-cn.com)

相似题:347. 前 K 个高频元素

难度:中等

给一非空的单词列表,返回前 k 个出现次数最多的单词。

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

示例 1:

输入: ["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 次。

注意:

  1. 假定 k 总为有效值, 1 ≤ k ≤ 集合元素数。
  2. 输入的单词均由小写字母组成。

扩展练习:

  1. 尝试以 O(n log k) 时间复杂度和 O(n) 空间复杂度解决。

思路:

目标:求前 k 个出现次数最多的单词

  • 使用最大堆求出前k个最多,也可以使用最小堆过滤去除前N-k个最少(此时最小堆中剩下的就是前k个最多)
  • 使用哈希表解决出现次数的计数问题,key表示单词列表中的不同元素,value表示key对应的元素在单词列表中出现的次数

解法1:选用 最大堆 + 哈希表 的组合结构

解法1步骤:

  1. 遍历字符串数组,key记录数组中不同的字符串,value记录同一个字符串出现的次数
  2. 重写最大堆的排序方式,重写成:value从大到小排列,相同的value则将key从小到大排列
  3. 遍历哈希表中的key,添加到最大堆中
  4. 依次删除前k个堆顶元素,并同时保存这k个元素到返回值res中,最终返回res

解法2:选用 最小堆 + 哈希表 的组合结构

解法2步骤:

  1. 遍历字符串数组,key记录数组中不同的字符串,value记录同一个字符串出现的次数
  2. 重写最小堆的排序方式,重写成:value从小到大排列,相同的value则将key从大到小排列(与最大堆正好相反)
  3. 遍历哈希表中的key,添加到最小堆中,当最小堆中的元素个数大于k个时,说明堆顶的元素不是前k个最多的单词,此时删除堆顶元素
  4. 将装有k个元素的最小堆依次进行删除堆顶元素,并按顺序保存这k个元素到返回值res中,此时的res中的单词排列顺序与正确的返回值的排列顺序是相反的,所以需要反转res,最终返回res

两个细节点:

  • 最大堆和最小堆重写的排序方法正好相反:
    • 最大堆的容量为n,前k个堆顶元素,就是前k个出现最多的单词
    • 最小堆的容量为k,前k个堆顶元素顺序的反转,就是前k个出现最多的单词
      • 原因:由于最小堆重写的排序方法与题目要求的排序方法正好相反,所以排在最小堆中前面的N-k个元素,是前N-k个出现次数最少的元素,将这N-k个元素过滤删除之后,得到的就是前k个出现次数最多的元素,但是需要将这k个元素的顺序反转,才能得到正确的返回值顺序
  • 由于最大堆和最小堆的容量不同:
    • 使用最大堆,每次插入元素的时间复杂度为O(logn)
    • 使用最小堆,每次插入元素的时间复杂度为O(logk)

因此:使用最小堆的时间复杂度低于最大堆

代码:

解法1:最大堆 + 哈希表

class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        HashMap<String, Integer> map = new HashMap<>();

        //难点:创建最大堆时,还需要同时重写最大堆的排序方法,重写成:value从大到小排列,相同的value则将key从小到大排列
        PriorityQueue<String> maxheap = new PriorityQueue<>(Collections.reverseOrder(new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                //注意!!
                //return s1.compareTo(s2)这种写法的排序结果是由小到大的,这是默认的排序方式   
                //return s2.compareTo(s1) 或者 return -s1.compareTo(s2)这种写法的排序结果是由大到小的 
                return map.get(s1).equals(map.get(s2)) ? -(s1.compareTo(s2)) : (map.get(s1) - map.get(s2));
                //这里的返回值表明:如果value相等,按照key从大到小排列,如果value不相等,则按照value从小到大排列,正好和需要重写的排序要求相反
                //但是,外面还有一层reversOrder,将里面的排序结果作了翻转,就满足了需要重写的排序要求
            }
        }));

        //根据输出要求是List<String>类型,初始化输出
        List<String> res = new ArrayList<>();

        for(String item : words){
            map.put(item, map.getOrDefault(item, 0) + 1); //这样写省去了map.containsKey(item)的判断,代码简洁,但是要注意将defaultValue设置为0
        }

        //直接遍历map中的key,使用String类型来接收
        for(String word : map.keySet()){
            maxheap.add(word);
        }

        while(k > 0){
            res.add(maxheap.poll());
            k--;
        }
        return res;
    }
}

解法2:最小堆 + 哈希表

class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        HashMap<String, Integer> map = new HashMap<>();
        
        //难点:创建最小堆时,还需要同时重写最小堆的排序方法,最小堆的重写方法和最大堆的重写方法正好相反!
        //即:value从小到大排列,相同的value则将key从大到小排列
        PriorityQueue<String> minheap = new PriorityQueue<>((new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                //return s1.compareTo(s2)这种写法的排序结果是由小到大的,这是默认的排序方式   
                //return s2.compareTo(s1) 或者 return -s1.compareTo(s2)这种写法的排序结果是由大到小的 
                return map.get(s1).equals(map.get(s2)) ? -(s1.compareTo(s2)) : (map.get(s1) - map.get(s2));
                //这里的返回值表明:如果value相等,按照key从大到小排列,如果value不相等,按照value从小到大排列
            }
        }));
        
        //根据输出要求是List<String>类型,初始化输出
        List<String> res = new ArrayList<>();
        
        for(String item : words){
            map.put(item, map.getOrDefault(item, 0) + 1); //这样写省去了map.containsKey(item)的判断,代码简洁,但是要注意将defaultValue设置为0
        }
        
        //直接遍历map中的key,使用String类型来接收
        for(String word : map.keySet()){
            minheap.add(word);
            
            //注意:最小堆的大小为k,即最小堆中只会存放k个元素
            if(minheap.size() > k){
                minheap.poll();
            }
        }
        
        while(k > 0){
            res.add(minheap.poll());
            k--;
        }
        
        //res中的元素是按照:出现的次数从小到大排列,相同的出现次数则按字母顺序从大到小排列
        //因此res中的元素顺序与正确的元素顺序正好相反,所以需要将res反转
        Collections.reverse(res); //本方法没有返回值,不要写成 res=Collections.reverse(res) 
        return res;
    }
}

复杂度分析:

解法1:

时间复杂度:O(nlogn),n为字符串数组的长度

  • 第一个for循环,遍历一遍字符数组并向map中不断添加,时间复杂度为O(n)与O(1)的乘积,为O(n);
  • 第二个for循环,向最大堆中添加元素的时间复杂度为O(logn),总的时间复杂度是O(n)与O(logn)的乘积,为O(nlogn);
  • 第三个while循环,从最大堆中删除堆顶元素的时间复杂度为O(1),总的时间复杂度为为O(k);
  • 因此三次循环的时间复杂度相加,得到的总时间复杂度为O(nlogn)

空间复杂度:O(n),使用哈希表和最大堆的空间复杂度均为O(n),因此总的空间复杂度为O(n)

解法2:

时间复杂度:O(nlogk),n为字符串数组的长度

  • 第一个for循环,遍历一遍字符数组并向map中不断添加,时间复杂度为O(n)与O(1)的乘积,为O(n);
  • 第二个for循环,向最小堆中添加元素的时间复杂度为O(logk),总的时间复杂度是O(n)与O(logk)的乘积,为O(nlogk);
  • 第三个while循环,从最小堆中删除堆顶元素的时间复杂度为O(1),总的时间复杂度为为O(k);
  • 因此三次循环的时间复杂度相加,得到的总时间复杂度为O(nlogk)

空间复杂度:O(n),使用哈希表堆的空间复杂度为O(n),最小堆的空间复杂度为O(k),因此总的空间复杂度为O(n)

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值