糟糕!我被三大排序包围了!

三大排序是基础算法的第一课,它指的是10种排序算法中的选择、插入和冒泡排序,他们是算法学习中的基石。在面试中,面试官基本不会让候选人直接写这三种算法,但如果你是一个初学者,要想直接理解算法思想并写出正确的代码还是有些难度的。此外,我们也可以借助这三种算法充分理解如何估算代码的时间复杂度。话不多说,让我们开始吧!

选择排序

想一下我们在打牌时,抓完牌是如何将手中的纸牌排好序的呢?是不是先找出手中最小的牌放到最边上,然后再找出次小的牌放到刚才牌的旁边,循环往复直到手中的牌排好序,这其实就是典型的选择排序。用一句话概括选择排序的算法思想:在要排序的一组数中,选出最小的数与第一个数交换,然后再在剩下的数中选出最小的数与第二个数交换,直到第n-1个数与第n个数比较为止。以数组[3, 2, 5, 1, 4]为例,设置一个指针iarr[0...i-1]部分就是我们认为的数组的有序部分。arr[i...n-1]部分是无序部分。初始状态i指向数组的0位置,有序部分为空,再设置一个指针j在数组的arr[i...n-1]位置寻找无序部分的最小值,然后将找到的最小值与arr[i]交换。在这个例子中,3位置的1是这一轮找到的最小值,所以将3位置的10位置的3交换位置,第一轮选择排序过后数组就变成了[1, 2, 5, 3, 4]

接下来,i指针指向1位置的2j指针查找1位置到4位置中元素的最小值,发现最小值就是i指针指向的1位置的2,自己和自己交换位置,数组不变。

i指针继续右移,指向2位置的5j指针在2, 3, 4三个位置查找最小值,发现是3位置的3,所以将它和2位置的5交换位置,数组变成[1, 2, 3, 5, 4]

i指针再右移,指向3位置的5,本轮数组中无序部分的最小值是4位置4,所以将这两个元素做交换,数组变成[1, 2, 3, 4, 5]

i指针继续右移,此时i已经来到了数组中的最后一个位置,意味着数组中的其他元素都已经来到了正确的位置,那么最后一个元素肯定也在正确的位置了,整个排序流程到这里就可以结束了。

选择排序的代码如下

java 复制代码
public static void selectionSort(int[] nums) {
    if (nums == null || nums.length <= 1) {
        return;
    }

    int n = nums.length;
    for (int i=0; i<n-1; i++) {
        int minIndex = i;
        for (int j=i+1; j<n; j++) {
            if (nums[j] < nums[minIndex]) {
                minIndex = j;
            }
        }
        swap(nums, i, minIndex);
    }
}

/**
 * 交换nums数组中的nums[i]和nums[j]
 */
public static void swap(int[] nums, int i, int j) {
    if (i == j) {
        return;
    }

    nums[i] = nums[i] ^ nums[j];
    nums[j] = nums[i] ^ nums[j];
    nums[i] = nums[i] ^ nums[j];
}

分析一下代码的时间复杂度,代码的主体是两层for循环,外层循环执行了n-1次,当i = 0时,内层循环j1n-1,执行了n-1次;当i = 1时,内层循环j2n-1,执行了n-2次;以此类推,当i = n - 2时,内层循环执行了1次。所以整段代码一共执行的次数就是(n-1) + (n-2) + (n-3) + ... + 1,根据等差数列的求和公式,上面这个公式的结果就应该是
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ ( n − 1 ) + 1 ] × ( n − 1 ) 2 = 1 2 n 2 − 1 2 n \frac {[(n-1) + 1] \times (n-1)}{2} = \frac 1 2 n^2 - \frac 1 2 n </math>2[(n−1)+1]×(n−1)=21n2−21n

时间复杂度一般是用于评估当数据量趋近于无穷大时代码执行次数的数量级,所以在计算的时候一般取公式中的最高阶,并忽略掉常数项,所以选择排序的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)

插入排序

明白了选择排序的流程,接下来再来看一下插入排序。用一句话描述插入排序的算法流程:先将数组的第0个元素看成是一个有序的子数组,然后从第1个元素开始将元素逐个插入有序部分,直到整个数组全部有序为止。还是以上面的数组为例,[3, 2, 5, 1, 4],首先将数组的第0个元素看成是有序部分,设置一个指针i指向数组的第1个位置,意思是现在想要将第1个元素2插入到前面数组的有序部分。

1位置的2和前面的3做比较,发现2 < 3,所以将两个元素交换位置,然后2就来到了数组的第0个位置,前面没有元素了,所以本次循环到此结束,数组的有序部分变成了nums[0...1]

接下来将i指针右移一位,指向2位置的5,意味着现在要将2位置的5插入到前面的有序部分中。5和前面的3做比较,发现5 > 3,这次循环到这里就可以直接结束了,数组的有序部分变成了nums[0...2]

i指针继续右移,指向3位置的1,将这个元素插入到数组的有序部分。所以将1和左侧的5做比较,发现1 < 5,所以交换位置,数组变成[2, 3, 1, 5, 4];继续和前面的位置比较,发现1 < 3,所以继续交换位置,数组变成[2, 1, 3, 5, 4],继续比较,1 < 2,交换位置数组变成[1, 2, 3, 5, 4],此时这个1已经来到了数组的0位置,左侧没有元素了,所以循环结束。

i指针继续右移,指向4位置的44小于左侧的5,所以交换位置,再和左侧的3比较,发现4 > 3,本次循环直接结束。此时i已经指向元素中最后一个位置了,整个数组已经有序,流程结束。

插入排序的代码贴在下面

java 复制代码
public static void insertionSort(int[] nums) {
    if (nums == null || nums.length <= 1) {
        return;
    }

    // nums[0]看成是数组的有序部分,将nums[1...n-1]向数组的有序部分插入
    int n = nums.length;
    for (int i=1; i<n; i++) {
        // 把nums[i]插入有序部分
        int j = i;
        while (j > 0 && nums[j] < nums[j-1]) {
            swap(nums, j, j-1);
            j--;
        }
    }
}

public static void swap(int[] nums, int i, int j) {
    if (i == j) {
        return;
    }

    nums[i] = nums[i] ^ nums[j];
    nums[j] = nums[i] ^ nums[j];
    nums[i] = nums[i] ^ nums[j];
}

看一下插入排序的时间复杂度。这段代码的主体也是这个双层循环,外层的for循环执行n-1次,里面的while循环执行了多少次呢?这个取决于你的数据情况。最好的情况是数组原本就是有序的,每一次for循环中nums[i]都是直接大于nums[i-1]的,这意味着每次while循环都判断循环条件不通过就直接结束了,这种情况下算法的时间复杂度是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n);

最差的情况下,数组原本是倒序的,每一次for循环中,nums[i]都要一步一步地移动到数组的最前面才能结束,while循环要执行i次。也就是说当i = 1时,while循环执行1次;i = 2时,while循环执行2次;i = n - 1时,while循环执行n - 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)

冒泡排序

最后来看一下冒泡排序。冒泡排序是通过数组中无序部分相邻元素的比较和位置的交换,使得最大的记录像气泡一般逐渐向上"漂浮",一直漂浮到无序部分的最后位置;还是以数组[3, 2, 5, 1, 4]为例,首先数组没有有序部分,全都是无序的,0位置的31位置的2比较,3 > 2,所以交换位置数组变成[2, 3, 5, 1, 4];接下来1位置的32位置的5做比较,不交换位置;2位置的53位置的1做比较,交换位置,数组变成[2, 3, 1, 5, 4]3位置的54位置的4做比较,交换位置,数组变成[2, 3, 1, 4, 5],本次循环结束,数组的有序部分是nums[4],无序部分是nums[0...3]

接下来,在数组的无序部分nums[0...3]继续冒泡,0位置的21位置的3比较,不交换位置;1位置的32位置的1做比较,交换位置,数组变成[2, 1, 3, 4, 5]2位置的33位置的4做比较,不交换位置,循环到此结束,数组的有序部分是nums[3...4],无序部分是nums[0...2]

接下来在数组的无序部分nums[0...2]继续冒泡,0位置的21位置的1做比较,交换位置,数组变成[1, 2, 3, 4, 5]1位置的22位置的3做比较,不交换位置,循环结束。

接下来数组的无序部分只剩下nums[0...1]了,对这两个元素做比较,不交换位置,循环结束,由于数组后n-1个元素都已经来到正确的位置了,所以第一个元素位置也是正确的,整个排序流程就结束了。

冒泡排序的代码如下

java 复制代码
public static void bubbleSort(int[] nums) {
    if (nums == null || nums.length <= 1) {
        return;
    }

    int n = nums.length;

    boolean changed = false;

    for (int i=n-1; i>0; i--) {
        // 要把最大的数冒到i位置
        for (int j=0; j<i; j++) {
            if (nums[j] > nums[j+1]) {
                changed = true;
                swap(nums, j, j+1);
            }
        }

        if (!changed) {
            // 如果一次循环中没有元素的交换,说明数组已经有序,可以直接跳出循环
            break;
        }
    }

}

public static void swap(int[] nums, int i, int j) {
    if (i == j) {
        return;
    }

    nums[i] = nums[i] ^ nums[j];
    nums[j] = nums[i] ^ nums[j];
    nums[i] = nums[i] ^ nums[j];
}

上面的代码中,如果数组原本就是有序,那么会在首次执行for循环时直接break跳出循环,这样算法的时间复杂度就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n);最差情况下,外层for循环执行了n-1次,i = n - 1时,内层for执行了n - 1次;i = n - 2时,内层for执行了n - 2次,以此类推,把这些执行次数加和之后仍然是一个等差数列的求和,所以算法的最差时间复杂度仍然是 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2)

相关推荐
自由的dream39 分钟前
0-1背包问题
算法
2401_857297911 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
良月澪二2 小时前
CSP-S 2021 T1廊桥分配
算法·图论
wangyue43 小时前
c# 线性回归和多项式拟合
算法
&梧桐树夏3 小时前
【算法系列-链表】删除链表的倒数第N个结点
数据结构·算法·链表
QuantumStack3 小时前
【C++ 真题】B2037 奇偶数判断
数据结构·c++·算法
今天好像不上班3 小时前
软件验证与确认实验二-单元测试
测试工具·算法
wclass-zhengge4 小时前
数据结构篇(绪论)
java·数据结构·算法
何事驚慌4 小时前
2024/10/5 数据结构打卡
java·数据结构·算法
结衣结衣.4 小时前
C++ 类和对象的初步介绍
java·开发语言·数据结构·c++·笔记·学习·算法