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

三大排序是基础算法的第一课,它指的是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)

相关推荐
Gpluso_od2 小时前
算法常用库函数——C++篇
数据结构·c++·算法
网络安全queen2 小时前
渗透测试面试问题
面试·职场和发展
bingw01142 小时前
25. 求满足条件的最长子串的长度
数据结构·算法
励志成为大佬的小杨2 小时前
关键字初级学习
c语言·开发语言·算法
机器懒得学习3 小时前
打造智能化恶意软件检测桌面系统:从数据分析到一键报告生成
人工智能·python·算法·数据挖掘
skaiuijing3 小时前
优化程序中的数据:从代数到向量解
线性代数·算法·性能优化·计算机科学
懿所思5 小时前
8.Java内置排序算法
java·算法·排序算法
sleP4o5 小时前
求各种排序算法的执行时间
算法·排序算法
码农老起5 小时前
选择排序:简单算法的实现与优化探索
数据结构·算法·排序算法
机器学习之心5 小时前
工程设计优化问题:改进海鸥算法(Matlab)
算法·matlab