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个。
知道这个以后,我们从前到后遍历a数组,以此来创建count数组,并在每次遍历的时候计算当前数的前缀和,
例如,从后向前第一个遍历到的6,那么nums[6]= 1,前缀和此时是0;第二个是3,nums[3]= 1,前缀和0;第三个2,nums[2] = 1,前缀和0;第四个nums[5] = 1,前缀和为2,说明a数组中后面有两个数有两个数字比当前数字小,逆序对数+2;下面一个5同理,最终逆序对数为4

但是因为a数组中的数据可能很大,不可能建立一个那么大的数组nums,因此需要离散化,因为我们关心的是数组中所有数的相对大小,而对它们具体是多大没有兴趣。因此可以先对原数组排序,然后二分查找找到每个数的“排名”,用这个“排名”来重新给原数组赋值,这样原数组中的元素就是1 ~ length - 1

而这个计算前缀和的操作,通过树状数组进行,从后向前给树状数组中添加当前元素,并计算前缀和

说真的,好难

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

        int[] temp = new int[l];
        System.arraycopy(nums, 0, temp, 0, l);
        //排序
        Arrays.sort(temp);
        //nums中改为每个数字的相对大小
        for(int i = 0; i < l; i++){
            nums[i] = Arrays.binarySearch(temp, nums[i]) + 1;
        }
        //构建树状数组
        TreeArray treeArray = new TreeArray(l);
        int res = 0;
        for(int i = l - 1; i >= 0; i--){
            res += treeArray.query(nums[i] - 1);
            treeArray.update(nums[i]);
        }
        return res;
    }
}

class TreeArray{
    int n;
    int[] tree;

    public TreeArray(int n){
        this.n = n;
        this.tree = new int[n + 1];
    }
    //取最低位的1
    public static int lowbit(int x){
        return x & (-x);
    }

    public void update(int x){
        while(x <= n){
            tree[x]++;
            x += lowbit(x);
        }
    }

    public int query(int x){
        int res = 0;
        while(x > 0){
            res += tree[x];
            x -= lowbit(x);
        }
        return res;
    }
}

这里我一直卡在一个地方,再好好想想,
这里要计算前缀和,而计算前缀和并且能动态更新的一个数据结构是树状数组。而在树状数组中存储的数字,不是传入的数组元素,而是数组中元素出现的次数!!!!!
明天再把另一道和这个题类似的题做一下,加深印象

2. 两数相加

题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

 

示例 1:

在这里插入图片描述

输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.
示例 2:

输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:

输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

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

思路

做这个题的动机呢,是今天没刷够三道题,并且,每次点题库看到第二道题没刷,挺难受的哈哈
然后就来做了,模拟这个过程就好了

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        //低位到高位依次加
        ListNode newhead = new ListNode(-1);
        ListNode index = newhead;
        int add = 0; 
        while(l1 != null && l2 != null){
            int cur = l1.val + l2.val + add;
            //当前结点放的是个位
            ListNode curr = new ListNode(cur % 10);
            index.next = curr;
            index = index.next;
            add = cur / 10;     //进位是0 或者 1
            l1 = l1.next;
            l2 = l2.next;
        }

        while(l1 != null){
            int cur = l1.val + add;
            ListNode curr = new ListNode(cur % 10);
            index.next = curr;
            index = index.next;
            add = cur / 10;     //进位是0 或者 1
            l1 = l1.next;
        }

        while(l2 != null){
            int cur = l2.val + add;
            ListNode curr = new ListNode(cur % 10);
            index.next = curr;
            index = index.next;
            add = cur / 10;     //进位是0 或者 1
            l2 = l2.next;
        }
        if(add == 1)
            index.next = new ListNode(add);
        return newhead.next;
    }
}

上面重复代码多,太繁琐,合并了一下

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        //低位到高位依次加
        ListNode newhead = new ListNode(-1);
        ListNode index = newhead;
        int add = 0; 
        while(l1 != null || l2 != null){
            int x = l1 == null ? 0 : l1.val;
            int y = l2 == null ? 0 : l2.val;
            int cur = x + y + add;
            //当前结点放的是个位
            ListNode curr = new ListNode(cur % 10);
            index.next = curr;
            index = index.next;
            add = cur / 10;     //进位是0 或者 1
            if(l1 != null)
                l1 = l1.next;
            if(l2 != null)
                l2 = l2.next;
        }
        if(add == 1)
            index.next = new ListNode(add);
        return newhead.next;
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值