排序
总结:
一、O(n^2)时间复杂度的排序算法
- 总结:平方级别的排序算法是最简单的排序算法,但是理解这些简单的排序算法有助于学习更高效、更复杂的排序算法,而且这些简单的排序算法很多都是组成复杂排序算法的一部分。
- 数据 有/无 关:是指排序算法的时间复杂度是否会随着数据的某些顺序特点而发生变化,有的排序算法无论数据什么样子时间复杂度都是相同的,而有的排序算法会因为数据的特殊性而变得高效,会省去很多计算步骤。
- 原地与否:是指排序算法是否在数组上直接进行排序,或者是否只占用常数的空间复杂度。
- 几种平方级别时间复杂度的排序算法对比及相关信息如下:
算法名称 | 是否数据相关 | 一般时间复杂度 | 原地排序 | 一万数据时间(s) | 其它说明 |
---|---|---|---|---|---|
选择 | 数据无关 | O(n^2) | 原地 | 0.19 | 无 |
插入 | 数据相关,插入排序的内层循环可能提前终止,因此序列需要排序的元素越少有序性越强,插入排序性能越好,在近乎有序时甚至可以达到O(n)时间复杂度 | O(n^2) | 原地 | 0.32 | 插入排序的两两交换操作相比较于对比操作比较耗时,因此实测时间多于选择排序 |
优化后的插入 | 数据相关,插入排序的内层循环可能提前终止,因此序列需要排序的元素越少有序性越强,插入排序性能越好,在近乎有序时甚至可以达到O(n)时间复杂度 | O(n^2) | 原地 | 0.15 | 插入排序的两两交换操作相比较于对比操作比较耗时,改进就是不再两两交换,而是只进行一次对比和赋值,因此实测时间短了 |
希尔 | 希尔排序是插入排序的引申版本 | ||||
冒泡 |
- 插入排序
/**
* InserSort类有两个插入排序的实现,一个是对数组全部数据进行由大到小排序
* 一个是对数组指定范围进行由大到小排序
* Name:LiYang
* DATE: 2019/11/15
* TIME: 10:34
*/
public class InsertSort {
/**
* 对数组nums[l...r]范围进行插入排序
* @param nums
* @param l
* @param r
*/
public static void sort(int[] nums, int l, int r){
if (l >= r)
return;
// [l ... i)为已经有序的数组范围 i是当前待处理的数值
for (int i = l + 1; i <= r; i++){
int value = nums[i];
// [j ... i)范围中的数值均大于当前待处理的数值value
// j - 1 为当前需要判断的是否大于value的数值
for (int j = i; j >= l + 1; j--){
if (nums[j - 1] > value)
nums[j] = nums[j - 1];
else{
nums[j] = value;
break;
}
}
}
}
/**
* 对nums的所有数字进行插入排序
* @param nums
*/
public static void sort(int[] nums){
for (int i = 1; i < nums.length; i++){
int value = nums[i];
for (int j = i; j >= 1; j--){
if (nums[j - 1] > value)
nums[j] = nums[j - 1];
else{
nums[j] = value;
break;
}
}
}
}
}
二、O(nlogn)时间复杂度的排序算法
- 归并排序
/**
* Name:LiYang
* DATE: 2019/11/15
* TIME: 9:32
* 归并排序的实现
* 递归的方式,自顶向下
*/
public class MergeSort {
public static void sort(int[] nums){
mergeSort(nums, 0, nums.length - 1);
}
/**
* 对数组nums 范围[l ... r]进行归并排序
* 将左半部分进行归并排序得到有序的结果
* 将右半部分进行归并排序得到有序的结果
* 对左右两部分进行归并操作,使得[l ... r]整个是有序的
* @param nums
* @param l
* @param r
*/
private static void mergeSort(int[] nums, int l, int r){
// 这个优化在快排中也可以用,快排也是一种递归排序
// 当剩余数量<=16时进行插入排序
// 插入排序对部分有序的情况排序速度很快
// 而数据量比较小的时候部分有序的概率比较大
// if (l >= r)
// return;
if (r - l <= 15)
InsertSort.sort(nums, l, r);
// 注意不要写 m = (r + l) / 2;
// 当r l的值都比较大的时候可能会造成int型变量的溢出
int m = (r - l) / 2 + l;
mergeSort(nums, l, m);
mergeSort(nums, m + 1, r);
// merge操作是将已经有序的左半部分和右半部分进行归并使得整体有序
// 当nums[m] <= nums[m + 1]时不需要merge操作
// 此时左半部分是有序的,右半部分也是有序的,而左半部分最大值的小于右半部分最小值
// 整体就已经是有序的,不需要merge操作
if (nums[m] > nums[m + 1])
merge(nums, l, m, r);
}
/**
* 对数组nums [l ... m] [m+1 ... r]两部分进行归并操作
* 这两部分都是有序的,归并操作使得[l ... r]是整体有序的
* 归并过程中需要额外的长度为 r - l + 1 额外空间
* @param nums
* @param l
* @param m
* @param r
*/
private static void merge(int[] nums, int l, int m, int r){
// 通过k遍历新数组,将需要归并的值按顺序放入新数组中
int[] newNums = new int[r - l + 1];
int k = 0;
// i遍历左半部分
// j遍历右半部分
int i = l;
int j = m + 1;
// 注意i j 遍历不要越界
while(i <= m && j <= r){
if (nums[i] < nums[j]){
newNums[k] = nums[i];
k++;
i++;
} else {
newNums[k] = nums[j];
k++;
j++;
}
}
// 可能遍历过程中只有一个索引越界,此时需要将剩余部分直接放入新数组中
while(i <= m){
newNums[k] = nums[i];
i++;
k++;
}
while(j <= r){
newNums[k] = nums[j];
j++;
k++;
}
// 通过该方法将有序的新数组值赋值到原数组原位置范围中
System.arraycopy(newNums, 0, nums, l, r - l + 1);
// 提醒jvm清理内存
newNums = null;
}
public static void main(String[] args) {
int[] nums = new int[]{9,8,7,6,5,4,1,2,3,3,2,1};
QuickSortRandom.sort(nums);
for (int x : nums)
System.out.print(x + " ");
}
}
/**
* Name:LiYang
* DATE: 2019/11/15
* TIME: 11:15
* 自底向上的归并排序
*/
public class MergeSortBToU {
public static void sortArray(int[] nums){
int n = nums.length;
for (int sz = 1; sz <= n; sz += sz){
for (int i = 0; i + sz <= n; i += sz * 2){
mergeArray(nums, i, i + sz - 1, Math.min(i + sz * 2 - 1, n - 1));
}
}
}
/**
* 对数组nums [l ... m] [m+1 ... r]两部分进行归并操作
* 这两部分都是有序的,归并操作使得[l ... r]是整体有序的
* 归并过程中需要额外的长度为 r - l + 1 额外空间
* @param nums
* @param l
* @param m
* @param r
*/
private static void mergeArray(int[] nums, int l, int m, int r){
// 通过k遍历新数组,将需要归并的值按顺序放入新数组中
int[] newNums = new int[r - l + 1];
int k = 0;
// i遍历左半部分
// j遍历右半部分
int i = l;
int j = m + 1;
// 注意i j 遍历不要越界
while(i <= m && j <= r){
if (nums[i] < nums[j]){
newNums[k] = nums[i];
k++;
i++;
} else {
newNums[k] = nums[j];
k++;
j++;
}
}
// 可能遍历过程中只有一个索引越界,此时需要将剩余部分直接放入新数组中
while(i <= m){
newNums[k] = nums[i];
i++;
k++;
}
while(j <= r){
newNums[k] = nums[j];
j++;
k++;
}
// 通过该方法将有序的新数组值赋值到原数组原位置范围中
System.arraycopy(newNums, 0, nums, l, r - l + 1);
// 提醒jvm清理内存
newNums = null;
}
public static void main(String[] args) {
int[] nums = new int[]{9,8,7,6,5,4,1,2,3,3,2,1};
QuickSortRandom.sort(nums);
for (int x : nums)
System.out.print(x + " ");
}
}
- 快速排序
/**
* Name:LiYang
* DATE: 2019/11/13
* TIME: 22:37
* 基础版的快排
*/
public class QuickSort {
/**
* 对数组nums进行快速排序
* @param nums 待排序的数组,原地排序
*/
public static void sort(int[] nums){
quickSort(nums, 0, nums.length - 1);
}
/**
* 对数组[l ... r]范围进行快速排序
* @param nums 待排序数组
* @param l 左边界
* @param r 右边界
*/
private static void quickSort(int[] nums, int l, int r){
// if (l >= r)
// return;
// 当数组量小于等于16个时采用插入排序,插入排序对小数据量的排序速度较快
// 所有的快速排序都可以采用这种方法
if (r - l <= 15)
InsertSort.sort(nums, l, r);
int p = partition(nums, l, r);
quickSort(nums, l, p - 1);
quickSort(nums, p + 1, r);
}
/**
* 对左边第一个数字进行排序将其排列到其应该在的位置上并返回其正确位置的索引
* @param nums 待partition的数组
* @param l 左边界
* @param r 右边界
* @return 排好顺序的数字应该在的索引位置
*/
private static int partition(int[] nums, int l, int r){
int v = nums[l];
// [l + 1, j]中的所有值都小于v [j + 1, i)中的所有值都大于v
int j = l;
for (int i = l + 1; i <= r; i++){
if (nums[i] < v){
swap(nums, j + 1, i);
j++;
}
}
swap(nums, l, j);
return j;
}
/**
* 交换数组中指定位置的两个值
* @param nums
* @param left
* @param right
*/
private static void swap(int[] nums, int left, int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
public static void main(String[] args) {
int[] nums = new int[]{9,8,7,6,5,4,1,2,3};
QuickSort.sort(nums);
for (int x : nums)
System.out.print(x + " ");
}
}
/**
* Name:LiYang
* DATE: 2019/11/14
* TIME: 19:33
* QuickSort 是最简单的快排,在数组是一个无重复值并且有序性很差的情况下时间复杂度是O(nlogn)
* 首先考虑一个问题,如果数组是一个近乎有序的数组,此时
* QuickSort 的性能并不好,快排的过程是一个递归树的过程,此时树的结构在最差的情况下会退化成一个链表,时间复杂度会从O(nlogn) 变成 O(n^2)
* 为了解决这个问题,我们在partition中每次不再简单的选择左边第一个值为标定值
* 而是通过随机数,随机的从待排序的范围中取一个数作为标定值,虽然这种情况下最差的时间复杂度
* 依然是O(n^2),但是产生这种情况的概率很小是 1 / n! 因此其时间复杂度的期望是O(nlogn)
* 在大概率的情况下会得到一个很快的排序速度
*/
public class QuickSortRandom {
/**
* 对传入的数组进行快速排序
* @param nums 待排序的数组
*/
public static void sort(int[] nums){
quickSort2(nums, 0, nums.length - 1);
}
/**
* 内部排序方法,内部排序方法是一个递归的过程
* 该递归函数的含义是对nums数组的 [l ... r] 范围进行快速排序
* @param nums
* @param l
* @param r
*/
private static void quickSort2(int[] nums, int l, int r){
// if (l >= r)
// return;
// 当数组量小于等于16个时采用插入排序,插入排序对小数据量的排序速度较快
// 所有的快速排序都可以采用这种方法
if (r - l <= 15)
InsertSort.sort(nums, l, r);
int p = partition(nums, l, r);
quickSort2(nums, l , p - 1);
quickSort2(nums, p + 1, r);
}
/**
* partition每次将nums数组的[l ... r]范围中的一个数字进行排序,找到其有序后的应该在
* 的位置,并将其防止在应有的位置上,并返回该位置索引
* 为了对部分有序的数组也有一个比较好的性能
* 随机的从[l ... r] 中选择一个值作为标定值,这样能大概率的防止出现树不平衡的情况
* @param nums 待排序的数组
* @param l 范围
* @param r 范围右边界
* @return 对某个值进行排序,返回该排好序的数字的位置索引
*/
private static int partition(int[] nums, int l, int r){
// 通过以下的swap,将l r区间内随机位置上的值和左边界交换
// 左边界的值就是一个随机选取的标定值
swap(nums, l, (int)Math.random() * (r - l + 1) + l);
int v = nums[l];
int j = l;
for (int i = l + 1; i <= r; i++){
if (nums[i] < v){
swap(nums, j + 1, i);
j++;
}
}
swap(nums, l, j);
return j;
}
/**
* 交换两个位置上的值
* @param nums 待交换的数组
* @param left 位置一
* @param right 位置二
*/
private static void swap(int[] nums, int left, int right){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
public static void main(String[] args) {
int[] nums = new int[]{9,8,7,6,5,4,1,2,3};
QuickSortRandom.sort(nums);
for (int x : nums)
System.out.print(x + " ");
}
}
/**
* Name:LiYang
* DATE: 2019/11/14
* TIME: 20:09
* QuickSortRandom通过随机选取标定值解决了待排序数组部分有序的性能问题
* 是一个很好的快排算法,通常数组没有大量重复数字存在时就采用这种方式
* 但是当待排序的数组中有大量重复数值时QuickSortRandom就遇到性能问题
* 可以这么考虑性能问题的产生,还是从递归树的平衡性上考虑,如果快排的
* 递归树是很平衡的树那么排序的速度就是很快的,如果有大量的重复值存在
* QuickSortRandom会将所有等于标定值的值都排到右侧,那么当有大量重复值
* 与标定值重复,会使得标定值左右两部分极不平衡,性能下降
* 为了解决这个问题,引入三路快排,之前的快排可以当作“两路快排”,因为有
* < v 和 >= v 两种情况,将标定值以外的所有数字分为两路
* 而三路快排就是分为 < v 、== v 、 > v 三种情况,将所有非标定值的数字
* 分为三路,因此叫做三路快排
*/
public class QuickSort3Ways {
public static void sort(int[] nums){
quickSort3Ways(nums, 0, nums.length - 1);
}
private static void quickSort3Ways(int[] nums, int l, int r){
// if (l >= r)
// return;
// 当数组量小于等于16个时采用插入排序,插入排序对小数据量的排序速度较快
// 所有的快速排序都可以采用这种方法
if (r - l <= 15)
InsertSort.sort(nums, l, r);
int[] p = partition(nums, l, r);
quickSort3Ways(nums, l, p[0]);
quickSort3Ways(nums, p[1], r);
}
private static int[] partition(int[] nums, int l, int r){
swap(nums, l, (int)Math.random() * (r - l + 1) + l);
int v = nums[l];
int lt = l;
int gt = r + 1;
int i = l + 1;
while(i <= r && i < gt){
if (nums[i] == v){
i++;
} else if (nums[i] < v){
swap(nums, lt + 1, i);
lt++;
i++;
} else {
swap(nums, i, gt - 1);
gt--;
}
}
swap(nums, l, lt);
return new int[]{lt, gt};
}
private static void swap(int[] nums, int l, int r){
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
}
public static void main(String[] args) {
int[] nums = new int[]{9,8,7,6,5,4,1,2,3};
QuickSortRandom.sort(nums);
for (int x : nums)
System.out.print(x + " ");
}
}