(一)剑指Offer 40. 最小的k个数
基本思路:
1.这是经典的topK问题,最直接的想法就是对其进行排序,然后取出排序后的前k个数。那么问题就转化为了如何选择排序算法上。
2.由于这里只需要找到前k个数,我们在选择排序算法时,希望能够有一个排序算法,通过有限的轮数找到排序好的前k个数,这样后面的排序过程我们就不需要再进行了。
3.一个较好的方法是使用快排,快排的时间复杂度为O(nlogn)。我们首先选择一个数,进行一轮排序,此时小于此数的均在左侧,大于的均在右侧;若左侧的个数少于k个,则我们对右侧的任意值再进行一轮快排;若多于k个,则对左侧的任意值再进行一轮快排。
class Solution {
public void quickSort(int[] arr, int left, int right,int k){
if(left >= right){
return ;
}
int l = left, r = right;//待排序的左右两端指针
int base = arr[l];//找到的基准数字
while(l < r){//当l=r时停止
//先找到比base小的数
while(arr[r] >= base && r > l){
//注意:r>l,因为l的左侧都是比base小的值,这是应该的
r--;
}
arr[l] = arr[r];//将其放置到base的位置
//找到比base大的数,放在r的位置
while(arr[l] <= base && l < r){
l++;
}
arr[r] = arr[l];
}
arr[l] = base;
if(l > k){
quickSort(arr,left,l-1,k);
}else if(l < k){
quickSort(arr,l+1,right,k);
}else if(l == k){
return;
}
}
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0) {
return new int[0];
} else if (arr.length <= k) {
return arr;
}
quickSort(arr,0,arr.length-1,k);
int[] res = new int[k];
for(int i = 0; i < k; i++){
res[i] = arr[i];
}
return res;
}
}
还有一个实现比较简单的方法是用小根堆,由于java中本身提供了PriotyQueue结构,所以我们只需要根据是最大根堆还是最小跟堆来重写比较器即可。
class Solution {
//堆
public int[] getLeastNumbers(int[] arr, int k) {
if (k == 0 || arr.length == 0) {
return new int[0];
}
// 默认是小根堆,实现大根堆需要重写一下比较器。
Queue<Integer> pq = new PriorityQueue<>((v1, v2) -> v2 - v1);
for (int num: arr) {
if (pq.size() < k) {
pq.offer(num);//添加num
} else if (num < pq.peek()) {//只保存k个,多出来的直接丢弃
pq.poll();
pq.offer(num);
}
}
// 返回堆中的元素
int[] res = new int[pq.size()];
int idx = 0;
for(int num: pq) {
res[idx++] = num;
}
return res;
}
}
(二)剑指offer 41. 数据流中的中位数
基本思路:
1.最直接的方法就是对数字进行排序,然后直接返回中间一个或两个数字即可。这种方法的时间主要用于排序,大概是O(nlogn)的时间复杂度,在这一题中超时了。
2.堆插入和删除需要 O(log n),于是考虑用堆来进行排序。这一题非常巧妙地采用了大顶堆和小顶堆结合的方法,小顶堆放序列中数值大的那部分,大顶堆放序列中较小的那部分。
3.将小顶堆中的根节点元素设为m,凡是比m大的放入小顶堆,比m小的放入大顶堆。但是在插入一个堆时,都不能直接插入,要先插入对方的堆,再从对方堆里面拿去堆顶部元素,这样才能保证取到的是最大(小)值。
class MedianFinder {
Queue<Integer> A, B;
public MedianFinder() {
A = new PriorityQueue<>(); // 小顶堆,保存较大的一半
B = new PriorityQueue<>((x, y) -> (y - x)); // 大顶堆,保存较小的一半
}
public void addNum(int num) {
if(A.size() != B.size()) {
//当A的元素个数大于B的元素个数时,向B添加一个元素,使二者相等
A.add(num);
B.add(A.poll());
} else {
//向A添加元素:小顶堆的元素个数可以大于大顶堆1个
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek() + B.peek()) / 2.0;
}
}
(三)剑指Offer45.把数组排成最小的数
基本思路:
1.这道题实际就是要对数字进行排序,首位数字越小的应当越前,但是由于存在位数不同等原因,需要对排序的依据进行修改。
2.排序方法采用快速排序,并对快速排序中比较的依据进行修改。若拼接字符串 x + y > y + x,则 x>y ;
class Solution {
void quickSort(String[] words, int l, int r){
//快速排序
if(l >= r) return;
// int base = l;
int i = l, j = r;
while(i < j){
while((words[j] + words[l]).compareTo(words[l] + words[j]) >= 0 && i < j) {
j--;
}//找到第一个比base小的值
while((words[i] + words[l]).compareTo(words[l] + words[i]) < 0 && i < j){//不是小于等于
i++;
} //找到第一个比base大的值
String tmp = words[i];
words[i] = words[j];
words[j] = tmp;
}
if((words[l]+words[i]).compareTo(words[i]+words[l]) > 0){
words[i] = words[l];
words[l] = words[j];
}
quickSort(words, l, i - 1);
quickSort(words, i+1 , r);
}
public String minNumber(int[] nums) {
//重新定义排序规则:若x+y > y+x,则认为x>y
//采用快速排序
String[] words = new String[nums.length];
for(int i = 0; i < nums.length; i++){
words[i] = String.valueOf(nums[i]);//转化为字符串
}
// 调用函数:strList.sort((s1, s2) -> (s1 + s2).compareTo(s2 + s1));
quickSort(words,0,words.length-1);
StringBuilder res = new StringBuilder();
for(String s : words){
res.append(s);
}
return res.toString();
}
}
(四)剑指 Offer 51. 数组中的逆序对
基本思路:
1.最简单的方法是遍历一遍数组,对于每个数组都查找前面比它大的值,显然这样的时间效率很低,是O(n^2)
2.这一题的做法非常巧妙,它基于归并排序的过程,归并排序分为两个步骤,排序和合并,其中排序是不断拆分递归,直到划分为只有一个元素的小组来实现的。故合并时,我们添加一个比较的环节,如果左边组的数字比右边的小,我们就发现了逆序数,应当计算进去。
public class Solution {
public int reversePairs(int[] nums) {
int len = nums.length;
if (len < 2) {
return 0;
}
int[] copy = new int[len];
for (int i = 0; i < len; i++) {
copy[i] = nums[i];
}
int[] temp = new int[len];//在最开始就声明一个空的temp,避免每次都创建!!!不然会超时!!!太恶心了
return reversePairs(copy, 0, len - 1, temp);
}
private int reversePairs(int[] nums, int left, int right, int[] temp) {
if (left == right) {
return 0;
}
int mid = left + (right - left) / 2;
int leftPairs = reversePairs(nums, left, mid, temp);
int rightPairs = reversePairs(nums, mid + 1, right, temp);
if (nums[mid] <= nums[mid + 1]) {
return leftPairs + rightPairs;
}
int crossPairs = mergeAndCount(nums, left, mid, right, temp);
return leftPairs + rightPairs + crossPairs;
}
private int mergeAndCount(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 count = 0;
for (int k = left; k <= right; k++) {
if (i == mid + 1) {
nums[k] = temp[j];
j++;
} else if (j == right + 1) {
nums[k] = temp[i];
i++;
} else if (temp[i] <= temp[j]) {
nums[k] = temp[i];
i++;
} else {
nums[k] = temp[j];
j++;
count += (mid - i + 1);
}
}
return count;
}
}