一篇文章让你彻底理解常见的排序算法

 一、比较类排序

        这类排序比较来确定元素之间的顺序,很多常见的排序方式都属于这一类,包括:冒泡排序,快速排序,简单插入排序,简单选择排序,以及堆排序等等。对于大多数情况,比较类排序都可以解决问题。

1、冒泡排序

最简单的排序,具体可以分为四个步骤:

  • 通过比较相邻的两个元素之间的大小关系,确定是否要进行交换(EX:需要整理成降序排序,那么如果第一个数比第二个小,则需要交换位置);
  • 对每一对相邻的组合都进行上面的操作,从头到尾,最后就能将最大(最小)的数放到最末尾;
  • 对每一个位置的元素都进行上两步的操作(已经确定的元素不需要再进行比较);
  • 一直重复前三步,直到所有元素完成。

从冒泡排序的操作流程可以看出,这个算法的时间复杂度平均是O(n²),因为每一个位置都需要单独进行一次。最好的情况下,复杂度为O(n);最坏情况为O(n²)。但是空间复杂度仅为O(1),因为基本不需要使用额外的空间进行操作,稳定性较高(稳定性高代表假如原本元素A在元素B的前面,同时A=B,那么在交换之后A依旧在B的前面;反之,如果同样的情况下A出现在了B的后面则算法稳定性差)。

public void bubbleSort(int[] arr) {
        int len = arr.length;
        for (int i = 0; i < len - 1; i++) {
            boolean flag = true;
            for (int j = 0; j < len - i - 1; j++) {
                if (arr[j] > arr[j + 1]) {
                    int tmp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = tmp;
                    flag = false;
                }
            }
            if (flag) {
                break;
            }
        }
    }

PS:如果有强迫症的同学可以把交换单独做,或者使用更简洁的方式进行。

2、选择排序

  • 从原始排序中按照要求找到最大(最小)的数放到排序的起点;
  • 继续在剩余的排序中按照第一步寻找下一个最大(最小)的数放到已经排好序的部分的尾巴上;
  • 重复上述步骤直到将原始排序整理完成。

选择排序的最好和最坏的时间复杂度都是O(n²),因为不论数据量以及数据情况怎样,对于每一个位置的元素都需要跟其他所有位置的元素进行一次比较,过程中并不存储之前的计算结果,因此不算是一个好的算法,尽管它的空间复杂度是O(1)。

public void selectionSort(int[] arr) {
        int len = arr.length;
        for (int i = 0; i < len - 1; i++) {
            int minVal = i;
            for (int j = i + 1; j < len; j++) {
                if (arr[minVal] > arr[j]) {
                    minVal = j;
                }
            }
            if (minVal != i) {
                int tmp = arr[i];
                arr[i] = arr[minVal];
                arr[minVal] = tmp;
            }
        }
    }

3、插入排序

  • 不同于选择排序需要首先确定极值,插入排序直接认为从第一个元素开始就是有序的;
  • 将每一个后来的元素都放到已经排好的有序数组中进行位置确定,这一过程可以是从前往后(或者从后往前);(EX:最终需要一个升序序列,那么每一个新元素就要跟已经排好的排序中的每一个数从前往后进行比较,如果新数大于旧数,则新数向后一位,直到插入末尾);
  • 对于每一个元素都重复这一步骤,最后就能得到需要的有序序列。

能够很轻易的发现,插入排序的平均时间复杂度为O(n²),最好情况下是O(1),最坏是O(n²)。假如原排序刚好和我们需要的结果顺序相反(顺序之于逆序),那么就会产生最坏情况。空间复杂度O(1),基本不需要额外的变量。

public void insertionSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int val = arr[i], j = i;
            while (j > 0 && val < arr[j - 1]) {
                arr[j] = arr[j - 1];
                j--;
            }
            arr[j] = val;
        }
    }

4、 堆排序

堆排序可以将一个数组看作是一棵树,并且一般都是一颗完全二叉树,也就是插入的节点都是从左往右依次插入新的数据。对于这样的堆排序,我们为了更加方便地给每一个位置地元素定位,将最顶层地根节点定位序号0,左子树的序号为2n+1(n为父节点的序号),右子树的序号为2n+2;并且将这个数组的长度定义为heapSize,这样的定义可以让代码在实现过程中准确的根据每个节点的位置确定到它的父节点或者任意一个子节点。

堆排序分为最大堆和最小堆;大根堆的定义是:对于任意一个节点,它的值都要比它的所有子节点的值大;小根堆则刚好相反。

现在我们假设一种情况,需要返回已有排序的最大的值,而此时的排序已经构建成为一个大根堆,那么显而易见的我们直接返回根节点的(序号为0的节点)值就可以;但是在返回这个节点后,树的结构会发生变化,我们需要重新确定一个新的“大根”,要怎么做呢?

  • 在移除了堆的顶点值后,我们直接将当前堆排序中最后一位的值(序号为heapSize的值)拷贝复制到根节点,并且同时将heapSize减一;
  • 然后先对这个新的根节点(current)的左右子树的值进行比较选出较大者(largest),将这个较大者和新的根节点值进行比较,如果largest大于current,则将两者在树中的位置进行交换(这里就会用到定义位置的方法);
  • 重复第二步的步骤直到发现largest不再大于current,或者current不再存在子树时停止;此时我们就会获得一个新的大根堆。
public class HeapSort {
    private static int heapLen;

    public static void heapSort(int[] arr) {//可以迭代可以递归,取决于具体情况
        heapLen = arr.length;
        for (int i = heapLen - 1; i >= 0; i--) {
            heapify(arr, i);
        }
        //要是想要简单写,可以在这里进行取数操作
        for (int i = heapLen - 1; i > 0; i--) {
            swap(arr, 0, heapLen - 1);
            heapLen--;
            heapify(arr, 0);
        }
    }

    private static void heapify(int[] arr, int idx) {
        int left = idx * 2 + 1, right = idx * 2 + 2, largest = idx;
        if (left < heapLen && arr[left] > arr[largest]) {
            largest = left;
        }
        if (right < heapLen && arr[right] > arr[largest]) {
            largest = right;
        }

        if (largest != idx) {
            swap(arr, largest, idx);
            heapify(arr, largest);
        }
    }

    private static void swap(int[] arr, int idx1, int idx2) {
        int tmp = arr[idx1];
        arr[idx1] = arr[idx2];
        arr[idx2] = tmp;
    }
}

在Java里,系统自带实现了基础最小堆功能的工具:PriorityQuene;

这代表着我们在处理比较简单的需要用到最小堆的情况时可以直接借用这个优先队列,但是也仅限于只需要add和poll操作的时候。这个优先队列有很大的限制条件,也就是没有办法在已经形成的堆结构中随意根据我们的想法去改变特定位置的值;所以在需要对指定位置做heapify的时候,依旧需要我们自己去手写一个堆结构,才能更高效的完成任务。

5、快速排序

快排可以说是现在面试中会经常考到需要手写的排序之一,非常重要!!

快排1.0

快速排序的基本操作流程可以说是非常清晰:

  • 对于任何一个给定的数组而言,都能够确定出一个中心值,这个值可以是第一个数,也可以是最后一个数,或者通过特殊计算的到的任何一个位置的数;(这里我们先假定将数组最后一个位置的数作为中心值)
  • 假如当前数组是这样的:0,4,2,5,8,3,9,1;我们将1作为中心值,同时设定两个指针,i指针指向0所在位置(第一位置),j指针指向9位置(倒数第二位置),通过比较这两个位置跟中心值1的大小关系,比1小的放前面,比1大的放后面;如果发现i位置的值大于1而j位置小于1,将两个位置的值进行交换,然后移动指针重复操作(因为这里我们是将原数组最后一个元素作为的中心值,所以最好是从左往右的先后顺序去移动指针);
  • 到什么时候可以停止交换呢,当我们已经将原数组除了最后一个中心值以外的所有数划分成了两个区域的时候;也就是前半部分小于中心值,后半部分大于中心值;此时我们将这两个区域看作小于等于区和大于区,然后将中心值与大于区的第一个数进行交换,这样一来就将原本无序数组划分成了两个部分并且确定了一个值的位置;这一过程叫做partition;
  • 之后再分别对上一步中分出来的两个区域继续进行partition操作,直到将整个数组全部排好。

通过这样一个看起来似乎是很复杂的操作我们就完成了一次最简单的快排1.0;但是!!!!细心的同学已经发现这样的排序一次只能确定一个中心值的位置,效率非常堪忧,所以有了快排2.0。

快排2.0

这一版本相较于1.0只升级了一个地方,就是中心值的确定不单单是只有一个数,而是一个等于区域,也就是说相比于1.0版本中的小于等于区和大于区,2.0版本将小于等于区也分开了,这样一来我们一次性就可以确定一批数字的位置,在特殊情况下可能会高效一点,大概的操作也是和1.0差不多的,唯一需要注意的就是需要一个单独的变量来记录等于区域。(或者我们可以直接将原来的两个指针i和j看作是等于区域的左右边界,这要看你自己是怎么理解的都可以)

PS:不管是上面的哪种快排的时间复杂度,最坏都是O(n²),这个比较简单就能想清楚。

快排3.0

经历了上面2.0的优化思路,我们可以看出对于快排的优化最重要的就是选取中心值的这个步骤能否尽可能地选取到能将两个部分划分为等长的区域的地方,所以,快排3.0诞生了。

这一版本只做一件事情就是随机的选取一个数作为中心值并将这个数和原数组的最后位置的数进行交换,其他步骤都一样,我们直接看代码:

public static void quickSort(int[] arr, int left, int right){//这一步就是将arr从left到right位置上排序
    if (left < right){
        swap(arr, left + (int) (Math.random() * (right - left + 1)), right);
        int[] position = partition(arr, left, right);//position只能是一个长度为二的数组,记录了等于区域的左右边界
        quickSort(arr, left, position[0] - 1);//这就是小于区域
        quickSort(arr, position[1] + 1, right);//这就是大于区域
    }
}


public static int[] partition(int[] arr, int left, int right){
    int less = left - 1;
    int more = right;
    while(left < more){
        if(arr[left] < arr[right]){
            swap(arr, ++less, left++);
        } else if (arr[left] > arr[right]){
            swap(arr, --more, left);
        } else {
            left++;
        }
    }
    swap(arr, more, right);
    return new int[] {less + 1, more};
}

那么现在快排3.0的时间复杂度就能够来到O(nlogn)的水平,空间复杂度达到O(logn)的程度,但是和前两种快排一样不具备很好的稳定性。

其实对于任何一种排序来说都是有利有弊的,实际任务中很难得直接使用单独的一种排序来作为最终策略,多数时候采用的都是多种排序结合使用,比如在数据量比较小的时候采用复杂度为N²的算法,当数据量变大了就选择nlogn的算法;但是有一点需要了解的就是不论什么排序方法都不能同时做到空间和时间都能非常优秀,所以在使用的时候一定会涉及到究竟是要空间换时间还是时间换空间的问题。

二、非比较类排序

1、桶排序

持续更新。。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值