文章目录
1.排序概论
3。排序算法的稳定性(重要)
稳定性:若待排序的集合中存在值相等的元素,经过排序之后,相同元素的相对位置没有发生改变。
2.排序分类
(无论是内部排序还是外部排序,最终数据的排序一定再内存中进行)
2.1内部排序
定义:排序过程无需借助外部储存器(如磁盘),所有排序操作均在内存中完成。默认说的排序都是内部排序。
内部排序按照排序思路分为以下四类:
2.1.1 插入排序法 (O(n^2))
概念:基于有序集合插入思想的排序算法
直接插入排序
思想:将待排序的数据分为两个区间,已排序区间与待排序区间。算法刚开始时,已排序空间有一个元素,在待排序空间中选择第一个元素与已排序空间的最后一个元素比较,若比已排序的最大元素大直接放入,若小则进行插入
代码:
package www.sort.java;
/**
* @Author : YangY
* @Description :直接插入排序
* @Time : Created in 20:44 2019/3/7
*/
public class Test2 {
public static void main(String[] args) {
int[] arr = new int[]{42,3,4,6,3,10};
insertSoprt(arr);
SortHelper.print(arr);
}
public static void insertSoprt(int[] arr) {
int n = arr.length;
if(n <= 1) {
return;
}
for(int i = 1; i < n; i++) {
int value = arr[i];
int j;
//在已排序集合中找到要插入的位置
for(j = i-1; j >=0; j--) {
if(arr[j] <= value) {
break;
}else {
arr[j+1] = arr[j];
}
}
//此处注意是 j+1 ,可以用极限法来想
arr[j+1] = value;
}
}
}
- 时间复杂度:
最好情况:O(n)
最坏情况:O(n^2)
平均复杂度:O(n^2) - 算法的内存消耗
O(1); - 算法的稳定性
属于稳定性排序方法
折半插入排序
相对于直接插入,优化:利用二分查找来节省时间;
package www.sort.java;
/**
* @Author : YangY
* @Description :
* @Time : Created in 9:58 2019/3/10
*/
public class Test3 {
public static void main(String[] args) {
int[] arr = new int[]{15,3,4,6,3,10,2,33,1,-1};
//int[] arr = SortHelper.produceArr(1000,2000,3000);
insertSoprt(arr);
SortHelper.print(arr);
}
public static void insertSoprt(int[] arr) {
int n = arr.length;
if(n <= 1) {
return;
}
for(int i = 1; i < n; i++) {
int value = arr[i];
int l = 0;
int r = i-1;
int m = l + (r-l)/2;
while(l <= r) {
if(value > arr[m]) {
l = m + 1;
}else {
r = m - 1;
}
m = l + (r-l)/2;
}
int j = i - 1;
for(; j >= r + 1; j--) {
arr[j+1] = arr[j];
}
arr[j+1] = value;
}
}
}
希尔排序
时间复杂度会根据分的长度不同而异,所以一般不考虑希尔排序的时间复杂度
public static void shellSort(int[] arr) {
int n = arr.length;
if(n <= 1) {
return;
}
int step = n/2;
while(step >= 1) {
for(int i = step; i < n; i++) {
int value = arr[i];
int j = i - step;
for(; j >= 0; j -=step) {
if(value < arr[j]) {
arr[j+step] = arr[j];
}else{
break;
}
}
arr[j+step] = value;
}
step /= 2;
}
}
}
优化:原先找到插入位置后,元素是一个一个向后移,损耗较大,而希尔排序正是优化了这一点;
2.1.2选择排序
选择排序 (O(n^2))
package www.sort.java;
/**
* @Author : YangY
* @Description :选择排序,不稳定排序;
* @Time : Created in 11:25 2019/3/10
*/
public class Test5 {
public static void main(String[] args) {
int[] arr = new int[]{1,5,3,7,8,2};
selectSort(arr);
SortHelper.print(arr);
}
static void selectSort(int[] arr) {
double start = System.currentTimeMillis();
int n = arr.length;
if(n <= 1) {
return;
}
for(int i = 0; i < n-1; i++) {
int minIndex = i;
for(int j = i+1; j < n; j++){
if(arr[j] < arr[minIndex]) {
minIndex = j;
}
}
int temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
}
堆排序(二叉树后再完善)
2.1.3 交换排序
冒泡排序(O(n^2))
冒泡排序只会操作相邻的两个元素 。每次对相邻的两个元素做大小比较,看是否满足大小关系。一次冒泡至少会让一个元素移动到最终位置;
注意之处:可以实现小优化:某次交换中若交换次数为0,则代表已经有序,直接返回;
package www.sort.java;
/**
* @Author : YangY
* @Description : 冒泡排序,时间复杂度O(n^2);
* @Time : Created in 19:25 2019/3/7
*/
public class Test1 {
public static void main(String[] args) {
int[] arr = new int[]{1,3,7,4,2,10,10,0,-1};
bubbleSort(arr);
SortHelper.print(arr);
}
public static void bubbleSort(int[] arr) {
int n = arr.length;
for(int i = 0; i < n-1; i++) {
boolean flag = true;
for(int j = 0; j < n-i-1; j++) {
if(arr[j] > arr[j+1]) {
flag = false;
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
//优化
if(flag) {
return;
}
}
}
}
总结:
-
算法的执行效率:
最好情况:
数据集本身就是一个有序集合,O(n^2);最坏情况:
数据集完全逆序:,O(n^2);平均情况:
一般与最坏情况一致,O(n^2); -
算法的内存消耗
无需开辟新的空间,仅仅是原有本身数据进行交换
所以空间复杂度为O(1),为一个原地排序算法;(此概念非常重要) -
算法的稳定性
冒泡排序由于相邻元素发生大小关系变换才会交换次序,所以当两个元素大小相等时,并不会交换次序,所以为稳定性排序方法;
快速排序
2.1.4归并排序(O(nlogn))
归并与快排都应用分治思想,如何在O(n)内寻找一个无序数组的第K大元素?(经典面试题)
所有使用分治思想解决的问题均可利用递归的技巧完美解决。
public static void mergeSort(int[] arr) {
int n = arr.length;
if(n <= 1) {
return;
}
mergeSortIntern(arr,0,n-1);
}
public static void mergeSortIntern(int[] arr, int left, int right) {
if(left >= right) {
return;
}
int mid = (left + right) / 2;
mergeSortIntern(arr, left, mid);
mergeSortIntern(arr,mid+1,right);
merge(arr,left,mid,right);
}
public static void merge(int[] arr, int left, int mid, int right) {
int[] newArr = new int[right - left + 1];
int k = 0;
//前半部分数组的起始
int i = left;
//后半部分数组的起始
int j = mid + 1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) { //这一定要为小于等于,这样才是稳定性排序,否则不是稳定性排序
newArr[k++] = arr[i];
i++;
} else {
newArr[k++] = arr[j];
j++;
}
}
//总会有一个数组没有拷贝完,此时通过以下方法将剩余的元素拷贝到新数组
if(i > mid) {
while(j <=right) {
newArr[k++] = arr[j++];
}
}else {
while(i <= mid) {
newArr[k++] = arr[i++];
}
}
//将newArr拷贝到原数组
for(int n = 0; n < right-left + 1; n++) {
arr[left+n] = newArr[n];
}
}
归并排序复杂度分析:
稳定性:取决于合并函数的写法,arr[i] <= arr[j] ;
时间复杂度:
分组次数:log 2 N最好:nlogn
最坏:nlogn
平均:nlogn
空间复杂度:O(n),临时数组在合并后空间会释放,就在最后一次合并开的空间最大为n,所以时间复杂度为O(n);
所以归并排序不是原地排序,因此没得到重用;
归并排序小优化: (小⭐)
对合并可进行优化:当左边数组最大元素都小于右边数组最小元素,说明整个数组有序,直接结束排序;
2.1.5 快速排序(⭐)
算法思路:从待排序的数组中选取任意一个元素[l,…r],成为分区点(基准值)
开始遍历过程,每当发现比基准值小的元素就放在基准值左边,每当发现>= 基准值的元素就放在基准值的右边。
当结束一次遍历时,基准元素一定在最终位置;
一路快排:
package www.sort.java;
/**
* @Author : YangY
* @Description :一路快排
* @Time : Created in 19:10 2019/3/14
*/
public class SortQuick {
public static void main(String[] args) {
int[] arr = new int[]{1,4,2,7,-1,10,4};
//int[] arr = SortHelper.produceArr(10000000,1000,10000);
//int[] arr = SortHelper.produceSortyArr(5500);
long start = System.currentTimeMillis();
quickSort(arr);
long end = System.currentTimeMillis();
System.out.println("所耗时间为:"+(end-start)+"ms");
SortHelper.print(arr);
}
public static void quickSort(int[] arr) {
int length = arr.length;
if(length <= 1) {
return;
}
quickSortInternal(arr,0,length-1);
}
private static void quickSortInternal(int[] arr, int l, int r) {
if(l >= r) {
return;
}
//获取基准值
int par = patition(arr, l, r);
quickSortInternal(arr, l, par-1);
quickSortInternal(arr, par+1, r);
}
private static int patition(int[] arr, int l, int r) {
//随机选取一个下标作为基准值,防止了原数组近乎有序导致的时间复杂度近乎降为O(n^2);
swap(arr,randomIndex(l,r),l); //1
int val = arr[l]; //2 1、2的顺序千万不能反,否则会错
//并不是不可以反,要反的话就得先定义一个random = randomIndex(l,r),不然你定义两次的话就会产生不同的随机值,这当时坑了我很久
//[l+1, j]区间内都是小于val
int j = l;
//[j+1, i]区间内都是大于等于val
int i = l+1;
for(; i<=r;i++) {
if(arr[i] < val) {
swap(arr, i, j+1);
j++;
}
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
private static int randomIndex(int l, int r) {
int random = (int)(Math.random()*(r-l+1)) + l;
return random;
}
}
平均时间复杂度:nlogn;
稳定性:不稳定性算法(若基准值为最后一个元素,5 4 3 2 6 1 5)
当待排元素接近有序时,若选区的基准值恰好为最小时,每次分区严重不均衡,导致分区时间复杂度退化为O(n) (假如每次均分,则为O(logn)),再加上合并的O(n),此时总的时间复杂度分层退化成0(n^2),若此时选取得基准值为最大值,则为0(n);
归并与快排都应用分治思想,如何在O(n)内寻找一个无序数组的第K大元素?(经典面试题)
快排优化:
1.当待排序的集合近乎有序时,由于默认选择的第一个元素作为基准值,会导致基准值划分的两个子数组严重不均衡,此时分层下来得结果近乎于n层,此时快排退化为复杂度为O(n^2)排序算法;
优化:选取一个随机下标的数作为基准值,然后将该下标的数与l(最左边)下标的数进行交换,后面过程完全不变;
2.当排序集合中有大量重复元素时,由于基准值相等的元素过多,也会导致划分数组严重不均衡,将时间复杂度退化为O(n^2);
优化:使用二路快排,代码如下:
当重复元素过多,待排元素数量稍大时就会溢出,但优化为二路快排后不仅不容易溢出(原先10w就会溢出,双路后千万都不会溢出),时间也提升了百倍(自己对比而来的)
二路快排:⭐
package www.sort.java;
/**
* @Author : YangY
* @Description : 双路快排的实现
* @Time : Created in 16:49 2019/3/15
*/
public class SortTwoQuick {
public static void main(String[] args) {
int[] arr = new int[]{1,3,5,2,7,-10};
twoQuickSort(arr);
SortHelper.print(arr);
}
public static void twoQuickSort(int[] arr) {
int length = arr.length;
if(length <= 1) {
return;
}
twoQuickSortInternal(arr,0,length-1);
}
private static void twoQuickSortInternal(int[] arr, int l, int r) {
if(l >= r) {
return;
}
//获取基准值
int par = patition(arr, l, r);
twoQuickSortInternal(arr, l, par-1);
twoQuickSortInternal(arr, par+1, r);
}
private static int patition(int[] arr, int l, int r) {
//找一个随机下标的数组值作为基准值
int random = randomIndex(l, r);
int val = arr[random];
swap(arr, l, random);
int i = l + 1;
int j = r;
while(true) {
//此处arr[i]<val和下面的arr[j]>val,不能改为<=和>=,否则起不到优化作用⭐
while(i<=r && arr[i]<val) {
i++;
}
while(j>=l+1 && arr[j]>val) {
j--;
}
if(i > j) {
break;
}
swap(arr, i, j);
i++;
j--;
}
swap(arr, l, j);
return j;
}
private static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
private static int randomIndex(int l, int r) {
int random = (int)(Math.random()*(r-l+1)) + l;
return random;
}
}
三路快排:(解决大量重复元素的数组)
package www.sort.java;
/**
* @Author : YangY
* @Description : 三路快排,对重复元素超多的数组有很大程度的优化,对重复元素较少的相对于二路快排也只慢一点点,值得推荐
* @Time : Created in 18:57 2019/3/15
*/
public class SortThreeQuick {
public static void main(String[] args) {
int[] arr = new int[]{1,4,2,6,-1,10,2,2,9,-10};
threeQuickSort(arr);
SortHelper.print(arr);
}
public static void threeQuickSort(int[] arr) {
int length = arr.length;
if(length <= 1) {
return;
}
threeQuickSortInternal(arr,0,length-1);
}
private static void threeQuickSortInternal(int[] arr, int l, int r) {
if(l >= r) {
return;
}
//找一个随机下标的数组值作为基准值
int random = randomIndex(l, r);
int val = arr[random];
swap(arr, l, random);
//[l,lt]都小于val
int lt = l;
//[lt+1,i-1]都等于val [i,rt-1]为待比较元素
int i = l+1;
//[rt,r]都大于val
int rt = r + 1;
while(i < rt) {
if(arr[i] <val) {
swap(arr, i, lt+1);
lt++;
i++;
}else if(arr[i] > val) {
//此种情况i不能自加,因为他是与后面一个数换的,后面那个数还不知道它的情况呢。
swap(arr, i, rt-1);
rt--;
}else {
i++;
}
}
swap(arr, l, lt);
threeQuickSortInternal(arr, l, lt-1); //这里r为lt也没错,但多执行了一步
threeQuickSortInternal(arr, rt, r);
}
private static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
private static int randomIndex(int l, int r) {
int random = (int)(Math.random()*(r-l+1)) + l;
return random;
}
}
进阶优化:
当元素个数比较小时(<=15最优),可调用直接插入排序,数量小时,直接插入的时间复杂度接近于O(n);
2.2外部排序
定义:若参与排序的元素过多,数据量过大,内存放不下,需要借助外部存储器来进行排序,如桶排序 ;
2.2.1 桶排序
将元素均匀分桶后进行快排
例如:如何在O(n)时间内按照年龄大小给100万个用户排序
在0~120间分成120个桶,然后每个桶分别进行快排;
2.2.2基数排序(每位排序都进行稳定性排序)
总结
稳定性排序:
冒泡
直接插入
折半插入
非稳定排序:
希尔排序
归并排序(取决于合并过程)
选择排序
快排
O(n^2) :冒泡 直接插入 折半插入 选择
O(n^3/2):希尔排序
O(nlogn): 归并 快排 (随机化 二路 三路)