稳定
插入排序、冒泡排序、归并排序、基数排序
不稳定
希尔排序、选择排序、堆排序、快速排序
O(n²)
插入排序、冒泡排序、选择排序
O(nlgn)
希尔排序、堆排序、快速排序、归并排序
O(n)
基数排序(不基于比较,基于桶)
1. 插入排序 数组排序,从小到大
思想:
默认第一位已排序,从第二位(记为a)开始遍历,在已排序序列中从后往前扫描,比a大的往后移,直到找到比a小或者相等的或者没找到,索引+1位插入a
时间复杂度:
最好:O(n) 数组已排好序,遍历数组,每位前面都已排好序,无需动
最差:O(n²) 每次都将前面所有已排序序列后移,在第一位插入未排序数据
平均:O(n²)
空间复杂度:
O(1) 占用常数内存,不占用额外内存
为什么稳定?
每次只有比未排序数据a大时才后移,相等也插在后面
public static void insertSort(int[] arr) {
//合法性检测
if (arr == null || arr.length == 0) {
System.out.println("数组无效");
}else{
//数组长度为1时,默认排好序
for (int i = 1; i < arr.length; i++) {
//暂存当前未排序数据
int current = arr[i];
//已排序序列从后往前扫描
int j = i - 1;
//只有已排序序列有值且比未排序数据大时,才后移
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j];
j--;
}
//情况一:出现比未排序数据小或者相等的元素
//情况二:已排序序列全都比未排序数据大
//(j+1)处都适合插入未排序数据
arr[j + 1] = current;
}
}
}
2. 希尔排序 插入排序的改进版,数组实现,从小到大排序
思想:
默认第一位已排序,每次动态生成一个增量gap(gap依次递减),从增量gap开始遍历(记为a)到序列末尾,在a之前的已排序序列(a-gap-gap...,a-gap-gap,a-gap)中从后往前扫描,比a大的元素依次往后移gap个位置,相当于跨gap插入排序的比较,直到找到比a小或者相等的或者没找到,索引+gap位插入a 最后一次gap为1
时间复杂度:不受输入数据的影响,外循环,跟gap有关,内循环,从gap处遍历到末尾
最好:O(nlgn) 数组已排好序,遍历数组,每位前面跨gap都已排好序,无需动
最差:O(nlgn) 每次都将前面跨gap序列后移,在第一位插入未排序数据
平均:O(nlgn)
空间复杂度:
O(1) 占用常数内存,不占用额外内存
为什么不稳定?
本来a=b,a在b前面,某趟后移了gap位,b在a前面,直到最后一趟gap=1排完,b还在a前面
public static void shellSort(int[] arr) {
//合法性检测
if (arr == null || arr.length == 0) {
System.out.println("数组无效");
}else{
//数组长度为1时,默认排好序
int gap = arr.length;
//动态定义间隔序列(增量):gap 最后一次,gap=1
while((gap/=2)>0){ //O(lgn) 跟gap有关
//从gap处开始遍历,一次完整的遍历(从gap开始,每间隔gap的子数组都已排序)
for(int i=gap;i<arr.length;i++){ //O(n)
//暂存当前未排序数据
int current = arr[i];
//已排序序列(gap间隔)从后往前扫描
int j = (i-gap);
//只有已排序序列(gap间隔)有值且比未排序数据大时,才后移
while(j>=0 && arr[j]>current){ //O(lgn) 跟gap有关
arr[j+gap] = arr[j];
j=j-gap;
}
//情况一:出现比未排序数据小或者相等的元素
//情况二:已排序序列全都比未排序数据大
//(j+gap)处都适合插入未排序数据
arr[j+gap]=current;
}
}
}
}
3. 选择排序 数组实现,从小到大排序
思想:
默认第一位已排序,从第二位开始遍历,从剩下的待排序序列中找最小值,插入到已排序序列的后面。怎么找:默认待排序序列第一位为最小值,往后遍历,比当前最小值小,最小值索引就更新至此处,直到待排序序列末尾。交换待排序序列第一位和最小值索引处的值
时间复杂度:不受输入数据的影响,外循环,确定(n-1)位,内循环,每次找最小值也得遍历完整个待排序序列
最好:O(n²)
最差:O(n²)
平均:O(n²)
空间复杂度:
O(1) 占用常数内存,不占用额外内存
为什么不稳定?
本来a=b,a在b前面,某次交换使得b在a前面,直到最后b还在a前面
public static void selectionSort(int[] arr) {
//合法性检测
if (arr == null || arr.length == 0) {
System.out.println("数组无效");
}else{
//数组长度为1时,默认排好序
//min:最小值索引
int min,temp;
for(int i=0;i<arr.length-1;i++){ //O(n) 只需要排到第length-2位,最后一位自然比前面大(小)
min = i; //默认待排序序列第一位为最小值
for(int j=(i+1);j<arr.length;j++){ //O(n)
if(arr[j]<arr[min]){ //比当前最小值小
min = j; //更新最小值索引
}
}
temp = arr[i]; //最小值与待排序序列第一位交换(即插入到已排序序列中)
arr[i] = arr[min];
arr[min] = temp;
}
}
}
4. 堆排序 数组实现,从小到大排序 数组就是堆(完全二叉树)层序遍历的结果
堆的性质:完全二叉树,对于每一个非叶节点而言,父节点值总是<(>)子节点值
思想:
初始时,数组即无序的二叉树。
数组长度>1?
是:1)构建大根堆,堆顶即最大元素。
2)堆顶元素和当前堆的最末位元素交换(大的往后放)
3)数组长度-1(当前堆的最末位元素脱离,到已排序序列中去)
继续调整判断数组长度>1?。。。
否:无需排序
时间复杂度:不受输入数据的影响,外循环不断确定堆顶元素,内循环,总是lgn(n为当前堆的规模)
最好:O(nlgn)
最差:O(nlgn)
平均:O(nlgn)
空间复杂度:
O(1) 占用常数内存,不占用额外内存
为什么不稳定?
本来a=b,a在b前面,某次交换使得b在a前面,直到最后b还在a前面
public static void heapSort(int[] arr) {
int len = arr.length;
//堆元素只有一个时,自然有序,否则堆顶元素可能并不是当前堆的最大元素,调整
while(len>1){ //O(n)
//构建大根堆:堆顶为最大元素
buildMaxHeap(arr,len);
//堆顶元素和当前堆的最末位元素交换(每趟最大的往后放)
change(arr, 0, len-1);
//堆规模减少一位(将当前堆的最末尾元素脱离)
len--;
}
}
//构建大根堆
public static void buildMaxHeap(int[] arr,int len){
//idx:最后一个非叶子节点的索引
for(int idx = len/2-1;idx>=0;idx--){ //O(lgn)
//如果左节点存在且节点值比当前节点大,就交换
if((idx*2+1<len) && (arr[idx*2+1]>arr[idx])){
change(arr, idx*2+1, idx);
}
//如果右节点存在且节点值比当前节点大,就交换
if((idx*2+2<len) && (arr[idx*2+2]>arr[idx])){
change(arr, idx*2+2, idx);
}
}
}
//俩元素交换
public static void change(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
5. 冒泡排序 数组实现,从小到大排序
思想:
第一趟,从0开始,相邻比较,大的后移,最后将最大的置于第(n-1)位,进入已排序序列
第二趟,从0开始,相邻比较,大的后移,最后将次大的置于第(n-2)位,进入已排序序列
每一趟,从0开始,相邻比较,大的后移,最后将待排序中最大的置于待排序最后一位,即进入已排序序列
最后,较大的放入第1位,总共(n-1)趟
时间复杂度:
最好:O(n) 数组已排好序,每趟都不需要移动交换元素,每个元素就在其位。
最差:O(n²) 数组逆序,要求从小到大,数组本身从大到小,对于每一趟都得将数组头部元素移到最后面去
平均:O(n²)
空间复杂度:
O(1) 占用常数内存,不占用额外内存
为什么稳定?
每次都是将较大的往后放,相等的不会处理,所以不改变相等元素的相对顺序
public static void bubbleSort(int[] arr) {
//合法性检测
if (arr == null || arr.length == 0) {
System.out.println("数组无效");
}else{
//数组长度为1时,默认排好序
for (int i = 0; i < arr.length-1; i++){ //0-(n-2) (n-1)趟
for(int j = 0;j < arr.length-1-i; j++){ //0-(n-2-i) 每趟只需比较到n-2-i
if(arr[j]>arr[j+1]){ //相邻两个元素比较,越大的往后放
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
6. 快速排序 数组实现,从小到大排序
思想:
1)只要数组元素不为空或者只有一个
2)以当前待排序序列的第一位为基准pivot,和最后一位进行交换。
3)划分:较小位索引位于待排序序列第一位,从第一位开始遍历到基准前面一位,比pivot小的往前挪(和前面比pivot大的元素进行交换),比pivot大的跳过。将较小位索引位置的元素与基准进行交换。形成左边<基准<右边的局面。返回基准所在的索引。
4)对左右两边分别快排。
时间复杂度:
最好:O(nlgn) 每次划分后,基准都位于当前待排序列的中间位置左右,这样左右两边可以同时继续递归快排
最差:O(n²) 数组有序(正序或者逆序),退化成冒泡排序,浪费时间
平均:O(nlgn)
空间复杂度:
递归调用需要保存基准,占用常数内存,不占用额外内存
最好:O(lgn)
最差:O(n) 退化成冒泡排序
为什么不稳定?
本来a=b,a在b前面,某次交换之后可能b在a前面,直到最后快排完,b还在a前面
public static void quickSort(int[] arr,int left,int right) {
//数组元素为空或者只有一个,无需进行快排(递归出口)
if(left<right){
//初始化基准位置
change(arr,left,right);
//一次划分过程,确定基准位置
int index = parition(arr,left,right); //最好情况:O(lgn)每次index都为当前数组的中间位置左右,这样两边数组可以同时继续划分,节省时间
//基准左边的小数组递归快排 //最差情况:O(n)数组有序(正序或者逆序),每次只能确定基准在最前面或者最后面,浪费时间
quickSort(arr, left, index-1);
//基准右边的大数组递归快排
quickSort(arr, index+1, right);
}
}
public static int parition(int[] arr,int l,int r){
//以最右边元素为基准
int p = arr[r];
//比基准小的元素的索引
int j=l;
for(int i=l;i<r;i++){ //不受输入数据影响,雷打不动:O(n)遍历整个数组,比基准小,就往前挪,放在j位,比基准大就直接跳过
if(arr[i]<=p){
if(j!=i){ //j==i时不需要交换两个位置上的数
change(arr, j, i);
}
j++;
}
}
change(arr, j, r); //确定一次划分后基准的正确位置
return j; //返回基准的最终正确位置,便于后续递归快排
}
//俩元素交换(注意:作用于数组,所以参数必须包含数组)
public static void change(int[] arr,int a,int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
7. 归并排序 数组实现,从小到大排序
思想:
将待排序序列拆分成左右两个子序列(数组),对两个子序列采用归并排序,直到子序列个数为1返回此数组(出口)。 将两个排好序的子序列合并成最终序列:最终序列长度为俩数组长度之和,分别从俩数组头开始遍历,哪个元素小拿哪个。最后哪个子序列有剩余元素,则填充到最终结果序列的后面。
时间复杂度:不受输入数据的影响,不管有序无序,都必须先拆分O(lgn),再合并O(n)
最好:O(nlgn)
最差:O(nlgn)
平均:O(nlgn)
空间复杂度:
O(n) 占用额外内存:左右子序列,结果序列
为什么稳定?
因为合并时是左子序列中的元素<=右子序列中的元素,所以a在b前,即使a=b,a还在前,b还在后
public static int[] mergeSort(int[] arr) {
//递归出口
if (arr.length == 1) {
return arr;
}
int index = (arr.length - 1) / 2; //O(lgn)
//左子数组
int[] leftArr = new int[index + 1];
//右子数组
int[] rightArr = new int[arr.length - 1 - index];
for (int i = 0; i <= index; i++) {
leftArr[i] = arr[i];
}
for (int j = (index + 1); j < arr.length; j++) {
rightArr[j - index - 1] = arr[j];
}
//拆分:递归归并排序
//合并排序后的子数组
return merge(mergeSort(leftArr), mergeSort(rightArr));
}
public static int[] merge(int[] leftArr, int[] rightArr) {
//左右子数组合并完的数组长度为俩子数组长度之和
int len = leftArr.length+rightArr.length;
//结果数组
int[] resultArr = new int[len];
//左游标
int i = 0;
//右游标
int j = 0;
//结果数组索引
int index = 0;
//分别从左右子数组开头开始遍历元素,哪个元素小取哪个
while (i < leftArr.length && j < rightArr.length) {
if (leftArr[i] <= rightArr[j]) { //O(n)
resultArr[index++] = leftArr[i++];
} else {
resultArr[index++] = rightArr[j++];
}
}
//哪个子数组有剩余元素,就把哪个子数组的剩余元素填充到结果数组后面
while (i < leftArr.length) {
resultArr[index++] = leftArr[i++];
}
while (j < rightArr.length) {
resultArr[index++] = rightArr[j++];
}
return resultArr;
}
8. 计数排序 数组实现,从小到大排序
思想:
1. 找出当前待排序序列的最小值min和最大值max
2.开辟一个辅助数组(桶)arr,数组长度k=(max-min+1) 0-(max-min)对应原数组的元素值min-max
3.将原数组元素依次放入桶中:原数组元素对应辅助数组下标,辅助数组初始值为0
4.计算原数组元素出现的次数,相同原数组元素对应相同的辅助数组下标,辅助数组元素值++
5.按照桶顺序(相当于原数组已排好序),桶值只要不为0反向填充原数组
时间复杂度:不是比较排序,而是根据原数组元素找到桶(利用桶下标有序),受原数组元素数据范围的影响,范围越大,时间开销越大。
最好:O(n+k)
最差:O(n+k)
平均:O(n+k)
空间复杂度:
O(k) 占用额外内存
为什么稳定?
a在b前面,a入桶,桶值加1,b入桶,桶值加1。输出时还是先输出a再输出b
public static int[] countSort(int[] arr) {
//找出当前待排序序列的最小值min和最大值max
int min = arr[0];
int max = arr[0];
for(int i=0;i<arr.length;i++){
if(arr[i]<min){
min = arr[i];
}
if(arr[i]>max){
max = arr[i];
}
}
//开辟一个辅助数组(桶)a,数组长度为(max-min+1)
int len = max-min+1;
//其中,索引0-(max-min)对应原数组的元素值min-max
//相当于将原数组元素依次放入一个排好序(利用数组下标)的桶中
int[] a = new int[len];
//计算原数组元素出现的次数,初始值为0
for(int j=0;j<arr.length;j++){ //遍历原数组 O(n)
int idx = arr[j]-min; //找到原数组元素对应的桶
a[idx]++; //放入桶中(次数+1)
}
//按照桶的顺序(相当于原数组元素排好序),根据桶值(原数组元素出现次数),反向填充原数组
int index = 0;
for(int m=0;m<len;m++){ //O(k)
while(a[m]>0){ //只要桶里有值(>0),说明原数组存在(min+下标)此值
arr[index++] = (min+m); //重新反向填充原数组
a[m]--; //输出一个(次数-1),直到没有
}
}
return arr;
}