荷兰国旗问题
给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)
package basic_algorithm.class_01;
import java.util.Arrays;
public class NetherlandsFlag {
public static void netherlandsFlag(int[] arr, int l, int r, int num) {
int min = l - 1;
int max = r + 1;
int cur = l;
while (cur < max) {
if (arr[cur] < num) {
swap(arr,++min,cur++);
}else if(arr[cur]>num){
swap(arr,--max,cur);
}else{
cur++;
}
}
}
//交换
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr={1,4,3,5,2,7,6,5,9,8,0};
int num=5;
netherlandsFlag(arr,0,arr.length-1,num);
System.out.println(Arrays.toString(arr));
}
}
经典快速排序
在荷兰国旗问题的代码上进行改进 将数组最后一个数作为标准划分,然后再把这个数放到正确的位置。经典快排存在的问题在于:当数据状况很差时,如(1,2,3,4,5,6,7)以7为标准划分,最后只有小于7和等于7的部分,再按6划分…这时每次都只确定了一个数(最后一个数)的位置,时间复杂度为O(N^2)。当数据状况好时,如相等的位置刚好在正中间,那时间复杂度可以通过master公式计算得O(N*logN)。
//快速排序
public static void quickSort(int[] arr) {
if (arr == null && arr.length < 2) return;
quickSort(arr, 0, arr.length - 1);
}
public static void quickSort(int[] arr, int l, int r) {
if (l < 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 min = l - 1;
int max = r;//跟荷兰国旗问题不同 这里是将数组的最后一个元素作为标准进行划分 而不用自己定义
while (l< max) {
if (arr[l] < arr[r]) {
swap(arr, ++min, l++);
} else if (arr[l] > arr[r]) {
swap(arr, --max, l);
} else {
l++;
}
}
swap(arr, r, max);//把arr[r]放到正确位置
return new int[]{min + 1, max};//返回相等部分的边界
}
//交换
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
随机快速排序(最常用)
随机的选择一个数与最后一个数交换,并将这个随机选择的数作为划分标准进行经典快速排序。这时数据状况很差的概率会大大降低(因为你随机选择一个数是个概率事件 不一定每次都选到状况差的情况)时间复杂度也会是个概率情况,是一个长期期望的复杂度(数学证明出来的 记住就好)。额外空间来自在进行每一次划分时要记录划分的那个位置(断点),否则无法知道下一步对哪个区域操作,进行多少次二分就有多少个断点,所以额外空间复杂度也是个概率情况,在最差情况时是O(N),长期期望是O(logN)。
关键代码: 在经典快排基础上加个随机数与末尾数交换即可
随机快排:时间复杂度O(N*logN),额外空间复杂度O(logN)。
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);
}
}
tips:有时设计算法时,如果想绕开样本原本的数据状况,业界最常用的技巧就是 随机 和 哈希,即打乱原本数据状况。
堆排序(重要!!!)
堆就是一个完全二叉树:(可以用数组表示)
大根堆 – 完全二叉树中任何一个子树的头部都是最大值
小根堆 – 完全二叉树中任何一个子树的头部都是最小值
堆结构非常重要
1, 堆结构的heapInsert与heapify
//该函数实现加入一个新节点并往上调整直到形成大根堆的过程
private static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
/**
* 功能:如果当前0~heapSize已经是大根堆 当index位置的值(变小则下沉)发生变化时 重新调该数组为大根堆
*
* @param arr 原始数组(以数组形式实现大根堆)
* @param index 该位置的值发生变化
* @param heapSize 0~heapSize为大根堆
*/
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) {//说明index的左孩子没越界
//返回左右孩子中较大数的下标 left+1即为右孩子
int maxInx = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//返回孩子节点与父节点中较大数的下标
maxInx = arr[maxInx] > arr[index] ? maxInx : index;
if (maxInx == index) {
break;
}
//交换
swap(arr, maxInx, index);
index = maxInx;
left = index * 2 + 1;
}
}
2, 堆结构的增大和减少 (添加或删去节点都要注意重新调整堆结构)
3, 如果只是建立堆的过程, 时间复杂度为O(N)
4, 优先级队列结构, 就是堆结构
堆排序:时间复杂度O(N*logN),额外空间复杂度O(logN)
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 0; i < arr.length; i++) {
//0~i上建立大根堆,建立的过程复杂度为O(N)
heapInsert(arr, i);
}
//实现堆排序:将当前堆顶元素与最后一个位置的元素交换 然后缩小堆的范围
// 即把堆顶(最大数)划分了出去 然后再将去掉最大数后的结构调成大根堆 重复划出去最大数
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {
heapify(arr, 0, heapSize);
swap(arr, 0, --heapSize);
}
}
排序算法的稳定性及其汇总
稳定性的含义:经排序算法后,相同的值在原始序列中的相对次序是不变。
稳定性的意义:现实世界中存在需要保持原始数据信息的情况。
-
平均时间复杂度为O(N^2)
- 冒泡排序:可以实现成稳定的算法,即遇到相同的值不交换
- 插入排序:可以实现成稳定的算法,即遇到相同的值就不往前放了
- 选择排序:不稳定
-
平均时间复杂度为O(N*logN)
- 归并排序:可以实现成稳定的算法,即左右两边遇到相同值时保证先copy左边
- 快速排序:不稳定
- 堆排序:不稳定
工程中的综合排序算法
- 当样本量比较小时(小于60) – 直接用插排(因为常数项低)
- 当数组中存储的是基础数据类型(int,char,double…)的数据 – 用快排(因为基础数据类型无需区分原始顺序 都一样)
- 当数组中存储的是自己定义的class – 用归并(因为可能需要保存原始信息)
有关排序问题的补充:
- 归并排序的额外空间复杂度可以变成O(1),但是非常难,不 需要掌握,可以搜“归并排序 内部缓存法”
- 快速排序可以做到稳定性问题,但是非常难,不需要掌握, 可以搜“01 stable sort”
- 有一道题目,是奇数放在数组左边,偶数放在数组右边,还 要求原始的相对次序不变,碰到这个问题,可以怼面试官。面试官非良人。
比较器
基于比较的排序最重要!!!
package basic_algorithm.class_01;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class MyComparator<S> {
public static class Student {
public String name;
public int id;
public int age;
public Student(String name, int id, int age) {
this.name = name;
this.id = id;
this.age = age;
}
}
//按id升序
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.id - o2.id;
}
}
//降id降序
public static class IdDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.id - o1.id;
}
}
//按age升序
public static class AgeAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
//按age降序
public static class AgeDescendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o2.age - o1.age;
}
}
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[]{student3, student2, student1};
printStudents(students);
Arrays.sort(students, new IdAscendingComparator());
printStudents(students);
Arrays.sort(students, new IdDescendingComparator());
printStudents(students);
Arrays.sort(students, new AgeAscendingComparator());
printStudents(students);
Arrays.sort(students, new AgeDescendingComparator());
printStudents(students);
//优先级队列就是堆
PriorityQueue<Student> heap = new PriorityQueue<>(new IdAscendingComparator());
heap.add(student1);
heap.add(student2);
heap.add(student3);
while (!heap.isEmpty()) {
Student student = heap.poll();
System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age);
}
}
}
桶排序、计数排序、基数排序
- 非基于比较的排序,与被排序的样本的实际数据状况很有关系,所以实际中并不经常使用。
- 时间复杂度O(N),额外空间复杂度O(N)
- 稳定的排序
桶排序是一个大的逻辑概念,不是具体的排序:
- 桶相当于按照数据状况设计的容器,然后把元素依次放进对应的桶中。
- 针对具体的实现桶排序又可以分为计数排序和基数排序。
- 计数排序和基数排序都是桶排序的具体体现。
- 基数排序用的桶比较少 一般是十个。
补充问题:给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N),且要求不能用非基于比较的排序。
思路分析:桶排序的思想 数组长度为n 准备n+1个桶 第0个桶存数组的最小值 第n个桶存数组的最大值 然后将数组中的其他数放到合适的桶中 因为共n个数据 n+1个桶 所以第1~n-1这些桶中一定存在一个空桶 我们分析可知 最大差值一定不会存在于同一个桶中 所以我们遍历这些桶 找到当前非空桶的前一个(左边距离最近)的非空桶 用当前桶中的最小值减去找到的这个桶中的最大值 即为最大差值
代码实现:
package basic_algorithm.class_01;
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;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; 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[] mins = new int[len + 1];
int[] maxs = 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;
}
//把元素放入合适的桶
private static int bucket(long num, long len, long min, long max) {
return (int) ((num - min) * len / (max - min));
}
public static void main(String[] args) {
int[] nums={12,23,10,9,34,56,11};
Arrays.sort(nums);
System.out.println(Arrays.toString(nums));//[9, 10, 11, 12, 23, 34, 56]
System.out.println(maxGap(nums));//22
}
}