当前位置 博文首页 > 吴斌的博客:常用排序算法

    吴斌的博客:常用排序算法

    作者:[db:作者] 时间:2021-06-28 09:17

    一、概述
    ????????? 我们在平时的开发中,排序算法可以说是最常用的算法之一,不同的排序算法,有着不一样的性能消耗,虽说前端开发对算法的要求没有那么高,但是对于一些常见的算法,我们还是要掌握的,它属于一个开发者的基本功,今天,我们就来总结一下常见的排序算法,请看下面这张表:

    下面排序都以由小到大排序为目的

    二、冒泡排序
    1、基本思想:两个数比较大小,较大的往下沉,较小的冒上来

    2、过程

    比较两个相邻的数,如果后面的数字大于前面的数字,则将两个数据交换,相当于把较小的数据往前推
    从后向前比较两个相邻的数据,重复上面过程,最终最小的数据被交换到了起始位置
    重复上面过程,依次将第2、3…n-2、n-1个位置设置当前最小数据
    分析上面的过程,就好像一个冒泡的过程,较小的数据不断往上冒,较大的数据不断往下沉,这也是冒泡排序名称的由来

    3、代码实现

    // 两个数比较大小,较大的往下沉,较小的冒上来
    public void bubbleSort(int[] array) {
        if (array != null && array.length > 1) { // 如果数组为空或者小于两个数据,则不需要排序
            int temp; // 临时变量定义在循环外面,较少重复定义消耗
            for (int i=0; i<array.length-1; i++) { // 遍历处理数组的第1个到倒数第二个位置的要填充的数据
                for (int j=array.length-1; j>i; j--) { // 从数组尾部开始,将较小的数字不断往前推,较大的数字往后排,直到将最小的数字推到第i个位置
                    if (array[j] < array[j-1]) { // 如果后面的数字小于前面的数字,则将后面的数字和前面的数字交换,相当于把较小的数字往前推
                        temp = array[j];
                        array[j] = array[j-1];
                        array[j-1] = temp;
                    }
                }
            }
        }
    }
    

    4、平均时间复杂度:O(n2)

    5、优化

    冒泡排序会一次将当前最小的数据排在第1、2…n-2、n-1个位置上,上面的代码中,在排某个位置的数据时,即使从这个位置到最后一个位置数据已经排序好了,还是会继续进行循环直到第n-1个位置。这就造成了不必要的浪费,我们知道,在上面的排序过程中,只要前面的数据大于后面的数据,就会进行数据交换,如果某次排序没有进行数据交换,则说明后面的数据都已经排序好了,不需要继续循环处理排序了。基于上面的思想,我们可以加上一个标志位,记录每次排序过程中是否进行了数据交换,如果有数据交换则继续,如果没有数据交换则证明数据已经排序完毕,退出循环,代码如下:

    // 两个数比较大小,较大的往下沉,较小的冒上来
    public void bubbleSort(int[] array) {
        if (array != null && array.length > 1) { // 如果数组为空或者小于两个数据,则不需要排序
            int temp; // 临时变量定义在循环外面,较少重复定义消耗
            boolean flag; // 记录每次排序的过程是否进行了数据交换
            for (int i=0; i<array.length-1; i++) { // 遍历处理数组的第1个到倒数第二个位置的要填充的数据
                flag = false;
                for (int j=array.length-1; j>i; j--) { // 从数组尾部开始,将较小的数字不断往前推,较大的数字往后排,直到将最小的数字推到第i个位置
                    if (array[j] < array[j-1]) { // 如果后面的数字小于前面的数字,则将后面的数字和前面的数字交换,相当于把较小的数字往前推
                        temp = array[j];
                        array[j] = array[j-1];
                        array[j-1] = temp;
                        flag = true; // 如果进行了数据交换则将其设置为true
                    }
                }
                if (!flag) { // 如果本次排序过程中没有进行数据交换,则说明后面的数据都已经排序好了,不需要继续排序,退出循环结束排序
                    break;
                }
            }
        }
    }
    

    上面的代码较少了一些多余的数据比较。

    三、选择排序
    1、基本思想:假设前k个数据已经排序好,循环遍历第k-n个元素, 找到最小元素的位置,比较最小元素的位置是否是第k个位置,如果不是则将第k个元素和最小元素交换数据,这样第k个位置的元素就排序好了,反复循环直到第n-1个位置排上了正确的数据

    2、过程

    假设前k个数据已经排序好,循环遍历第k-n个元素, 找到最小元素的位置
    比较最小元素的位置是否是第k个位置,如果不是则将第k个元素和最小元素交换数据,这样第k个位置的元素就排序好了
    反复循环直到第n-1个位置排上了正确的数据
    3、代码实现

    // 每次找到最小元素位置,和第一个数据交换
    public void selectSort(int[] array) { // 如果数组为空或者小于两个数据,则不需要排序
        if (array != null && array.length > 1) {
            int temp;
            int minPos;
            for (int i=0; i<array.length - 1; i++) {
                // 找到第i个位置到最后一个位置之间最小数据的位置
                minPos = i;
                for (int j=i+1; j<array.length; j++) {
                    if (array[j] < array[minPos]) {
                        minPos = j; // 记录最小位置
                    }
                }
                if (minPos != i) { // 如果最小数据的位置不是第i个,则将其数据和第i个位置的数据交换
                    temp = array[i];
                    array[i] = array[minPos];
                    array[minPos] = temp;
                }
            }
        }
    }
    

    4、平均时间复杂度:o(n2)

    四、插入排序
    1、基本思想:假设前k个元素已经排序好,现在需要将第k+1个元素按照顺序插入到前k个元素中,这样前k+1个元素就排序好了,循环遍历直至处理到最后一个元素,排序完毕

    2、过程

    从第二个元素开始,将元素按照顺序插入到前面排序好的元素中
    重复上面过程直到最后一个元素
    3、代码实现

    // 假设前k个元素是排序好了的,将第k+1个元素按照排序规则插入到前k个元素中,则前k+1个元素也就排序好了
    public void insertSort(int[] array) {
    if (array != null && array.length > 1) { // 如果数组为空或者小于两个数据,则不需要排序
    int temp;
    for (int i=0; i<array.length - 1; i++) { // 从0到第i个元素是已经排序好的数据
    for (int j=i+1; j>0; j–) { // 将第i+1个元素按照排序规则插入到前i个元素中
    if (array[j] < array[j-1]) { // 不断的将较小的元素往前推直到前面的元素都比它小
    temp = array[j];
    array[j] = array[j-1];
    array[j-1] = temp;
    } else { // 因为前面的元素是排序好了的,所以只要当前元素小于它,就说明前面的元素都要小于它
    break;
    }
    }
    }
    }
    }
    4、平均时间复杂度:o(n2)

    五、希尔排序
    1、基本思想:将数据分为若干组,分别对每组数据进行插入排序,排序完后缩小分组组数继续重复上面过程直到只有一组数据,然后对这一组数据进行插入排序即可完成整个排序过程。希尔排序是为了缩小插入排序数据交换的次数,在普通的插入排序中,每次将元素插入到对应的位置都需要进行较多次数的移动,但是如果数据序列基本有序,插入排序的数据移动次数会大大减小,希尔排序就是基于以上思想,将数据分为若干组,分别对每一组数据进行插入排序,使得数据越来越有序,当分组为1时,数据已经基本有序,在使用插入排序效率就会大大提高。

    2、过程

    将数据分为若干组,分别对每一组数据进行插入排序
    缩小分组数为原来的一半,重复上面过程
    当分组数为1时,进行最后一次插入排序,排序完成
    3、代码实现

    // 希尔排序,将数据分为若干组,分别对每组数据进行插入排序,排序完后缩小分组组数继续重复上面过程直到只有一组数据,然后对这一组数据进行插入排序即可完成整个排序过程
    public void shellSort(int[] array) {
        if (array != null && array.length > 1) {
            int temp;
            // 不断的将数据分隔成grap组,对每一组数据分别进行插入排序
            int grap = array.length / 2;
            while (grap > 0) { // 当grap=0表示排序完成
                // 对grap组数据分别进行插入排序
                for (int k=0; k<grap; k++) {
                    for (int i=k+grap; i<array.length; i+=grap) {
                        for (int j=i; j>k; j-=grap) {
                            if (array[j] < array[j-grap]) {
                                temp = array[j];
                                array[j] = array[j-grap];
                                array[j-grap] = temp;
                            }
                        }
                    }
                }
                grap = grap / 2; // 每次处理完缩小grap的值
            }
        }
    }
    

    4、时间复杂度:o(n2)

    六、快速排序
    1、基本思想:快速排序是一种效率很高的排序算法,其主要思想是在所有元素中任意取一个元素key,将比其小的元素统一放在其左边,将比其大的元素放在其右边,这样,我们就将所有元素分为了两个部分,比key小的在key的左边,比key大的在key的右边,然后我们继续基于以上思想分别对key左边的元素和右边的元素进行排序,排序完毕后,所有元素也就排序完成了。快速排序的核心思想是分治,采用分而治之的方法,将一个复杂的问题简单化,可以大大提高效率。

    2、过程

    在所有元素中任意取一个值key,将比其小的元素放在key的左边,比其大的元素放在key的右边
    分别对key左右两边的元素进行上面的操作
    当划分的排序集合只有一个元素时,则该排序集合趴下完成
    3、代码实现

    // 快速排序,采用分治的思想,在所有元素中任意取一个值key,将比其大的元素排在key的左边,比其小的元素排在数组的右边,然后分别对左右两边的元素进行快速排序
    public void quickSort(int[] array, int start, int end) {
        if (array != null && array.length > 0) { // 如果数组为空或者小于两个数据,则不需要排序
            if (end > array.length-1) {
                end = array.length - 1;
            }
            if (start < end) { // 如果数组中只有一个元素,则不需要对其进行排序
                int i=start;
                int j = end;
                int key = array[start]; // 这里取数组中的第一个元素作为key值,也可以随机从数组中选取,选取完后和第一个数据交换即可
                while (i<j) { // 位置i左边的都是比key值小的元素,位置j右边的都是比key值大的元素,当i等于j时,证明整个数组都处理完毕,位置i左边的元素都是比key值小的,位置i右边的元素都是比key值大的
                    while (j>i && array[j] >= key) { // 从右向左查找,直到找到第一个比key值小的元素
                        j--;
                    }
                    if (j != i) { // 将右边找到的比key值小的元素放在数组的左边
                        array[i] = array[j];
                        i++;
                    }
                    while (i<j && array[i] <= key) { // 从左向右查找,直到找到第一个比key值大的元素
                        i++;
                    }
                    if (i != j) { // 将左边找到的比key值大的元素放在数组的右边
                        array[j] = array[i];
                        j--;
                    }
                }
                array[i] = key; // 将key设置给位置i
                quickSort(array, start, i-1); // 利用递归对数组左边的元素进行快速排序
                quickSort(array, i+1, end); // 利用递归对数组右边的元素进行快速排序
            }
        }
    }
    

    4、时间复杂度:O(N*logN)

    七、归并排序
    1、基本思想:归并排序的基本思想是将数据分为两个部分,然后分别对其进行排序,最后将两个有序数组进行合并,合并完成后整个数组也就排序完成了。归并排序的核心思想也是分治,将复杂的问题简单化,最后我们只需要关注数据合并的逻辑。

    2、过程

    将数组分为两个部分,分别对其进行归并排序
    将排序好的两个部分数据合并起来
    3、代码实现

    // 归并排序
    public void memerySort(int[] array, int start, int end, int[] temp) { // 需要创建一个临时数组来临时保存合并的数据
        if (array != null && array.length > 0 && temp != null && temp.length >= array.length) { // 如果数组为空或者小于两个数据,则不需要排序
            if (start < end) { // 当分组中只有一个元素时,不需要对该分组进行排序
                int middle = (start + end) / 2; // 找到需要排序的数组的中间位置
                memerySort(array, start, middle, temp); // 利用递归对左边的元素进行归并排序
                memerySort(array, middle + 1, end, temp); // 利用递归对右边的元素进行归并排序
                memery(array, start, middle, end, temp); // 将左右两边排序好的元素进行合并
            }
        }
    }
    
    // 合并排序好的两个分组(start到middle是一个分组,middle+1到end时一个分组)
    private void memery(int[] array, int start, int middle, int end, int[] temp) {
        int i = start;
        int j = middle + 1;
        int k = start;
        while (i <= middle && j <= end) { // 首先循环比较两个分组直到其中一个结束,将两个分组中较小的元素放在临时数组中
            if (array[i] < array[j]) {
                temp[k++] = array[i++];
            } else {
                temp[k++] = array[j++];
            }
        }
        while (i <= middle) { // 如果前面的分组有剩余元素,则将其添加到临时数组尾部,注意,两个分组中只有一个会有剩余元素
            temp[k++] = array[i++];
        }
        while (j <= end) { // 如果后面的分组有剩余元素,则将其添加到临时数组尾部
            temp[k++] = array[j++];
        }
    
        for (int ii = start; ii <= end; ii++) { // 将合并好的临时数组数据拷贝到数组中
            array[ii] = temp[ii];
        }
    }
    

    在实现归并排序函数时,首先要了解它的核心思想:将两个有序数组合并。所以我们先要写合并的函数,我们在这里用start、middle、end来将数组分为两个部分,并且合并需要一个临时数组,这个临时数组最好是由外部传入,使得整个归并排序共用一个临时数组,这样可以减少频繁创建数组的消耗。写好合并算法后,我们再来写归并排序的算法,因为是递归调用,所以归并排序需要传入start、end来标记需要排序的部分,并且需要传入一个临时数组提供给合并函数。首先我们要判断排序的部分是否大于一个元素,如果只有一个元素则不用进行排序,然后将需要排序部分从中间分开,分别对两部分进行归并排序(递归调用归并排序函数),排序好了,然后调用合并函数对两部分数据进行合并即可。

    4、时间复杂度:O(N*logN)

    八、基数排序
    1、基本思想:将整数按位数切割成不同的数字,然后按每个位数分别比较。

    2、过程

    先找到数组中的最大值,获得最大值的位数
    根据最大值的位数,依次根据每一位的值对数组进行排序
    基数排序将对整个数组的排序转换为对数组数据单个位数的排序,因为单个位数上只有可能出现0-9这10个数字,我们可以通过记录每一位数字出现的次数来完成排序过程,具体排序过程见代码

    3、代码实现

    // 基数排序,依次根据每一位的值排序数组,最终完成排序
    public void radixSort(int[] array) {
        // 找到数组中的最大值,以找到最大要排序的位数
        int max = array[0];
        for (int i=1; i<array.length; i++) {
            if (max < array[i]) {
                max = array[i];
            }
        }
    
        for (int exp=1; max/exp > 0; exp *= 10) { // 依次根据每一位的值排序数组
            countSort(array, exp);
        }
    }
    
    // 根据对应位数的值排列数组,如果exp为1表示根据个位数的值来排列,如果exp=10表示根据十位数来排列,以此类推
    private void countSort(int[] array, int exp) {
        int[] temp = new int[array.length];
        int buckets[] = new int[10];
        for (int i=0; i<buckets.length; i++) {
            buckets[i] = 0;
        }
        for (int i=0; i<array.length; i++) { // buckets记录了相应位数每位数字(0-9)出现的次数
            buckets[(array[i]/exp)%10]++;
        }
        /**
         * buckets现在记录了相应位数每位数字加上之前所有数字出现的次数,
         * 比如buckets[7]就记录了当前位数上0-7出现的总次数,通过buckets
         * 我们就可以知道对应数字应该插入到哪个范围内,比如对应位数值为7
         * 的数据就应该插入在[buckets[6],buckets[7])这个区间内,并且之前位数
         * 较大的值应该插入在后面,较小的值应该插入在前面
          */
        for (int i=0; i<buckets.length-1; i++) {
            buckets[i+1] += buckets[i];
        }
        // 注意一定要倒序处理,因为针对对应位数相同数字的数据,也是倒序插入到临时数组中的,所以应该把之前位数较大的先插入
        for (int i=array.length - 1; i>=0; i--) {
            temp[--buckets[(array[i]/exp)%10]] = array[i];
        }
        // 复制临时数组到array中
        for (int i=0; i<array.length; i++) {
            array[i] = temp[i];
        }
    }
    

    基尔排序是一种很巧妙的排序算法,它将对整个数据的排序问题转换为对数据的每一位进行排序,因为每一位可能的取值是固定的,可以通过数组记录每个可能取值出现的次数,在通过计算就能知道每个可能取值所在的区间范围,然后依次将其对应的数据插入到区间即可完成单个位数的排序。

    九、堆排序
    1、基本思想:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

    对排序设计到很多数据结构二叉树方面的知识,具体算法等以后有时间再分析,可以参考:图解排序算法(三)之堆排序,这篇文章讲的很详细。

    十、总结
    排序算法是我们平时开发中最常见也是最基础的算法,这篇文章借少了八大排序算法并提供了其代码实现,每种算法的消耗和适用场景都不一样。在这八大排序算法中,冒泡、选择、插入是最原始的排序算法,性能一般,希尔排序算是对插入排序的一种改进算法,它缩小了数据的交换次数。快速排序、归并排序、基数排序、堆排序这四种排序算法的性能会好一些,其中,快速排序和归并排序是重点,快速排序和归并排序的核心思想都是分治,不断的缩小排序数组的大小来提高性能。基数排序也是一种很好的排序算法,它将对整个数组的排序问题转换为依次对每一位位数上的数据进行排序,而每一位位数的可能取值是有限的(0-9),我们可以通过记录每一位数字出现的次数来完成排序过程。堆排序充分利用了完全二叉树的特性。我们在学习算法时,主要是学习算法的思想,尽量能够达到举一反三的效果,比如归并排序和快速排序中用到的分治的思想,就是很多算法的核心思想之一。
    ————————————————
    版权声明:本文为CSDN博主「xiao_nian」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/xiao_nian/article/details/81974554