8种常见排序算法(快排、冒泡排序、堆排序、希尔排序等)

692 阅读8分钟

1.介绍

通过算法将指定的一组数据按照顺序进行排列的过程

1.1.分类

  • 内排序:将需要处理的所有数据都加载到内存进行排序

  • 外排序:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序

1.2.常见的排序算法

1.3.时间复杂度

  1. 时间频度:一个算法花费的时间与算法中语句执行的次数成正比例,算法中语句执行次数越多,花费的时间越多。一个算法中语句的执行次数称为时间频度。记为 T(n)

  2. 时间复杂度算法中的基本操作语句的执行次数是问题规模n的某个函数,用 T(n) 表示,若有某个辅助函数 f(n) ,使n趋近于无穷大时,T(n)/f(n) 的极限值为不等于零的常数,则称 f(n)T(n)同数量级函数。记作 T(n) = O(f(n)),称 O(f(n)) 为算法的渐近时间复杂度,简称时间复杂度

  3. 计算时间复杂度的方法:

    1. 如果运行时间是常数量级,用常数1表示;
    2. 只保留时间函数中的最高阶项
    3. 如果最高阶项存在,则省去最高阶项前面的系数
  4. 常见的时间复杂度

常数阶 O(1) < 对数阶 O(logn)< 线性阶 O(n)< 线性对数阶 O(nlogn)< 平方阶 O(n^2)< 立方阶 O(n^3)< k次方阶 O(n^k)< 指数阶 O(2^n)

1.4.空间复杂度

空间复杂度是运行完一个程序所需要的内存的大小。这里包括了存储算法本身所需要的空间,输入与输出数据所占空间,以及一些临时变量所占用的空间。一般而言,我们只比较额外空间,来比较算法的空间优越性。

2.冒泡排序(Bubble Sorting)

  • 介绍

通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若逆序则交换,使值较大的元素逐渐向后移动

  • 以数组 [24,69,80,57,13]为例:

  1. 一共进行数组长度 - 1 轮排序,每轮排序依次确定一个位置的数,每轮排序的比较次数在减少;
  2. 如果在某轮排序中没有发生交换,说明数组已经是有序的,可以提前结束冒泡排序
  • 代码实现:
public static void BubbleSorting(int[] arr){

    int temp = 0;//临时变量

    boolean flag = false;//标志变量,记录是否进行了交换

    for (int i = 0; i < arr.length - 1; i++) {//外层循环

        for (int j = 0; j < arr.length - 1 - i; j++) {//内层循环

            if (arr[j] > arr[j + 1]){//如果两个数逆序则交换

                flag = true;

                temp = arr[j];

                arr[j] = arr[j + 1];

                arr[j + 1] = temp;

            }

        }

        System.out.println("第" + (i + 1) + "趟排序后的数组:");

        System.out.println(Arrays.toString(arr));

        if (!flag){

            return;//没有交换说明数组已经有序,直接返回

        }else {

            flag = false;//发生过交换,重置标志变量

        }

    }

}
  • 冒泡排序是稳定的排序算法
  • 冒泡排序的最好时间复杂度:O(n)最坏时间复杂度:O(n^2),平均时间复杂度:O(n^2)
  • 空间复杂度:O(1)

3.*快速排序(Quick Sorting)

  • 介绍

快速排序是对冒泡排序的一种改进。基本思想:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据都要小,然后再按这个方法对两部分分别进行快速排序,整个排序过程可以递归进行,以此达到整个序列称为有序序列

  • 快速排序示意图

  • 代码实现:

    • 上图基准值取的最后一个,所以先从前遍历,下面的代码取第一个数作为基准值,所以后面的变量先遍历
//快速排序

public static void quickSorting(int[] arr,int left,int right){

    if(left >= right){

        return;

    }

    int low = left;

    int high = rzight;

    int temp = 0;//基准值

    temp = arr[low];//基准值取第一个

    while(low < high){

        //尾部先向前查找小于基准值的元素

        while(low < high && arr[high] >= temp){

            high --;

        }

        //找到了就赋给left

        arr[low] = arr[high];

        //前端向后查找大于基准值的元素

        while (low < high && arr[low] <= temp){

            low ++;

        }

        //找到后赋值给right

        arr[high] = arr[low];

    }

    //退出循环时left = right,在基准值正确的位置

    arr[low] = temp;

    //对基准值左右的序列分别进行排序

    quickSorting(arr,left,low - 1);

    quickSorting(arr,low + 1,right);

    //System.out.println(Arrays.toString(arr));

}
  • 快速排序是不稳定的排序算法
  • 快速排序的最好时间复杂度:O(nlogn),最坏时间复杂度:O(n^2),平均时间复杂度:O(nlogn)
  • 最好情况空间复杂度:O(logn),最坏情况空间复杂度:O(n)

4.选择排序(Select Sorting)

  • 介绍

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置的数交换;然后在剩下的数当中再找最小(或者最大)的与第2个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较为止,得到一个从小到大(从大到小)的有序序列

  • 选择排序思路分析

  • 一共进行数组长度 - 1轮排序,每轮排序都有如下循环:

    • 假定当前数是最小数
    • 当前数和后面每个数进行比较,存入更小的数
    • 遍历完序列,得到本轮最小数的下标
  • 代码实现:
//选择排序

public static void selectSorting(int[] arr){

    int minIndex = 0;//存储最小数的下标

    int temp = 0;//交换变量

    for (int i = 0; i < arr.length - 1; i++) {

        minIndex = i;

        for (int j = i + 1; j < arr.length; j++) {

            if (arr[j] < arr[minIndex]){

                minIndex = j;

            }

        }

        temp = arr[minIndex];

        arr[minIndex] = arr[i];

        arr[i] = temp;

        System.out.println("第" + (i + 1) + "趟排序后的数组:");

        System.out.println(Arrays.toString(arr));

    }

}
  • 选择排序是不稳定的排序算法
  • 选择排序的最好时间复杂度:O(n^2)最坏时间复杂度:O(n^2),平均时间复杂度:O(n^2)
  • 空间复杂度:O(1)

5.*堆排序(Heap Sorting)

  • 堆排序是利用堆这种数据结构设计的一种算法,是一种选择排序

  • 堆是具有以下性质的完全二叉树:

    • 大顶堆:每个结点的值都大于等于其左右孩子结点的值(arr[i] >= arr[2 * i + 1] && arr[i] >= arr[2 * i + 2]
    • 小顶堆:每个结点的值都小于等于其左右孩子结点的值(arr[i] <= arr[2 * i + 1] && arr[i] <= arr[2 * i + 2]
    • 一般升序采用大顶堆,降序采用小顶堆
  • 基本思想:

    • 首先将待排序的数组构造成一个大顶堆,此时,整个数组的最大值就是堆结构的顶端
    • 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
    • 将剩余的n-1个数再构造成大顶堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
  • 堆排序分析图

以数组 {3,6,8,5,7} 为例

  • 构造堆

插入6的时候,6大于他的父结点3,即arr(1)>arr(0),则交换;此时,保证了0~1位置是大顶堆结构

插入8的时候,8大于其父结点6,即arr(2)>arr(0),则交换;此时,保证了0~2位置是大根堆结构

插入5的时候,5大于其父结点3,则交换,交换之后,5又发现比8小,所以不交换;此时,保证了0~3位置大根堆结构

插入7的时候,7大于其父结点5,则交换,交换之后,7又发现比8小,所以不交换;此时整个数组已经是大根堆结构

  • 固定最大值再构造堆

我们已经得到一个大顶堆,下面将顶端的数与最后一位数交换,然后将剩余的数再构造成一个大顶堆

此时最大数8已经来到末尾,则固定不动,后面只需要对顶端的数据进行操作即可,拿顶端的数与其左右孩子较大的数进行比较,如果顶端的数大于其左右孩子较大的数,则停止,如果顶端的数小于其左右孩子较大的数,则交换,然后继续与下面的孩子进行比较

下图中,5的左右孩子中,左孩子7比右孩子6大,则5与7进行比较,发现5<7,则交换;交换后,发现5已经大于他的左孩子,说明剩余的数已经构成大顶堆,后面就是重复固定最大值,然后构造大根堆

顶端数7与末尾数3进行交换,固定好7

剩余的数开始构造大根堆 ,然后顶端数与末尾数交换,固定最大值再构造大根堆,重复执行上面的操作,最终会得到有序数组

  • 代码实现:
//堆排序

public static void heapSort(int[] arr) {

    //构造大顶堆

    heapInsert(arr);

    int size = arr.length;

    while (size > 1) {

        //固定最大值

        swap(arr, 0, size - 1);

        size--;

        //构造大顶堆

        heapify(arr, 0, size);

    }

    System.out.println(Arrays.toString(arr));

}



//堆排序——构造大顶堆(通过新插入的数上升)

public static void heapInsert(int[] arr) {

    for (int i = 1; i < arr.length; i++) {

        //当前插入的索引

        int currentIndex = i;

        //父结点索引

        int fatherIndex = (currentIndex - 1) / 2;

        //如果当前插入的值大于其父结点的值,则交换值,并且将索引指向父结点

        //然后继续和上面的父结点值比较,直到不大于父结点,则退出循环

        while (arr[currentIndex] > arr[fatherIndex]) {

            //交换当前结点与父结点的值

            swap(arr, currentIndex, fatherIndex);

            //将当前索引指向父索引

            currentIndex = fatherIndex;

            //重新计算当前索引的父索引

            fatherIndex = (currentIndex - 1) / 2;

        }

    }

}



//堆排序——将剩余的数构造成大顶堆(通过顶端的数下降)

public static void heapify(int[] arr, int index, int size) {

    int left = 2 * index + 1;

    int right = 2 * index + 2;

    while (left < size) {

        int largestIndex;

        //判断孩子中较大的值的索引(要确保右孩子在size范围之内)

        if (arr[left] < arr[right] && right < size) {

            largestIndex = right;

        } else {

            largestIndex = left;

        }

        //比较父结点的值与孩子中较大的值,并确定最大值的索引

        if (arr[index] > arr[largestIndex]) {

            largestIndex = index;

        }

        //如果父结点索引是最大值的索引,那已经是大顶堆了,则退出循环

        if (index == largestIndex) {

            break;

        }

        //父结点不是最大值,与孩子中较大的值交换

        swap(arr, largestIndex, index);

        //将索引指向孩子中较大的值的索引

        index = largestIndex;

        //重新计算交换之后的孩子的索引

        left = 2 * index + 1;

        right = 2 * index + 2;

    }



}



//堆排序——交换数组中两个元素的值

public static void swap(int[] arr, int i, int j) {

    int temp = arr[i];

    arr[i] = arr[j];

    arr[j] = temp;

}
  • 堆排序是不稳定的排序算法
  • 堆排序的最好时间复杂度:O(nlogn)最坏时间复杂度:O(nlogn),平均时间复杂度:O(nlongn)
  • 堆排序的空间复杂度:O(1)

6.插入排序(Insertion Sorting)

  • 介绍

将待排序的序列看成一个有序序列和一个无序序列,初始时有序序列只有一个元素,无序序列有 n - 1 个元素,每次从无序序列中取出第一个元素,把它依次与有序表中的每一个元素比较,将它插入到适当的位置,使之成为新的有序表,直到无序表中没有元素。

  • 插入排序分析图

  • 进行序列长度 - 1 次排序
  • 每次排序都为无序表中的第一个元素找到合适的位置
  • 代码实现:
//插入排序

public static void insertionSorting(int[] arr) {

    int insertValue = 0;//要排序的元素

    int insertIndex = 0;//插入的位置

    for (int i = 1; i < arr.length; i++) {

        insertValue = arr[i];//无序表的第一个元素

        insertIndex = i - 1;//从有序表的最后一个元素开始比较

        //比插入元素大的值向后移

        while (insertIndex >= 0 && insertValue < arr[insertIndex]) {

            arr[insertIndex + 1] = arr[insertIndex];

            insertIndex--;//索引向前遍历

        }

        //退出循环说明找到插入的位置

        arr[insertIndex + 1] = insertValue;

        System.out.println("第" + i + "轮排序后:");

        System.out.println(Arrays.toString(arr));

    }

}
  • 插入排序是稳定的排序算法
  • 插入排序的最好时间复杂度:O(n)最坏时间复杂度:O(n^2),平均时间复杂度:O(n^2)
  • 空间复杂度:O(1)

7.*希尔排序(Shell Sorting)

  • 介绍

希尔排序是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序

  • 基本思想:把序列按下标的一定增量分组,对每组使用简单插入排序;每次排序后折半缩小分组的组数再排序,随着分组的减少每组包含的关键词增多,当分组减至1时,整个序列成为一组,再进行最后一次简单插入排序,算法终止。
  • 希尔排序分析图

  • 代码实现
//希尔排序

public static void shellSorting(int[] arr){

    int gap = arr.length / 2;//增量值

    while (gap >= 1){//循环至序列分为一组,即增量为1

        //从 i = gap 位置开始遍历每组数据

        for (int i = gap; i < arr.length; i++) {

            int index = i;//插入位置

            int temp = arr[i];//保存要排序的数

            //本组前一位数大于temp就向后移

            while (index - gap >= 0 && arr[index - gap] > temp){

                arr[index] = arr[index - gap];

                index -= gap;

            }    

            //退出循环说明找到了插入位置为index

            arr[index] = temp;

        }

        //缩小增量

        gap /= 2;

    }

    System.out.println(Arrays.toString(arr));

}
  • 希尔排序是不稳定的排序算法
  • 希尔排序的最好时间复杂度:O(nlogn),最坏时间复杂度:O(n^2logn),平均时间复杂度:O(nlogn)
  • 希尔排序空间复杂度:O(1)

8.*归并排序(Merge Sorting)

  • 介绍

归并排序是利用归并的思想实现的排序方法,采用的经典的分治策略。

  • 归并排序分析图

    • 分治过程分析:

在分的阶段可以理解为递归拆分子序列的过程

在治的阶段将两个已经有序的子序列合并成一个有序序列,上图是最后一次合并的步骤

  • 代码实现
//归并排序

//归并——分离算法

public static void mergeSorting(int[] arr, int left, int right, int[] temp) {

    if (left < right) {

        int mid = (left + right) / 2;//求中间索引

        //递归分离左边数组

        mergeSorting(arr, left, mid, temp);

        //递归分离右边数组

        mergeSorting(arr, mid + 1, right, temp);

        //合并

        merge(arr, left, mid, right, temp);

    }

}



//归并——合并算法

 /**

 *  @param arr:需排序的数组

 *  @param left:数组第一个索引

 *  @param mid:数组中间索引

 *  @param right:数组最后一个元素的索引

 *  @param temp:辅助数组

 */

public static void merge(int[] arr, int left, int mid, int right, int[] temp) {

    int i = left;//左边数组初始索引

    int j = mid + 1;//右边数组初始索引

    int t = left;//temp数组的初始索引

    //左右数组比较元素把较小值存入temp,一边数组检测完为止

    while (i <= mid && j <= right) {

        temp[t++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];

    }

    //退出循环说明有一边数组遍历完毕,另一边直接添加到temp

    //左边数组还有剩余元素

    while (i <= mid) temp[t++] = arr[i++];

    //右边数组还有剩余元素

    while (j <= right) temp[t++] = arr[j++];

    //将temp数组复制到arr

    for (int k = left; k <= right; k++) {

        arr[k] = temp[k];

    }

    System.out.println(Arrays.toString(arr));

}
  • 归并排序是稳定的排序算法
  • 归并排序的最好时间复杂度:O(nlogn)最坏时间复杂度:O(nlogn),平均时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

9.基数排序(Radix Sorting)

  • 介绍

基数排序是桶排序(Bucket Sorting)的拓展,它是通过键值的各个位的值,将要排序的元素分配到某些“桶”中,达到排序的目的

  • 桶排序的基本思想:创建若干数量的数组作为桶(一般为初始数组的长度),各个桶用来存放不同范围区间的元素,各个桶之间自排序,最后合并为一个有序的数组

  • 基数排序基本思想:将所有待排序数统一为同样的数位长度,数位较短的前面补零,然后依次从最低位进行排序。从最低位一直排序到最高位以后,数列就变成了一个有序序列
  • 基数排序分析图

以数组 {53,3,542,748,14,214}为例:

  • 排序的轮次取决于数列中最大数的数位maxLength,例子中是百位,所以进行了3轮排序
  • 需要1个有10个一维数组bucket[][]的二维数组存储各个位数对应的值,一维数组的长度为arr.length
  • 需要1个数组counts记录每个位数对应的一维数组的元素个数(这个数组每轮要清空,用来覆盖上一轮的排序)
  • 遍历arr数组,将本次遍历的位数放入对应的桶中,遍历结束后按顺序从桶中取出元素放入arr
  • 遍历全部有数据的桶(counts大于零说明本轮该桶中有数据),依次取出存放进arr
  • 遍历maxLength次,arr数组变为有序
  • 代码实现:
//基数排序

public static void radixSorting(int arr[]) {

    //找到元素的最大位数

    int maxLength = 0;//最大位数

    int max = 0;//最大的数

    for (int i = 0; i < arr.length; i++) {//遍历数组找到最大的数

        max = arr[i] > max ? arr[i] : max;

    }

    maxLength = (max + "").length();//求出最大数的位数

    int[][] bucket = new int[10][arr.length];//作为桶的数组

    int[] counts = new int[10];//记录每个桶内元素个数的数组

    //整个循环进行maxLength次

    for (int i = 0, n = 1; i < maxLength; i++, n *= 10) {//每轮循环取高一位,n的步长为10

        //第 i 次循环,遍历数组arr

        for (int j = 0; j < arr.length; j++) {

            //求该轮的位数值

            int Byte = arr[j] / n % 10;

            //按位数值把该元素放入桶中

            bucket[Byte][counts[Byte]++] = arr[j];

        }

        //一轮循环结束,按顺序从桶中取出元素放入arr

        int index = 0;//辅助索引循环插入arr

        for (int j = 0; j < bucket.length; j++) {

            if (counts[j] > 0) {//如果该桶中有数据则进行遍历取出

                for (int k = 0; k < counts[j]; k++) {

                    arr[index++] = bucket[j][k];

                }

            }

            //将每个counts[j]重置

            counts[j] = 0;

        }

        System.out.println("第" + (i + 1) + "轮:" + Arrays.toString(arr));

    }

}
  • 基数排序是稳定的排序算法
  • 基数排序的最好时间复杂度:O(n * k)最坏时间复杂度:O(n * k),平均时间复杂度:O(n * k) (k 为桶的个数)
  • 空间复杂度:O(n + k)

10.总结与对比

平均时间复杂度最好情况最坏情况空间复杂度稳定性
冒泡排序O(n^2)O(n)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
插入排序O(n^2)O(n)O(n^2)O(1)稳定
希尔排序O(nlogn)O(nlogn)O(n^2logn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(nlogn)O(n^2)O(n)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
基数排序O(n * k)O(n * k)O(n * k)O(n + k)稳定