一文搞懂常见十大排序算法

Posted 编程指南针

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞懂常见十大排序算法相关的知识,希望对你有一定的参考价值。

简介1:十大排序算法

  • 简单排序:插入排序、选择排序、冒泡排序(必学)

  • 分治排序:快速排序、归并排序(必学,快速排序还要关注中轴的选取方式)

  • 分配排序:桶排序、基数排序

  • 树状排序:堆排序(必学)

  • 其他:计数排序(必学)、希尔排序

简介2:术语铺垫

  • 稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 仍然在 b 的前面,则为稳定排序。

  • 非稳定排序:如果 a 原本在 b 的前面,且 a == b,排序之后 a 可能不在 b 的前面,则为非稳定排序。

  • 原地排序:原地排序就是指在排序过程中不申请多余的存储空间,只利用原来存储待排数据的存储空间进行比较和交换的数据排序。

  • 非原地排序:需要利用额外的数组来辅助排序。

  • 时间复杂度:一个算法执行所消耗的时间。

  • 空间复杂度:运行完一个算法所需的内存大小。

简介3:排序算法对比

1.插入排序

我们在玩打牌的时候,你是怎么整理那些牌的呢?一种简单的方法就是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。当我们给无序数组做排序的时候,为了要插入元素,我们需要腾出空间,将其余所有元素在插入之前都向右移动一位,这种算法我们称之为插入排序

过程简单描述:

1、从数组第2个元素开始抽取元素。

2、把它与左边第一个元素比较,如果左边第一个元素比它大,则继续与左边第二个元素比较下去,直到遇到不比它大的元素,然后插到这个元素的右边。

3、继续选取第3,4,….n个元素,重复步骤 2 ,选择适当的位置插入。

动图演示:

思路:   在待排序的元素中,假设前n-1个元素已有序,现将第n个元素插入到前面已经排好的序列中,使得前n个元素有序。按照此法对所有元素进行插入,直到整个序列有序。   但我们并不能确定待排元素中究竟哪一部分是有序的,所以我们一开始只能认为第一个元素是有序的,依次将其后面的元素插入到这个有序序列中来,直到整个序列有序为止。

代码如下:

 /**
     * 插入排序法
     * @param arr
     * @return
     */
    public static int[] insertSort(int[] arr)
        //判断数组是否为空或者长度是否大于2
        if (arr == null || arr.length <2)
            return arr;
        
        //循环遍历数组
        for (int i = 1; i <arr.length ; i++) 
            //定义变量保存要插入的值
             int tem = arr[i];
             int k = i - 1;
             while (k >= 0 && arr[k] > tem)
                 arr[k+1] = arr[k];
                 k -- ;
             //元素插入
             arr[k+1] = tem;
        
        return arr;
    

性质:

(1)时间复杂度:

最坏情况下为O(N*N),此时待排序列为逆序,或者说接近逆序 最好情况下为O(N),此时待排序列为升序,或者说接近升序。 (2)空间复杂度:O(1)

(3)稳定排序

(4)原地排序

2.选择排序

思路: 每次从待排序列中选出一个最小值,然后放在序列的起始位置,直到全部待排数据排完即可。

过程简单描述:

首先,找到数组中最小的那个元素,其次,将它和数组的第一个元素交换位置(如果第一个元素就是最小元素那么它就和自己交换)。其次,在剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。这种方法我们称之为选择排序。

动图如下:

快速理解:

初始一个无序数组

先从这些元素中选出一个最小的(或最大的),和第一个元素进行交换,这样第一个元素就是最小的,第一个元素位置就变成有序区间了

同理,在剩下的无序区间选择最小的元素,将最小元素与无序区间的第一个元素进行交换,交换后原来无序区间的第一个元素就变为有序区间的最后一个元素了,有序区间递增一

以此类推,全部元素就可以通过这样不断地选择以及交换排完序

那如何选出最小的一个元素呢?

很容易想到:先随便选一个元素假设它为最小的元素(默认为无序区间第一个元素),然后让这个元素与无序区间中的每一个元素进行比较,如果遇到比自己小的元素,那更新最小值下标,直到把无序区间遍历完,那最后的最小值就是这个无序区间的最小值

代码如下:

//直接选择排序
public class SelectSort 
    
     public static int[] selectSort(int[] a) 
        int n = a.length;
        for (int i = 0; i < n - 1; i++) 
            int min = i;
            for (int j = i + 1; j < n; j++) 
                if(a[min] > a[j]) min = j;
            
            //交换
            int temp = a[i];
            a[i] = a[min];
            a[min] = temp;
        
        return a;
      

性质:

(1)时间复杂度:最坏情况:O(N^2)       最好情况:O(N^2) (2)空间复杂度:O(1)

(3)非稳定排序

(4)原地排序

3.冒泡排序

思路: 左边大于右边交换一趟排下来最大的在右边

解释:

原理就如算法名字一样,就像水中的气泡一样,每次我都把最大的或最小的放到最后面,这样总共需要n-1趟即可完成排序,这就是第一层循环,第二次循环就是遍历未被固定的那些数(理解成数组左边的数,因为每层循环都会把最大或最小的数升到最右边固定起来,下次就不遍历这些数了),两层循环遍历结束后,所有的数就排好序了。 两层循环所以冒泡排序算法的时间复杂度是O(N^2),是一个非常高的时间复杂度,下面的代码进行了优化,加了一个标志位,如果上一次循环未发生交换,就说明已经是有序的了,就不继续下去了,反之继续进行下一轮。

动图如下:

快速理解:

冒泡排序需要利用到双层循环。

第一层循环是控制趟数

第二层就是控制第 i+1趟(因为i从0开始)所比较的次数,第 i +1 趟比较了 N - 1 -i 次”。

代码如下:

//冒泡排序算法
public class BubbleSort 
​
    //冒泡排序
    public static int[]  bubbleSort(int[] arr)  
        
         if (arr == null || arr.length < 2) 
            return arr;
        
        
        boolean flag = true; //加一个标志位,记录上一次是否发生了交换,如果是,我们则进行下一轮,如果没有,说明已经冒泡好了
​
        for (int i = 1; i < arr.length && flag; i++)  //控制次数,第几趟排序,只需要n-1趟,有交换时进行,只有flag=false就说明上一次一个元素都没有进行交换
​
            flag = false; //假定未交换
​
            for (int j = 0; j < arr.length - i; j++) 
​
                if (arr[j] > arr[j + 1])  
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = true;
                
            
        
    

时间复杂度:最坏情况:O(N^2)       最好情况:O(N) 空间复杂度:O(1)

4.快速排序

解释: 快速排序就是每次找一个基点(第一个元素),然后两个哨兵,一个从最前面往后走,一个从最后面往前面走,如果后面那个哨兵找到了一个比基点小的数停下来,前面那个哨兵找到比基点大的数停下来,然后交换两个哨兵找到的数,如果找不到最后两个哨兵就会碰到一起就结束,最后交换基点和哨兵相遇的地方的元素,然后就将一个序列分为比基点小的一部分和比基点大的一部分,然后递归左半部分和右半部分,最后的结果就是有序的了。

快速理解:

首先选一个主元,可以是第一个元素为主元,也可以最后一个元素为主元。假设数组arr的范围为[left, right],即起始下标为left,末尾下标为right。源数组如下

源数组如下

然后用令变量i = left + 1,j = right。然后让 i 和 j 从数组的两边向中间扫描。

i 向右遍历的过程中,如果遇到大于或等于主元的元素时,则停止移动,j向左遍历的过程中,如果遇到小于或等于主元的元素则停止移动。

当i和j都停止移动时,如果这时i < j,则交换 i, j 所指向的元素。此时 i < j,交换8和3

然后继续向中间遍历,直到i >= j。

此时i >= j,分割结束。

最后在把主元与 j 指向的元素交换(当然,与i指向的交换也行)。

这个时候,j 左边的元素一定小于或等于主元,而右边则大于或等于主元。

到此,分割调整完毕

代码如下:

public static int[] quickSort(int[] arr, int left, int right) 
        if (left < right) 
            //获取基点元素所处的位置
            int mid = partition(arr, left, right);
            //进行分割
            arr = quickSort(arr, left, mid - 1);
            arr = quickSort(arr, mid + 1, right);
        
        return arr;
    
​
    private static int partition(int[] arr, int left, int right) 
        //选取基点元素
        int pivot = arr[left];
        int i = left + 1;
        int j = right;
        while (true) 
            // 向右找到第一个小于等于 pivot 的元素位置
            while(i <= j && arr[i] <= pivot) i++;
            // 向左找到第一个大于等于 pivot 的元素位置
            while(i <= j && arr[j] >= pivot ) j--;
            if(i >= j)
                break;
            //交换两个元素的位置,使得左边的元素不大于pivot,右边的不小于pivot
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        
        arr[left] = arr[j];
        // 使中轴元素处于有序的位置
        arr[j] = pivot;
        return j;
    

时间复杂度: 快速排序的过程类似于二叉树其高度为logN,每层约有N个数,如下图所示:

5.堆排序

先理解下大顶堆和小顶堆,看图 大顶堆,双亲结点的值比每一个孩子结点的值都要大。根结点值最大 小顶堆,双亲结点的值比每一个孩子结点的值都要小。根结点值最小

简单解释: 构建好大顶堆或小顶堆结构,这样最上面的就是最大值或最小值,那么我们取出堆顶元素,然后重新构建结构,一直取,一直重新构建,那么最后达到排序的效果了。

快速理解:

插入一个节点。

刚才我们说二叉堆具有完全二叉树的特性,因此,我们在插入一个节点的时候,应该先保证节点插入后,它仍然是一颗完全二叉树,然后再来进行调整,使它满足二叉堆的另一个特性。

所以,在插入的时候,我们把新节点插到完全二叉树的最后一个位置。例如:

插入0。

之后我们再来进行调整,调整的原则是:让新插入的节点与它的父节点进行比较,如果新节点小于父节点,则让新节点上浮,即和父节点交换位置。

上浮之后继续和它的父节点进行比较,直到父节点的值小于或等于该节点,才停止上浮,即插入结束。例如:

0比5小,上浮。

0比2小于,上浮。

0比1小,上浮。

已经到达堆顶了,插入结束。

删除节点。

前面说了,删除节点一般删除的是根节点。

和插入一样,由于二叉堆具有完全二叉树的特性,因为删除时候,首先我们是要马上恢复它具有完全二叉树的特性,所以我们是采取这样的策略:把根节点删除之后,用二叉堆的最后一个元素顶替上来,然后在进行调整恢复。例如:

把0删除了之后,5替换上来。

之后再来调整,调整的规则和插入差不多类似,采取下沉的策略:让5和左右孩子节点相比较,如果左右节点有小于5的,则让那个较小的孩子代替5的位置,然后5下沉。

下沉之后,5继续和左右孩子相比,直到左右孩子都大于或等于5才结束。

如:5比1,3都大,1代替5的位置

5比4,2都大,2代替5的位置。

5已经不在具有左右孩子了,删除结束。

构建二叉堆

所谓构建,就是给你一个有n个节点的无序的完全二叉树,然后把它构建成二叉堆。

刚才我们在删除一个节点的时候,把最后一个元素补到根元素的位置上去,然后再让这个元素依次下沉,实际上,在这个元素还没有下沉之前,它就可以看作是一颗无序的完全二叉树了

也就是说,要把一个无序的完全二叉树调整为二叉堆,我们可以让所有非叶子节点依次下沉。不过下沉的顺序不是从根节点开始下沉(想一下相必你就 知道不能从根节点开始下沉),而是从下面的非叶子节点下称,在依次往上。举个例子:

对于这样一颗无序的完全二叉树

8进行下沉。

接着,5进行下沉。

2没问题,之后让7进行下沉

调整完成,构建结束。

代码实现:

//堆排序
    public static void heapSort(int[] arr) 
​
        //1.构建大顶堆
        for (int i = arr.length / 2 - 1; i >= 0; i--) 
            //从第一个非叶子结点从下至上,从右至左调整结构
            sift(arr, i, arr.length );
        
​
        //2.调整堆结构+交换堆顶元素与末尾元素
        for (int j = arr.length - 1; j > 0; j--) 
​
            //现在的数组第一个就是根结点,最小值所在,进行交换,把它放到最右边
            int temp = arr[j];
            arr[j] = arr[0];
            arr[0] = temp;
​
            //重新建立堆
            sift(arr, 0, j ); //重新对堆进行调整
        
    
​
    /**
     * 建立堆的方法
     * 私有方法,只允许被堆排序调用
     * @param arr     要排序数组
     * @param parent  当前的双亲节点
     * @param len     数组长度
     */
    private static void sift(int[] arr, int parent, int len) 
​
        int value = arr[parent]; //先取出当前元素i
​
        for (int child = 2 * parent + 1; child < len; child = child * 2 + 1)  //从parent结点的左子结点开始,也就是2*parent+1处开始
​
            if (child+1 < len && ( arr[child] < arr[child + 1] ))  //如果左子结点小于右子结点,child指向右子结点
                child++; //右孩子如果比左孩子大,我们就将现在的孩子换到右孩子
            
​
            //判断是否符合大顶堆的特性, 如果右孩子大于双亲,自然左孩子也大于双亲,符合
            //如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
            if ( value < arr[child] ) 
                arr[parent]=arr[child];
                parent = child;
            
            else //如果不是,说明已经符合我们的要求了。
                break;
            
        
        arr[parent] =value; //将value值放到最终的位置
    

6.基数排序

算法解释:

基数排序的排序思路是这样的:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小……

排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是用“桶”来排序的。

由于某位数(个位/十位….,不是一整个数)的大小范围为0-9,所以我们需要10个桶,然后把具有相同数值的数放进同一个桶里,之后再把桶里的数按照0号桶到9号桶的顺序取出来,这样一趟下来,按照某位数的排序就完成了

算法动图:

快速理解:

基数排序,是一种基数“桶”的排序,他的排序思路是这样的:先以个位数的大小来对数据进行排序,接着以十位数的大小来多数进行排序,接着以百位数的大小……

排到最后,就是一组有序的元素了。不过,他在以某位数进行排序的时候,是采用“桶”来排序的,基本原理就是把具有相同个(十、百等)位数的数放进同一个桶里。

例如我们现在要对这组元素来排序:

由于我们是以每个数的某位数来排序的,这位数的范围是0-9,所以我们需要10个桶。

第一遍,先以个位数排序,把具有相同个位数的数放进桶里,结果如下:

之后再按照从0号桶到9号桶的顺序取出来,结果如下

个位数排序完成。

第二遍,以十位数来排,结果如下:

再取出来放回去:

十位数排序完成,最终的结果就是一组有序的元素。如果元素中有百位数的话,大不了就按照百位数再给他重复排一遍。

代码实现:

public class Radiosort 
​
    public static int[] radioSort(int[] arr) 
        if(arr == null || arr.length < 2) return arr;
​
        int n = arr.length;
        int max = arr[0];
        // 找出最大值
        for (int i = 1; i < n; i++) 
            if(max < arr[i]) max = arr[i];
        
        // 计算最大值是几位数
        int num = 1;
        while (max / 10 > 0) 
            num++;
            max = max / 10;
        
        // 创建10个桶
        ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(10);
        //初始化桶
        for (int i = 0; i < 10; i++) 
            bucketList.add(new LinkedList<Integer>());
        
        // 进行每一趟的排序,从个位数开始排
        for (int i = 1; i <= num; i++) 
            for (int j = 0; j < n; j++) 
                // 获取每个数最后第 i 位是数组
                int radio = (arr[j] / (int)Math.pow(10,i-1)) % 10;
                //放进对应的桶里
                bucketList.get(radio).add(arr[j]);
            
            //合并放回原数组
            int k = 0;
            for (int j = 0; j < 10; j++) 
                for (Integer t : bucketList.get(j)) 
                    arr[k++] = t;
                
                //取出来合并了之后把桶清光数据
                bucketList.get(j).clear();
            
        
        return arr;
    

7.归并排序

解释:

将一个大的无序数组有序,我们可以把大的数组分成两个,然后对这两个数组分别进行排序,之后在把这两个数组合并成一个有序的数组。由于两个小的数组都是有序的,所以在合并的时候是很快的。

通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了,之后再把两个数组大小为1的合并成一个大小为2的,再把两个大小为2的合并成4的 ….. 直到全部小的数组合并起来。

算法演示动图:

代码实现:

//归并排序算法
public class MergeSort 
     // 归并排序
   public static int[] mergeSort(int[] arr, int left, int right) 
       // 如果 left == right,表示数组只有一个元素,则不用递归排序
        if (left < right) 
            // 把大的数组分隔成两个数组
            int mid = (left + right) / 2;
            // 对左半部分进行排序
            arr = mergeSort(arr, left, mid);
            // 对右半部分进行排序
            arr = mergeSort(arr, mid + 1, right);
            //进行合并
            merge(arr, left, mid, right);
        
        return arr;
    
​
    // 合并函数,把两个有序的数组合并起来
    // arr[left..mif]表示一个数组,arr[mid+1 .. right]表示一个数组
    private static void merge(int[] arr, int left, int mid, int right) 
        //先用一个临时数组把他们合并汇总起来
        int[] a = new int[right - left + 1];
        int i = left;
        int j = mid + 1;
        int k = 0;
        while (i <= mid && j <= right) 
            if (arr[i] < arr[j]) 
                a[k++] = arr[i++];
             else 
                a[k++] = arr[j++];
            
        
        while(i <= mid) a[k++] = arr[i++];
        while(j <= right) a[k++] = arr[j++];
        // 把临时数组复制到原数组
        for (i = 0; i < k; i++) 
            arr[left++] = a[i];
        
    

8.计数排序

解释:

计数排序是一种适合于最大值和最小值的差值不是不是很大的排序。

基本思想:就是把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m, 表示元素 i 一共出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来是数据是有序的。

算法动图:

代码实现:

public class CountSort 
     public static int[] countSort(int[] arr) 
        if(arr == null || arr.length < 2) return arr;
 
        int n = arr.length;
        int max = arr[0];
        // 寻找数组的最大值
        for (int i = 1; i < n; i++) 
            if(max < arr[i])
                max = arr[i];
        
        //创建大小为max的临时数组
        int[] temp = new int[max + 1];
        //统计元素i出现的次数
        for (int i = 0; i < n; i++) 
            temp[arr[i]]++;
        
        int k = 0;
        //把临时数组统计好的数据汇总到原数组
        for (int i = 0; i <= max; i++) 
            for (int j = temp[i]; j > 0; j--) 
                arr[k++] = i;
            
        
        return arr;
    

性质:1、时间复杂度:O(n+k) 2、空间复杂度:O(k) 3、稳定排序 4、非原地排序

注:K表示临时数组的大小,下同

算法优化:

上面的代码中,我们是根据 max 的大小来创建对应大小的数组,假如原数组只有10个元素,并且最小值为 min = 10000,最大值为 max = 10005,那我们创建 10005 + 1 大小的数组不是很吃亏,最大值与最小值的差值为 5,所以我们创建大小为6的临时数组就可以了。

也就是说,我们创建的临时数组大小 (max - min + 1)就可以了,然后在把 min作为偏移量。优化之后的代码如下所示:

public class Counting 
    public static int[] sort(int[] arr) 
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int min = arr[0];
        int max = arr[0];
        // 寻找数组的最大值与最小值
        for (int i = 1; i < n; i++) 
            if(max < arr[i])
                max = arr[i];
            if(min > arr[i])
                min = arr[i];
        
        int d = max - min + 1;
        //创建大小为max的临时数组
        int[] temp = new int[d];
        //统计元素i出现的次数
        for (int i = 0; i < n; i++) 
            temp[arr[i] - min]++;
        
        int k = 0;
        //把临时数组统计好的数据汇总到原数组
        for (int i = 0; i < d; i++) 
            for (int j = temp[i]; j > 0; j--) 
                arr[k++] = i + min;
            
        
        return arr;
    

9.希尔排序

步骤:希尔排序可以说是插入排序的一种变种。无论是插入排序还是冒泡排序,如果数组的最大值刚好是在第一位,要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。

希尔排序就是为了加快速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。

希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 gap 的元素有序,刚开始 gap 的大小可以是 gap = n / 2,接着让 gap = n / 4,让 gap 一直缩小,当 gap = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。

动图如下:

思路: 希尔排序,先将待排序列进行预排序,使待排序列接近有序,然后再对该序列进行一次插入排序,此时插入排序的时间复杂度为O(N),

代码如下:

//希尔排序
public class ShellSort 

    public static int[] shellSort(int[] arr) 

        if (arr == null || arr.length < 2) 
            return arr;
        
        for(int d = arr.length/2;d>0;d/=2)
             //对各个局部分组进行插入排序
            for(int i=d;i< arr.length;i++)
                int temp = arr[i];
                int j=0;
                for(j=i-d;j>=0&&temp<arr[j];j-=d)
                    arr[j+d]=arr[j];
                
                arr[j+d] = temp;
            
        
     return arr;
    

性质:

(1)时间复杂度:O(nlogn)

(2)空间复杂度:O(1)

(3)非稳定排序

(4)原地排序

10.桶排序

算法解释:

桶排序就是把最大值和最小值之间的数进行瓜分,例如分成 10 个区间,10个区间对应10个桶,我们把各元素放到对应区间的桶中去,再对每个桶中的数进行排序,可以采用归并排序,也可以采用快速排序之类的。

之后每个桶里面的数据就是有序的了,我们在进行合并汇总。

算法过程图:

代码实现:

public class BucketSort 
    public static int[] BucketSort(int[] arr) 
        if(arr == null || arr.length < 2) return arr;

        int n = arr.length;
        int max = arr[0];
        int min = arr[0];
        // 寻找数组的最大值与最小值
        for (int i = 1; i < n; i++) 
            if(min > arr[i])
                min = arr[i];
            if(max < arr[i])
                max = arr[i];
        
        //和优化版本的计数排序一样,弄一个大小为 min 的偏移值
        int d = max - min;
        //创建 d / 5 + 1 个桶,第 i 桶存放  5*i ~ 5*i+5-1范围的数
        int bucketNum = d / 5 + 1;
        ArrayList<LinkedList<Integer>> bucketList = new ArrayList<>(bucketNum);
        //初始化桶
        for (int i = 0; i < bucketNum; i++) 
            bucketList.add(new LinkedList<Integer>());
        
        //遍历原数组,将每个元素放入桶中
        for (int i = 0; i < n; i++) 
            bucketList.get((arr[i]-min)/d).add(arr[i] - min);
        
        //对桶内的元素进行排序,我这里采用系统自带的排序工具
        for (int i = 0; i < bucketNum; i++) 
            Collections.sort(bucketList.get(i));
        
        //把每个桶排序好的数据进行合并汇总放回原数组
        int k = 0;
        for (int i = 0; i < bucketNum; i++) 
            for (Integer t : bucketList.get(i)) 
                arr[k++] = t + min;
            
        
        return arr;
    

以上是关于一文搞懂常见十大排序算法的主要内容,如果未能解决你的问题,请参考以下文章

一文搞定十大经典排序算法(Java实现)

干货收藏:一文掌握十大经典排序算法(动态演示+代码)

一文搞掂十大经典排序算法

十大经典排序算法

数据结构与算法笔记 —— 十大经典排序及算法的稳定性

经典十大排序算法之8种内部常见排序算法