目录
快速排序
定义:
快速排序是一种非常高效的排序算法,采用了分治策略来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。
基本步骤:
- 选择基准值:从序列中挑选一个元素作为基准值(pivot)。通常选择序列的第一个元素或者最后一个元素。
- 分区操作:重新排列序列中的元素,所有比基准值小的元素都移到基准前面,所有比基准值大的元素都移到基准后面。这个操作称为分区操作。分区操作完成后,基准元素会处于最终排序后的位置。
- 递归排序子序列:递归地对基准左边的子序列和右边的子序列重复上述过程。递归的终止条件是子序列为空或只有一个元素。
复杂度分析
时间复杂度分析:
最好情况:每次分区操作都能均匀划分序列,时间复杂度为O(n log n)。
平均情况:也是O(n log n)。
最坏情况:当输入序列已经是正序或逆序,且总是选择第一个或最后一个元素作为基准时,每次分区只将序列分成一个较大部分和一个空的部分,时间复杂度退化为O(n^2)。
空间复杂度:
快速排序是原地排序算法,但因为递归调用,所以空间复杂度主要取决于递归栈的深度,最好情况下为O(log n),最坏情况下为O(n)。
稳定性:在进行哨兵划分时,基准数可能会被交换至相等元素的右侧。因此,快速排序是一种 不稳定排序算法。
实现示例(Java):
以下是一个使用Java实现的快速排序示例:
/* 元素交换 */
void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 哨兵划分 */
int partition(int[] nums, int left, int right) {
// 以 nums[left] 为基准数
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= nums[left])
j--; // 从右向左找首个小于基准数的元素
while (i < j && nums[i] <= nums[left])
i++; // 从左向右找首个大于基准数的元素
swap(nums, i, j); // 交换这两个元素
}
swap(nums, i, left); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
堆排序
定义:
堆排序是一种基于比较的排序算法,它利用了堆数据结构的特性来进行排序。堆排序可以分为两种主要类型:大顶堆排序和小顶堆排序。大顶堆保证每个父节点的值都大于或等于其子节点的值,而小顶堆则相反,每个父节点的值都小于或等于其子节点的值
基本步骤:
- 输入数组并建立大顶堆。完成后,最大元素位于堆顶。
- 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减 1 ,已排序元素数量加 1 。
- 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
- 循环执行第
2.
步和第3.
步。循环 n−1 轮后,即可完成数组排序。
复杂度分析
时间复杂度分析:
- 时间复杂度为 O(nlogn)、非自适应排序:建堆操作使用 O(n) 时间。从堆中提取最大元素的时间复杂度为 O(logn) ,共循环 n−1 轮。
空间复杂度:
- 空间复杂度为 O(1)、原地排序:几个指针变量使用 O(1) 空间。元素交换和堆化操作都是在原数组上进行的。
稳定性:在交换堆顶元素和堆底元素时,相等元素的相对位置可能发生变化,因此属于非稳定排序。
实现示例(Java):
以下是一个使用Java实现的堆排序示例:
/* 堆的长度为 n ,从节点 i 开始,从顶至底堆化 */
void siftDown(int[] nums, int n, int i) {
while (true) {
// 判断节点 i, l, r 中值最大的节点,记为 ma
int l = 2 * i + 1;
int r = 2 * i + 2;
int ma = i;
if (l < n && nums[l] > nums[ma])
ma = l;
if (r < n && nums[r] > nums[ma])
ma = r;
// 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出
if (ma == i)
break;
// 交换两节点
int temp = nums[i];
nums[i] = nums[ma];
nums[ma] = temp;
// 循环向下堆化
i = ma;
}
}
/* 堆排序 */
void heapSort(int[] nums) {
// 建堆操作:堆化除叶节点以外的其他所有节点
for (int i = nums.length / 2 - 1; i >= 0; i--) {
siftDown(nums, nums.length, i);
}
// 从堆中提取最大元素,循环 n-1 轮
for (int i = nums.length - 1; i > 0; i--) {
// 交换根节点与最右叶节点(交换首元素与尾元素)
int tmp = nums[0];
nums[0] = nums[i];
nums[i] = tmp;
// 以根节点为起点,从顶至底进行堆化
siftDown(nums, i, 0);
}
}
计数排序
定义:
计数排序(Counting Sort)是一种非比较型的整数排序算法,适用于一定范围内的整数排序。它的基本思想是统计数组中每个数值出现的次数,然后根据这些计数来构造排序后的数组。计数排序的优势在于它的时间复杂度为 O(n + k),其中 n 是数组的长度,k 是数组中数值的范围。这意味着在某些情况下,它可以比基于比较的排序算法(如快速排序或归并排序)更高效,尤其是当 k 不是特别大时。
基本步骤:
- 确定数值范围:首先确定待排序数组中的最大值和最小值,从而知道数值的范围。
- 初始化计数数组:创建一个长度为 k 的计数数组,用来统计每个数值出现的次数。将所有元素初始化为0。
- 计数:遍历待排序的数组,对于数组中的每个元素,将其值作为索引,在计数数组的相应位置加1,以记录该数值出现的次数。
- 累计计数:修改计数数组,使得每个元素的值变成小于或等于该索引值的所有元素的个数。这一步是为了确定每个数值在最终排序数组中的位置。
- 输出排序后的数组:创建一个新的数组,用于存放排序后的结果。遍历原数组,根据每个元素的值和计数数组中的累计计数,将元素放置在正确的位置上。每放置一个元素,就将计数数组中相应位置的值减1。
- 返回排序后的数组:完成上述步骤后,新的数组就是排序后的结果。
复杂度分析
时间复杂度分析:
- 时间复杂度为 O(n+m)、非自适应排序:涉及遍历
nums
和遍历counter
,都使用线性时间。一般情况下 n≫m ,时间复杂度趋于 O(n) 。
空间复杂度:
- 空间复杂度为 O(n+m)、原地排序:借助了长度分别为 n 和 m 的数组
res
和counter
。
稳定性:由于向 res
中填充元素的顺序是“从右向左”的,因此倒序遍历 nums
可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 nums
也可以得到正确的排序结果,但结果是非稳定的。
实现示例(Java):
以下是一个使用Java实现的计数排序示例:
/* 计数排序 */
// 完整实现,可排序对象,并且是稳定排序
void countingSort(int[] nums) {
// 1. 统计数组最大元素 m
int m = 0;
for (int num : nums) {
m = Math.max(m, num);
}
// 2. 统计各数字的出现次数
// counter[num] 代表 num 的出现次数
int[] counter = new int[m + 1];
for (int num : nums) {
counter[num]++;
}
// 3. 求 counter 的前缀和,将“出现次数”转换为“尾索引”
// 即 counter[num]-1 是 num 在 res 中最后一次出现的索引
for (int i = 0; i < m; i++) {
counter[i + 1] += counter[i];
}
// 4. 倒序遍历 nums ,将各元素填入结果数组 res
// 初始化数组 res 用于记录结果
int n = nums.length;
int[] res = new int[n];
for (int i = n - 1; i >= 0; i--) {
int num = nums[i];
res[counter[num] - 1] = num; // 将 num 放置到对应索引处
counter[num]--; // 令前缀和自减 1 ,得到下次放置 num 的索引
}
// 使用结果数组 res 覆盖原数组 nums
for (int i = 0; i < n; i++) {
nums[i] = res[i];
}
}
桶排序
定义:
桶排序(Bucket Sort)是一种分布式排序算法,它将数组分到有限数量的“桶”中,每个桶再分别排序(可以使用任何其他的排序算法),最后将各个桶中的元素合并成一个有序的结果。桶排序适用于数据分布较为均匀的情况,通常假设输入数据服从某种统计分布,比如均匀分布
基本步骤:
- 确定桶的数量和范围:首先,需要确定桶的数量以及每个桶应该覆盖的值的范围。桶的数量和范围取决于输入数据的特性和预期的排序效率。
- 分配元素到桶中:遍历输入数组,将每个元素放入相应的桶中。元素被放置在哪个桶中,取决于其值与桶的范围的关系。
- 对每个桶进行排序:对每个非空的桶内部的元素进行排序。这里可以使用任何排序算法,例如插入排序、快速排序或其他适合小数据量的排序算法。
- 合并桶中的元素:将所有桶中的元素按顺序合并成一个单一的有序数组。
复杂度分析
时间复杂度分析:
- 桶排序的平均时间复杂度是O(n + k),其中n是输入数组的长度,k是桶的数量。但是,如果桶内排序使用的是O(n^2)的排序算法,那么桶排序的最坏情况时间复杂度可能退化到O(n^2)。如果使用O(n log n)的排序算法,则最坏情况时间复杂度为O(n log n)。
空间复杂度:
- 空间复杂度为 :桶排序的空间复杂度是O(n + k),因为它需要额外的空间来存储桶和桶中的元素。
稳定性:由于向 res
中填充元素的顺序是“从右向左”的,因此倒序遍历 nums
可以避免改变相等元素之间的相对位置,从而实现稳定排序。实际上,正序遍历 nums
也可以得到正确的排序结果,但结果是非稳定的。
实现示例(Java):
以下是一个使用Java实现的桶排序示例:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class BucketSort {
public static void bucketSort(float[] arr) {
// 创建桶
List<List<Float>> buckets = new ArrayList<>();
int bucketSize = arr.length;
for (int i = 0; i < bucketSize; i++) {
buckets.add(new ArrayList<>());
}
// 分配元素到桶中
for (float value : arr) {
int index = (int) Math.floor(bucketSize * value);
buckets.get(index).add(value);
}
// 对每个桶进行排序
for (List<Float> bucket : buckets) {
Collections.sort(bucket);
}
// 合并桶中的元素
int index = 0;
for (List<Float> bucket : buckets) {
for (float value : bucket) {
arr[index++] = value;
}
}
}
}
基数排序
定义:
计数排序(Counting Sort)是一种非比较型的整数排序算法,适用于一定范围内的整数排序。它的基本思想是统计数组中每个数值出现的次数,然后根据这些计数来构造排序后的数组。计数排序的优势在于它的时间复杂度为 O(n + k),其中 n 是数组的长度,k 是数组中数值的范围。这意味着在某些情况下,它可以比基于比较的排序算法(如快速排序或归并排序)更高效,尤其是当 k 不是特别大时。
基数排序有两种主要的方法:
最低位优先(Least Significant Digit, LSD):从最低有效位开始排序,然后逐步向高位移动。
最高位优先(Most Significant Digit, MSD):从最高有效位开始排序,然后逐步向低位移动。
基本步骤(LSD):
- 确定最大数的位数:找出待排序数组中最大的数,确定其位数,这将是排序的轮数。
- 按位数排序:从最低位开始,对每一位进行排序。通常使用计数排序作为子程序来完成这一任务,因为计数排序适合处理小范围的整数。
- 重复步骤2:对下一位进行排序,直到最高位为止。
复杂度分析
时间复杂度分析:
- 基数排序的时间复杂度为O(d * (n + b)),其中d是最大数的位数,n是数组的长度,b是基数(通常为10,如果处理的是十进制数)。当d固定时,基数排序的时间复杂度接近线性,这使得它在处理大量数据时非常高效。
空间复杂度:
- 基数排序的空间复杂度为O(n + k),其中k是基数。这是因为需要额外的空间来存储计数数组和输出数组。
稳定性:当计数排序稳定时,基数排序也稳定;当计数排序不稳定时,基数排序无法保证得到正确的排序结果。
实现示例(Java):
以下是一个使用Java实现的基数排序示例:
public class RadixSort {
public static void radixsort(int[] arr) {
int max = getMax(arr);
for (int exp = 1; max/exp > 0; exp *= 10) {
countingSort(arr, exp);
}
}
private static void countingSort(int[] arr, int exp) {
int[] output = new int[arr.length];
int[] count = new int[10];
Arrays.fill(count, 0);
for (int i = 0; i < arr.length; i++) {
count[(arr[i]/exp)%10]++;
}
for (int i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
for (int i = arr.length - 1; i >= 0; i--) {
output[count[(arr[i]/exp)%10] - 1] = arr[i];
count[(arr[i]/exp)%10]--;
}
System.arraycopy(output, 0, arr, 0, arr.length);
}
private static int getMax(int[] arr) {
int max = arr[0];
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
}