LeetCode692. 前K个高频单词 / 剑指 Offer 50. 第一个只出现一次的字符 / 剑指 Offer 51. 数组中的逆序对 / 2. 两数相加

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode692. 前K个高频单词 / 剑指 Offer 50. 第一个只出现一次的字符 / 剑指 Offer 51. 数组中的逆序对 / 2. 两数相加相关的知识,希望对你有一定的参考价值。

692. 前K个高频单词

2021.5.20每日一题,又是一个人过的520,不过还是要520快乐呀

题目描述

给一非空的单词列表,返回前 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 次。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/top-k-frequent-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

和昨天一样,找前k个,所以还是堆排序,先用哈希表统计每个单词出现的次数,然后堆中存储前k个出现频率最高的字符串
注意堆中排序的时候,优先考虑次数,次数最小的在堆顶,所以是小顶堆;次数相同,考虑字母顺序,大的在堆顶!!!另外,最后输出要反转一下

还有这里学习一下,在堆中或者其他数据结构中存储数据对的方法,例如这里是<字符串,整数>
官解中给出的是:PriorityQueue<Map.Entry<String, Integer>>,然后用哈希表的entrySet方法可以直接存储进来
三叶姐给的是PriorityQueue<Object[]> ,加入队列中用的是q.add(new Object[]{cnt, s},比较的时候是强转,第一次见,牛呀牛呀

			int c1 = (Integer)a[0], c2 = (Integer)b[0];
            if (c1 != c2) return c1 - c2;
            // 如果词频相同,根据字典序倒序
            String s1 = (String)a[1], s2 = (String)b[1];
            return s2.compareTo(s1);
class Solution {
    public List<String> topKFrequent(String[] words, int k) {
        //第一反应,因为不需要考虑出现顺序,所以直接排序
        int l = words.length;
        //先用哈希表统计每个单词出现的次数
        Map<String, Integer> map = new HashMap<>();

        for(String s : words){
            map.put(s, map.getOrDefault(s, 0) + 1);
        }

        //然后用堆来排序,先放入k个单词,并且记录最低的次数,如果后面有次数比这个大的放入堆中
        //所以应该用最小堆
        PriorityQueue<String> pq = new PriorityQueue<>((s1, s2) ->{
            //如果次数相同,按字母顺序从大到小排
            if(map.get(s1) == map.get(s2)){
                return s2.compareTo(s1);
            }else{
                //不同,按次数多的排
                return map.get(s1) - map.get(s2);
            }
        });
        //把哈希表的键部分遍历
        for(String s : map.keySet()){
            pq.offer(s);
            //如果超过k了,就把堆顶弹出
            if(pq.size() > k){
                pq.poll();
            }
        }

        List<String> res = new LinkedList<>();
        for(int i = 0; i < k; i++){
            res.add(pq.poll());
        }
        //因为是从小到大的,要反转
        Collections.reverse(res);
        return res;
    }
}

归并排序

今天学习内容是归并排序和快速排序,再复习一下这两种排序

public class Solution {

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        int[] temp = new int[len];
        mergeSort(nums, 0, len - 1, temp);
        return nums;
    }

    /**
     * 递归函数语义:对数组 nums 的子区间 [left.. right] 进行归并排序
     *
     * @param nums
     * @param left
     * @param right
     * @param temp  用于合并两个有序数组的辅助数组,全局使用一份,避免多次创建和销毁
     */
    private void mergeSort(int[] nums, int left, int right, int[] temp) {
        // 1. 递归终止条件
        if (left == right) {
            return;
        }

        // 2. 拆分,对应「分而治之」算法的「分」
        int mid = (left + right) / 2;

        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);

        // 3. 在递归函数调用完成以后还可以做点事情

        // 合并两个有序数组,对应「分而治之」的「合」
        mergeOfTwoSortedArray(nums, left, mid, right, temp);
    }


    /**
     * 合并两个有序数组:先把值复制到临时数组,再合并回去
     *
     * @param nums
     * @param left
     * @param mid   mid 是第一个有序数组的最后一个元素的下标,即:[left..mid] 有序,[mid + 1..right] 有序
     * @param right
     * @param temp  全局使用的临时数组
     */
    private void mergeOfTwoSortedArray(int[] nums, int left, int mid, int right, int[] temp) {
        for (int i = left; i <= right; i++) {
            temp[i] = nums[i];
        }

        int i = left;
        int j = mid + 1;

        int k = left;
        while (i <= mid && j <= right) {
            if (temp[i] <= temp[j]) {
                // 注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i];
                k++;
                i++;
            } else {
                nums[k] = temp[j];
                k++;
                j++;
            }
        }

        while (i <= mid) {
            nums[k] = temp[i];
            k++;
            i++;
        }
        while (j <= right) {
            nums[k] = temp[j];
            k++;
            j++;
        }
    }
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/recursion-and-divide-and-conquer/rnazmc/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
import java.util.Random;

public class Solution {

    /**
     * 随机化是为了防止递归树偏斜的操作,此处不展开叙述
     */
    private static final Random RANDOM = new Random();

    public int[] sortArray(int[] nums) {
        int len = nums.length;
        quickSort(nums, 0, len - 1);
        return nums;
    }

    /**
     * 对数组的子区间 nums[left..right] 排序
     *
     * @param nums
     * @param left
     * @param right
     */
    private void quickSort(int[] nums, int left, int right) {
        // 1. 递归终止条件
        if (left == right) {
            return;
        }

        int pIndex = partition(nums, left, right);

        // 2. 拆分,对应「分而治之」算法的「分」
        quickSort(nums, left, pIndex - 1);
        quickSort(nums, pIndex + 1, right);

        // 3. 递归完成以后没有「合」的操作,这是由「快速排序」partition 的逻辑决定的
    }


    /**
     * 将数组 nums[left..right] 分区,返回下标 pivot,
     * 且满足 [left + 1..lt) <= pivot,(gt, right] >= pivot
     *
     * @param nums
     * @param left
     * @param right
     * @return
     */
    private int partition(int[] nums, int left, int right) {
        int randomIndex = left + RANDOM.nextInt(right - left + 1);
        swap(nums, randomIndex, left);

        int pivot = nums[left];
        int lt = left + 1;
        int gt = right;

        while (true) {
            while (lt <= right && nums[lt] < pivot) {
                lt++;
            }

            while (gt > left && nums[gt] > pivot) {
                gt--;
            }

            if (lt >= gt) {
                break;
            }

            // 细节:相等的元素通过交换,等概率分到数组的两边
            swap(nums, lt, gt);
            lt++;
            gt--;
        }
        swap(nums, left, gt);
        return gt;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/recursion-and-divide-and-conquer/rn26pi/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

剑指 Offer 50. 第一个只出现一次的字符

题目描述

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

示例:

s = "abaccdeff"
返回 "b"

s = "" 
返回 " "

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/di-yi-ge-zhi-chu-xian-yi-ci-de-zi-fu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

用哈希表统计次数

class Solution {
    public char firstUniqChar(String s) {
        int l = s.length();
        int[] count = new int[26];
        for(int i = 0; i < l; i++){
            count[s.charAt(i) - 'a']++;
        }
        for(int i = 0; i < l; i++){
            if(count[s.charAt(i) - 'a'] == 1)
                return s.charAt(i);
        }  
        return ' ';    
    }
}

当然字符不一定只有26个
学习一下有序哈希表,知道有这个数据结构

class Solution {
    //有序哈希表LinkedHashMap,遍历顺序和存入顺序相同
    public char firstUniqChar(String s) {
        Map<Character, Boolean> dic = new LinkedHashMap<>();
        char[] sc = s.toCharArray();
        for(char c : sc)
        //如果只有一个,就是true,如果没有或者有多个就是false
            dic.put(c, !dic.containsKey(c));
        for(Map.Entry<Character, Boolean> d : dic.entrySet()){
           if(d.getValue()) return d.getKey();
        }
        return ' ';
    }
}

书中还给了三道扩展题,互变异位词好像做过,三道题和这个题一样,也是哈希表
第一个:从第一个字符串中删除第二个字符串中出现的所有字符
第二个:删除字符串中所有重复出现的字符
第三个:互变异位词

附加题目:字符流中第一个只出现一次的字符

字符流是每次添加进来一个字符,动态的更新,而不是一个固定的字符串了
还是用哈希表存储每个字符的状态,每次加进来一个字符,如果哈希表中没有这个字符,将该字符出现的位置存储起来;如果有这个字符,将哈希表中对应的“值”置为-1;
取第一个只出现一次的字符,就是遍历哈希表,将值不等于-1的字符取出来,比较下标的大小,并将最小的输出

剑指 Offer 51. 数组中的逆序对

题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。


示例 1:

输入: [7,5,6,4]
输出: 5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

最简单的方法,两个for循环,超时了

class Solution {
    public int reversePairs(int[] nums) {
        //最简单的方法,两个for循环
        int res = 0;
        int l = nums.length;
        for(int i = 0; i < l; i++){
            for(int j = i + 1; j < l; j++){
                if(nums[i] > nums[j])
                    res++;
            }
        }
        return res;
    }
}

是在想不出怎么优化了,感觉之前好像还做过,但是还是想不起来了

题解是归并排序,刚看了归并排序,就用到了,哈哈
思想就是,利用归并排序的分治思想,先分成一个一个单独的数,然后再合并过程中,判断左边数组的数和右边数组的数的关系,以此来统计逆序对的个数
归并排序这里有个小的优化, 就是当左右两个数组,已经是左边的最大数小于右边的最小数,就无需合并了,直接就是有序的。没加这个优化之前,超过7%,加上超过99%,离谱!

归并排序
class Solution {
    public int reversePairs(int[] nums) {
        //分治,归并排序的思想,在合并的时候统计逆序的数对
        int l = nums.length;
        if(l < 2)
            return 0;
        int[] temp = new int[l];

        return mergeSort(nums, 0, l - 1, temp);
    }

    //temp临时数组
    public int mergeSort(int[] nums, int left, int right, int[] temp){
        if(left >= right)
            return 0;
        //先分,找中心点
        int mid = ((right - left) >> 1) + left;
        //左边的逆序对
        int pairsl = mergeSort(nums, left, mid, temp);
        //右边的逆序对
        int pairsr = mergeSort(nums, mid + 1, right, temp);

        //这里是看题解多加的,
        // 如果整个数组已经有序,则无需合并,注意这里使用小于等于
        if (nums[mid] <= nums[mid + 1]) {
            return pairsr + pairsl;
        }

        //合并,获得当前合并过程中产生的逆序对
        int cur = mergeSortArray(nums, left, mid, right, temp);

        return pairsl + pairsr + cur;
    }

    //合并两个排序的数组,并统计逆序对的个数
    public int mergeSortArray(int[] nums, int left, int mid, int right, int[] temp){
        //先复制到临时数组里面
        for(int i = left; i <= right; i++){
            temp[i] = nums[i];
        }
        int count = 0;
        int i = left;
        int j = mid + 1;
        int index = left;
        while(i <= mid && j <= right){
            //如果位置i的数大于位置j的数,那么左边数组中i的右边所有数都可以和位置j的数组成逆序对
            if(temp[i] > temp[j]){
                count += mid - i + 1;
                nums[index++] = temp[j++];
            }else{
                nums[index++] = temp[i++];
            }
        }
        while(i <= mid){
            nums[index++] = temp[i++];
        }
        while(j <= right){
            nums[index++] = temp[j++];
        }
        return count;
    }
}
树状数组

第二个方法是树状数组,前几天刚看了树状数组,现在能记住的就是树状数组的那个结构了,这个还是印象挺深刻的,记得是动态维护前缀和的一个数据结构。
再复习树状数组,看之前自己写的大概知道个意思,最终还是去看了weiwei哥的解释,有了更深刻的理解,还是补充在之前那篇文章里吧,这里贴个代码

这里知道用树状数组,但是还是想不到和逆序对有啥关系

首先要明白在哪里用到了前缀和,首先根据所给的数组,建立一个新的数组,来统计每个数字出现的个数,例如a={5,5,2,3,6},那么nums[2] = 1,nums[3] = 1, num[5] = 2,nums[6] =1,即下面的操作

int[] count = new int[a.length + 1];
for(int i = 0; i < a.length; i++){
	nums[a[i]]++;
}

这是nums = [0,0,1,1,0,2,1];
对于当前位置i,例如i = 5,它的i - 1位的前缀和,就表示有多少个数比当前数小,这里就是4,意思是比6小的数就有4个。
知道这个以后,我们从前到后遍

以上是关于LeetCode692. 前K个高频单词 / 剑指 Offer 50. 第一个只出现一次的字符 / 剑指 Offer 51. 数组中的逆序对 / 2. 两数相加的主要内容,如果未能解决你的问题,请参考以下文章

leetcode-692-前K个高频单词

LeetCode:692. 前K个高频单词

leetcode692. 前K个高频单词

leetcode 692. 前K个高频单词

topK问题 前K个高频元素 leetcode692

topK问题 前K个高频元素 leetcode692