1
问题一 -> 分两层
给定一个数组arr, 和一个数num, 请把小于等于num的数放在数组的左边, 大于num的数放在数组的右边。
要求额外空间复杂度O(1), 时间复杂度O(N)
令0~x为小于等于num区(初始看作-1之前的区域),遍历不断扩充这个区,类似选择排序
问题二(荷兰国旗问题) -> 分三层
给定一个数组arr, 和一个数num, 请把小于num的数放在数组的左边, 等于num的数放在数组的中间, 大于num的数放在数组的右边。
要求额外空间复杂度O(1), 时间复杂度O(N)
从两边向中间挤(-1之前为小,length之后为大且为待定区域),大的指标与遍历指标相遇时停止
public class NetherlandsFlag {
public static int[] partition(int[] arr, int L, int R, int num) {
int less = L - 1;
int more = R + 1;
while(L < more){
if(arr[L] < num){
swap(arr, ++less, L++);
}
else if(arr[L] > num){
swap(arr, --more, L);
// 此处易错点:从后面往前面换,当前位置要再次判断
}
else{
L++;
}
}
// 返回相等区域的下标
return new int[] {less + 1, more -1};
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
//1 4 8 5 7 9 正确
// arr[i] = arr[i] ^ arr[j];
// arr[j] = arr[i] ^ arr[j];
// arr[i] = arr[i] ^ arr[j];
//1 4 0 5 7 9 错误
// arr[i] = arr[i] + arr[j];
// arr[j] = arr[i] - arr[j];
// arr[i] = arr[i] - arr[j];
//1 4 0 5 7 9 错误
// arr[i] = arr[i] * arr[j];
// arr[j] = arr[i] / arr[j];
// arr[i] = arr[i] / arr[j];
//1 4 1 5 7 9 错误
}
//for test
public static void printArray(int[] arr){
if(arr == null){
return;
}
for(int x:arr){
System.out.print(x + " ");
}
}
public static void main(String[] args){
int[] arr = new int[]{9,1,5,8,4,7};
int[] cur = partition(arr, 0, arr.length - 1, 4);
printArray(arr);
}
}
2
随机快速排序的细节和复杂度分析
可以用荷兰国旗问题来改进快速排序
时间复杂度O(N*logN), 额外空间复杂度O(logN) (用于随机快排记录划分点) 经典快排每次搞定一个数即最后一个数放到正确的位置,问题:与数据状况有关系,如果是排好的容易出现O(N^2)
随机快排(工程上最常用,但不能用递归)(从中间堆积选一个书换到最后一位)的复杂度O(N*logN)是一个长期期望的复杂度(如何改写为非递归版本??)
技巧:用随机数或hash打乱原始数据状况
public class QuickSort{
public static void quickSort(int[] arr, int L, int R){
if(L < R){
//加上下面一行变为随机快排
swap(arr, L + (int)(Math.random()*(R-L+1)), R);
int[] p = partition(arr, L, R);
quickSort(arr, L, p[0] - 1);
quickSort(arr, p[1] + 1, R);
}
}
public static int[] partition(int[] arr, int L, int R){
int less = L - 1;
int more = R; //不用再管R位置已排好的这个数
while(L < more){
if(arr[L] < arr[R]){
swap(arr, ++less, L++);
}
else if(arr[L] > arr[R]){
swap(arr, --more, L);
}
else{
L++;
}
}
swap(arr, more, R); //将这个数换到正确位置
return new int[]{less + 1, more - 1};
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
3
堆排序的细节和复杂度分析
时间复杂度O(N*logN), 额外空间复杂度O(1)
堆结构非常重要(堆:完全二叉树(满二叉树或从左到有依次补齐的树))
1, 堆结构的heapInsert与heapify
2, 堆结构的增大和减少
3, 如果只是建立堆的过程, 时间复杂度为O(N)=log1+log2+…+logN-1 数组实现完全二叉树(脑补):i节点的父节点**(i-1)/2**,左孩子**(2i + 1),右孩子(2i + 2)**
大根堆:任何一个子树的最大值是其头部,小根堆同理
将完全二叉树变为大根堆(heapInsert):遍历数组,与父节点比较,大就换,直到没有父节点比我大
改变堆中值,更新堆(heapify):从改变位置与较大孩子节点交换,小就换,直到没有孩子节点比我大
将大根堆排序:每次将堆顶与当前堆尾交换,堆大小减1(排好一位),更新堆,直到堆消失
4, 优先级队列结构, 就是堆结构
public class HeapSort{
public static void heapSort(int[] arr){
if(arr == null || arr.length < 2){
return;
}
for(int i = 0; i < arr.length; i++){
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while(size > 0){
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
public static void heapInsert(int[] arr,int index){
while(arr[index] > arr[(index - 1) / 2]){
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void swap(int[] arr, int i, int j){
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
public static void heapify(int[] arr, int index, int heapSize){
int left = index * 2 + 1;
while(left < heapSize){
int largeIndex = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left; //此处易错,要将left+1的判断合在一起,所以要写前面
largeIndex = arr[index] > arr[largeIndex] ? index : largeIndex;
if(largeIndex == index){
break;
}
swap(arr, index, largeIndex);
index = largeIndex;
left = index * 2 + 1;
}
}
}
4
排序算法的稳定性及其汇总
稳定性:相同值在排序之后原始相对次序是否变化
O(N^2):
冒泡排序:稳定;
插入排序:稳定;
选择排序:不稳定
O(N*logN):
归并排序:稳定;
快速排序:不稳定;
堆排序:不稳定;
5
有关排序问题的补充:
1, 归并排序的额外空间复杂度可以变成O(1), 但是非常难, 不需要掌握, 可以搜“归并排序 内部缓存法”;
2, 快速排序可以做到稳定性, 但是非常难, 不需要掌握,可以搜“01 stable sort”; 面试题陷阱:奇偶稳定排序
3, 有一道题目, 是奇数放在数组左边, 偶数放在数组右边, 还要求原始的相对次序不变, 碰到这个问题, 可以怼面试官。 面试官非良人。
6
认识比较器的使用
在笔试或面试中直接使用,可以嵌入sort()、优先级队列和红黑树中
import java.util.Arrays;
import java.util.Comparator;
public class Demo {
public static class Student{
//要比较的对象
private String name;
private int id;
private int age;
public Student(String name,int id, int age){
this.name = name;
this.id = id;
this.age = age;
}
}
public static class IdAscendingComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2){
//return -, 即o1比o2小,o1排前面;
// +
// 0
return o1.id - o2.id;
}
}
public static class IdDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2){
return o2.id - o1.id;
}
}
public static void printStudents(Student[] students){
for(Student student : students){
System.out.println("Name:" + student.name + ",Id:" + student.id + ",Age:" + student.age);
}
System.out.println("========================");
}
public static void main(String[] args){
Student student1 = new Student("A", 1, 23);
Student student2 = new Student("B", 2, 21);
Student student3 = new Student("C", 3, 22);
Student[] students = new Student[] {student1, student2, student3};
printStudents(students);
Arrays.sort(students, new IdAscendingComparator());
//如果不给比较器,那么比较的就是对象的地址
printStudents(students);
Arrays.sort(students, new IdDescendingComparator());
printStudents(students);
//堆的使用
//TreeMap<> 使用同理
PriorityQueue<Student> heap = new PriorityQueue<>(new IdDescendingComparator());
heap.add(student1); //入堆
heap.add(student2);
heap.add(student3);
while(!heap.isEmpty()){
Student student = heap.poll(); //弹出堆顶,堆大小减1
System.out.println("Name:" + student.name + ",Id:" + student.id + ",Age:" + student.age);
}
}
}
7
桶排序:计数排序、 基数排序的介绍
1, 非基于比较的排序, 与被排序的样本的实际数据状况很有关系, 所以实际中并不经常使用
2, 时间复杂度O(N), 额外空间复杂度O(N)
3, 稳定的排序
8
补充问题(面试题)
给定一个数组, 求如果排序之后, 相邻两数的最大差值, 要求时间复杂度O(N), 且要求不能用非基于比较的排序。 借用桶排序概念
1、N个数,准备N+1个桶;
2、找出min、max,min=max,则为0;
3、将min到max等分成N+1份,分别放在首尾,因为只有N个数,所以至少会有一个空桶,但只能屏蔽掉同一桶中的数;
4、每个同设置3个变量,是否为空(boolean)、min、max;
5、设置全剧最大值, 遍历,每个非空桶的最小值与它前面非空桶的最大值计算差值。
为什么不直接找空桶两边?
有例外:19、null、30、49
import java.util.Arrays;
public class MaxGap{
public static int maxGap(int[] nums){
if(nums == null || nums.length < 2){
return 0;
}
int len = nums.length;
//???此处未理解 ——> 为min、max赋初值
int min = Integer.MAX_VALUE; //int的最大值
int max = Integer.MIN_VALUE;
//找到min,max
for(int i=0; i<len; i++){
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
if(min == max){
return 0;
}
boolean[] hasNum = new boolean[len + 1];
int[] maxs = new int[len + 1];
int[] mins = new int[len + 1];
int bid = 0;
for(int i=0; i<len; i++){
bid = bucket(nums[i], len, min, max);
mins[bid] = hasNum[bid]? Math.min(mins[bid], nums[i]) : nums[i];
maxs[bid] = hasNum[bid]? Math.max(maxs[bid], nums[i]) : nums[i];
hasNum[bid] = true;
}
int res = 0;
int lastMax = maxs[0];
for(int i=1; i<=len; i++){
if(hasNum[i]){
res = Math.max(res, mins[i] - lastMax);
lastMax = maxs[i];
}
}
return res;
}
public static int bucket(long num, long len, long min, long max){
return (int) ((num - min) * len / (max - min));
}
}
9
介绍一下工程中的综合排序算法
样本量小:插入排序(常数项低);
样本量大:
基础数据类型:快排(不需要稳定);
引用类型数据:归并(稳定);