入门基础3
1.算法稳定性概念与排序汇总
先上个图,简单理解:
1)概念:
-
① 定义:能保证两个相等的数,经过排序之后,其在序列的前后位置顺序不变。(A1=A2,排序前A1在A2前面,排序后A1还在A2前面)
-
② 意义:稳定性本质是维持具有相同属性的数据的插入顺序,如果后面需要使用该插入顺序排序,则稳定性排序可以避免这次排序。
比如,公司想根据“能力”和“资历”(以进入公司先后顺序为标准)作为本次提拔的参考,假设A和B能力相当,如果是稳定性排序,则第一次根据“能力”排序之后,就不需要第二次根据“资历”排序了,因为“资历”排序就是员工插入员工表的顺序。如果是不稳定排序,则需要第二次排序,会增加系统开销。
2)分类
① 稳定性排序:冒泡排序,插入排序、归并排序、基数排序
② 不稳定性排序:选择排序、快速排序、希尔排序、堆排序
3)时间复杂度与空间复杂度的汇总表
详细的内容借鉴:【排序算法】(1)排序的稳定性
4)工程中的综合排序算法
在工程中,会先判断数组中的值是基础类型(基础类型会用快排)还是对象类型(就需要用到比较器,使用归并排序),但是如果数组很短,不选选择快排,也不会选择归并,会直接用插入排序。(基础数据类型没有区分先后的必要,工程业务上的对象,自己创建的bean就需要区分先后。)
5)笔试/面试问题
1.在综合排序中,样本量很小的时候为什么选择复杂度高的算法?
因为常数项低。
2.在综合排序中,基本类型的排序为什么选择归并排序?
因为稳定性好,复杂度相对低。
一般面试有压力面
6)有关排序问题的补充
1,归并排序的额外空间复杂度可以变成O(1),但是非常难,不需要掌握,可以搜“归并排序 内部缓存法”
2,快速排序可以做到稳定性问题,但是非常难,不需要掌握,可以搜“01 stable sort”
3,有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变(时间复杂度要求O(N),空间复杂度要求O(1) ),碰到这个问题,可以怼面试官。
2. 比较器
一般的编程语言中都会提供排序算法,但是他们比较的都是基础数据类型(int,char等),要想实现使用他们提供的排序算法就需要使用到比较器了。下边来个实现自己定义的Student对象的比较器,具体案例代码:
public class ComparatorDemo {
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;
}
}
public static class IdAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
//return 负数;//表示o1应该放在前边
//return 正数;//表示o2应该放在前边
//return 0;//认为两个东西一样大
//if(o1.id < o2.id){
// return 负数;//表示o1应该放在前边
//}else if(o1.id > o2.id){
// return 正数;//表示o2应该放在前边
//}else {
// return 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 class AgeAscendingComparator implements Comparator<Student> {
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.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);//在没有使用比较器的时候会直接报错!Exception in thread "main" java.lang.ClassCastException: xxx$Student cannot be cast to java.lang.Comparable
//printStudent(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> queue = new PriorityQueue<>(new IdComparactor());
queue.add(stu1);
queue.add(stu2);
queue.add(stu3);
while(!queue.isEmpty()){
Student student = queue.poll();
System.out.println("name:"+student.name+" , id:"+student.id+" ,age:"+student.age);
}
System.out.println("------------------------------");
//同样地,在红黑树中也可以使用比较器
TreeSet<Student> tree = new TreeSet<Student>(new IdComparactor());
tree.add(stu1);
tree.add(stu2);
tree.add(stu3);
while (!tree.isEmpty()){
Student student = tree.pollFirst();
System.out.println("name:"+student.name+" , id:"+student.id+" ,age:"+student.age);
}
可以在上一个代码的main函数中添加这段代码,然后进行测试,测试的结果:
3.桶排序、计数排序、基数排序
桶排序是一个大的概念,时间、空间复杂度都是O(N),不基于比较的排序一般不是那么重要。
3.1先来个案例说明计数排序:
假如有20个数值范围在0-10之间的整数,那么可以建立一个长度为11的数组,数组下标从0到10,元素初始值全为0,如下所示:
且先假设20个随机整数的值是:
9, 3, 5, 4, 9, 1, 2, 7, 8,1,3, 6, 5, 3, 4, 0, 10, 9, 7, 9
首先先遍历这个无序的随机数组,每一个整数按照其值对号入座,对应数组下标的元素进行加1操作。
比如第一个整数是9,那么辅助数组下标为9的元素加1:
第二个整数是3,那么数组下标为3的元素加1:
继续遍历数列并修改数组…
最终,数列遍历完毕时,数组的状态如下:
数组中的每一个值,代表了数列中对应整数的出现次数。
有了这个统计结果,排序就很简单了,直接遍历数组,输出数组元素的下标值,元素的值是几,就输出几次:
0, 1, 1, 2, 3, 3, 3, 4, 4, 5, 5, 6, 7, 7, 8, 9, 9, 9, 9, 10
显然,这个输出的数列已经是有序的了。
这就是计数排序的基本过程,它适用于一定范围的整数排序。在取值范围不是很大的情况下,它的性能在某些情况甚至快过那些O(nlogn)的排序,例如快速排序、归并排序。
代码实现:(借鉴什么是计数排序?)
public static int[] countSort(int[] array) {
//1.得到数列的最大值
int max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
}
//2.根据数列的最大值确定统计数组的长度
int[] coutArray = new int[max + 1];
//3.遍历数列,填充统计数组
for(int i = 0; i < array.length; i++)
coutArray[array[i]]++;
//4.遍历统计数组,输出结果
int index = 0;
int[] sortedArray = new int[array.length];
for (int i = 0; i < coutArray.length; i++) {
for (int j = 0; j < coutArray[i]; j++) {
sortedArray[index++] = i;
}
}
return sortedArray;
}
3.2 桶排序
一句话总结:划分多个范围相同的区间,每个自区间自排序,最后合并。
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
来个案例(借鉴【排序】图解桶排序):
核心代码:
public static void bucketSort(int[] arr){
// 计算最大值与最小值
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
// 计算桶的数量
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
// 将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
// 对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
// 将桶中的元素赋值到原序列
int index = 0;
for(int i = 0; i < bucketArr.size(); i++){
for(int j = 0; j < bucketArr.get(i).size(); j++){
arr[index++] = bucketArr.get(i).get(j);
}
}
}
3.3 桶排序概念的实际应用【面试】
题目描述: 给定一个数组,求如果排序之后,相邻两数的最大差值。
要求时间复杂度O(N),且要求不能用非基于比较的排序。
分析:
①使用桶的思想,设置N+1个桶,根据鸽笼原理,必然有一个空桶,那么就排除了最大差值在一个桶内,因为空桶两侧的差距肯定大于桶内的差距
②但,最大差值不见得是空桶左侧max和空桶右侧min,需要依次遍历求差值(见下图解释)
③使用桶的思想时间复杂度是N,但没有使用桶排序,桶排序是非基于比较的排序,桶就是容器的含义,计数排序和基数排序是桶排序的具体实现,是稳定的排序,时间复杂度为N
代码实现:
public class Bucket_MaxGap {
public static int getMaxGap(int[] arr){
if(arr == null || arr.length < 2){
return 0;
}
//第一步:先比较得出数组的最大最小值
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
int len = arr.length;
for (int i = 0; i < len; i++) {
min = Math.min(min,arr[i]);
max = Math.max(max,arr[i]);
}
if(max == min){//如果最大最小相等,就直接返回
return 0;
}
//第二步:创建一个桶,包含三个数组:一个存放已存在数否的,一个存放最大值,一个存放最小值
boolean[] hasNum = new boolean[len + 1];
int[] maxArr = new int[len + 1];
int[] minArr = new int[len + 1];
int bid = 0;//用以记录桶的号数
//第三步,遍历数组,填充桶
for (int i = 0; i < len; i++) {
bid = bucket(arr[i],len,min,max);
maxArr[bid] = hasNum[bid] ? Math.max(maxArr[bid],arr[i]) : arr[i];
minArr[bid] = hasNum[bid] ? Math.min(minArr[bid],arr[i]) : arr[i];
hasNum[bid] = true;
}
//第四步:比较相邻桶的最大值和最小值,得出结果
int lastMax = maxArr[0];//默认0号桶的最大值为上个桶的最大值,不用从0号开始遍历
int i = 1;
int res = 0;//用于存放结果
for (; i < len + 1; i++) {//遍历所有的桶
if(hasNum[i]){
res = Math.max(res,(minArr[i] - lastMax);
lastMax = maxArr[i];
}
}
return res;
}
public static int bucket(int num,int len,int min,int max){
//(num-min)/(max-min)就是占所有的比例
//返回的结果是第几个桶(不理解的话,可以拿1-10这10个数试试)
return (int) (num - min) * len/ (max - min);//找出当前数字应放在哪个桶,记录下这个桶号
}
public static void main(String[] args) {
int[] arr = {3,1,6,2,7};
int maxGap = getMaxGap(arr);
System.out.println("最大差值:"+maxGap);
}
}
注意,不一定是空桶左侧的最大值和右侧的最小值的差值为最大差值,比如下边这种情况:
还有一个问题就是,同一个桶中不可能出现最大差值,解释:
根据抽屉原理:把N个苹果放到N+1个抽屉里面,必然至少有一个抽屉不存在苹果。
而我们这里,一个桶代表一个差值,而我们这样设计的结果就是,必然存在一个空桶。
所以这个最大差值必然大于一个桶代表的范围。
3.4 基数排序
基本思想
基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
案例:(借鉴【排序算法(七)】基数排序)
通过基数排序对数组{53, 3, 542, 748, 14, 214, 154, 63, 616},它的示意图如下:
在上图中,首先将所有待比较数统一为统一(最长)位数长度,接着从最低位开始,依次进行排序。
1.按照个位数进行排序。
2.按照十位数进行排序。
3.按照百位数进行排序。
4.排序后,数列就变成了一个有序序列。
代码实现:
public class RadixSort{
private int a[];
public RadixSort(int a[]) {
this.a = a;
}
public void radixSort() {
int n = a.length - 1;
//找到最大值
int max = a[0];
for(int i = 1 ; i < n ; i ++)
if(a[i] > max)
max =a[i];
//求出最大值有多少位
int keysNum = 0;
while( max > 0 ) {
max /= 10;
keysNum ++;
}
List<LinkedList<Integer>> buckets = new ArrayList<>();
for(int i = 0 ; i < 10 ; i ++)
buckets.add(new LinkedList<>());
for(int i = 0 ; i < keysNum ; i ++) {
countSort(buckets , i);
}
}
private void countSort(List<LinkedList<Integer>> buckets, int i) {
for(int j = 0 ; j < a.length ; j ++) {
int key = (int) (a[j] % Math.pow(10, i + 1) / Math.pow(10, i));
buckets.get(key).add(a[j]);
}
int count = 0 ;
for(int k = 0 ; k < 10 ; k ++) {
LinkedList<Integer> bucket = buckets.get(k);
while(bucket.size() > 0) {
a[count ++] = bucket.remove(0);
}
}
System.out.print("the " + ( i + 1 ) +" time sort: ");
display();
}
private void display() {
for(int i = 0 ; i < a.length ; i ++ )
System.out.print(a[i] + " ");
System.out.println();
}
public static void main(String[] args) {
int[] a = {53, 3, 542, 748, 14, 214, 154, 63, 616, 55 , 58};
RadixSort rs = new RadixSort(a);
System.out.print("before sort: ");
rs.display();
rs.radixSort();
}
}
复杂度分析
初看起来,基数排序的执行效率似乎好的让人无法相信,所有要做的只是把原始数据项从数组复制到链表,然后再复制回去。如果有10个数据项,则有20次复制,对每一位重复一次这个过程。假设对5位的数字排序,就需要205=100次复制。如果有100个数据项,那么就有2005=1000次复制。复制的次数与数据项的个数成正比,即O(n)。这是我们看到的效率最高的排序算法。
不幸的是,数据项越多,就需要更长的关键字,如果数据项增加10倍,那么关键字必须增加一位(多一轮排序)。复制的次数和数据项的个数与关键字长度成正比,可以认为关键字长度是N的对数。因此在大多数情况下,基数排序的执行效率倒退为O(N*logN),和快速排序差不多。