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. 两数相加的主要内容,如果未能解决你的问题,请参考以下文章