排序算法

排序算法过程动态图:https://visualgo.net/zh/sorting

一、排序算法基础理论

原地排序:特指空间复杂度是 O ( 1 ) O(1) O(1)的排序算法

排序算法的稳定性:如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后排序不变,这种排序算法称为稳定的排序算法

二、冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置(最大的元素移动到最后的位置),重复n次,就完成了n个数据的排序工作

对一组数据4、5、6、3、2、1,从小到大进行排序。第一次冒泡操作的详细过程如下:

在这里插入图片描述

经过一次冒泡操作之后,6这个元素已经存储在正确的位置上。要想完成所有数据的排序,只要进行6次这样的冒泡操作就行了

在这里插入图片描述

冒泡排序的优化:当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。下图的例子里面有6个元素排序,只需要4次冒泡操作就可以了

在这里插入图片描述

代码实现:

    public void bubbleSort(int[] nums, int n) {
        for (int i = 0; i < n; ++i) {
            boolean flag = false;//提前退出冒泡循环的标志位
            for (int j = 0; j < n - i - 1; ++j) {
                if (nums[j + 1] < nums[j]) {//交换
                    int tmp = nums[j];
                    nums[j] = nums[j + 1];
                    nums[j + 1] = tmp;
                    flag = true;//表示有数据交换
                }
            }
            if (!flag) break;//没有数据交换,提前退出
        }
    }

算法分析:

冒泡排序包含两个操作:比较和交换,平均时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)原地排序算法),在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,我们不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法

三、插入排序

将数组中的数据分为两个区间,已排序区间未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束

如图所示,要排序的数据是4,5,6,1,3,2,其中左侧为已排序区间,右侧是未排序区间

在这里插入图片描述

插入排序也包含两种操作,一种是元素的比较,一种是元素的移动。当需要将一个数组a插入到已排序区间时,需要拿a与已排序区间的元素依次比较大小,找到合适的插入位置。找到插入点之后,还需要将插入点之后的元素顺序往后移动一位,这样才能腾出位置给元素a插入

代码实现:

    public void insertionSort(int[] nums, int n) {
        for (int i = 1; i < n; ++i) {
            int value = nums[i];
            int j = i - 1;
            //查找插入的位置
            for (; j >= 0; --j) {
                if (nums[j] > value) {
                    nums[j + 1] = nums[j];//数据移动
                } else {
                    break;
                }
            }
            nums[j + 1] = value;//插入数据
        }
    }

算法分析:

插入排序包含两个操作:元素的比较和移动,平均时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)原地排序算法),在插入排序中,对于值相同的元素,可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法

四、选择排序

选择排序算法的实现思路有点类似于插入排序,也分为已排序区间未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的尾部

在这里插入图片描述

代码实现:

    public void selectionSort(int[] nums, int n) {
        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;
                }
            }
            //交换
            int tmp = nums[i];
            nums[i] = nums[minIndex];
            nums[minIndex] = tmp;
        }
    }

算法分析:

插入排序包含两个操作:元素的比较和移动,平均时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)原地排序算法),选择排序是一种不稳定的排序算法。比如5,8,5,2,9这样一组数据,使用选择排序算法来排序的话,第一次找到最小元素2,与第一个5交换位置,那第一个5和中间的5顺序就变了,所以就不稳定了

五、归并排序

归并排序的核心思想:如果要排序一个数组,先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了(分治思想)

在这里插入图片描述

递推公式:
merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r))
终止条件:
p >= r 不用再继续分解

如何将A[p…q]和A[q+1…r]合并成一个有序的数组:

如下图所示,申请一个临时数组tmp,大小与A[p…r]相同。用两个游标i和j,分别指向A[p…q]和A[q+1…r]的第一个元素。比较这两个元素A[i]和A[j],如果A[i]<=A[j],就把A[i]放入到临时数组tmp,并且i后移一位,否则将A[j]放入到数组tmp,j后移一位

继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组tmp中的数据拷贝到原数组A[p…r]中
在这里插入图片描述

代码实现:

    //nums是需要进行排序的数组,n表示数组大小
    public void mergeSort(int[] nums, int n) {
        mergeSortInternally(nums, 0, n - 1);
    }

    //递归调用函数
    private void mergeSortInternally(int[] nums, int p, int r) {
        //递归终止条件
        if (p >= r) return;
        //取p到r之间的中间位置q
        int q = p + ((r - p) >> 1);
        //分治递归
        mergeSortInternally(nums, p, q);
        mergeSortInternally(nums, q + 1, r);
        //将A[p...q]和A[q+1...r]合并为A[p...r]
        merge(nums, p, q, r);
    }

    private void merge(int[] nums, int p, int q, int r) {
        int i = p;
        int j = q + 1;
        int k = 0;//初始化变量i,j,k
        int[] tmp = new int[r - p + 1];//申请一个大小跟a[p...r]一样的临时数组
        while (i <= q && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[k++] = nums[i++];
            } else {
                tmp[k++] = nums[j++];
            }
        }
        //判断哪个子数组中有剩余的数据
        int start = i;
        int end = q;
        if (j <= r) {
            start = j;
            end = r;
        }
        //将剩余的数据拷贝到临时数组tmp
        while (start <= end) {
            tmp[k++] = nums[start++];
        }
        //将tmp中的数组拷贝回nums[p...r]
        for (i = 0; i < tmp.length; ++i) {
            nums[p + i] = tmp[i];
        }
    }

算法分析:

归并排序合并的过程中,如果A[p…q]和A[q+1…r]之间有值相同的元素,可以先把A[p…q]中的元素放入tmp数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法

归并排序时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn)

归并排序不是原地排序算法,空间复杂度是 O ( n ) O(n) O(n)

六、快速排序

快排的核心思想:如果要排序数组中下标从p到r之间的一组数据,选择p到r之间的任意一个数据作为pivot(分区点)。遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的

根据分治、递归的处理思想,我们可以用递归排序下标从p到q-1之间的数据和下标从q+1到r之间的数据,直到区间缩小为1,就说明所有的数据都有序了

在这里插入图片描述

递推公式:
quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)

终止条件:
p >= r

通过游标i把A[p…r-1]分成两部分。A[p…i-1]的元素都是小于pivot的,暂且叫它已处理区间,A[i…r-1]是未处理区间。每次都从未处理的区间A[i…r-1]中取一个元素A[j],与pivot对比,如果小于pivot,则将其加入到已处理区间的尾部,也就是A[i]的位置。只需要将A[i]与A[j]交换,就可以在 O ( 1 ) O(1) O(1)时间复杂度内将A[j]放到下标为i的位置
在这里插入图片描述

代码实现:

    //nums是需要进行排序的数组,n表示数组的大小
    public void quickSort(int[] nums, int n) {
        quickSortInternally(nums, 0, n - 1);
    }

    private void quickSortInternally(int[] nums, int p, int r) {
        if (p >= r) return;
        int q = partition(nums, p, r);//获取分区点
        quickSortInternally(nums, p, q - 1);
        quickSortInternally(nums, q + 1, r);
    }

    private int partition(int[] nums, int p, int r) {
        int pivot = nums[r];
        int i = p;
        for (int j = p; j < r; ++j) {
            if (nums[j] < pivot) {
                if (i == j) {
                    ++i;
                } else {
                    int tmp = nums[i];
                    nums[i++] = nums[j];
                    nums[j] = tmp;
                }
            }
        }
        int tmp = nums[i];
        nums[i] = nums[r];
        nums[r] = tmp;
        return i;
    }

算法分析:

快速排序分区的过程涉及交换操作,如果数组中有两个相同的元素,比如序列6,8,7,6,3,5,9,4,在经过第一次分区操作之后,两个6的相对先后顺序就会改变。所以,快速排序不是一个稳定的排序算法

极端情况的例子,如果数组中的数据原来已经是有序的了,比如1,3,5,6,8。如果我们每次选择最后一个元素作为pivot,那每次分区得到的两个区间都是不均等的。需要进行大约n次分区操作,才能完成快排的整个过程。每次分区平均要扫描大约 n / 2 n/2 n/2个元素,这种情况下,快排的时间复杂度就从 O ( n l o g n ) O(nlogn) O(nlogn)退化到 O ( n 2 ) O(n^2) O(n2)快速排序在大部分情况下的时间复杂度都可以做到 O ( n l o g n ) O(nlogn) O(nlogn),只有在极端情况下,才会退化到 O ( n 2 ) O(n^2) O(n2)

快速排序是一个原地排序算法,空间复杂度为 O ( 1 ) O(1) O(1),解决了归并排序占用太多内存的问题,所以快速排序的应用更加广泛

快速排序的优化:

快速排序最理想的分区点是:被分区点分开的两个分区中,数据的数量差不多。如果直接选择第一个或者最后一个分区点,不考虑数据的特点,在某些情况下就会出现 O ( n 2 ) O(n^2) O(n2)的时间复杂度,可以采用三数取中法或者随机法来选取分区点

三数取中法:从区间的首、尾、中间,分别取出一个数,然后对比大小,取这3个数的中间值作为分区点

随机法:每次从要排序的区间中,随机选择一个元素作为分区点

总结

Java中Arrays.sort的实现:

  • 若数组元素个数总数小于47,使用插入排序
  • 若数据元素个数总数在47~286之间,使用快速排序。采用三数取中法进行优化
  • 若大于286,使用归并排序

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

邋遢的流浪剑客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值