文章目录
- 题目
- 标题和出处
- 难度
- 题目描述
- 要求
- 示例
- 数据范围
- 前言
- 冒泡排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 选择排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 插入排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 希尔排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 归并排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 快速排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 堆排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 计数排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 桶排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
- 基数排序
- 原理
- 示例
- 代码
- 复杂度分析
- 稳定性分析
题目
标题和出处
标题:排序数组
出处:912. 排序数组
难度
4 级
题目描述
要求
给定一个整数数组 nums \texttt{nums} nums,请将该数组升序排序。
示例
示例 1:
输入: nums = [5,2,3,1] \texttt{nums = [5,2,3,1]} nums = [5,2,3,1]
输出: [1,2,3,5] \texttt{[1,2,3,5]} [1,2,3,5]
示例 2:
输入: nums = [5,1,1,2,0,0] \texttt{nums = [5,1,1,2,0,0]} nums = [5,1,1,2,0,0]
输出: [0,0,1,1,2,5] \texttt{[0,0,1,1,2,5]} [0,0,1,1,2,5]
数据范围
- 1 ≤ nums.length ≤ 5 × 10 4 \texttt{1} \le \texttt{nums.length} \le \texttt{5} \times \texttt{10}^\texttt{4} 1≤nums.length≤5×104
- -5 × 10 4 ≤ nums[i] ≤ 5 × 10 4 \texttt{-5} \times \texttt{10}^\texttt{4} \le \texttt{nums[i]} \le \texttt{5} \times \texttt{10}^\texttt{4} -5×104≤nums[i]≤5×104
前言
排序算法有很多种,常见的排序算法有 10 10 10 种,其中 7 7 7 种排序算法是比较类排序算法, 3 3 3 种排序算法是非比较类排序算法。比较类排序算法又可以分成两大类: 4 4 4 种排序算法的平均时间复杂度是 O ( n 2 ) O(n^2) O(n2),称为初级排序算法; 3 3 3 种排序算法的平均时间复杂度是 O ( n log n ) O(n \log n) O(nlogn),称为高级排序算法。其中 n n n 是待排序数组的长度。
因此,常见的 10 10 10 种排序算法可以分成三大类:
- 初级排序算法,平均时间复杂度是 O ( n 2 ) O(n^2) O(n2),包括冒泡排序、选择排序、插入排序和希尔排序;
- 高级排序算法,平均时间复杂度是 O ( n log n ) O(n \log n) O(nlogn),包括归并排序、快速排序和堆排序;
- 线性时间排序算法,时间复杂度是线性,包括计数排序、桶排序和基数排序。
说明:希尔排序是插入排序的优化版本,其时间复杂度在 O ( n 1.25 ) O(n^{1.25}) O(n1.25) 和 O ( n 2 ) O(n^2) O(n2) 之间,虽然时间复杂度一般低于 O ( n 2 ) O(n^2) O(n2),但是高于 O ( n log n ) O(n \log n) O(nlogn),可以认为希尔排序的时间复杂度的一个宽松的上界是 O ( n 2 ) O(n^2) O(n2),因此将希尔排序归入初级排序算法。
以下介绍 10 10 10 种排序算法。在没有特别说明的情况下,默认为升序排序。
由于这道题的数据范围规定数组长度最大为 5 × 1 0 4 5 \times 10^4 5×104,因此初级排序算法可能超出时间限制,高级排序算法和线性时间排序算法可以通过。
冒泡排序
原理
冒泡排序的原理是多次遍历数组,每次比较相邻的两个元素,如果顺序错误则交换。排序过程中,大的元素会移动到数组的末尾,如同水中的气泡上浮到顶端,故名冒泡排序。
初始时,整个数组都是未排序的数组。每一次遍历数组,未排序的子数组中的最大元素将会移动到该子数组的末尾,使得未排序的子数组的长度减 1 1 1。当所有元素都移动到正确的位置时,排序结束。
如果在一次遍历数组的过程中没有发现相邻的两个元素顺序错误的情况,则说明所有的元素都在正确的位置,此时可以提前结束排序。这是冒泡排序的一个可以优化的点。
示例
考虑以下数组的冒泡排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
每一次遍历之后,数组的变化情况如下。
[ 2 , 6 , 1 , 3 , 0 , 7 , 8 , 5 , 4 , 9 ] [2, 6, 1, 3, 0, 7, 8, 5, 4, 9] [2,6,1,3,0,7,8,5,4,9]
[ 2 , 1 , 3 , 0 , 6 , 7 , 5 , 4 , 8 , 9 ] [2, 1, 3, 0, 6, 7, 5, 4, 8, 9] [2,1,3,0,6,7,5,4,8,9]
[ 1 , 2 , 0 , 3 , 6 , 5 , 4 , 7 , 8 , 9 ] [1, 2, 0, 3, 6, 5, 4, 7, 8, 9] [1,2,0,3,6,5,4,7,8,9]
[ 1 , 0 , 2 , 3 , 5 , 4 , 6 , 7 , 8 , 9 ] [1, 0, 2, 3, 5, 4, 6, 7, 8, 9] [1,0,2,3,5,4,6,7,8,9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {boolean needNextPass = true;int length = nums.length;for (int i = 1; i < length && needNextPass; i++) {needNextPass = false;for (int j = 0; j < length - i; j++) {if (nums[j] > nums[j + 1]) {int temp = nums[j];nums[j] = nums[j + 1];nums[j + 1] = temp;needNextPass = true;}}}return nums;}
}
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。冒泡排序最多需要遍历数组 n n n 次,每次遍历数组需要 O ( n ) O(n) O(n) 的时间,比较操作和交换操作的次数都是 O ( n 2 ) O(n^2) O(n2),因此冒泡排序的平均时间复杂度和最差时间复杂度都是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( 1 ) O(1) O(1)。
稳定性分析
由于冒泡排序每次都是比较相邻的两个元素,只有当相邻的两个元素不相等且顺序错误时才会交换,因此相等元素之间的相对顺序总是保持不变。冒泡排序是稳定的排序算法。
选择排序
原理
选择排序的原理是多次遍历数组,每次在未排序的子数组中找到最小元素,将其与该子数组中的首个元素交换。如果未排序的子数组中的最小元素与首个元素相等,则不执行交换操作。
初始时,整个数组都是未排序的数组。每一次遍历数组,未排序的子数组中的最小元素将会移动到该子数组的起始位置,使得已排序的子数组的长度加 1 1 1,未排序的子数组的长度减 1 1 1。当所有元素都交换结束时,排序结束。
示例
考虑以下数组的选择排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
每一次遍历之后,数组的变化情况如下。
[ 0 , 2 , 7 , 1 , 3 , 6 , 8 , 9 , 5 , 4 ] [0, 2, 7, 1, 3, 6, 8, 9, 5, 4] [0,2,7,1,3,6,8,9,5,4]
[ 0 , 1 , 7 , 2 , 3 , 6 , 8 , 9 , 5 , 4 ] [0, 1, 7, 2, 3, 6, 8, 9, 5, 4] [0,1,7,2,3,6,8,9,5,4]
[ 0 , 1 , 2 , 7 , 3 , 6 , 8 , 9 , 5 , 4 ] [0, 1, 2, 7, 3, 6, 8, 9, 5, 4] [0,1,2,7,3,6,8,9,5,4]
[ 0 , 1 , 2 , 3 , 7 , 6 , 8 , 9 , 5 , 4 ] [0, 1, 2, 3, 7, 6, 8, 9, 5, 4] [0,1,2,3,7,6,8,9,5,4]
[ 0 , 1 , 2 , 3 , 4 , 6 , 8 , 9 , 5 , 7 ] [0, 1, 2, 3, 4, 6, 8, 9, 5, 7] [0,1,2,3,4,6,8,9,5,7]
[ 0 , 1 , 2 , 3 , 4 , 5 , 8 , 9 , 6 , 7 ] [0, 1, 2, 3, 4, 5, 8, 9, 6, 7] [0,1,2,3,4,5,8,9,6,7]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 9 , 8 , 7 ] [0, 1, 2, 3, 4, 5, 6, 9, 8, 7] [0,1,2,3,4,5,6,9,8,7]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;for (int i = 0; i < length; i++) {int currMinIndex = i;for (int j = i + 1; j < length; j++) {if (nums[j] < nums[currMinIndex]) {currMinIndex = j;}}if (currMinIndex != i) {int temp = nums[i];nums[i] = nums[currMinIndex];nums[currMinIndex] = temp;}}return nums;}
}
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。由于选择排序需要执行 n ( n − 1 ) 2 \dfrac{n(n - 1)}{2} 2n(n−1) 次比较操作,最多需要执行 n − 1 n - 1 n−1 次交换操作,因此选择排序的平均时间复杂度和最差时间复杂度都是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( 1 ) O(1) O(1)。
稳定性分析
由于选择排序每次交换的元素不一定相邻,因此相等元素之间的相对顺序可能改变。选择排序是不稳定的排序算法。
考虑以下数组的选择排序: [ 5 , 5 , 4 ] [5, 5, 4] [5,5,4]。
第一次遍历,将最小的元素 4 4 4 与下标 0 0 0 处的元素 5 5 5 交换,此时数组变成 [ 4 , 5 , 5 ] [4, 5, 5] [4,5,5],已经有序,之后的遍历不会有任何的元素交换。在仅有的一次元素交换中,下标 0 0 0 处的 5 5 5 交换到下标 2 2 2 处,该元素交换前位于下标 1 1 1 的元素 5 5 5 之前,交换后位于下标 1 1 1 的元素 5 5 5 之后,因此两个元素 5 5 5 的相对顺序改变。
插入排序
原理
插入排序的原理是维护一个已排序的子数组,每次将一个未排序的元素插入到已排序的子数组中的正确位置。
初始时,只有首个元素为已排序的子数组。每次在已排序的子数组中插入元素时,需要在已排序的子数组中找到比待插入元素大的最小元素,其所在位置即为插入位置。插入元素的具体操作是,将已排序的子数组中从插入位置到子数组末尾的元素都向后移动一位,然后将待插入的元素移动到插入位置。每次插入元素之后,已排序的子数组的长度加 1 1 1。当已排序的子数组的长度等于整个数组的长度时,排序结束。
示例
考虑以下数组的插入排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
每一次遍历之后,数组的变化情况如下。
[ 2 , 6 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [2, 6, 7, 1, 3, 0, 8, 9, 5, 4] [2,6,7,1,3,0,8,9,5,4]
[ 2 , 6 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [2, 6, 7, 1, 3, 0, 8, 9, 5, 4] [2,6,7,1,3,0,8,9,5,4]
[ 1 , 2 , 6 , 7 , 3 , 0 , 8 , 9 , 5 , 4 ] [1, 2, 6, 7, 3, 0, 8, 9, 5, 4] [1,2,6,7,3,0,8,9,5,4]
[ 1 , 2 , 3 , 6 , 7 , 0 , 8 , 9 , 5 , 4 ] [1, 2, 3, 6, 7, 0, 8, 9, 5, 4] [1,2,3,6,7,0,8,9,5,4]
[ 0 , 1 , 2 , 3 , 6 , 7 , 8 , 9 , 5 , 4 ] [0, 1, 2, 3, 6, 7, 8, 9, 5, 4] [0,1,2,3,6,7,8,9,5,4]
[ 0 , 1 , 2 , 3 , 6 , 7 , 8 , 9 , 5 , 4 ] [0, 1, 2, 3, 6, 7, 8, 9, 5, 4] [0,1,2,3,6,7,8,9,5,4]
[ 0 , 1 , 2 , 3 , 6 , 7 , 8 , 9 , 5 , 4 ] [0, 1, 2, 3, 6, 7, 8, 9, 5, 4] [0,1,2,3,6,7,8,9,5,4]
[ 0 , 1 , 2 , 3 , 5 , 6 , 7 , 8 , 9 , 4 ] [0, 1, 2, 3, 5, 6, 7, 8, 9, 4] [0,1,2,3,5,6,7,8,9,4]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;for (int i = 1; i < length; i++) {int num = nums[i];int insertIndex = i;for (int j = i - 1; j >= 0 && nums[j] > num; j--) {nums[j + 1] = nums[j];insertIndex = j;}if (insertIndex != i) {nums[insertIndex] = num;}}return nums;}
}
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。插入排序需要对 n − 1 n - 1 n−1 个元素寻找插入位置,对于每个元素,最多需要遍历所有已排序的元素寻找插入位置,因此插入排序的平均时间复杂度和最差时间复杂度都是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( 1 ) O(1) O(1)。
稳定性分析
由于插入排序从左到右依次遍历每个元素,每次插入元素时都是将元素插入到比该元素大的元素左侧,不会将元素插入到和该元素相等的元素左侧,因此相等元素之间的相对顺序总是保持不变。插入排序是稳定的排序算法。
希尔排序
原理
希尔排序由 D. L. Shell 提出,是插入排序的改进版本。为了和希尔排序区分,插入排序有时也称为直接插入排序。
希尔排序也称缩小增量排序,原理是将元素根据下标的增量分组,下标之差为增量的倍数的元素位于同一组,同一组元素使用插入排序。每一轮排序的增量减小,当增量变成 1 1 1 时为最后一轮,即插入排序。
希尔排序的时间复杂度取决于增量数组,增量数组的选择应满足增量依次减少且最后一轮的增量必须是 1 1 1。一种常见的增量数组是:初始增量取 ⌊ n 2 ⌋ \Big\lfloor \dfrac{n}{2} \Big\rfloor ⌊2n⌋,其余的每个增量都取前一个增量的一半向下取整,直到增量变成 1 1 1。
当 n n n 较大时,初始增量较大,分组较多,同一组元素的个数较少且下标差较大,可以快速完成排序,使距离较远的元素相对有序。虽然无法使整个数组完全有序,但是可以使整个数组更接近有序,当增量减小时,排序速度会更快。因此,希尔排序的性能优于插入排序。
示例
考虑以下数组的希尔排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
数组的长度是 10 10 10,增量依次取 5 5 5、 2 2 2、 1 1 1。对于每个增量依次执行排序之后,数组的变化情况如下。
[ 0 , 2 , 7 , 1 , 3 , 6 , 8 , 9 , 5 , 4 ] [0, 2, 7, 1, 3, 6, 8, 9, 5, 4] [0,2,7,1,3,6,8,9,5,4]
[ 0 , 1 , 3 , 2 , 5 , 4 , 7 , 6 , 8 , 9 ] [0, 1, 3, 2, 5, 4, 7, 6, 8, 9] [0,1,3,2,5,4,7,6,8,9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;for (int inc = length / 2; inc > 0; inc /= 2) {for (int i = inc; i < length; i++) {int num = nums[i];int insertIndex = i;for (int j = i - inc; j >= 0 && nums[j] > num; j -= inc) {nums[j + inc] = nums[j];insertIndex = j;}if (insertIndex != i) {nums[insertIndex] = num;}}}return nums;}
}
复杂度分析
-
时间复杂度:希尔排序的时间复杂度在 O ( n 1.25 ) O(n^{1.25}) O(n1.25) 和 O ( n 2 ) O(n^2) O(n2) 之间,具体时间复杂度取决于待排序的数组以及增量数组的选取,其中 n n n 是数组的长度。
-
空间复杂度: O ( 1 ) O(1) O(1)。
稳定性分析
由于希尔排序每次交换的元素不一定相邻,因此相等元素之间的相对顺序可能改变。希尔排序是不稳定的排序算法。
考虑以下数组的希尔排序: [ 5 , 5 , 4 , 8 ] [5, 5, 4, 8] [5,5,4,8]。
初始增量是 2 2 2,下标 0 0 0 处的元素 5 5 5 与下标 2 2 2 处的元素 4 4 4 交换位置,此时数组变成 [ 4 , 5 , 5 , 8 ] [4, 5, 5, 8] [4,5,5,8],已经有序,之后的排序过程不会有任何的元素交换。在仅有的一次元素交换中,下标 0 0 0 处的元素 5 5 5 交换到下标 2 2 2 处,该元素交换前位于下标 1 1 1 的元素 5 5 5 之前,交换后位于下标 1 1 1 的元素 5 5 5 之后,因此两个元素 5 5 5 的相对顺序改变。
归并排序
原理
归并排序是分治算法的一个典型应用。首先将原数组拆分成多个不相交的子数组,对每个子数组排序,然后将有序子数组合并,合并过程中确保合并后的子数组仍然有序。合并结束之后,整个数组排序结束。将两个有序子数组合并成一个有序子数组的操作称为归并,基于归并算法的排序算法称为归并排序。
归并排序可以使用自顶向下的方式递归实现,也可以使用自底向上的方式迭代实现。对于同一个数组,使用自顶向下和自底向上两种方式实现的中间过程可能有所区别,但是都能得到正确的排序结果。
自顶向下的实现过程如下。
- 如果当前数组的长度大于 1 1 1,则将当前数组拆分成长度之差不超过 1 1 1 的两个子数组,如果子数组的长度大于 1 1 1 则继续将子数组拆分成更短的子数组,直到子数组的长度等于 1 1 1。
- 对每个子数组分别排序,然后将排序后的子数组合并,合并过程中确保合并后的子数组仍然有序。当所有元素都合并结束时,排序结束。
自底向上的实现过程如下。
- 初始时半子数组长度是 1 1 1,子数组长度是 2 2 2。将每个子数组中的两个半子数组合并,合并过程中确保合并后的子数组仍然有序。如果最后一个子数组的长度不超过半子数组长度,则忽略最后一个子数组。
- 将半子数组长度和子数组长度都乘以 2 2 2,重复上述合并操作。当所有元素都合并结束时,排序结束。
示例
考虑以下数组的归并排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
-
自顶向下
数组拆分情况如下。
[ [ 6 , 2 , 7 , 1 , 3 ] , [ 0 , 8 , 9 , 5 , 4 ] ] [[6, 2, 7, 1, 3], [0, 8, 9, 5, 4]] [[6,2,7,1,3],[0,8,9,5,4]]
[ [ [ 6 , 2 , 7 ] , [ 1 , 3 ] ] , [ [ 0 , 8 , 9 ] , [ 5 , 4 ] ] ] [[[6, 2, 7], [1, 3]], [[0, 8, 9], [5, 4]]] [[[6,2,7],[1,3]],[[0,8,9],[5,4]]]
[ [ [ [ 6 , 2 ] , [ 7 ] ] , [ [ 1 ] , [ 3 ] ] ] , [ [ [ 0 , 8 ] , [ 9 ] ] , [ [ 5 ] , [ 4 ] ] ] ] [[[[6, 2], [7]], [[1], [3]]], [[[0, 8], [9]], [[5], [4]]]] [[[[6,2],[7]],[[1],[3]]],[[[0,8],[9]],[[5],[4]]]]
[ [ [ [ [ 6 ] , [ 2 ] ] , [ 7 ] ] , [ [ 1 ] , [ 3 ] ] ] , [ [ [ [ 0 ] , [ 8 ] ] , [ 9 ] ] , [ [ 5 ] , [ 4 ] ] ] ] [[[[[6], [2]], [7]], [[1], [3]]], [[[[0], [8]], [9]], [[5], [4]]]] [[[[[6],[2]],[7]],[[1],[3]]],[[[[0],[8]],[9]],[[5],[4]]]]
数组归并情况如下。
[ [ [ [ 2 , 6 ] , [ 7 ] ] , [ 1 , 3 ] ] , [ [ [ 0 , 8 ] , [ 9 ] ] , [ 4 , 5 ] ] ] [[[[2, 6], [7]], [1, 3]], [[[0, 8], [9]], [4, 5]]] [[[[2,6],[7]],[1,3]],[[[0,8],[9]],[4,5]]]
[ [ [ 2 , 6 , 7 ] , [ 1 , 3 ] ] , [ [ 0 , 8 , 9 ] , [ 4 , 5 ] ] ] [[[2, 6, 7], [1, 3]], [[0, 8, 9], [4, 5]]] [[[2,6,7],[1,3]],[[0,8,9],[4,5]]]
[ [ 1 , 2 , 3 , 6 , 7 ] , [ 0 , 4 , 5 , 8 , 9 ] ] [[1, 2, 3, 6, 7], [0, 4, 5, 8, 9]] [[1,2,3,6,7],[0,4,5,8,9]]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
-
自顶向上
数组归并情况如下。
[ [ 6 ] , [ 2 ] , [ 7 ] , [ 1 ] , [ 3 ] , [ 0 ] , [ 8 ] , [ 9 ] , [ 5 ] , [ 4 ] ] [[6], [2], [7], [1], [3], [0], [8], [9], [5], [4]] [[6],[2],[7],[1],[3],[0],[8],[9],[5],[4]]
[ [ 2 , 6 ] , [ 1 , 7 ] , [ 0 , 3 ] , [ 8 , 9 ] , [ 4 , 5 ] ] [[2, 6], [1, 7], [0, 3], [8, 9], [4, 5]] [[2,6],[1,7],[0,3],[8,9],[4,5]]
[ [ 1 , 2 , 6 , 7 ] , [ 0 , 3 , 8 , 9 ] , [ 4 , 5 ] ] [[1, 2, 6, 7], [0, 3, 8, 9], [4, 5]] [[1,2,6,7],[0,3,8,9],[4,5]]
[ [ 0 , 1 , 2 , 3 , 6 , 7 , 8 , 9 ] , [ 4 , 5 ] ] [[0, 1, 2, 3, 6, 7, 8, 9], [4, 5]] [[0,1,2,3,6,7,8,9],[4,5]]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
以下代码为归并排序的自顶向下实现。
class Solution {public int[] sortArray(int[] nums) {sortArray(nums, 0, nums.length - 1);return nums;}public void sortArray(int[] nums, int low, int high) {if (low < high) {int mid = low + (high - low) / 2;sortArray(nums, low, mid);sortArray(nums, mid + 1, high);merge(nums, low, mid, high);}}public void merge(int[] nums, int low, int mid, int high) {int currLength = high - low + 1;int[] temp = new int[currLength];int i = low, j = mid + 1, k = 0;while (i <= mid && j <= high) {if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}while (i <= mid) {temp[k++] = nums[i++];}while (j <= high) {temp[k++] = nums[j++];}System.arraycopy(temp, 0, nums, low, currLength);}
}
以下代码为归并排序的自底向上实现。
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;for (int halfLength = 1, currLength = 2; halfLength < length; halfLength *= 2, currLength *= 2) {for (int low = 0; low < length - halfLength; low += currLength) {int mid = low + halfLength - 1;int high = Math.min(low + currLength - 1, length - 1);merge(nums, low, mid, high);}}return nums;}public void merge(int[] nums, int low, int mid, int high) {int currLength = high - low + 1;int[] temp = new int[currLength];int i = low, j = mid + 1, k = 0;while (i <= mid && j <= high) {if (nums[i] <= nums[j]) {temp[k++] = nums[i++];} else {temp[k++] = nums[j++];}}while (i <= mid) {temp[k++] = nums[i++];}while (j <= high) {temp[k++] = nums[j++];}System.arraycopy(temp, 0, nums, low, currLength);}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组的长度。用 T ( n ) T(n) T(n) 表示对 n n n 个元素归并排序的时间。由于归并排序每次将待排序的数组拆分成两个子数组,对两个子数组分别递归排序,然后使用 O ( n ) O(n) O(n) 的时间将两个有序的子数组合并,因此有 T ( n ) = 2 T ( n 2 ) + O ( n ) T(n) = 2T\Big( \dfrac{n}{2} \Big) + O(n) T(n)=2T(2n)+O(n),根据主定理可以得到 T ( n ) = O ( n log n ) T(n) = O(n \log n) T(n)=O(nlogn)。
-
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。自顶向下实现时需要递归调用栈的空间是 O ( log n ) O(\log n) O(logn),自底向上实现时可以省略递归调用栈的空间。无论是自顶向下实现还是自底向上实现,归并过程需要 O ( n ) O(n) O(n) 的辅助空间。
稳定性分析
由于归并排序只有在合并两个子数组时才可能改变元素之间的相对顺序,且只有当左边的元素大于右边的元素时才会改变这两个元素的相对顺序,因此相等元素之间的相对顺序总是保持不变。归并排序是稳定的排序算法。
快速排序
原理
快速排序是利用分治思想的排序算法。其原理是在待排序的数组中选择一个基准元素,使用基准元素将数组分区,然后对分区后的两个子数组排序。当每个子数组都排序结束时,整个数组排序结束。
快速排序的操作包括选择基准元素和分区,只有当数组的长度大于 1 1 1 时才需要选择基准元素和分区。选择基准元素和分区的操作如下。
- 选择基准元素。可以选择数组的首个元素作为基准元素,也可以在数组中随机选择一个元素作为基准元素并将基准元素与数组的首个元素交换位置,此时数组的首个元素为基准元素。
- 分区。分区的含义是将数组分成两个子数组,基准元素左侧子数组的元素都小于等于基准元素,基准元素右侧子数组的元素都大于基准元素。具体做法如下。
- 用 start \textit{start} start 和 end \textit{end} end 分别表示当前数组的开始下标和结束下标,初始化两个指针 low = start + 1 \textit{low} = \textit{start} + 1 low=start+1 和 high = end \textit{high} = \textit{end} high=end。
- 将 low \textit{low} low 从左往右遍历,直到遇到大于基准元素的元素;将 high \textit{high} high 从右往左遍历,直到遇到小于等于基准元素的元素。此时如果 low < high \textit{low} < \textit{high} low<high,则交换 low \textit{low} low 和 high \textit{high} high 处的元素。重复该操作,直到 low ≥ high \textit{low} \ge \textit{high} low≥high 时结束该操作。
- 将 high \textit{high} high 从右往左遍历,直到遇到小于等于基准元素的元素。
- 此时 high \textit{high} high 指向基准元素应该放置下标的位置。如果 high > start \textit{high} > \textit{start} high>start,则交换 start \textit{start} start 和 high \textit{high} high 处的元素。
- 使用同样的方法对基准元素左右两侧的两个子数组排序,直到子数组的长度不超过 1 1 1 时,不需要继续分区。
示例
考虑以下数组的快速排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
快速排序过程中,每次选择子数组的首个元素作为基准元素。数组的变化情况和分区情况如下。
[ [ 5 , 2 , 4 , 1 , 3 , 0 ] , 6 , [ 9 , 8 , 7 ] ] [[5, 2, 4, 1, 3, 0], 6, [9, 8, 7]] [[5,2,4,1,3,0],6,[9,8,7]]
[ [ 0 , 2 , 4 , 1 , 3 ] , 5 , 6 , [ 9 , 8 , 7 ] ] [[0, 2, 4, 1, 3], 5, 6, [9, 8, 7]] [[0,2,4,1,3],5,6,[9,8,7]]
[ 0 , [ 2 , 4 , 1 , 3 ] , 5 , 6 , [ 9 , 8 , 7 ] ] [0, [2, 4, 1, 3], 5, 6, [9, 8, 7]] [0,[2,4,1,3],5,6,[9,8,7]]
[ 0 , 1 , 2 , [ 4 , 3 ] , 5 , 6 , [ 9 , 8 , 7 ] ] [0, 1, 2, [4, 3], 5, 6, [9, 8, 7]] [0,1,2,[4,3],5,6,[9,8,7]]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , [ 9 , 8 , 7 ] ] [0, 1, 2, 3, 4, 5, 6, [9, 8, 7]] [0,1,2,3,4,5,6,[9,8,7]]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , [ 7 , 8 ] , 9 ] [0, 1, 2, 3, 4, 5, 6, [7, 8], 9] [0,1,2,3,4,5,6,[7,8],9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {sortArray(nums, 0, nums.length - 1);return nums;}public void sortArray(int[] nums, int start, int end) {if (start < end) {int pivotIndex = partition(nums, start, end);sortArray(nums, start, pivotIndex - 1);sortArray(nums, pivotIndex + 1, end);}}public int partition(int[] nums, int start, int end) {int randomIndex = start + (int) (Math.random() * (end - start + 1));swap(nums, start, randomIndex);int pivot = nums[start];int low = start + 1, high = end;while (low < high) {while (low < high && nums[low] <= pivot) {low++;}while (low < high && nums[high] > pivot) {high--;}if (low < high) {swap(nums, low, high);}}while (high > start && nums[high] > pivot) {high--;}if (high > start) {swap(nums, start, high);}return high;}public void swap(int[] nums, int index1, int index2) {int temp = nums[index1];nums[index1] = nums[index2];nums[index2] = temp;}
}
复杂度分析
-
时间复杂度:平均情况是 O ( n log n ) O(n \log n) O(nlogn),最差情况是 O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度。快速排序中每次分区的时间复杂度是 O ( n ) O(n) O(n),总时间复杂度和递归调用层数有关。平均情况下,递归调用层数是 O ( log n ) O(\log n) O(logn),快速排序的时间复杂度是 O ( n log n ) O(n \log n) O(nlogn)。最差情况下,每次分区时选择的基准元素都是数组中的最小值或最大值,递归调用层数是 O ( n ) O(n) O(n),快速排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度:平均情况是 O ( log n ) O(\log n) O(logn),最差情况是 O ( n ) O(n) O(n),其中 n n n 是数组的长度。空间复杂度取决于递归调用层数。平均情况下,递归调用层数是 O ( log n ) O(\log n) O(logn),快速排序的空间复杂度是 O ( log n ) O(\log n) O(logn)。最差情况下,递归调用层数是 O ( n ) O(n) O(n),快速排序的空间复杂度是 O ( n ) O(n) O(n)。
稳定性分析
由于快速排序每次交换的元素不一定相邻,因此相等元素之间的相对顺序可能改变。快速排序是不稳定的排序算法。
考虑以下数组的快速排序: [ 5 , 5 , 4 , 8 ] [5, 5, 4, 8] [5,5,4,8]。
选择首个元素 5 5 5 作为基准元素,基准元素与下标 2 2 2 处的元素 4 4 4 交换位置,此时数组变成 [ 4 , 5 , 5 , 8 ] [4, 5, 5, 8] [4,5,5,8],已经有序,之后的排序过程不会有任何的元素交换。在仅有的一次元素交换中,下标 0 0 0 处的元素 5 5 5 交换到下标 2 2 2 处,该元素交换前位于下标 1 1 1 的元素 5 5 5 之前,交换后位于下标 1 1 1 的元素 5 5 5 之后,因此两个元素 5 5 5 的相对顺序改变。
堆排序
原理
堆排序是利用二叉堆实现的排序算法。二叉堆是一个完全二叉树,其中的元素按照特定规则排列。升序排序利用的是大根堆,大根堆中的每个结点元素都大于其子结点元素,根结点元素是堆中最大的。
堆排序的原理是用待排序的数组中的所有元素构建大根堆,将根结点元素与末尾元素交换,此时末尾元素即为最大元素,为已排序的元素,然后将其余未排序的元素重新构成大根堆并重复排序过程。当所有元素都变成已排序的元素时,排序结束。
由于二叉堆是完全二叉树,因此二叉堆可以使用数组表示。使用数组表示二叉堆时,下标 0 0 0 处的元素为二叉堆的根结点元素,下标 i i i 处的元素的左子结点和右子结点的下标分别是 2 × i + 1 2 \times i + 1 2×i+1 和 2 × i + 2 2 \times i + 2 2×i+2。
对于长度为 n n n 的数组,初始时 n n n 个元素都位于大根堆中,下标 0 0 0 处的元素为最大元素。将下标 0 0 0 处的元素与下标 n − 1 n - 1 n−1 处的元素交换,此时下标 n − 1 n - 1 n−1 处的元素为最大元素且已经到正确的位置,大根堆中剩余 n − 1 n - 1 n−1 个未排序的元素,对大根堆中剩余的元素继续维护大根堆的性质并完成排序。
在构建大根堆和排序过程中可能出现不符合大根堆的性质的情况,此时需要调整元素使得元素之间符合大根堆的性质。用 x x x 表示待调整的元素,调整方法是:如果 x x x 小于至少一个子结点元素,则将 x x x 和较大的子结点元素交换,其效果是较大的子结点元素上浮, x x x 下沉,然后比较 x x x 与新的子结点元素之间的大小关系并重复上述过程,直到 x x x 到达叶结点或者大于等于全部子结点元素。
示例
考虑以下数组的堆排序: [ 6 , 2 , 7 , 1 , 3 , 0 , 8 , 9 , 5 , 4 ] [6, 2, 7, 1, 3, 0, 8, 9, 5, 4] [6,2,7,1,3,0,8,9,5,4]。
将每个元素按照原数组中的顺序依次加入大根堆,得到的大根堆是: [ 9 , 6 , 8 , 5 , 4 , 0 , 7 , 1 , 2 , 3 ] [9, 6, 8, 5, 4, 0, 7, 1, 2, 3] [9,6,8,5,4,0,7,1,2,3]。
每一轮堆排序,首先将根结点元素与未排序元素中的末尾元素交换,使得未排序元素的个数减 1 1 1,然后将未排序元素调整,使得元素之间符合大根堆的性质。数组的变化情况如下。
[ 8 , 6 , 7 , 5 , 4 , 0 , 3 , 1 , 2 , 9 ] [8, 6, 7, 5, 4, 0, 3, 1, 2, 9] [8,6,7,5,4,0,3,1,2,9]
[ 7 , 6 , 3 , 5 , 4 , 0 , 2 , 1 , 8 , 9 ] [7, 6, 3, 5, 4, 0, 2, 1, 8, 9] [7,6,3,5,4,0,2,1,8,9]
[ 6 , 5 , 3 , 1 , 4 , 0 , 2 , 7 , 8 , 9 ] [6, 5, 3, 1, 4, 0, 2, 7, 8, 9] [6,5,3,1,4,0,2,7,8,9]
[ 5 , 4 , 3 , 1 , 2 , 0 , 6 , 7 , 8 , 9 ] [5, 4, 3, 1, 2, 0, 6, 7, 8, 9] [5,4,3,1,2,0,6,7,8,9]
[ 4 , 2 , 3 , 1 , 0 , 5 , 6 , 7 , 8 , 9 ] [4, 2, 3, 1, 0, 5, 6, 7, 8, 9] [4,2,3,1,0,5,6,7,8,9]
[ 3 , 2 , 0 , 1 , 4 , 5 , 6 , 7 , 8 , 9 ] [3, 2, 0, 1, 4, 5, 6, 7, 8, 9] [3,2,0,1,4,5,6,7,8,9]
[ 2 , 1 , 0 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [2, 1, 0, 3, 4, 5, 6, 7, 8, 9] [2,1,0,3,4,5,6,7,8,9]
[ 1 , 0 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [1, 0, 2, 3, 4, 5, 6, 7, 8, 9] [1,0,2,3,4,5,6,7,8,9]
[ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 ] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] [0,1,2,3,4,5,6,7,8,9]
代码
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;for (int i = length / 2; i >= 0; i--) {sink(nums, i, length);}for (int i = length - 1; i > 0; i--) {swap(nums, 0, i);sink(nums, 0, i);}return nums;}public void sink(int[] nums, int currIndex, int length) {for (int i = currIndex * 2 + 1; i < length; i = i * 2 + 1) {if (i + 1 < length && nums[i] < nums[i + 1]) {i++;}if (nums[i] <= nums[currIndex]) {break;}swap(nums, currIndex, i);currIndex = i;}}public void swap(int[] nums, int index1, int index2) {int temp = nums[index1];nums[index1] = nums[index2];nums[index2] = temp;}
}
复杂度分析
-
时间复杂度: O ( n log n ) O(n \log n) O(nlogn),其中 n n n 是数组的长度。二叉堆的高度是 O ( log n ) O(\log n) O(logn),每次调整元素需要 O ( log n ) O(\log n) O(logn) 的时间,因此构建大根堆需要 O ( n log n ) O(n \log n) O(nlogn) 的时间。排序过程中,每次将一个元素与未排序元素中的末尾元素交换之后调整元素需要 O ( log n ) O(\log n) O(logn) 的时间,因此排序过程需要 O ( n log n ) O(n \log n) O(nlogn) 的时间。
-
空间复杂度: O ( 1 ) O(1) O(1)。
稳定性分析
由于堆排序每次交换的元素不一定相邻,因此相等元素之间的相对顺序可能改变。堆排序是不稳定的排序算法。
考虑以下数组的堆排序: [ 5 , 5 , 4 ] [5, 5, 4] [5,5,4]。
该数组已经符合大顶堆,下标 0 0 0 处的元素 5 5 5 已经是最大元素。下标 0 0 0 处的元素 5 5 5 与下标 2 2 2 处的元素 4 4 4 交换位置,此时数组变成 [ 4 , 5 , 5 ] [4, 5, 5] [4,5,5],下标 2 2 2 处的元素 5 5 5 不会与任何元素交换。在这次元素交换中,下标 0 0 0 处的 5 5 5 交换到下标 2 2 2 处,该元素交换前位于下标 1 1 1 的元素 5 5 5 之前,交换后位于下标 1 1 1 的元素 5 5 5 之后,因此两个元素 5 5 5 的相对顺序改变。
计数排序
原理
计数排序的原理是统计每个元素在数组中出现的次数,然后根据出现次数将元素存储在排序后的下标位置。因此计数排序要求元素必须是有确定范围的整数。
计数排序的实现过程如下。
- 遍历数组,得到数组中的最大值和最小值,计算数组中的元素范围。
- 创建计数数组,其长度等于元素范围。遍历数组,在计数数组中记录每个元素在数组中出现的次数。每个元素在计数数组中对应的下标为元素值平移之后的结果,最小值在计数数组中对应的下标为 0 0 0。
- 将计数数组转换成前缀和数组,前缀和数组中的每个下标处的计数表示不超过特定元素的元素个数(特定元素等于前缀和数组中对应的下标平移回去的结果)。
- 创建与原数组长度相同的有序数组用于存储排序后的元素。反向遍历原数组,将原数组中的每个元素填到有序数组中的正确下标处。
- 当一个元素在前缀和数组中对应的计数为 x x x 时,表示不超过该元素的值有 x x x 个,因此该元素在有序数组中应该存放的下标位置是 index = x − 1 \textit{index} = x - 1 index=x−1,将该元素填到有序数组中的下标 index \textit{index} index 处。
- 将该元素填到有序数组中之后,将前缀和数组中该元素对应的计数减 1 1 1。
当所有元素都填到有序数组中之后,排序结束。
示例
考虑以下数组的计数排序: [ 0 , 5 , 3 , 3 , − 2 , − 3 , − 5 , 0 , − 5 , 2 ] [0, 5, 3, 3, -2, -3, -5, 0, -5, 2] [0,5,3,3,−2,−3,−5,0,−5,2]。
最大值是 5 5 5,最小值是 − 5 -5 −5,元素范围是 11 11 11 个元素。
将每个元素减去最小值之后计算每个元素在数组中出现的次数,得到计数数组: [ 2 , 0 , 1 , 1 , 0 , 2 , 0 , 1 , 2 , 0 , 1 ] [2, 0, 1, 1, 0, 2, 0, 1, 2, 0, 1] [2,0,1,1,0,2,0,1,2,0,1]。
将计数数组转换成前缀和数组: [ 2 , 2 , 3 , 4 , 4 , 6 , 6 , 7 , 9 , 9 , 10 ] [2, 2, 3, 4, 4, 6, 6, 7, 9, 9, 10] [2,2,3,4,4,6,6,7,9,9,10]。
反向遍历原数组,对于每个元素更新有序数组与前缀和数组。有序数组与前缀和数组的变化情况如下,有序数组中的下划线表示尚未填入元素。
[ _ , _ , _ , _ , _ , _ , 2 , _ , _ , _ ] , [ 2 , 2 , 3 , 4 , 4 , 6 , 6 , 6 , 9 , 9 , 10 ] [\_, \_, \_, \_, \_, \_, 2, \_, \_, \_], [2, 2, 3, 4, 4, 6, 6, 6, 9, 9, 10] [_,_,_,_,_,_,2,_,_,_],[2,2,3,4,4,6,6,6,9,9,10]
[ _ , − 5 , _ , _ , _ , _ , 2 , _ , _ , _ ] , [ 1 , 2 , 3 , 4 , 4 , 6 , 6 , 6 , 9 , 9 , 10 ] [\_, -5, \_, \_, \_, \_, 2, \_, \_, \_], [1, 2, 3, 4, 4, 6, 6, 6, 9, 9, 10] [_,−5,_,_,_,_,2,_,_,_],[1,2,3,4,4,6,6,6,9,9,10]
[ _ , − 5 , _ , _ , _ , 0 , 2 , _ , _ , _ ] , [ 1 , 2 , 3 , 4 , 4 , 5 , 6 , 6 , 9 , 9 , 10 ] [\_, -5, \_, \_, \_, 0, 2, \_, \_, \_], [1, 2, 3, 4, 4, 5, 6, 6, 9, 9, 10] [_,−5,_,_,_,0,2,_,_,_],[1,2,3,4,4,5,6,6,9,9,10]
[ − 5 , − 5 , _ , _ , _ , 0 , 2 , _ , _ , _ ] , [ 0 , 2 , 3 , 4 , 4 , 5 , 6 , 6 , 9 , 9 , 10 ] [-5, -5, \_, \_, \_, 0, 2, \_, \_, \_], [0, 2, 3, 4, 4, 5, 6, 6, 9, 9, 10] [−5,−5,_,_,_,0,2,_,_,_],[0,2,3,4,4,5,6,6,9,9,10]
[ − 5 , − 5 , − 3 , _ , _ , 0 , 2 , _ , _ , _ ] , [ 0 , 2 , 2 , 4 , 4 , 5 , 6 , 6 , 9 , 9 , 10 ] [-5, -5, -3, \_, \_, 0, 2, \_, \_, \_], [0, 2, 2, 4, 4, 5, 6, 6, 9, 9, 10] [−5,−5,−3,_,_,0,2,_,_,_],[0,2,2,4,4,5,6,6,9,9,10]
[ − 5 , − 5 , − 3 , − 2 , _ , 0 , 2 , _ , _ , _ ] , [ 0 , 2 , 2 , 3 , 4 , 5 , 6 , 6 , 9 , 9 , 10 ] [-5, -5, -3, -2, \_, 0, 2, \_, \_, \_], [0, 2, 2, 3, 4, 5, 6, 6, 9, 9, 10] [−5,−5,−3,−2,_,0,2,_,_,_],[0,2,2,3,4,5,6,6,9,9,10]
[ − 5 , − 5 , − 3 , − 2 , _ , 0 , 2 , _ , 3 , _ ] , [ 0 , 2 , 2 , 3 , 4 , 5 , 6 , 6 , 8 , 9 , 10 ] [-5, -5, -3, -2, \_, 0, 2, \_, 3, \_], [0, 2, 2, 3, 4, 5, 6, 6, 8, 9, 10] [−5,−5,−3,−2,_,0,2,_,3,_],[0,2,2,3,4,5,6,6,8,9,10]
[ − 5 , − 5 , − 3 , − 2 , _ , 0 , 2 , 3 , 3 , _ ] , [ 0 , 2 , 2 , 3 , 4 , 5 , 6 , 6 , 7 , 9 , 10 ] [-5, -5, -3, -2, \_, 0, 2, 3, 3, \_], [0, 2, 2, 3, 4, 5, 6, 6, 7, 9, 10] [−5,−5,−3,−2,_,0,2,3,3,_],[0,2,2,3,4,5,6,6,7,9,10]
[ − 5 , − 5 , − 3 , − 2 , _ , 0 , 2 , 3 , 3 , 5 ] , [ 0 , 2 , 2 , 3 , 4 , 5 , 6 , 6 , 7 , 9 , 9 ] [-5, -5, -3, -2, \_, 0, 2, 3, 3, 5], [0, 2, 2, 3, 4, 5, 6, 6, 7, 9, 9] [−5,−5,−3,−2,_,0,2,3,3,5],[0,2,2,3,4,5,6,6,7,9,9]
[ − 5 , − 5 , − 3 , − 2 , 0 , 0 , 2 , 3 , 3 , 5 ] , [ 0 , 2 , 2 , 3 , 4 , 4 , 6 , 6 , 7 , 9 , 9 ] [-5, -5, -3, -2, 0, 0, 2, 3, 3, 5], [0, 2, 2, 3, 4, 4, 6, 6, 7, 9, 9] [−5,−5,−3,−2,0,0,2,3,3,5],[0,2,2,3,4,4,6,6,7,9,9]
代码
class Solution {public int[] sortArray(int[] nums) {int length = nums.length;int minNum = nums[0], maxNum = nums[0];for (int num : nums) {minNum = Math.min(minNum, num);maxNum = Math.max(maxNum, num);}int offset = minNum;int interval = maxNum - minNum + 1;int[] counts = new int[interval];for (int num : nums) {counts[num - offset]++;}for (int i = 1; i < interval; i++) {counts[i] += counts[i - 1];}int[] sorted = new int[length];for (int i = length - 1; i >= 0; i--) {int num = nums[i];int index = counts[num - offset] - 1;sorted[index] = num;counts[num - offset]--;}return sorted;}
}
复杂度分析
-
时间复杂度: O ( n + k ) O(n + k) O(n+k),其中 n n n 是数组的长度, k k k 是元素范围大小(即元素范围不同元素的个数)。计数排序需要 O ( n ) O(n) O(n) 的时间寻找最大值和最小值、计算元素范围和统计元素出现的次数,需要 O ( k ) O(k) O(k) 的时间计算计数数组的前缀和数组,以及需要 O ( n ) O(n) O(n) 的时间将元素填入有序数组,因此计数排序的平均时间复杂度和最差时间复杂度都是 O ( n + k ) O(n + k) O(n+k)。
-
空间复杂度: O ( k ) O(k) O(k),其中 k k k 是元素范围大小(即元素范围不同元素的个数)。计数排序需要创建计数数组统计元素出现的次数(前缀和数组为计数数组的复用),需要 O ( k ) O(k) O(k) 的空间,返回值不计入空间复杂度。
如果要求直接修改原数组,则有序数组的空间复杂度不可省略,空间复杂度是 O ( n + k ) O(n + k) O(n+k)。
稳定性分析
计数排序的排序过程为反向遍历原数组并将原数组中的每个元素填到有序数组中的正确下标处。对于相等的元素,填到有序数组中的顺序是从后往前,和反向遍历原数组的过程中遍历元素的顺序相同,因此相等元素之间的相对顺序总是保持不变。计数排序是稳定的排序算法。
桶排序
原理
桶排序的原理是将数组中的元素分到多个桶内,对每个桶内的元素分别排序,最后将每个桶内的有序元素合并,即可得到排序后的数组。
桶排序需要预先设定桶的大小(即每个桶最多包含的不同元素个数)和桶的个数,将元素按照元素值均匀分布到各个桶内。对于每个元素,根据该元素与最小元素之差以及桶的大小计算该元素应该分到的桶的编号,可以确保编号小的桶内的元素都小于编号大的桶内的元素。当所有元素都分到桶内之后,对于每个桶分别排序,由于每个桶内的元素个数较少,因此每个桶的排序可以使用插入排序实现。当每个桶都排序结束之后,按照桶的编号从小到大的顺序依次取出每个桶内的元素,拼接之后得到的数组即为排序后的数组。
示例
考虑以下数组的桶排序: [ 44 , 10 , 13 , 38 , 12 , 25 , 19 , 22 , 50 , 39 ] [44, 10, 13, 38, 12, 25, 19, 22, 50, 39] [44,10,13,38,12,25,19,22,50,39]。
最大值是 50 50 50,最小值是 10 10 10。将每个桶的大小设为 10 10 10(即每个桶最多包含 10 10 10 个不同元素),则可以将所有元素分成 5 5 5 个桶:
0 : [ 10 , 13 , 12 , 19 ] 0: [10, 13, 12, 19] 0:[10,13,12,19]
1 : [ 25 , 22 ] 1: [25, 22] 1:[25,22]
2 : [ 38 , 39 ] 2: [38, 39] 2:[38,39]
3 : [ 44 ] 3: [44] 3:[44]
4 : [ 50 ] 4: [50] 4:[50]
对于每个桶分别排序:
0 : [ 10 , 12 , 13 , 19 ] 0: [10, 12, 13, 19] 0:[10,12,13,19]
1 : [ 22 , 25 ] 1: [22, 25] 1:[22,25]
2 : [ 38 , 39 ] 2: [38, 39] 2:[38,39]
3 : [ 44 ] 3: [44] 3:[44]
4 : [ 50 ] 4: [50] 4:[50]
依次拼接每个桶内的元素,即可得到排序后的数组:
[ 10 , 12 , 13 , 19 , 22 , 25 , 38 , 39 , 44 , 50 ] [10, 12, 13, 19, 22, 25, 38, 39, 44, 50] [10,12,13,19,22,25,38,39,44,50]
代码
class Solution {static final int BUCKET_SIZE = 100;public int[] sortArray(int[] nums) {int minNum = nums[0], maxNum = nums[0];for (int num : nums) {minNum = Math.min(minNum, num);maxNum = Math.max(maxNum, num);}int bucketCount = (maxNum - minNum) / BUCKET_SIZE + 1;List<Integer>[] buckets = new List[bucketCount];for (int i = 0; i < bucketCount; i++) {buckets[i] = new ArrayList<Integer>();}for (int num : nums) {int bucketIndex = (num - minNum) / BUCKET_SIZE;buckets[bucketIndex].add(num);}int index = 0;for (int i = 0; i < bucketCount; i++) {List<Integer> bucket = buckets[i];sort(bucket);for (int num : bucket) {nums[index++] = num;}}return nums;}public void sort(List<Integer> bucket) {int size = bucket.size();for (int i = 1; i < size; i++) {int num = bucket.get(i);int insertIndex = i;for (int j = i - 1; j >= 0 && bucket.get(j) > num; j--) {bucket.set(j + 1, bucket.get(j));insertIndex = j;}if (insertIndex != i) {bucket.set(insertIndex, num);}}}
}
复杂度分析
-
时间复杂度:平均情况是 O ( n + k ) O(n + k) O(n+k),最差情况是 O ( n 2 ) O(n^2) O(n2),其中 n n n 是数组的长度, k k k 是桶的个数。平均情况下,每个桶内的元素个数较少,因此时间复杂度主要取决于将元素分到桶内和拼接每个桶的元素的时间,桶排序的时间复杂度是 O ( n + k ) O(n + k) O(n+k)。最差情况下,多个元素被分到同一个桶内,对于这个桶排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2),桶排序的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( n + k ) O(n + k) O(n+k),其中 n n n 是数组的长度, k k k 是桶的个数。桶排序需要创建 k k k 个桶存放全部元素,因此桶排序的空间复杂度是 O ( n + k ) O(n + k) O(n+k)。
稳定性分析
桶排序的排序过程包括将元素分到桶内、对每个桶分别排序和拼接排序后的结果,其中只有对每个桶分别排序会改变元素之间的相对顺序。只要对每个桶分别排序时使用稳定的排序算法,就能确保相等元素之间的相对顺序总是保持不变,此时桶排序是稳定的排序算法。
基数排序
原理
基数排序的原理是基于关键字对元素排序。当元素有多个关键字时,需要执行多轮排序,每一轮基于一个关键字排序,使用关键字的顺序是从最低关键字到最高关键字。例如,使用基数排序对数字数组排序时,依次按照最低有效位到最高有效位的顺序对数字数组排序。
为了确保基数排序的结果正确,每一轮基于一个关键字排序时必须使用稳定排序。基于一个关键字排序时,常用的方法是计数排序。
基数排序的实现过程如下。
- 由于待排序的数组中可能有负数,为了方便处理,需要首先得到数组中的最大值和最小值,计算数组中的元素范围,并将数组中的每个元素平移,平移的含义是将元素值减去最小值,最小值平移之后变成 0 0 0。
- 根据平移之后的最大值,可以得到平移之后的元素值的最大长度,即每个元素的有效位数。
- 依次按照最低有效位到最高有效位的顺序,对数组使用计数排序的方法排序。
- 排序结束之后,将数组中的每个元素反向平移得到原始的元素排序之后的数组。
基数排序一般基于十进制,也可以基于非十进制。
示例
考虑以下数组的基数排序: [ 379 , 979 , 118 , 716 , 945 , 441 , 121 , 194 , 248 , 775 ] [379, 979, 118, 716, 945, 441, 121, 194, 248, 775] [379,979,118,716,945,441,121,194,248,775]。
最大值是 979 979 979,最小值是 118 118 118。将数组中的每个元素平移之后,得到数组: [ 261 , 861 , 0 , 598 , 827 , 323 , 3 , 76 , 130 , 657 ] [261, 861, 0, 598, 827, 323, 3, 76, 130, 657] [261,861,0,598,827,323,3,76,130,657]。平移之后的最大值是 861 861 861,是三位数,因此有三个有效位。
依次按照最低有效位到最高有效位的顺序排序。数组的变化情况如下。
[ 0 , 130 , 261 , 861 , 323 , 3 , 76 , 827 , 657 , 598 ] [0, 130, 261, 861, 323, 3, 76, 827, 657, 598] [0,130,261,861,323,3,76,827,657,598]
[ 0 , 3 , 323 , 827 , 130 , 657 , 261 , 861 , 76 , 598 ] [0, 3, 323, 827, 130, 657, 261, 861, 76, 598] [0,3,323,827,130,657,261,861,76,598]
[ 0 , 3 , 76 , 130 , 261 , 323 , 598 , 657 , 827 , 861 ] [0, 3, 76, 130, 261, 323, 598, 657, 827, 861] [0,3,76,130,261,323,598,657,827,861]
排序结束之后,将数组中的每个元素反向平移得到原始的元素排序之后的数组:
[ 118 , 121 , 194 , 248 , 379 , 441 , 716 , 775 , 945 , 979 ] [118, 121, 194, 248, 379, 441, 716, 775, 945, 979] [118,121,194,248,379,441,716,775,945,979]
代码
class Solution {static final int RADIX = 10;public int[] sortArray(int[] nums) {int length = nums.length;int minNum = nums[0], maxNum = nums[0];for (int num : nums) {minNum = Math.min(minNum, num);maxNum = Math.max(maxNum, num);}int maxDigitCount = getDigitCount(maxNum - minNum);for (int i = 0; i < length; i++) {nums[i] -= minNum;}for (int digitCount = 1, unit = 1; digitCount <= maxDigitCount; digitCount++, unit *= RADIX) {sort(nums, unit);}for (int i = 0; i < length; i++) {nums[i] += minNum;}return nums;}public int getDigitCount(int num) {int digitCount = 0;while (num > 0) {num /= RADIX;digitCount++;}return digitCount;}public void sort(int[] nums, int unit) {int length = nums.length;int[] counts = new int[RADIX];for (int num : nums) {int digit = num % (unit * RADIX) / unit;counts[digit]++;}for (int i = 1; i < RADIX; i++) {counts[i] += counts[i - 1];}int[] sorted = new int[length];for (int i = length - 1; i >= 0; i--) {int num = nums[i];int digit = num % (unit * RADIX) / unit;int index = counts[digit] - 1;sorted[index] = num;counts[digit]--;}System.arraycopy(sorted, 0, nums, 0, length);}
}
复杂度分析
-
时间复杂度: O ( d ( n + k ) ) O(d(n + k)) O(d(n+k)),其中 n n n 是数组的长度, k k k 是基数排序使用的进制数, d d d 是每个元素最多的有效位数。基于一个有效位使用计数排序需要 O ( n + k ) O(n + k) O(n+k) 的时间,需要对 d d d 个有效位分别执行计数排序,因此基数排序的平均时间复杂度和最差时间复杂度都是 O ( d ( n + k ) ) O(d(n + k)) O(d(n+k))。
-
空间复杂度: O ( n + k ) O(n + k) O(n+k),其中 n n n 是数组的长度, k k k 是基数排序使用的进制数。基于每个有效位的计数排序需要 O ( n + k ) O(n + k) O(n+k) 的空间,因此基数排序的空间复杂度是 O ( n + k ) O(n + k) O(n+k)。
稳定性分析
基数排序的实现为依次按照最低有效位到最高有效位的顺序使用计数排序。由于计数排序是稳定的排序算法,不会改变相等元素之间的相对顺序,因此基数排序的过程中,相等元素之间的相对顺序总是保持不变。基数排序是稳定的排序算法。