其他分享
首页 > 其他分享> > 1.1.6.3 排序

1.1.6.3 排序

作者:互联网

目录

6.3 排序

内容导视:

我们知道了如何定义数组来存放元素,那就试试对数组中的元素排序吧。

排序:一组元素按指定顺序排列的过程。生活中,排序无处不在,我会依次介绍,默认顺序:从小到大排。

对于初学者,只需要了解冒泡排序与简单选择排序、直接插入排序,其它不用看,因为大部分人不可能一次学会;如果是天才当我没说。(怨念~_~)

排序算法分为:

在完成功能的情况下,我们需要考虑如何让程序运行时间更短,占用空间更小。(你也不希望软件卡半天没响应,又特别占用内存,对吧)

由此引申出来时间复杂度与空间复杂度,接下来一一介绍。

6.3.1 时间复杂度

描述了解决特定问题的步骤称为算法,如排序算法解决了元素无序的问题。只不过在计算机上,可以使用代码描述。

解决问题的方法有多种,不同算法的效率有高有低;最直观的方法就是编写不同的程序实现不同的算法,然后输入不同数据,进行编译,运行时对它们进行计时,用时最短的就是最好的。但这种事后测量的方法有很大的缺点与不确定性:

这时就需要我们自己在编写程序前,能够粗略估计代码的运行时间。(事前分析估算方法)

语句执行次数

也称语句频度、时间频度,记为 T(n),n 代表数据的个数。假设运行一行基础代码就算执行一次语句。代码花费的时间与语句执行次数成正比例,执行的语句越多,花费时间越多。

求 m1 方法内语句执行次数:

public static void m1(int[] arr) {
    System.out.println("你好");// 执行 1 次
    System.out.println("我饿了");// 执行 1 次
}

T(n) = 2;

此时数组中元素的个数 n 不会对语句执行次数产生影响,也就是说无论 arr.length 有多大,语句执行次数也就是 2。

求 for 内的语句执行次数:

public static void m2(int[] arr) {
    int n = arr.length;
    // 从 0 到 n - 1,一共循环 n 次,每次循环执行 2 个语句
    for (int i = 0; i < n; i++) {
        System.out.println("我不想排序");// 一共执行 n 次
        System.out.println("我想睡觉");// 一共执行 n 次
    }
}

T(n) = 2n;

求 for 内的语句执行次数:

public static void m3(int[] arr) {
    int n = arr.length;
    for (int i = 1; i < n; i *= 2) {
        System.out.println("没想到吧");
        System.out.println("还有乘等");
    }
}

for 循环执行了几次?

每次循环 i *= 2
第 1 次循环 i = 1 = 20;
第 2 次循环 i = 2 = 21;
第 3 次循环 i = 4 = 22;
...
第 x+1 次循环 i = 2x;

假设第 x+1 次循环时,i >= n,不满足条件,退出循环,得出不等式:
2x >= n
解出:x >= log2n

第 log2n + 1 次循环时,i 正好等于 n,退出了循环,这次不算,那么一共执行了 log2n 次循环,每次循环时执行 2 个语句。

T(n) = 2 * log2n;

若循环次数不是整数,向上取整,如 2.321928 记作 3。

对数:https://baike.baidu.com/item/对数/91326

对数公式:https://baike.baidu.com/item/对数公式/5557846

求方法中输出语句的执行次数:

public static void m4(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            System.out.println("i=" + i
                + ",j=" + j);
        }
    }
}

外层 for 一共循环 n 次,里层 for 一共循环 n 次,里面的输出语句被执行了 n * n 次。

T(n) = n2;

有了语句执行次数的函数,难道就能比较不同代码的运行时间吗?

比如 T(n) = 100n + 1 与 T(n) = n2 + 7,依然无法清晰看出谁的运行时间更短,此时需要看运行时间如何随 n 的增长而变化,选出增速最小的算法。

引出渐进时间复杂度(asymptotic time complexity)这个概念:

渐进时间复杂度

简称时间复杂度,定义如下:

代码执行语句次数记作 T(n);存在函数 f(n),当 n 趋近无穷大时,T(n) / f(n) 的比值为一个不等于 0 的常数,说明 f(n) 与 T(n) 的增长率相同,是同一量级;如 \(\lim\limits_{x \to \infty} \frac{2n + 10}{n}\) = 2。

记作 T(n) = O( f(n) ),称 O( f(n) ) 为代码的时间复杂度;时间复杂度描述了代码执行时间随数据量增长的变化趋势。

如何推出 f(n)?

次数 n a:2n + 10 a`:n b:2n2 + 5 b`:n2
1 12 1 7 1
100 210 100 20005 10000
10000 20010 10000 200000005 100000000
1000000 2000010 1000000 2000000000005 1000000000000

当 n 越来越大时,常数、低次数项已经变得不太重要,如同 a 再怎么努力,也追不上 b,因此被可以省略。

如 T(n) = 20,记作 T(n) = O(1);

T(n) = 2 * log2n + 98 = 2 * log2k * logkn + 98,只保留最高次数那项,且去掉系数,记作 T(n) = O(logkn);

设 k 为任意常数,由换底公式得:log2n = log2k * logkn

T(n) = 2n2 + 3n + 9,记作 T(n) = O(n2)。

image-20220405141508691

很明显随着 n 的增大,O(n2) 的时间复杂度(增长速度)远大于其它两个。

常见的时间复杂度

从小到大排(n 趋向无穷大时):

常数时间:O(1)

对数时间:O(logkn)

线性时间:O(n)

线性对数时间:O(n * logkn)

平方时间:O(n2)

立方时间:O(n3)

指数时间:O(2n)

阶乘时间:O(n!)

O(nn)

一般代码的时间复杂度为指数阶及以上,就不用考虑了,哪怕 n 只有 10000,结果也是天大的数字,除非你确定 n 十分的小,将来也不会增加。

计算 1 到 n 的数之和,来看看哪种算法时间效率高:

普通人想到的是一个个累加,用代码描述:

int n = 100;// 1
int sum = 0;// 1

for (int i = 1; i <= n; i++) {// n + 1
    sum += i;// n
}
System.out.println(sum);// 1

记录所有语句执行次数:2n + 4,时间复杂度:O(n);

前 n 次顺利通过 for 循环的判断条件,第 n + 1 次时判断失败,没有进入。

高斯想到的是首尾相加 * 个数 / 2:

int n = 100;// 1
int sum = (1 + n) * n / 2;// 1
System.out.println(sum);// 1

时间复杂度:O(1);

很明显高斯给出的算法效率更高;那么如果在某个程序中,需要解决求和问题,就可以选择此算法;而不是等到程序写好了,才掐着秒表,运行一下,一个个比哪个算法用的时间少。

我们可以看到,执行的代码就算有再多行,但如果与 n 的取值无关,通通记为 O(1),所以我只计算循环内某段语句的执行次数(受 n 影响),如 sum += i,其外的忽略不计,这样方便点。

算法优劣、数据的不同、数据量决定了程序的运行时间长短。

当数据量很少时,如 n = 2,计算机运行速度很快的,时间差异几乎是 0;使用事后计时的方法,是无法准确区分算法优劣的。

备注,高斯思路描述如下:

sum =     1 +    2  + ... + 100

sum = 100 +  99  + ... +     1

2sum = 101 + 101 + ... + 101 = 101 * 100

sum = 2sum / 2 = 101 * 50 = 5050

6.3.2 空间复杂度

代码耗费的存储空间,记作 S(n),同样也有 S(n) = O( f(n) ),O( f(n) ) 记为空间复杂度。

要求:记录一张 n * n 棋盘上的黑白棋子。

image-20220405165947443

代码 1:使用二维数组记录棋盘,空记为 0,黑记为 1,白记为 2;假设 n = 9:

int[][] arr = {
    {0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 2, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 1, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 1, 0, 0, 0, 0},
    {0, 2, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 1, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0},
};

数组所占空间:一个 int 类型是 4 个字节,S(n) = 4 * n2,空间复杂度为 O(n2);

想要获取第 row 行、第 column 列的信息:

public static void getState(int[][] arr, int row, int column) {
    // 注意下标从 0 开始,并不是 1,所以需要减去 1
    // 例:第 1 行第 1 列,即 arr[0][0]
    int data = arr[row - 1][column - 1];
    System.out.println("第 " + row + " 行、第 " + column + " 列的棋子为:" + printChess(data));
}
public static String printChess(int data) {
    String state = "?";
    switch(data) {
        case 0 :
            state = "空";
            break;
        case 1 :
            state = "黑";
            break;
        case 2 :
            state = "白";
            break;
        default:
    }
	return state;
}

时间复杂度为 O(1)。

代码 2:创建二维数组保存数据

第一行记录数组一共 i 行 j 列,有 count 个棋子(目前是 9、9、5);剩下 count 行记录这些棋子的位置(下标从 0 开始)与保存的值;数组长度为 1 + count。

int[][] arr = {
    {9, 9, 5},// 棋盘的总行、列数、棋子的个数
    {1, 3, 2},// 第一个棋子的行数 - 1、列数 - 1、保存的值
    {2, 5, 1},
    {4, 4, 1},
    {5, 1, 2},
    {6, 3, 1}
};

count 的范围处于 [0, n2] 之间;

如果棋盘上没有一个棋子,最好情况;只记录棋盘几行几列、棋子的个数为 0,数组所占空间:S(n) = 4 * 3 = 12,空间复杂度为 O(1);
如果棋盘上放满了棋子,最差情况;需要记录 n2 个棋子的位置,数组所占空间:S(n) = 4 * 3 * (n2 + 1) = 12n2 + 12,空间复杂度为 O(n2)。

获取第 row 行、第 column 列的信息,比如获取第 3 行第 6 列的信息,如果棋子存在,那么二维数组中肯定记录了 2、5、棋子保存的值。

我们需要从头遍历,找到开头为 2、5 的一维数组;如果找到,说明棋子存在,获取这个一维数组第 3 个元素(保存的值);

如果没有找到,棋子不存在,记为空。

public static void getState(int[][] arr, int row, int column) {
    int i = row - 1;
    int j = column - 1;
    
    int data = 0;
    for(int m = 1; m < arr.length; m++) {
        if (arr[m][0] == i && arr[m][1] == j) {
            data = arr[m][2];
            break;
        }
    }
    System.out.println("第 " + row + " 行、第 " + column + " 列的棋子为:" + printChess(data));
}

这种方式时间复杂度十分不稳定,最好情况 count = 0,不需要遍历,得到结果为空,时间复杂度为 O(1);最差情况 count = n2,且对应一维数组在最后一个,时间复杂度为 O(n2)。

当棋子较少时,使用代码 2 更节省空间;想要获取某行某列的信息,使用代码 1 时间效率更高。到底是用空间换时间,还是用时间换空间,凭自己取舍。

也可以组合:当用户下棋时,为了提升时间效率,用空间换时间,使用代码 1;退出棋盘时并不需要获取某行某列的信息,只需记录棋盘位置,使用代码 2 更节省空间。

6.3.3 冒泡排序

规定从小到大排序,那么小的要在前面,否则就需要交换。

Bubble Sort 算法思路:比较相邻元素,逆序就交换。

以 5,6,74,2,36,7 为例,每次比较中,我会用灰色标记大数。

第一轮:5,6,74,2,36,7

首先比较第 1 个与第 2 个元素的大小,5 < 6,不需要交换;
比较第 2 个与第 3 个,6 < 74,不需要交换;
比较第 3 个与第 4 个,74 > 2,需要交换,交换后:5,6,2,74,36,7
比较第 4 个与第 5 个,74 > 36,需要交换,交换后:5,6,2,36,74,7
比较第 5 个与第 6 个,74 > 7,需要交换,交换后:5,6,2,36,7,74

未命名文件

可以看到第一轮只是把最大的数归位了,继续找出第二大数吧。

第二轮:5,6,2,36,7,74

比较第 1 个与第 2 个,5 < 6,不需要交换;
比较第 2 个与第 3 个,6 > 2,需要交换,交换后:5,2,6,36,7,74
比较第 3 个与第 4 个,6 < 36,不需要交换;
比较第 4 个与第 5 个,36 > 7,需要交换,交换后:5,2,6,7,36,74

未命名文件 (1)

这一轮找出了第二大数 36。

有人这时可能会问,需不需要比较第 5 个与第 6 个?

要知道我们第一轮排序时,已经把最大的数放在了最后面,其它的数肯定不会大于最大数,所以没有必要进行比较了,剩下几轮同理,如下一轮只需比较到第 3 个与第 4 个,找出第三大数。

第三轮:5,2,6,7,36,74

比较第 1 个与第 2 个,5 > 2,需要交换,交换后:2,5,6,7,36,74
比较第 2 个与第 3 个,5 < 6,不需要交换;
比较第 3 个与第 4 个,6 < 7,不需要交换;

第三轮找出了第三大数 7。

未命名文件 (2)

第四轮:2,5,6,7,36,74

比较第 1 个与第 2 个,2 < 5,不需要交换;
比较第 2 个与第 3 个,5 < 6,不需要交换;

未命名文件 (3)

找出了第四大数 6。

第五轮:2,5,6,7,36,74

比较第 1 个与第 2 个,2 < 5,不需要交换;

未命名文件 (4)

找出了第五大数 5,接下来就只剩 2,没必要比了,到此结束。可以看出一个长度为 6 的数组,需要 5 轮才能排好序,轮数正是数组长度 - 1。

使用代码实现:

之前在整数类型中讲过两数如何交换值,这里借助第三个变量 temp。

注意下标从 0 开始,如第一轮,arr[0] 与 arr[1] 比较、arr[1] 与 arr[2] 比较、...、arr[4] 与 arr[5] 比较,一共 5 次,那么定义一个变量从 0 到 4 即可。

int[] arr = {5, 6, 74, 2, 36, 7};
int temp = 0;

// 第一轮
for (int j = 0; j < 5; j++) {
    // 如果逆序就交换
    if (arr[j] > arr[j + 1]) {// 比较 5 次
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
    }
}
// 第二轮
for (int j = 0; j < 4; j++) {
    if (arr[j] > arr[j + 1]) {// 比较 4 次
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
    }    
}
// 第三轮
for (int j = 0; j < 3; j++) {
    if (arr[j] > arr[j + 1]) {// 比较 3 次
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
    }    
}
// 第四轮
for (int j = 0; j < 2; j++) {
    if (arr[j] > arr[j + 1]) {// 比较 2 次
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
    }    
}
// 第五轮
for (int j = 0; j < 1; j++) {
    if (arr[j] > arr[j + 1]) {// 比较 1 次,即 arr[0] 与 arr[1]
        temp = arr[j];
        arr[j] = arr[j + 1];
        arr[j + 1] = temp;
    }    
}

// 遍历arr
for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + ",");
}

可是你发现了,每轮对应的 for 循环都几乎一模一样,只是判断条件从 j < 5,j < 4 ...到 j < 1,可以考虑使用外层循环将其包裹,定义一个 i 从 5 到 1,让 j < i,而这个 5 正是数组的长度 - 1。

for (int i = arr.length - 1; i > 0; i--) {
    for (int j = 0; j < i; j++) {
        if (arr[j] > arr[j + 1]) {
            temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }    
    } 
}

改进:

可以发现第三轮时就已经排好序了,再也没有交换元素,剩下几轮都是无用功。可以定义一个 boolean 变量,默认为 true。

每次循环开始赋值 false;如果交换了,说明有逆序,需要继续循环,改为 true;如果没有交换,证明已经排好序了,需要退出循环。

boolean flag = true;
// flag 为 false 时,退出循环
for (int i = arr.length - 1; i > 0 && flag; i--) {
    flag = false;
    for (int j = 0; j < i; j++) {
        // 逆序就交换
        if (arr[j] > arr[j + 1]) {
            temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
            // 还要继续循环
            flag = true;
        }    
    } 
}

改进后,设参与排序的数组长度为 n;

分析排序算法时,传统方式是衡量元素之间的比较和交换的次数。

最坏情况:数组为逆序,每次比较都需要交换,比较次数为:\(\sum\limits_{i=1}^{n-1}i\) = 1 + 2 + ... + (n - 1) = n * (n - 1) / 2,时间复杂度为 O(n2);

最好情况:数组为顺序,当 i = n - 1 时,进入外循环,里循环 j 从 0 到 n - 2,一共 n - 1 次比较,没有进入 if 语句内,flag = false,下一次外循环直接退出。时间复杂度为 O(n)。

空间复杂度为 O(1)。(与 n 无关,统统记为 O(1))

输入的数据如数组 arr,只取决于问题本身,与算法无关,不计入空间复杂度,只需计入算法实现所用的辅助空间,如 temp。

包括输出数组的内容(查看数组是否已经排序好了),都不计入。

6.3.4 简单选择排序

Simple Selection Sort 算法思路:找出小数,放在前面;第 i 轮找出第 i 小数,放在第 i 个位置。

小数:两数比较中,更小的数,我说的并不是 1.241 这种小数。

以 {5,2,6,252,1} 为例;小数使用灰色标记。

第一轮:5,2,6,252,1

比较第 1 个与第 2 个元素大小,5 > 2,小数为第 2 个;
比较第 2 个与第 3 个,2 < 6,小数为第 2 个;
比较第 2 个与第 4 个,2 < 252,小数为第 2 个;
比较第 2 个与第 5 个,2 > 1,小数为第 5 个;结束。

最小数在第 5 个位置上,应该放在第 1 个位置上。
交换第 5 个与第 1 个元素位置后:1,2,6,252,5

未命名文件 (5)

第一轮,找出了最小数并放在了合适的位置,那么接着找第二小数。

第二轮:1,2,6,252,5

比较第 2 个与第 3 个,2 < 6,小数为第 2 个;
比较第 2 个与第 4 个,2 < 252,小数为第 2 个;
比较第 2 个与第 5 个,2 < 5,小数为第 2 个;结束。

第二小数在第 2 个位置上,正好合适,不需要交换:1,2,6,252,5

未命名文件 (6)

不需要比较第 1 个与第 2 个,因为第 1 个是最小数,但我们找的是第二小数,下面同理。

第三轮:1,2,6,252,5

比较第 3 个与第 4 个,6 < 252,小数为第 3 个;
比较第 3 个与第 5 个,6 > 5,小数为第 5 个;结束。

第三小数在第 5 个位置上,应该放在第 3 个位置上。
交换第 5 个与第 3 个元素位置:1,2,5,252,6

未命名文件 (7)

第四轮:1,2,5,252,6

比较第 4 个与第 5 个,252 > 6,小数为第 5 个;结束。

第四小数在第 5 个位置上,应该放在第 4 个位置上。
交换第 5 个与第 4 个元素位置:1,2,5,6,252

未命名文件 (8)

5 个数,已经确定了 4 位,剩下一个数自动归位;轮数为数组长度 - 1。

使用代码实现:

我们可以看到每轮都是拿小数与其它数进行比较,所以需要定义变量如 min,记录小数的下标;设数组长度为 n。

如第 1 轮假设最小数下标也就是 min 为 0,然后让 arr[min] 与剩下的数,即与下标为 1、2、3 ... n - 1 的元素比较;期间,若发现更小数,将 min 替换为此数的下标,然后拿 arr[min] 继续与其它数比较。

第 2 轮设 min 为 1;

第 3 轮设 min 为 2;

...

第 n - 1 轮设 min 为 n - 2。

int[] arr = {5, 2, 6, 252, 1};
int n = arr.length;// n = 5
int temp = 0;

// 第一轮:找最小数,假设最小数下标为 0
int min = 0;
// 下标为 0 的数,与下标为 1、2、3、4 的元素进行比较
for (int j = 1; j < n; j++) {

    // 比较过程中,发现有更小的,将 min 替换为此数下标,保证 min 一直记录的是最小数的下标
    if (arr[j] < arr[min]) {// 比较 4 次
        min = j;
    }
}
// 如果 min 不等于 0,证明最小数的下标不是 0,需要将最小数与下标为 0 的元素交换位置
if (min != 0) {
    temp = arr[0];
    arr[0] = arr[min];
    arr[min] = temp;
}

// 第二轮:找出第二小数,假设第二小数下标为 1(因为下标为 0 的元素已经被确定为最小数,无需参与比较)
min = 1;
// 下标为 1 的数,与下标为 2、3、4 的元素进行比较
for (int j = 2; j < n; j++) {
    if (arr[j] < arr[min]) {// 比较 3 次
        min = j;
    }
}
if (min != 1) {
    temp = arr[1];
    arr[1] = arr[min];
    arr[min] = temp;
}

// 第三轮
min = 2;
for (int j = 3; j < n; j++) {
    if (arr[j] < arr[min]) {// 比较 2 次
        min = j;
    }
}
if (min != 2) {
    temp = arr[2];
    arr[2] = arr[min];
    arr[min] = temp;
}

// 第四轮
min = 3;
for (int j = 4; j < n; j++) {
    if (arr[j] < arr[min]) {// 比较 1 次
        min = j;
    }
}
if (min != 3) {
    temp = arr[3];
    arr[3] = arr[min];
    arr[min] = temp;
}
// 利用 Arrays 类的 toString 方法输出 arr 的内容
System.out.println(Arrays.toString(arr));

每轮对应的 for 循环几乎一致,观察每轮的变化之处:min = 0、1、2 ... n - 2,j = 1、2、3 ... n - 1。

考虑外层 for,定义 i 从 0 到 n - 2,让 min = i,j = i + 1。

for (int i = 0; i < n - 1; i++) {
    int min = i;
    for (int j = i + 1; j < n; j++) {
        if (arr[j] < arr[min]) {
            min = j;
        }
    }
    if (min != i) {
        temp = arr[i];
        arr[i] = arr[min];
        arr[min] = temp;
    }
}

简单选择排序,比较时没有交换元素,分开讨论:

比较次数是固定的,一共为 1 + 2 + ... + (n - 1) = n * (n - 1) / 2。

交换次数,最好情况不用交换,次数为 0;最坏情况,每次位置都不对,需要交换,次数为 n - 1。

综合,时间复杂度为 O(n2),空间复杂度为 O(1)。

6.3.5 直接插入排序

Insert Sort 算法思路:新元素与前面已排序的元素进行比较,并将它插入到正确的位置。

以 {25, 6, 50, 26} 一共四个元素为例,灰色元素的集合代表有序序列,当所有元素都为灰色时代表已经排好序了。

第一轮:25,6,50,26

arr[1] 与 arr[0] 比较,6 < 25,逆序,需要交换,交换后:6,25,50,26

未命名文件

那么前面两个元素已经排好序了。

第二轮:6,25,50,26

arr[2] 与 arr[1] 比较,50 > 25,顺序,不需要交换

未命名文件 (1)

arr[2] 不需要跟 arr[0] 比较,因为通过第一轮排序,确定了 arr[1] > arr[0],如果 arr[2] > arr[1],必然有 arr[2] > arr[0],大小已经确定,无需比较,下面同理。(如果后面的元素大于前面,不需要交换,说明顺序是对的,无需比较,直接终止操作)

前面三个元素排好序了。

第三轮:6,25,50,26

arr[3] 与 arr[2] 比较,26 < 50,交换后:6,25,26,50
arr[2] 与 arr[1] 比较,26 > 25,不需要交换

未命名文件 (2)

前面四个元素排好序了;到此为止,一共 arr.length - 1 轮。

代码描述:

只有后面元素小于前面元素才需要交换,否则就终止循环;在循环期间,需要保证后面元素下标大于 0(避免下标越界)。

int[] arr = {25, 46, 3, 8};
int temp = 0;

// 第一轮
for (int j = 1; j > 0 && arr[j] < arr[j - 1]; j--) {
    temp = arr[j];
    arr[j] = arr[j - 1];
    arr[j - 1] = temp;
}

// 第二轮,最差情况比较 2 次。
for (int j = 2; j > 0 && arr[j] < arr[j - 1]; j--) {
    temp = arr[j];
    arr[j] = arr[j - 1];
    arr[j - 1] = temp;
}

// 第三轮
/*
最差情况,比较 3 次;即 arr[3]、arr[2] 比较,arr[2]、arr[1] 比较,
arr[1]、arr[0] 比较。
*/
for (int j = 3; j > 0 && arr[j] < arr[j - 1]; j--) {
    temp = arr[j];
    arr[j] = arr[j - 1];
    arr[j - 1] = temp;
}

j 从 1 到 arr.length - 1,考虑定义外层 for 循环,i 从 1 到 arr.length - 1;让 j = i。

结合:

for (int i = 1; i < arr.length; i++) {
    for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
        temp = arr[j];
        arr[j] = arr[j - 1];
        arr[j - 1] = temp;
    }
}

最差情况:参与排序的数组为逆序,每次的新元素都比前面所有的元素小,需要移动到数组最前面,设数组长度为 n,一共 n - 1 轮排序,比较次数为 1 + 2 + ... + (n - 1) = \(\frac{n(n-1)}{2}\);时间复杂度为 O(n2)。

最好情况:参与排序的数组为顺序,每次新元素都大于前面的元素,刚进入 for 循环,比较结果为 false,直接退出循环;一共 n - 1 比较,时间复杂度为 O(n)。

空间复杂度为 O(1)。

变体:

将新元素与前面的元素比较,获取新元素应该插入的位置,并把已有元素往后移,给新元素腾出一个空间。

以 {6,3,5,7,1} 为例:

第一轮:6,3,5,7,1

记录新元素的值:insertVal = arr[1] = 3
insertVal 与 arr[0] 比较,3 < 6,记录新元素应该插入的位置:insertIndex = 0
将 arr[0] 后移腾出空间:6,6,5,7,1
结束
根据记录的下标插入新元素:arr[insertIndex] = arr[0] = insertVal = 3
插入后:3,6,5,7,1

未命名文件 (3)

第二轮:3,6,5,7,1

记录 insertVal = arr[2] = 5
insertVal 与 arr[1] 比较,5 < 6,记录下标 1,将 arr[1] 后移:3,6,6,7,1
insertVal 与 arr[0] 比较,5 > 3,结束
插入新元素,arr[1] = insertVal = 5
3,5,6,7,1

未命名文件 (4)

第三轮:3,5,6,7,1

记录 insertVal = arr[3] = 7
insertVal 与 arr[2] 比较,7 > 6,结束

未命名文件 (5)

第四轮:3,5,6,7,1

记录 insertVal = arr[4] = 1
insertVal 与 arr[3] 比较,1 < 7,记录下标 3,将 arr[3] 往后移:3,5,6,7,7
insertVal 与 arr[2] 比较,1 < 6,记录下标 2,将 arr[2] 往后移:3,5,6,6,7
insertVal 与 arr[1] 比较,1 < 5,记录下标 1,将 arr[1] 往后移:3,5,5,6,7
insertVal 与 arr[0] 比较,1 < 3,记录下标 0,将 arr[0] 往后移:3,3,5,6,7
结束
arr[0] = insertVal = 1
1,3,5,6,7

未命名文件 (6)

int[] arr = {6, 3, 5, 7, 1};
for (int i = 1; i < arr.length; i++) {
    // 记录下标
    int insertIndex = i;
    // 新元素的值
    int insertVal = arr[i];
    
    // 拿新元素与之前的元素比较
    for (int j = i; j > 0 && insertVal < arr[j - 1]; j--) {
        // 记录新元素应该插入的下标
        insertIndex = j - 1;
        // 将 arr[j - 1] 后移,即让 arr[j] 保存 arr[j - 1] 的值
        arr[j] = arr[j - 1];
    }
    
    if (i != insertIndex) {
        arr[insertIndex] = insertVal;
    }
}

代码的第 16 行说明:insertIndex 默认为 i(新元素的下标),经过循环后 insertIndex 如果还是 i,说明新元素的位置正合适,不用变动。

这种实现比第一种方式,由于交换次数的减少,稍微快一点点。

标签:arr,1.1,temp,min,int,复杂度,6.3,排序,比较
来源: https://www.cnblogs.com/cqhh/p/16147485.html