选择排序SelectSort全面解读+代码+优化+分析复杂度+稳定性,看这篇就够了

选择排序

最简单的适合学习但是也是事实上不常使用的排序算法

本章学习重点

算法思想+代码实现

如何计算时间和空间复杂度

算法基本思想

一遍又一遍,找到数组里最小的数,与数组前面换位

下图为例
  • 第一遍,遍历数组找到3号位的value(1)是最小的把1与0号位的5对调位置

  • 第二遍,因为确认0号位的1已经是整个数组最小的,已经无需将他参加比较了,所以从1号位向后遍历,分别是4、2、5、3,找到位于2号位的value(2)是最小的与0号位对调

  • 第三遍,剩下数组是4、5、3,找到4号位的3为最小,与2号位的4对调

  • 第四遍,剩下数组是5、4,4最小与5对调

  • 第五遍,数组只剩一个5,说明比对 完毕,排序完毕

分析一下

我们可以看到其实每次对调的都是有规律的,无论这个最小值是在哪,我们总得记住他

所以这里的重点就在,一个指针一直指着当前假设的最小值的下标

用最简单的算法来学习以后怎么写自己的算法题目
  • 简单到复杂

  • 验证一步走一步

  • 多打印中间结果

  • 局部到整体

  • 没思路的时候先细分问题

  • 粗糙到精细 

  • 变量更名

  • 语句合并

  • 边界处理

算法实现

具体代码如下:

package algorithm.Day1;

public class SelectionSort {
    public static void main(String[] args) {
        int[] arr= {5,4,2,1,3};

        for (int i=0;i<arr.length;i++){
            int minPos=i;
            for (int j=i+1;j<arr.length;j++) { //这次for循环的作用就是找到最小的位置
                //在中间判断当前数和后面的数组哪个小
                if (arr[j] < arr[minPos]) {
                    minPos = j;//完成比较
                }
            }
            //退出循环,在第一次遍历进行交换
                System.out.println("minPos:"+minPos);
                int temp = arr[i];
                arr[i]=arr[minPos];
                arr[minPos]=temp;

        }
        for (int j : arr) {
            System.out.print(j + " ");
        }
    }
}

一步一步讲解:

在一开始,我们应该确认语言描述的算法过程怎么实现,例如当前我们可以想象出来,这个算法就是在第一位置和后面所有位置的最小值做交换,结束后接着向下遍历,我们可以抓到两个关键词

  • 第一位置和后面所有位置

  • 后面所有位置需要比出最小值的位置

  • 接着还要往下遍历

自此我们可以从第一点和第三点得知一次循环,而第二点很明显是另一次循环且每次都在第一次遍历的后面所以肯定是双重循环,所以先写出一个模板

并加上print输出看下是否符合我们的模板

public class SelectionSort {
    public static void main(String[] args) {
        int[] arr= {5,4,2,1,3};
        for (int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
            for (int j=i+1;j<arr.length;j++) {
                System.out.print(arr[j]);
            }
            System.out.println();
        }
    }
}

效果输出

5 4213
4 213
2 13
1 3
3 

接着阅读题目,我们其实每次只是要用当前数组的第一位 和 后面遍历的最小值 做交换

这里同样也可以提取出三个关键点

  • 需要锁定当前数组的第一位

  • 需要找出内嵌遍历的最小值

  • 需要实现第一位和最小值的交换

一个一个来写

锁定当前外围遍历的第一位

//第一位,只需要在第一次循环中,定义一个minPos去锁定第一位
        for (int i=0;i<arr.length;i++){
            for (int j=i+1;j<arr.length;j++) {
              
            }
            int temp = arr[i];
        }

内嵌遍历的最小值

//找出遍历的最小值也很容易,只需要在内嵌循环中每次判断j位置的值是否小于minPos位置的值
//应该不需要解释吧,就是遍历找出最小值
        for (int i=0;i<arr.length;i++){
            //假设最小值的位置是开头,能遍历全部
            //且能保证如果接下来的内嵌遍历没有比数组开头的更小,那么最小的就是当前位置
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
            //判断
                if(arr[j]<arr[minPos]){
                    minPos=j;
                }
            }
            int temp = arr[i];
        }

把第一位和内嵌遍历交换

int temp = arr[i];
arr[i]=arr[minPos];
arr[minPos]=temp;

至此就写完了这个选择算法,完整代码在本小条目的开头

代码的最后:优化和提问

优化:if语句用三元表达式简化

我们在内嵌循环中写的判断语句改为三元表达式

for (int j=i+1;j<arr.length;j++) {
    minPos = arr[j] < arr[minPos] ? j : minPos;
}
我想要看到每一步的改动

我们思考一下,每次的改动是在交换后所以肯定不在内嵌循环体内,而又需要看到说明又一次需要遍历数组,那么至此就很简单了

       for (int i=0;i<arr.length;i++){
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
                minPos=arr[j]<arr[minPos] ? j : minPos;
            }
            int temp = arr[i];
            arr[i]=arr[minPos];
            arr[minPos]=temp;
            
            System.out.println("第"+(i+1)+"遍查找排序");
            for (int k : arr){
                System.out.print(k+" ");
            }
            System.out.println("");
            
        }

效果:

第1遍查找排序
1 4 2 5 3 
第2遍查找排序
1 2 4 5 3 
第3遍查找排序
1 2 3 5 4 
第4遍查找排序
1 2 3 4 5 
第5遍查找排序
1 2 3 4 5 
优化:重复改成单独方法

这里比如打印数组是常用的,写在main方法外static 方法

    static void print(int[] arr){
        for(int i:arr){
            System.out.print(i+" ");
        }
        System.out.println("");
    }

在使用时

        for (int i=0;i<arr.length;i++){
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
                minPos=arr[j]<arr[minPos] ? j : minPos;
            }
            int temp = arr[i];
            arr[i]=arr[minPos];
            arr[minPos]=temp;
            System.out.println("第"+(i+1)+"遍查找排序");
            print(arr);//原先是
//            for (int k : arr){
//                System.out.print(k+" ");
//            }
//            System.out.println("");
            
        }
优化:剥离交换方法,让各个模块耦合度降低

其实就是我们尽可能把每个行为独立化

    static void swap(int[] arr,int a,int b){
        int temp = arr[a];
        arr[a]=arr[b];
        arr[b]=temp;
    }

使用

        for (int i=0;i<arr.length;i++){
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
                minPos=arr[j]<arr[minPos] ? j : minPos;
            }
            swap(arr, i, minPos);
            System.out.println("第"+(i+1)+"遍查找排序");
            print(arr);
        }

可以发现,我们现在main方法中语句已经大大缩短了

优化:还记得我们前面文字分析时的最后一步吗
....
第五遍,数组只剩一个5,说明比对 完毕,排序完毕

欸发现没有,事实上最后一次的外循环是毫无意义的,因为整个数组只剩他一位了,而他难道还需要进一次循环判断判断有没有比他小的吗,显然不用,所以我们这里把代码稍微修改一下

        for (int i=0;i<arr.length-1;i++){
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
                minPos=arr[j]<arr[minPos] ? j : minPos;
            }
            swap(arr, i, minPos);
            System.out.println("第"+(i+1)+"遍查找排序");
            print(arr);
        }

重点:Big O分析

用这题简单的内容来学习一下,O复杂度到底是如何计算的

我们把概述中的BigO计算定义复制过来

  • 不考虑这个算法的必须操作,例如循环、初值、初始化

  • 不考虑常数项,例如2n、5n,记O(n)

  • 不考虑低次项、例如n^2+n+5,记为O(n^2)

然后把算法代码拿出来,先对时间复杂度计算

        int[] arr= {5,4,2,1,3};
        for (int i=0;i<arr.length;i++){
            int minPos = i;
            for (int j=i+1;j<arr.length;j++) {
                minPos=arr[j]<arr[minPos] ? j : minPos;
            }
            swap(arr, i, minPos);
            System.out.println("第"+(i+1)+"遍查找排序");
            print(arr);
        }

int[] arr= {5,4,2,1,3};这条是代码初始化,不计算

关于print的也不计算

时间复杂度

计算外循环
for (int i=0;i<arr.length;i++){
    int minPos = i;
    //for内嵌
    swap(arr, i, minPos);
}

int i = 0 : O(1)&#x20;

i<arr.length : O(n)

i++ : O(n)

int minPos = i : O(n)

循环了n次每次都把minPos设为第一位,当然是O(n)

swap(arr, i, minPos) : O(n)

交换每次都执行,哪怕i就是和minPos是一个位置,仍然需要进入交换,执行n次

计算内循环
for(外循环){
  for (int j=i+1;j<arr.length;j++) {
      minPos=arr[j]<arr[minPos] ? j : minPos;
  }
}

int j=i+1 :O(n)

因为在外部i++一次,这里就得重新赋值一次,i遍历下来是n遍,这里就是O(n)

j<arr.length: O(n^2)

这里不同了在j去判断arr是否小于数组的长度其实是要执行n^2次的,因为外面的n次的其中每一次在这里都要在判断n次,所以自然是n*n=n^2的数量级

j++:同上O(n^2)

minPos=arr[j]<arr[minPos] ? j : minPos : O(n^2)

关键计算

上面三个,严格计算意义上来说,并不是循环了n^2次,因为每次j都是从i+1,开始向下遍历的。第一次j=i+1,i=0,所以j需要循环n-1次,而第二次i=1,j需要循环n-2次,……,直到最后一次i=n,j=1,所以j循环了多少次,这应该是个递减的等差数列,且差为1。而等差数列的和为n(n-1)/2=(n^2-n)/2

又因为计算规则中的忽略常数(1/2忽略),就剩n^2-n,再因为忽略低次项(n),所以最终答案O(n^2)

空间复杂度

在这里,简单说一下,因为在本算法中,并没有额外的空间开辟,例如开辟一段数组用以复制,或者开辟一段空间又拷贝回来这样去对空间开销,这样的我们认为是算法开销空间,记录空间复杂度。但对于例如for循环刚开始那个int i = 0;我们不认为是开辟空间,原因是从大小而言太小,再者当for循环每次的break都会释放掉,所以这种并不记录空间复杂度

所以这里查找排序的空间复杂度为O(1)

讲一下,选择排序为什么不用

效率低下

不管是最坏的情况要O(n^2),甚至最好的情况,意思就是已经排好序的一段数组也仍然要循环中循环每次做排序,居然都需要O(n^2)

不稳定

在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。

数字可能不那么凸显,但是想象一下,如果班级排座位按照姓名,正好你和另一个哥们(这只是我的口头禅和男女无关)名字相同,但是因为一个选择排序你和他的位置发生了本不该发生的错乱,例如原本坐在女神旁边的是你!!你应该会挺生气的,哈哈哈开个玩笑,熬夜写到现在,希望能帮助到您,如果觉得Jenis写的不错,请给我点个赞吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值