数组中的第k个元素
给定整数数组 nums 和整数 k,请返回数组中第 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
提示:
1 <= k <= nums.length <= 104
-104 <= nums[i] <= 104
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array
很明显本题可以先进行排序,然后返回下标为nums.length - k的元素即可。
因此本体讲解的解题思路是快速选择。
在计算机科学中,快速选择(英语:Quickselect)是一种从无序列表找到第k小元素的选择算法。它从原理上来说与快速排序有关。 同样地,它在实际应用是一种高效的算法,具有很好的平均时间复杂度,然而最坏时间复杂度则不理想。快速选择及其变种是实际应用中最常使用的高效选择算法。
快速选择的总体思路与快速排序一致,选择一个元素作为基准来对元素进行分区,将小于和大于基准的元素分在基准左边和右边的两个区域。不同的是,快速选择并不递归访问双边,而是只递归进入一边的元素中继续寻找。这降低了平均时间复杂度,从O(n log n)至O(n),不过最坏情况仍然是O(n2)。
与快速排序一样,快速选择一般是以原地算法的方式实现,除了选出第k小的元素,数据也得到了部分地排序。
怎么实现快速选择不递归访问双边,而是递归进入一边的元素中继续寻找呢?
我们都知道在进行快速排序的时候,需要选出一个中间枢纽,确定这个中心枢纽的位置之后,此时中间枢纽已经将数组分成了两个子数组,然后进行递归进行快速排序,从而实现数组有序。在整个过程中,真正知道一个元素的下标的,只有枢纽这个数字。并且目标数字在排序之后的数组中的下标是nums.length - k,所以需要比较target = nums.length - k以及枢纽的下标mid来进行划分区间,因此基于这个原理,有:
- target == mid,表示枢纽的下标等于目标数字的下标,因此直接返回nums[mid]就是前k个元素的值
- 否则,在不相等的情况下,如果target > mid,表示目标数字的下标在枢纽的右边,所以在枢纽的右边进行递归进行快速选择,否则,target < mid,表示目标数字在枢纽的左边,进入枢纽的左边进入快速选择。
对应的代码:
class Solution {
/*
利用快速选择(类似于快速排序)
如果数组排好序之后,数组中的第k大元素对应的下标为nums.length - k
同时,在利用快速排序的时候,可以唯一确定一个下标,这个下标就是中间枢纽
的下标。所以只要判断最后返回的中间枢纽的下标是否等于nums.length - k这
个下标即可,如果等于,直接返回这个数,否则如果小于,说明在枢纽的右边,
此时更新left为mid + 1,否则更新right为mid - 1
*/
public int findKthLargest(int[] nums, int k) {
if(nums.length == 1)
return nums[0];
int left = 0,right = nums.length - 1,target = nums.length - k;
while(true){
int mid = quickSelection(nums,left,right);//获取[left,right]区间中间枢纽的下标
if(mid == target)//如果mid等于第k大元素的下标,那么直接返回对应的值
return nums[mid];
else if(mid > target)//中间枢纽的下标大于第k大元素的下标,表示目标元素在中间枢纽的左边,更新right,否则更新left
right = mid - 1;
else
left = mid + 1;
}
}
//进行快速排序,并将中间枢纽的下标返回
public int quickSelection(int[] nums,int left,int right){
if(left == right)//只有一个元素的时候,那么直接返回这个数,表示中间枢纽的下标
return left;
/*
利用三数中值法获取中间枢纽的值,并且假设中间枢纽的下标位于high - 1
,所以退出循环时还需要进行交换,将i和high - 1两者进行交换
*/
int pivot = getPivot(nums,left,right);
int i = left,j = right - 1;
while(i < j){
while(i < j && nums[++i] <= pivot);
while(i < j && nums[--j] >= pivot);
if(i >= j)
break;
swap(nums,i,j);
}
swap(nums,i,right - 1);
return i;
}
public int getPivot(int[] nums,int low,int high){
int mid = (low + high) / 2;
if(nums[low] > nums[mid])
swap(nums,low,mid);
if(nums[low] > nums[high])
swap(nums,low,high);
if(nums[mid] > nums[high])
swap(nums,mid,high);
swap(nums,mid,high - 1);
return nums[high - 1];
}
public void swap(int[] nums,int i,int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
运行结果:
前k个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
k 的取值范围是 [1, 数组中不相同的元素的个数]
题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/top-k-frequent-elements
利用优先队列,从而实现元素按照频率降序列.需要注意优先队列中的泛型,如果只是一个Integer,那么导致在优先队列中仅仅含有次数,却没有与次数形成对应的元素,从而导致错误,所以定义的泛型是一个长度为2的一维数组,其中下标为0表示这个元素出现的次数,下标为1的值表示数组中的一个元素。
对应的代码:
class Solution {
public int[] topKFrequent(int[] nums, int k) {
//获取数组中的最小值,然后减去这个最小值,从而使得所有元素都是非负数
int min = Integer.MAX_VALUE,i;
for(int num: nums){
if(num < min)
min = num;
}
int[] bucket = new int[100001];
for(i = 0; i < nums.length; ++i){
//考虑到元素可能含有负数,所以需要找到数组的最小值,然后每一个元素都减去这个最小值,从而保证了bucket的下标是非负数的
nums[i] -= min;
bucket[nums[i]]++;
}
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>(){
/*
定义一个优先队列,从而使得它是根据降序进行排序,因此跳出队列是
从队列头开始的,其中对应的泛型是一个长度2的一维数组,下标为0的
值表示数字出现的次数,下标为1的值表示这个数字是数组中的哪一个
元素
如果时i1[0] - i2[0]则为升序排序,即按照频率升序进行排序,这时
候跳出队列需要从队列为开始跳出
*/
public int compare(int[] i1,int[] i2){
return i2[0] - i1[0];
}
});
for(i = 0; i < nums.length;++i ){
if(bucket[nums[i]] != 0){
/*
int[]中下标为1的元素为nums[i] + min是因为在一开始的时候考虑元素存在负数的情况,
如果不做任何措施,那么利用桶的时候导致下标发生越界,因此需要将每一个元素都减去min,从而
保证bucket[num[i]]不会发生越界
但是因为-=min,从而导致nums[i]的值发生了改变,所以添加的时候需要加上min恢复到原来的nums[i]值
*/
queue.add(new int[]{bucket[nums[i]],nums[i] + min});
bucket[nums[i]] = 0;//存在重复元素的时候,避免nums[i]这个数再次压入到队列中
}
}
int[] res = new int[k];
int j = 0;
while(k > 0){
res[j++] = queue.poll()[1];
k--;
}
return res;
}
}
运行结果:
根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
示例 1:
输入:
“tree”
输出:
“eert”
解释:
'e’出现两次,'r’和’t’都只出现一次。
因此’e’必须出现在’r’和’t’之前。此外,"eetr"也是一个有效的答案。
示例 2:
输入:
“cccaaa”
输出:
“cccaaa”
解释:
'c’和’a’都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
示例 3:
输入:
“Aabb”
输出:
“bbAa”
解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意’A’和’a’被认为是两种不同的字符。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/sort-characters-by-frequency
道理同上面一题,对应的代码:
class Solution {
public String frequencySort(String s) {
char[] chars = s.toCharArray();
HashMap<Character,Integer> map = new HashMap<Character,Integer>();
Integer count;
//统计每一个字符出现得次数
for(char ch: chars){
count = map.get(ch);
if(count == null){
map.put(ch,1);
}else{
map.put(ch,count + 1);
}
}
PriorityQueue<Object[]> queue = new PriorityQueue<Object[]>(new Comparator<Object[]>(){
public int compare(Object[] o1,Object[] o2){
//按照那个降序进行排序,其中下标为0的元素是字符,下标为1的值表示字符出现的次数(由于两个下标对应的元素不同,所以泛型是Object[]数组)
return (int)o2[1] - (int)o1[1];
}
});
//遍历哈希表,从而将哈希表中得元素添加到队列中
for(Character ch: map.keySet()){
queue.add(new Object[]{ch,map.get(ch)});
}
StringBuilder sb = new StringBuilder();
Object[] tmp;
int k;
while(!queue.isEmpty()){
tmp = queue.poll();
k = (int)tmp[1];
while(k > 0){
sb.append(tmp[0]);
k--;
}
}
return sb.toString();
}
}
运行结果: