看到同事偷偷学习选择排序算法

排序算法作为实用且面试常考的算法,虽然各大编程语言都有其相关的API实现。但是通过学习各种排序算法来训练编码能力,锻炼算法思维,入门算法何乐而不为呢?

上回我们一起学习了「冒泡排序」。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端(升序或降序排列),就如同碳酸饮料二氧化碳的气泡最终会上浮到顶端一样,故名"冒泡排序"。

同时我们知道了定义一个算法优劣的两个指标:

  1. 时间复杂度
  2. 空间复杂度

同时又引入了一个排序算法的额外指标:「稳定性」:对于存在相等元素 的序列,排序过后,原相等元素的排序结果中的相对位置相比原输入序列不变。

冒泡排序的各项指标是:

  1. 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( N 2 ) O(N^2) </math>O(N2)
  2. 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
  3. 稳定

接下来我今天就来学习以下另一个较为简单的排序算法「选择排序」。

下面是各大排序算法的时间复杂度和空间复杂度,以及稳定性

排序算法 平均时间 最好时间 最坏时间 空间 稳定性*
冒泡 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 稳定
选择 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 不稳定
插入 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 稳定
希尔 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) ~ <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 不稳定
希尔 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g 3 n ) O(nlog_3n) </math>O(nlog3n) ~ <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 2 ) O(n^\frac{3}{2}) </math>O(n23) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g 3 n ) O(nlog_3n) </math>O(nlog3n) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 3 2 ) O(n^\frac{3}{2}) </math>O(n23) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 不稳定
归并 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 稳定
快速 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( l o g n ) O(logn) </math>O(logn) 不稳定
<math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1) 不稳定
计数 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + k ) O(n + k) </math>O(n+k) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + k ) O(n + k) </math>O(n+k) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + k ) O(n + k) </math>O(n+k) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + k ) O(n + k) </math>O(n+k) 稳定
基数 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( d ( n + k ) ) O(d(n + k)) </math>O(d(n+k)) <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 为常数 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( d ( n + k ) ) O(d(n + k)) </math>O(d(n+k)) <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 为常数 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( d ( n + k ) ) O(d(n + k)) </math>O(d(n+k)) <math xmlns="http://www.w3.org/1998/Math/MathML"> k k </math>k 为常数 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n + k ) O(n + k) </math>O(n+k) 稳定
<math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2) or <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n l o g n ) O(nlogn) </math>O(nlogn) <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n) 稳定

算法描述

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。------ 百度百科

对于一个要排序的数组,设置一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> m i n I d x minIdx </math>minIdx记录最小数字下标。

  1. 先假设第一个数字最小,即minIdx=0
  2. 后将minIdx与后续数字逐一比较,当遇到更小的数字时,将下标赋值给minIdx
  3. 第一轮比较将找出此时数组中最小的数字,然后将swap(arr,0,minIdx)minIdx下标的元素与下标为0处的元素进行交换(排序)。
  4. 然后开始第二轮比较,令minIdx=1重复上述过程。每一轮的比较将使得当前未排序数字中的最小值排序,未排序数字总数将会减1。所以arr.length-1轮结束排序完成。

稳定性

这里的稳定性是不稳定的。当数组为 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 5 1 , 5 2 , 1 ] [5_1,5_2,1] </math>[51,52,1]时,按照上述描述进行升序排序操作。

  1. 第一轮找到的最小值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1,会与数组第一个元素进行交换 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 5 2 , 5 1 ] [1,5_2,5_1] </math>[1,52,51]
  2. 第二轮找到的最小值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 2 5_2 </math>52,会与数组第二个元素进行交换 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 5 2 , 5 1 ] [1,5_2,5_1] </math>[1,52,51]
  3. 第三轮找到的最小值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 3 5_3 </math>53,会与数组第三个元素进行交换 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 , 5 2 , 5 1 ] [1,5_2,5_1] </math>[1,52,51]

此时是不符合稳定性定义的。所以是不保证其稳定性的。

代码实现

java 复制代码
    public static void selectSort(int[] arr) {
        int n = arr.length;
        // n - 1 轮次执行,当前 n - 1 个元素排好后,最后一个元素无需执行,故 i < arr.length - 1
        for (int i = 0; i < n - 1; ++i) {
            //最小值下标
            int minIndex = i;
            //j=i+1 可以减少一次j=i的循环判断
            for (int j = i + 1; j < n; ++j) {
                //当前元素小于最小值元素
                if (arr[j] < arr[minIndex]) minIndex = j;
            }
            // 若本轮第一个数字不是最小值,则交换位置
            if (minIndex != i) swap(arr, i, minIndex);
        }
    }
    
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
    

代码优化

我们可以发现,我们每一次遍历寻找找到的只是一个最小值,那么其实我们也是可以找到最大值的。当我们进行升序排序的时候同时定义两个值minIdxmaxIdx,双元选择优化。最小值从0开始往右放,最大值从arr.length-1开始左放。这样可以一轮遍历确定两个元素的位置,遍历次数减少了一半,但是每轮的操作变多,因此该优化只能少量提升选择排序的速度。复杂度介于单元选择排序及其一半之间,只有系数的区别。

java 复制代码
    public static void selectSortOpt(int[] arr) {
        int n = arr.length;
        for (int i = 0; i < n - 1 - i; ++i) {
            int minIndex = i, maxIndex = i;
            //找到本轮中最大、最小值下标
            for (int j = i + 1; j < n - i; ++j) {
                if (arr[j] < arr[minIndex]) minIndex = j;
                if (arr[j] > arr[maxIndex]) maxIndex = j;
            }
            //本轮最大值已经等于最小值,说明已经有序
            if (minIndex == maxIndex) break;
            // 若本轮第一个数字不是最小值,则交换位置(与本轮第一个数字交换位置)
            if (minIndex != i) Swap.swap(arr, i, minIndex);
            //在交换i和minIdx时,有可能出现i即maxIdx的情况,因为上面已经将i和minIndex交换 所以此时需要修改maxIdx为minIdx
            if (maxIndex == i) maxIndex = minIndex;
            // 若本轮最后一个数字不是最小值,则交换位置(与本轮最后一个数字交换位置)
            if (maxIndex != n - 1 - i) swap(arr, n - 1 - i, maxIndex);
        }
    }
    
    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

时间空间复杂度

时间复杂度:两层 <math xmlns="http://www.w3.org/1998/Math/MathML"> f o r for </math>for 循环,第1轮比较 <math xmlns="http://www.w3.org/1998/Math/MathML"> n − 1 n - 1 </math>n−1 次 ( n = arr.length ) ,最后一轮比较 1 次。总比较次数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n ∗ ( n − 1 ) / 2 n*(n - 1) / 2 </math>n∗(n−1)/2 次,时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)。 双元选择优化版本也是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)。

同冒泡排序和选择排序的比较次数均为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2),但选择排序的交换次数是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n),而冒泡排序的平均交换次数仍然是二次的。

空间复杂度:算法中只有常数项变量, <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)。

选择排序总体而言还是比较简单的,复杂度和冒泡排序也是一样的。不过它们之间的区别,在于是否能够保证「稳定性」上。

  1. 冒泡排序是能够保证稳定性的。
  2. 选择排序是不能保证稳定性的。

所以在会这两种算法的前提上,如果要选择一个算法进行排序,那么唯一需要考虑的是需求是否需要保证其「稳定性」上了~

认为本文写的不错的话,请来一个点赞,收藏,关注吧~~~~

相关推荐
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
好奇龙猫2 小时前
【学习AI-相关路程-mnist手写数字分类-win-硬件:windows-自我学习AI-实验步骤-全连接神经网络(BPnetwork)-操作流程(3) 】
人工智能·算法
sp_fyf_20243 小时前
计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-11-01
人工智能·深度学习·神经网络·算法·机器学习·语言模型·数据挖掘
香菜大丸3 小时前
链表的归并排序
数据结构·算法·链表
jrrz08283 小时前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
oliveira-time3 小时前
golang学习2
算法
customer083 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源