算法和数据结构

选择排序算法

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)中的元素还都是无序的状态。

留言