选择排序算法
Contents
1 定义和说明
所谓的选择排序算法,就是对于一个给定的数组,首先,选择其中的最小元素放在第一个位置处;然后,从剩下的元素中,再选择其中的最小元素,并把它放到数组的第二个位置处;接下来,重复这一过程,直到最后一个元素,它自己就是剩余元素的最小元素,自然就放到了最后的数组的位置处。
2 实现思路
对于一个给定的数组arr,首先,假定第1个元素是最小的,然后从第2个到最后1个元素中,选择一个最小的元素出来,如果选择出来的这个最小元素比我们假定最小的那个元素(即第1个元素)还要小的话,则交换它们的位置。如果选择出来的这个最小元素不小于我们假定最小的那个元素(即第1个元素)的话,则意味着我们假定最小的那个元素(即第1个元素),的确就是整个数组中最小的元素,所以它就呆在原来的位置处,我们什么也不用做。这样下来,就保证第1个元素一定是整个数组中最小的那个元素。
接下来,我们假定第2个元素是最小的元素,然后从第3个到最后1个元素中,选择1个最小的元素和第2个元素进行比较和交换的过程,处理之后,这第2个元素就一定是整个数组中除第1个元素之外的最小元素。
以此类推,当我们处理完数组的倒数第2个元素之后,就可以不用再走下去了,剩余最后1个元素一定是整个数组中剩余元素中的最小那个元素,因为就剩下它自己了,没得比了,同时,它也一定是整个数组中最大的那个元素了,所以,它就呆在数组的最后位置处。
3 代码实现
import java.util.Random; public class SelectionSort{ //私有化构造方法 private SelectionSort(){}; //选择排序方法 public static void sort(int[] arr){ for(int i=0;i<arr.length-1;i++){ //这里,有一个比较巧妙的方法,就是for循环从数组的第0个元素开始,直到倒数第2个元素,每次都假定第i个元素是最小的元素,然后把这个最小元素的下标赋给一个变量来记录。 //每次循环的时候,都先假定这第i个元素是最小的,并把这个下标赋给minIndex. int minIndex=i; //接下来,就是要从第[i+1]开始到最后一个元素中,找出一个最小的元素,且该元素比minIndex还要小的那个元素。如果找到了的话,则把这2个元素交换位置。即:交换i和minIndex这2个元素。 for(int j = i+1;j < arr.length; j++){ if(arr[j] < arr[minIndex]){ //注意,这里找到的第j个元素不但比arr[minIndex]要小,而且它一定还是【i+1,arr.lengh】区间中最小的那个元素。如果找到了,则把我们原先假定的最小的那个元素,和这个真正的最小的元素交换位置。 minIndex = j; } } swap(arr,i,minIndex); } } //因为sort()是static的,所以这里的swap()也必须是static private static void swap(int[] arr,int i;int j){ int temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } //写一个main方法,对排序算法进行测试 public static void main(String[] args){ int[] arr={7,2,5,4,3,100}; SelectionSort.sort(arr); for(int key:arr){ System.out.print(key+" "); } System.out.println(); Random random=new Random(); arr=new int[100]; for(int i=0;i<100;i++){ arr[i]=random.nextInt(1000); } SelectionSort.sort(arr); for(int key:arr){ System.out.print(key+" "); } } }
4 “低效版”选择排序算法
public class SelectionSort{ private SelectionSort(){}; public static void sortSlow(int[] arr){ for(int i=0;i<arr.length-1;i++){ for(int j=i+1;j<arr.length;j++){ if(arr[j]<arr[i]){ swap(arr,i,j); } } } } private static void swap(int[] arr,int i,int j){ int temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } }
对比这个“低效版”选择排序算法,可以看到,我们在从第[i+1,arr.length]区间中,每当有元素比arr[i]小的话,则立即交换它们的顺序,这样一来,如果后面还有更小的元素,则仍需要继续交换。
而前一种选择排序算法,则是先从第[i+1,arr.length]区间中找到该区间的最小元素,并记录下它的索引位置minIndex,最后再交换第i个和第minIndex个元素。显然,其性能会快很多,因为其需要交换元素的次数少了很多。这也正是一个好的算法的魅力所在。
5选择排序算法和“低效版”选择排序算法性能测试对比
完整的代码如下:
import java.util.Arrays; import java.util.Random; /** * @Author:asher * @Date:2021/3/4 19:52 * @Description:DataStructureAlgorithm.SelectionSort * @Version:1.0 */ public class SelectionSort { private SelectionSort(){} public static void sort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { int minIndex = i; for (int j = i+1; j <arr.length; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } swap(arr, i, minIndex); } } public static void sortSlow(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { for (int j = i+1; j <arr.length ; j++) { if (arr[j] < arr[i]) { swap(arr, i,j); } } } } 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) { Random random = new Random(); int count=100000; //初始化1个100000个元素的数组 int[] arr1 = new int[count]; for (int i = 0; i <count; i++) { //实例化这个数组,每个元素都是随机生成,元素大小上限也为100000 arr1[i] = random.nextInt(count); } //copy这个数组,便于后续排序对比 int[] arr2 = Arrays.copyOf(arr1, arr1.length); //选择排序算法测试 long t1=System.currentTimeMillis(); SelectionSort.sort(arr1); long t2 = System.currentTimeMillis(); System.out.println("sort time ms:"+ (t2 - t1)); //"低效版"选择排序算法测试 long t3=System.currentTimeMillis(); SelectionSort.sortSlow(arr2); long t4 = System.currentTimeMillis(); System.out.println("sortSlow time ms:"+ (t4 - t3)); } } //测试执行结果: //sort time ms:4604 //sortSlow time ms:17710
6 带泛型的选择排序算法
前面的选择排序算法,只是针对整型数组的排序,但是其通用性还不强,接下来,我们动手把它改造为支持任意类型数组的选择排序算法,即带泛型的选择排序算法。
import java.util.Random; public class SelectionSort{ private SelectionSort(){}; //这里只需要改为泛型方法即可,并不需要把类改为泛型类,因为该类被当做工具类来使用,并没涉及到带泛型的成员变量 //这里的参数中数组类型是带泛型T的,因此方法的返回值void前也需要指定泛型。 //同时T extends Comparable<T>,此处的extends意味着这个泛型类T是实现Comparable接口,而不是当做继承来理解 //此处要实现Comparable接口的原因是,arr[j]和arr[minIndex]此时被当做对象实例来比较,需要调用compareTo()方法,而不能再是使用小于<来比较大小了 public static <T extends Comparable<T>> void sort(T[] arr){ for(int i=0;i<arr.length-1;i++){ int minIndex=i; for(int j=i+1;j<arr.length;j++){ //arr[j]和arr[minIndex]此时被当做对象实例来比较大小 if(arr[j].compareTo(arr[minIndex])<0){ minIndex=j; } } swap(arr,i,minIndex); } } private static <T> void swap(T[] arr,int i,int j){ T temp=arr[i]; arr[i]=arr[j]; arr[j]=temp; } public static void main(String[] args){ int count=100000; Random random=new Random(); Integer[] arr=new Integer[count]; for(int i=0;i<count;i++){ arr[i]=random.nextInt(count); } long beginTime=System.currentTimeMillis(); SelectionSort.sort(arr); long endTime=System.currentTimeMillis(); System.out.println("Generics Array selection sort time:"+(endTime-beginTime)); } } //输出结果: //Generics Array selection sort time:13643
这里,可以看到,同样的选择排序算法,同样的数据量10万个,对于基本类型的数组int[]排序耗时4604ms,而对于包装类型Integer[]排序耗时13643。差别还是比较明显的,原因在于包装类型的数据是放在heap内存区,被当做对象来比较,而基本类型的数据是直接放在stack内存区进行比较大小的。
7 对自定义类使用选择排序进行排序
我们对选择排序算法改造成了可以支持泛型的排序算法,即对任意数据类型的数据进行排序。我们自定义一个Student类,让其实现Comparable接口,在compareTo()方法中,先按身高排序,再按年龄进行排序。
public class Student implements Comparable<Student>{ private String name; private int height; private int age; public Student(String name,int height,int age){ this.name=name; this.height=height; this.age=age; } @Override public int compareTo(Student stu){ if(this.height!=stu.height){ return this.height-stu.height; }else{ return this.age-stu.age; } } @Override public String toString(){ return "Student: name="+name+" height="+height+" age="+age; } }
在SelectionSort的main方法中,对Student[]数组进行排序,代码片段如下:
public static void main(String[] args){ Student[] students={new Student("Jim",120,7), new Student("Kate",120,7), new Student("Tom",110,5)}; SelectionSort.sort(students); for(Student stu:students){ System.out.println(stu); } } //结果如下: Student: name= Tom height=110 age=5 Student: name= kate height=120 age=7 Student: name= Jim height=120 age=7
从上,可以看到,支持泛型的选择排序算法,可以对任意类型的数据进行排序了。同时,看到排序之后,Jim和Kate这2个学生对象的身高,年龄一致,且在原数据中,Jim排在前面,但是排序之后的结果却在后面。所以,验证了选择排序是不稳定的排序算法。
8 时间复杂度分析
public static <T extends Comparable<T>> void sort(T[] arr){ for(int i=0;i<arr.length-1;i++){ int minIndex=i; for(int j=i+1;j<arr.length;j++){ //arr[j]和arr[minIndex]此时被当做对象实例来比较大小 if(arr[j].compareTo(arr[minIndex])<0){ minIndex=j; } } swap(arr,i,minIndex); } }
假定数组长度为n,当外循环i=0时,内循环执行n-1次,i=1时,内循环执行n-2次,,,直到i=n-1时,内循环执行0次。总计执行次数就是一个等差数列,首项是0,尾项是n-1,则其和为:n(0+n-1)/2=1/2(n2-n),而我们在评估时间复杂度时遵循的原则是去掉常数项、去掉低阶项,去掉系数项。所以,评估出的时间复杂度是O(n2)。同时,我们可以分别测试1万、10万个数据项的数组,执行选择排序,来看其执行时间是否大概差别100倍?如果后者执行耗时大概是前者的100倍的话,而数据量是前者的10倍,那么就可以证明该算法的复杂度是O(n2)。我在本地测试的结果不太准,同样的算法思路,不同的测试结果:
SelectionSort ,10000 ,0.134 s SelectionSort ,100000 ,13.731 s ----------------------------------------------------------- 10000 time: 0.082 100000 time: 12.608
9 小结
选择排序算法,见名知意,就是第1次从所有元素中选择一个最小的元素放在第1个位置处,第2次从剩下的元素中选择一个最小的放在第2个位置处,以此类推,直到最后1个元素。同时,我们可以说,对于第i次选择的时候,数组中从[0,i)的所有元素都已经是有序的,从[i,n)中的元素还都是无序的状态。