力扣75.颜色分类
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
class Solution { //时间复杂度为O(n)
//其实,变量 zero 相当于我们在三路快速排序算法中的 lt;
//变量 two 相当于我们在三路快速排序算法中的 gt。
public void sortColors(int[] nums) {
// nums[0...zero] == 0, nums[zero + 1, i] == 1, nums[two, n - 1] == 2
// 定义三个指针:zero、i、two,分别表示0的最右边界、当前处理的元素、2的最左边界
int zero = -1, i = 0, two = nums.length;
while(i < two){// 当前处理元素的指针小于2的最左边界时,继续循环
// 如果当前元素为0,将其与zero右边的元素交换,并将zero和i都向右移动一位
if(nums[i] == 0){
zero++;
swap(nums,zero,i);
i++;
}
// 如果当前元素为2,将其与two左边的元素交换,并将two向左移动一位
else if (nums[i] == 2){ // 注意此时不需要i右移,因为交换后的元素还需要继续判断
two --;
swap(nums, i, two);
}
else{ //如果当前元素是1,不需要操作,直接继续向右遍历
i ++;
}
}
}
// 交换数组中指定位置的两个元素
private void swap(int[] nums, int i, int j){
int t = nums[i];
nums[i]= nums[j];
nums[j] = t;
}
}
关于SelectK问题:即在一个无序数组中,找出第K小(大)的元素
我们首先来封装一个 selectK 的方法。封装好了这个方法以后,这三个问题都可
以快速求解。
我们的 selectK 的接口是这样的:// 在 arr[l...r] 的范围里求解整个数组的第 k 小元素并返回 // k 是索引,即从 0 开始计算 int selectK(int[] arr, int l, int r, int k, Random rnd)
因为我们的 partition 过程需要随机选取标定点,所以我们还需要传一个 Random(快排的优化)
类的对象 rnd。
定义好函数签名以后,下面我们来书写相应的逻辑。
- 首先,selectK 的过程,我们就是要执行一遍 partition。在这里,我使用双路快速排序的 partition。(partition属于核心代码块要掌握)
- 注意,因为在这个问题中,我们肯定我们处理的数据类型是 int,所以,在代码中,我不再使用泛型:
private int partition(int[] arr, int l, int r, Random rnd){
// 生成 [l, r] 之间的随机索引
int p = l + rnd.nextInt(r - l + 1);
swap(arr, l, p);
// arr[l+1...i-1] <= v; arr[j+1...r] >= v
int i = l + 1, j = r;
while(true){
while(i <= j && arr[i] < arr[l])
i ++;
while(j >= i && arr[j] > arr[l])
j --;
if(i >= j) break;
swap(arr, i, j);
i ++;
j --;
}
swap(arr, l, j);
return j;
}
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
有了 partition,我们的 selectK 的主题逻辑非常简单。
首先,进行 partition,假设结果是 p。我们只需要将 k 和 p 做比较。
- 如果 k == p,直接返回 arr[p] 即可;
- 如果 k < p,在 arr[l, p - 1] 的范围继续找,即调用 selectK(arr, l, p - 1, k, rnd);
- 如果 k > p,在 arr[p + 1, r] 的范围继续找,即调用 selectK(arr, p + 1, r, k, rnd);
就有了下面的代码:
private int selectK(int[] arr, int l, int r, int k, Random rnd){
int p = partition(arr, l, r, rnd);
if(k == p) return arr[p];
if(k < p) return selectK(arr, l, p - 1, k, rnd);
return selectK(arr, p + 1, r, k, rnd);
}
这样,我们就完成了 select K 的过程。是不是非常简单!
下面,我们用我们写的 select K,先来解决 Leetcode 上第 215 号问题:
这个问题是求第 k 大元素,但是我们的 selectK 求得是第 k 小元素。怎么办?
非常简单,我们只需要在调用 select K 之前,将求第 k 大元素的这个 k,转换成
对应求的是第几小元素对应的索引就好了。
按照题目描述,如果 k 是 1,对应就是要找最大元素,那么相应的我们的
select K 的索引,就是 nums.length - 1。(如果10个数,K=1,第一个最大的数,就是SelectK索引为9的那个的元素)
如果 k 是 nums.length,其实就是求最小元素,那么相应的我们的 selectK 的
索引,就是 0。 (如果10个数,第10个最大的数,就是SelectK索引为0的那个的元素,最小值)
他们之间的转换关系是 nums.length - k。
力扣215.数组中的第K个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
import java.util.Random; //导入Random包
class Solution {
public int findKthLargest(int[] nums, int k) {
//只有两行,其他的内容全部复用我们上面实现的 selectK,是不是很酷?
//我们的SelectK是第K最小元素,所以这里findKthLargest传入下标要处理一下
//转换关系是 nums.length - k
Random rnd = new Random();
return selectK(nums, 0, nums.length - 1, nums.length - k, rnd);
}
//有了 partition,我们的 selectK 的主题逻辑非常简单。
private int selectK(int[] arr, int l, int r, int k, Random rnd){
//首先,进行 partition,假设返回值结果是 p。我们只需要将 k 和 p 做比较。
int p = partition(arr, l, r, rnd);
//如果 k == p,直接返回 arr[p] 即可;
if(k == p) return arr[p];
//如果 k < p,在 arr[l, p - 1] 的范围继续找,即调用 selectK(arr, l, p - 1, k, rnd);
if(k < p) return selectK(arr, l, p - 1, k, rnd);
//如果 k > p,在 arr[p + 1, r] 的范围继续找,即调用 selectK(arr, p + 1, r, k, rnd);
return selectK(arr, p + 1, r, k, rnd);
}
private int partition(int[] arr, int l, int r, Random rnd){
// 生成 [l, r] 之间的随机索引
int p = l + rnd.nextInt(r - l + 1);
swap(arr, l, p);
// arr[l+1...i-1] <= v; arr[j+1...r] >= v
int i = l + 1, j = r;
while(true){
while(i <= j && arr[i] < arr[l])
i ++;
while(j >= i && arr[j] > arr[l])
j --;
if(i >= j) break;
swap(arr, i, j);
i ++;
j --;
}
swap(arr, l, j);
return j;
}
//数组指定索引,两数交换
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
面试题17.14最小K个数&&剑指offer40&&LCR159.库存管理
下面,我们解决《剑指 Offer》上的 40 号问题,最小的 k 个数:
https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
对于这个问题,我们只要使用上面的 selectK,找到第 k 小的数。
然后,selectK 的过程由于调用了 partiton,所以会调整整个数组的内容。
此时,这个第 k 小的 数的前面所有元素,就是整个数组最小的 k 个数字了。
这里要注意,我们求的第 k 小的数字,对应的索引是 k - 1,因为题目给的下标k从0开始
所以我们调用 selectK 的时候,需要传入的索引参数是 k - 1。
另外,对于这个问题,k 可能取 0,此时,我们不需要执行 selectK,直接返回一
个含有 0 个元素的new int[] 就好了。怎么样,是不是很简单?具体过程如下:
import java.util.Arrays;
import java.util.Random;
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if(k == 0) return new int[0];
Random rnd = new Random();
selectK(arr, 0, arr.length - 1, k - 1, rnd);
//Arrays.copyOf()是java.util.Arrays 类中的一个方法。它会创建一个指定长度的新数组,并将原始数组中的元素复制到新数组中。
return Arrays.copyOf(arr, k);
}
private int selectK(int[] arr, int l, int r, int k, Random rnd){
int p = partition(arr, l, r, rnd);
if(k == p) return arr[p];
if(k < p) return selectK(arr, l, p - 1, k, rnd);
return selectK(arr, p + 1, r, k, rnd);
}
private int partition(int[] arr, int l, int r, Random rnd){
// 生成 [l, r] 之间的随机索引
int p = l + rnd.nextInt(r - l + 1);
swap(arr, l, p);
// arr[l+1...i-1] <= v; arr[j+1...r] >= v
int i = l + 1, j = r;
while(true){
while(i <= j && arr[i] < arr[l])
i ++;
while(j >= i && arr[j] > arr[l])
j --;
if(i >= j) break;
swap(arr, i, j);
i ++;
j --;
}
swap(arr, l, j);
return j;
}
private void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
}
如果让调用java里库的方法,先 排序 后取值就更容易,不过 有点easy,面试时可能不让用
//时间复杂度:O(nlogn),其中n是数组arr的长度。算法的时间复杂度即排序的时间复杂度。
//空间复杂度:O(logn),排序所需额外的空间复杂度为 O(logn)。
class Solution {
public int[] smallestK(int[] arr, int k) {
int[] vec = new int[k];
Arrays.sort(arr); //调用Array.sort方法,给arr从小到大排序,然后取前k个数
for (int i = 0; i < k; ++i) {
vec[i] = arr[i];
}
return vec; //返回对应数组
}
}