十大排序算法总结

时间复杂度为O(n²)的排序算法

一、冒泡排序
1.介绍:

将相邻元素两两之间相互比较,当一个元素大于右侧相邻元素时,交换它们的位置,小于或等于时位置不变。最右边为有序区,刚开始为空,每进行一轮,就选出一个最大的元素放在右边有序区,第二轮,倒数第二大的,以此类推。有序区的元素不再参与到后面的比较。当有n个元素时,总共需要比较n-1轮,每一轮比较n-1次(第一次比较,所有元素都要比较,为n-1,第二次,有序区已经有一个元素,剩余n-1个元素,两两之间比较n-2次…)。冒泡排序也是稳定排序,即值相等的元素并不会打乱原来的顺序。最好情况元素已经为升序排序,时间复杂度为O(n),最坏情况为降序排列O(n²),平均为O(n²),空间复杂度为O(1)。

图解:
在这里插入图片描述
代码:

    public static void bubbleSort(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                int temp = 0;
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(arr));
    }
2.优化
2.1 逻辑值判断

在倒数第二轮时,还剩两个元素,此时元素已经有序,最后一轮无需再比较。解决办法,做标记,初始都为true,若存在交换则为false,否则就直接跳出大循环。

public static void bubbleSort1(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) { // 控制轮数
            boolean flag = true;
            for (int j = 0; j < arr.length - 1 - i; j++) { // 控制次数
                int temp = 0;
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                }
            }

            // 不存在元素交换,证明当前已经有序,就不用再进行后面的比较
            if (flag) {
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
2.2 有序区边界的问题

若排序时(2,1,3,4,5,6)后面的数已经是有序状态,那么有序区就不能还根据轮数来进行判断,要记录下最后一次交换的位置,在那个位置后之后的都为有序区,每一轮遍历所有的无序区的数据

在这里插入图片描述

public static void bubbleSort2(int[] arr) {
        int lastExchangeIndex = 0;// 最后一次交换的位置,该位置以后的都为有序区
        int sortBorder = arr.length - 1;// 无序区的边界
        for (int i = 0; i < arr.length - 1; i++) { // 控制轮数
            boolean flag = true;
            for (int j = 0; j < sortBorder; j++) { // 控制次数
                int temp = 0;
                if (arr[j] > arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    flag = false;
                    lastExchangeIndex = j;
                }
            }
            sortBorder = lastExchangeIndex;
            // 不存在元素交换,证明当前已经有序,就不用再进行后面的比较
            if (flag) {
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
二.鸡尾酒排序
1.介绍

鸡尾酒排序是冒泡排序的优化,冒泡排序是从左到右比较元素,单向的进行交换,鸡尾酒排序的元素比较和交换过程是双向的。第一轮从左到右把最大的元素移动到右边,第二轮从右往左把最小的移动到左边,然后是第二大和第二小的元素,它也是稳定性算法。时间复杂度为O(n²),空间复杂度为O(1)。

public static void cocktailSort(int[] arr) {
        int m = 0, n = arr.length - 1;
        while (m < n) {
            for (int i = m; i < n; i++) {
                int temp = 0;
                if (arr[i] > arr[i + 1]) {
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                }
            }
            n--;
            for (int i = n; i > m; i--) {
                int temp = 0;
                if (arr[i] < arr[i - 1]) {
                    temp = arr[i];
                    arr[i] = arr[i - 1];
                    arr[i - 1] = temp;
                }
            }
            m++;
        }
        System.out.println(Arrays.toString(arr));
    }
2.优化

采用和冒泡排序相同的优化方式,逻辑值判断和有序区边界

public static void cocktailSort2(int[] arr) {
        int m = 0, n = arr.length - 1;
        int lastExchangeIndex = m;// 最后一次交换的位置,该位置以后的都为有序区
        int rightSortBorder = n;// 右侧无序区的边界
        int leftSortBorder = m;
        while (m < n) {
            boolean flag = true;
            for (int i = m; i < rightSortBorder; i++) {
                int temp = 0;
                if (arr[i] > arr[i + 1]) {
                    temp = arr[i];
                    arr[i] = arr[i + 1];
                    arr[i + 1] = temp;
                    flag = false;
                    lastExchangeIndex = i;
                }
            }
            rightSortBorder = lastExchangeIndex;
            n--;
            if (flag) {
                break;
            }
            flag = true;
            for (int i = n; i > leftSortBorder; i--) {
                int temp = 0;
                if (arr[i] < arr[i - 1]) {
                    temp = arr[i];
                    arr[i] = arr[i - 1];
                    arr[i - 1] = temp;
                    flag = false;
                    lastExchangeIndex = i;
                }
            }
            leftSortBorder = lastExchangeIndex;
            m++;
            if (flag) {
                break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
三.选择排序
1.介绍

选择排序,定第一个元素为有序区,在无序区中每一轮选出一个最小值,交换到左边有序区,关键在于找到无序区中最小元素的位置,交换两个元素。冒泡排序是相邻元素间比较和交换,选择排序是通过对整体的选择。它是不稳定算法。时间复杂度为O(n²),空间复杂度为O(1)。

步骤:

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第 2 步,直到所有元素均排序完毕。

在这里插入图片描述

public static void selectionSort(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int minIndex = i;// 无序区最小元素的位置
            // j:无序区的起始位置
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[minIndex]) {
                    minIndex = j;
                }
            } 
            if (i != minIndex) {
                int temp = arr[i];
                arr[i] = arr[minIndex];
                arr[minIndex] = temp;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
四.插入排序
1.介绍

插入排序操作类似于打扑克牌,摸牌并将其从大到小排列。每次摸到一张牌后,根据其点数与前面有序区进行比较插入到确切位置。平均时间复杂度为O(n²),最好情况元素已经为升序排序,时间复杂度为O(n),最坏情况为降序排列O(n²),空间复杂度为O(1)。冒泡排序是拿一个元素和无序区元素进行比较,移到到有序区,有序区的元素不再参与到比较,而插入排序是拿无序区的元素和有序区的元素进行比较,在有序区的合适位置插入。

步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤 3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤 2~5。

在这里插入图片描述

public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0; j--) {//移动新元素到有序数组,j指向i-1,该位置是有序数组的末尾位置
                if (arr[j] > arr[j + 1]) {//j+1指向新加入的元素
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                } else
                    break;
            }
        }
        System.out.println(Arrays.toString(arr));
    }
五、时间复杂度为O(n²)的排序算法的对比

性能:

冒泡排序和插入排序取决于数据的有序程度,如果原始数据已经接近有序,只需要比较较少的交换次数就可以完成排序。插入排序的性能略高于冒泡排序,冒泡排序每轮需要进行多次比较和交换操作,交换操作的开销较大。而插入排序每轮只需要进行一次插入操作,插入排序每次只需要将一个元素插入到已排序序列中,因此,对于小规模的数据集,插入排序的性能更优。然而,对于大规模的数据集,冒泡排序和插入排序都不是最优选择,更高效的排序算法如快速排序和归并排序更适合处理大规模数据。选择排序,它的元素比较次数是固定的,和原始数据的有序程度无关。而鸡尾酒排序在在某种程度上减少冒泡排序的轮数,和插入排序相比,性能相差不大。

稳定性:

冒泡排序和插入排序是稳定性排序,选择排序是非稳定性排序。

时间复杂度为O(nlogn)的排序算法
一、快速排序
1.介绍

快速排序使用分治法的思想,在每一轮挑选出一个基准元素,比它大的移动到它的右边,比它小的移到到左边。原数组在每一轮被拆分成两部分,每一部分又在下一轮拆分成两部分,知道不可再分为止。

步骤

1.从数列中挑出一个元素成为基准,

2.分区,重新排序数列,比基准小的移到左边,大的移到右边

3.递归的把小于基准的数列和大于基准的数列进行排序。

排序关键在于寻找到临界值,有两种方法,左右指针法和快慢指针法。平均时间复杂度为O(nlogn),最坏情况,第一个值是数列的最大值或最小值,发挥不出分治法的优势,时间复杂度为O(n²)。快速排序的空间复杂度就是递归算法的空间复杂度 (O(logn)),快速排序是一种不稳定排序。

1.左右指针法:

若key设定为最左边则right先开始移动,反之则left先移动。right从后往前找到第一个不大于key的元素停止,left从前往后找到第一个不小于key的元素停止,交换left和right所对应的元素,直到left= end,while循环结束。然后交换begin(key)和left,返回left或right=index。此时key左边全为小于它的元素,key右边全为大于它的元素。分为两个数组,arr[begin,index-1]和arr[index+1,end],然后递归调用这两个数组。

public static int partition(int[] arr, int begin, int end) {
        int key = arr[begin];//若选取最左边为key,则end先开始走
        //定义left和right两个变量,left指向最左边,right指向最右边,来与key做比较
        int left = begin;
        int right = end;
        while (left < right) {
            while (arr[right] >= key && right > left) {
                right--;
            }
            while (arr[left] <= key && left < right) {
                left++;
            }
            swap(arr, left, right);
        }
        swap(arr, begin, left);
        return left;
    }

2.快慢指针法:

以最右边为基准,fast指针指向小于基准的数时,slow+1且不等于fast,交换,循环结束后,交换基准值与slow+1,分别递归再进行左右两数组的排序。

public static int partition1(int[] arr, int begin, int end) {
        if (begin >= end) {
            return begin;
        }
        int slow = begin - 1;
        int pivot = arr[end];
        for (int fast = begin; fast < end; fast++) {
            if (arr[fast] < pivot) {
                slow++;
                if (slow != fast) {
                    swap(arr, fast, slow);
                }
            }
        }
        swap(arr, slow + 1, end);
        return slow + 1;
    }
public static void QuickSort(int[] arr, int begin, int end) {
        if (begin < end) {
            int index = partition(arr, begin, end);
            // int index = partition1(arr, begin, end);
            QuickSort(arr, begin, index - 1);
            QuickSort(arr, index + 1, end);
        }
    }
2.优化
2.1 切换到插入排序

因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。

2.2 三数取中

最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。一种折中方法是取 3 个元素,并将大小居中的元素作为切分元素。

2.3 三向切分

快排在处理大量重复元素的数组,会产生大量的临时数组,占用大量的内存空间。对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。

三向切分快速排序对于有大量重复元素的随机数组可以在线性时间内完成排序。

public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            int[] pivotRange = partition(arr, low, high);
            quickSort(arr, low, pivotRange[0] - 1);
            quickSort(arr, pivotRange[1] + 1, high);
        }
    }

    public static int[] partition(int[] arr, int low, int high) {
        int pivot = arr[low];
        int lt = low;
        int gt = high;
        int i = low + 1;

        while (i <= gt) {
            if (arr[i] < pivot) {
                swap(arr, i, lt);
                i++;
                lt++;
            } else if (arr[i] > pivot) {
                // 当前元素大于pivot,移动gt指针,不移动i指针,是因为还要降运动过来的元素进行判断
                swap(arr, i, gt);
                gt--;
            } else {
                i++;
            }
        }

        return new int[]{lt, gt};
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
二、归并排序

归并排序采用分治法的思想。其基本思想是将待排序的数组分成两个子数组,然后对每个子数组进行递归排序,最后再将排好序的子数组合并成一个有序数组。归并排序的时间复杂度为 O(n log n),是一种稳定的排序算法。mergeSort方法用于递归地拆分数组,直到子数组的大小变为1,而merge方法用于将两个已排序的子数组合并。它的空间复杂度为O(n),需要开辟一个同样大小的数组来进行合并。

步骤:

归并排序算法是一个递归过程,边界条件为当输入序列仅有一个元素时,直接返回,具体过程如下:

  1. 如果输入内只有一个元素,则直接返回,否则将长度为 n 的输入序列分成两个长度为 n/2 的子序列;
  2. 分别对这两个子序列进行归并排序,使子序列变为有序状态;
  3. 设定两个指针,分别指向两个已经排序子序列的起始位置;
  4. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间(用于存放排序结果),并移动指针到下一位置;
  5. 重复步骤 3 ~4 直到某一指针达到序列尾;
  6. 将另一序列剩下的所有元素直接复制到合并序列尾。
private static void mergeSort(int[] array, int[] tempArray, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(array, tempArray, left, mid);
            mergeSort(array, tempArray, mid + 1, right);
            merge(array, tempArray, left, mid, right);
        }
    }

    private static void merge(int[] array, int[] tempArray, int left, int mid, int right) {
        int i = left; // 左半部分数组的起始位置
        int j = mid + 1; // 右半部分数组的起始位置
        int k = left; // 临时数组的起始位置

        // 比较左右两个子数组的元素,将较小的元素放入临时数组
        while (i <= mid && j <= right) {
            if (array[i] <= array[j]) {
                tempArray[k] = array[i];
                i++;
            } else {
                tempArray[k] = array[j];
                j++;
            }
            k++;
        }

        // 将剩余的元素复制到临时数组中
        while (i <= mid) {
            tempArray[k] = array[i];
            i++;
            k++;
        }

        while (j <= right) {
            tempArray[k] = array[j];
            j++;
            k++;
        }

        // 将临时数组的内容复制回原数组
        for (k = left; k <= right; k++) {
            array[k] = tempArray[k];
        }
    }
三、希尔排序

希尔排序是一种改进的插入排序算法,它通过将待排序的元素按照一定的间隔进行分组,然后对每个分组进行插入排序,最后逐步缩小间隔,直到间隔为1时完成最后一次插入排序,从而达到排序的目的。希尔排序是一种不稳定的排序算法。希尔排序的效率取决于增量值gap的选取,时间复杂度并不是一个定值,最快可以达到O(n^1.3)。

步骤

  1. 选择一个间隔序列(也称为增量序列),该序列是一个递减的正整数序列。常用的间隔序列有希尔原始序列(N/2,N/4,N/8…)和Hibbard序列(1,3,7…)等。
  2. 根据选定的间隔序列,将待排序的元素分成多个子序列,每个子序列包含间隔个元素。对每个子序列进行插入排序。
  3. 逐步缩小间隔,重复步骤2,直到间隔为1。
public static void shellSort(int[] array) {
        int n = array.length;

        // 初始步长设为数组长度的一半,然后每次减半
        for (int gap = n / 2; gap > 0; gap /= 2) {
            // 进行插入排序
            for (int i = gap; i < n; i++) {
                int temp = array[i];
                int j = i;

                // 将当前元素与前面的已排序元素比较,如果小于前面的元素,则向前移动位置
                while (j >= gap && array[j - gap] > temp) {
                    array[j] = array[j - gap];
                    j -= gap;
                }
                // 将当前元素插入正确的位置
                array[j] = temp;
            }
        }
    }
四、堆排序

堆排序是一种基于二叉堆数据结构的排序算法,堆是一种完全二叉树,用数组来进行储存,时间O(nlogn) 空间O(1)。利用最大堆/最小堆中子节点的值小于或大于父节点的性质进行的排序。堆排序是一种不稳定排序。

步骤:

  1. 将无序数组构建成二叉堆(升序-最大堆,降序-最小堆)
  2. 循环删除堆顶的元素,替换到二叉堆的末尾,调整产生新的堆顶
  3. 重复步骤2,直到整个数组有序。
public static void heapSort(int[] array) {
        int n = array.length;

        // 将数组构建成最大堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(array, n, i);
        }

        // 逐个从堆中提取元素
        for (int i = n - 1; i >= 0; i--) {
            // 将当前的根节点与最后一个节点交换
            int temp = array[0];
            array[0] = array[i];
            array[i] = temp;

            // 对剩余的堆进行调整
            heapify(array, i, 0);
        }
    }

    private static void heapify(int[] array, int n, int i) {
        int largest = i;
        int leftChild = 2 * i + 1;
        int rightChild = 2 * i + 2;

        // 在根、左子和右子中找到最大的
        if (leftChild < n && array[leftChild] > array[largest]) {
            largest = leftChild;
        }
        if (rightChild < n && array[rightChild] > array[largest]) {
            largest = rightChild;
        }

        // 如果最大的不是根,则交换并继续heapify
        if (largest != i) {
            int swap = array[i];
            array[i] = array[largest];
            array[largest] = swap;

            heapify(array, n, largest);
        }
    }
五、时间复杂度为O(nlogn)的排序算法的对比

共同点

归并排序、快速排序和堆排序都是基于分而治之的思想的排序算法,它们的共同点是通过将原问题拆分成更小的子问题,并对子问题进行处理,最后将子问题的解合并起来得到原问题的解。

性能

快速排序的平均时间复杂度是O(nlogn),但是在极端情况下,最坏的时间复杂度是O(n²),而归并排序和堆排序的时间复杂度稳定在O(nlogn)。但堆排序的性能比这两者性能略低,是因为堆排序的父子结点在内存中不是连续的。

稳定性

归并排序是稳定排序,快速排序和堆排序是不稳定排序。

此外,快速排序和堆排序是原地排序,不需要开辟额外空间,归并排序的merge操作组要借助额外数组来完成。

Java的Arrays.sort()方法

Java的Arrays.sort()方法

在原始数据类型的比较中,使用三向切分的快速排序。

若数组长度较小,长度<47,将使用插入排序。因为在数组长度很小时插入排序将快于快排。若数组长度>=47,将使用快速排序。当数组长度>=286时,使用三向切分的快速排序.当数组长度>=286时,DualPivotQuicksort.sort方法,会对数组的结构性进行判断,若数组基本有序,则将使用归并排序,若数组的元素排列较为混乱,进行快速排序。虽然快排和归并排序的(平均)时间复杂度是一样的,但对于基本有序的数组,归并排序的速度会比快速排序快,而对于近乎无序的数组,归并排序速度会比快速排序慢。
在引用类型的数据比较中,使用归并排序。

注意,如果要对一个Object数组进行排序,数组中的元素必须实现Comparable接口,否则将会抛出ClassCastException异常。

三种非比较类排序
一、计数排序

这种排序不是基于元素间的比较,而是利用数组下标来确定元素正确的位置。根据原始数据的范围,找出最大值max和最小值min,建立一个长度为max-min+1的数组,初始值都为0,然后遍历原始数据,每一个元素按值找到数组对应下标并加1,数组下标的值对应这个数出现的次数。

在这里插入图片描述

步骤

  1. 找出待排序的数组中最大和最小的元素
  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
  3. 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
  4. 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
private static int[] getMinAndMax(int[] arr) {
    int maxValue = arr[0];
    int minValue = arr[0];
    for (int i = 0; i < arr.length; i++) {
        if (arr[i] > maxValue) {
            maxValue = arr[i];
        } else if (arr[i] < minValue) {
            minValue = arr[i];
        }
    }
    return new int[] { minValue, maxValue };
}

public static int[] countingSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int[] extremum = getMinAndMax(arr);
    int minValue = extremum[0];
    int maxValue = extremum[1];
    int[] countArr = new int[maxValue - minValue + 1];
    int[] result = new int[arr.length];

    for (int i = 0; i < arr.length; i++) {
        countArr[arr[i] - minValue] += 1;
    }
    for (int i = 1; i < countArr.length; i++) {
        countArr[i] += countArr[i - 1];
    }
    for (int i = arr.length - 1; i >= 0; i--) {
        int idx = countArr[arr[i] - minValue] - 1;
        result[idx] = arr[i];
        countArr[arr[i] - minValue] -= 1;
    }
    return result;
}

计数排序是一种稳定排序,当输入的元素是 n 个 0 到 k 之间的整数时,时间复杂度为O(n+k),空间复杂度为O(n+k)。

缺点

  1. 只能用于整数
  2. 当数列中最大值和最小值差距过大时,开辟的数组过长,会大量浪费空间。
二、桶排序

桶排序是计数排序的升级版,桶排序中的桶表示一个区间范围,里面可以装一个或多个元素,然后在对每一个桶中的元素进行排序(排序算法可自由选择)。平均时间复杂度是O(n+k),最好情况O(n+k),最坏情况O(n²).

为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

步骤

  1. 确定桶的数量以及桶的区间范围
  2. 遍历原始数列,把元素放入对应区间的桶中
  3. 每个桶的内部分别进行排序
  4. 遍历所有的桶,输出所有元素

下面代码中,桶的数量为原始数列的元素数量,区间跨度为(最大值-最小值)/ (桶的数量-1),每个桶内部的排序算法使用JDK自带的Collections.sort()方法,底层是归并排序,时间复杂度为O(nlogn)。

public static double[] bucketSort(double[] array) {
        //1.得到数列的最大值和最小值,并算出差值d
        double max = array[0];
        double min = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i] > max) {
                max = array[i];
            }
            if (array[i] < min) {
                min = array[i];
            }
        }
        double d = max - min;
        //2.初始化桶
        int bucketNum = array.length;
        ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum);
        for (int i = 0; i < bucketNum; i++) {
            bucketList.add(new LinkedList<Double>());
        }
        //3.遍历原始数组,将每个元素放入桶中

        for (int i = 0; i < array.length; i++) {
            int num = (int) ((array[i] - min) * (bucketNum - 1) / d);
            bucketList.get(num).add(array[i]);
        }

        //4.对每个通内部进行排序
        for (int i = 0; i < bucketList.size(); i++) {
        //JDK底层采用了归并排序或归并的优化版本
            Collections.sort(bucketList.get(i));
        }
        //5.输出全部元素
        double[] sortedArray = new double[array.length];
        int index = 0;
        for (LinkedList<Double> list : bucketList) {
            for (double element : list) {
                sortedArray[index] = element;
                index++;
            }
        }

        return sortedArray;

    }


    public static void main(String[] args) {
        double[] array = new double[]{4.12, 6.421, 0.0023, 3.0, 2.123, 8.122, 4.12, 10.09};
        double[] sortedArray = bucketSort(array);
        System.out.println(Arrays.toString(sortedArray));
    }

缺点:

如果桶内元素分布极不均衡,极端条件下第一个桶有n-1个元素,最后一个桶有一个元素,此时的时间复杂度将会退化为O(n²)。

三、基数排序

原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。平均时间复杂度O(n×k),最好情况O(n×k ),最坏情况O(n×k),空间复杂度O(n+k)。

  • MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
  • LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序

步骤

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  2. 从最低位开始,依次进行一次排序。
  3. 这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。
public static int[] radixSort(int[] arr) {
    if (arr.length < 2) {
        return arr;
    }
    int N = 1;
    int maxValue = arr[0];
    for (int element : arr) {
        if (element > maxValue) {
            maxValue = element;
        }
    }
    while (maxValue / 10 != 0) {
        maxValue = maxValue / 10;
        N += 1;
    }
    for (int i = 0; i < N; i++) {
        List<List<Integer>> radix = new ArrayList<>();
        for (int k = 0; k < 10; k++) {
            radix.add(new ArrayList<Integer>());
        }
        for (int element : arr) {
            int idx = (element / (int) Math.pow(10, i)) % 10;
            radix.get(idx).add(element);
        }
        int idx = 0;
        for (List<Integer> l : radix) {
            for (int n : l) {
                arr[idx++] = n;
            }
        }
    }
    return arr;
}

三种非比较排序对比:

三种都是稳定排序。都属于线性时间复杂度的排序算法。计数排序的时间复杂度是O(n+k),其中k是原始数组的整数范围,桶排序在桶数量为n时,时间复杂度为O(n),基数排序的时间复杂度是O(k(n+m)),k是元素的最大位数,m是每一位的取值范围。

  • 基数排序:根据键值的每位数字来分配桶
  • 计数排序:每个桶只存储单一键值
  • 桶排序:每个桶存储一定范围的数值
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

学抓哇的小杨

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

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

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

打赏作者

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

抵扣说明:

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

余额充值