算法学习系列(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),和快速排序差不多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值