目录
一、概念
时间复杂度:评估执行程序所需的时间。可以估算出程序对处理器的使用程度。
空间复杂度:评估执行程序所需的存储空间。可以估算出程序对计算机内存的使用程度。
稳定性:两个相等的数据,如果经过排序后,排序算法能够保证其相对位置不发生变化,则称该算法稳定。
一个稳定的算法,可以被实现为不稳定的排序。
但一个不稳定的算法,无法被实现为稳定的算法。
常见的排序算法:
二、直接插入排序
2.1 原理
插入:前提是[0,i-1]下标的数组都有序,将array[i]放在合适的位置,使得[0,i]之间的元素都有序。
具体操作:遍历array[i-1]到array[0]之间的元素,用array[j]来表示,如果比array[j]比array[i]大,array[j]就往后挪动,即array[j+1] = array[j]。j--,当遇见array[j]<array[i]或者j<0(遍历结束都还没找到比array[i]小的),就让array[j+1]= tmp。
2.2 实现
public static void insertSort(int[] array){
if(array == null){
return;
}
for(int i = 1;i<array.length;i++){
int tmp = array[i];
int j = i-1;
for( ;j>=0;j--){
if(array[j]>tmp){
array[j+1] = array[j];
}else{
break;
}
}
array[j+1] = tmp;
}
}
2.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 稳定 |
插入排序,数据越接近有序,时间效率越高。一般插入排序会用到一些需要优化的地方。
三、希尔排序
3.1 原理
希尔排序是对直接插入排序的一个优化。在排序之前,先分组,组内进行直接插入排序。
分组采用的缩小增量法,最后的增量必须为1.
直接插入排序就是希尔排序gap=1的情况。
格局小了,快来康康大佬是如何分组的。
3.2 实现
public static void shellSort(int[] array){
if(array == null){
return;
}
int gap = array.length;
while(gap > 1){
gap = gap/2;
for(int i =gap;i<array.length;i++){
int tmp = array[i];
int j = i-gap;
for( ; j>=0;j-=gap){
if(array[j]>tmp){
array[j+gap] = array[j];
}else{
break;
}
}
array[j+gap] = tmp;
}
}
}
3.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 不稳定 |
四、选择排序
4.1 原理
假设数组长度为elem,数组长度为len.
第一趟,假设第一个元素elem[0]是最小的,从elem[1]开始遍历数组,若是遇见的元素比elem[0]小,交换这两个元素。保证elem[0]存到元素是最小的。第一遍遍历结束后,最小的元素存在elem[0]当中。
第二趟,由于elem[0]已经存放最小的元素,不用再调整位置了,第二次假设elem[1]是elem[1]到elem[len-1]中最小的元素,从elem[2]开始遍历数组,若是遇见比elem[1]小的元素就交换位置,保证elelm[1]中存的是elem[1]-elem[len-1]中最小的元素。
即:第i (从i=0开始) 次遍历数组,假设elem[i]是最小的元素,从elem[i+1]开始遍历数组,遇见比elem[i]小的就交换位置。
基于上述思路,选择排序有两种思路:
第一种:假设elem[i]是最小的元素,从elem[i+1]开始遍历数组,遇见比elem[i]小的就交换位置。这种思路每次遇见比elem[i]小的都会交换。
第二种:假设min下标所指的元素最小,未比较之前假设min=i;从elem[i+1]开始遍历数组,遇见比elem[i]小的就跟新 i 的值,遍历结束后,若是 min != i,证明最小的值不是elem[i],交换elem[min]和elem[i]的位置。若min=i,证明elem[i]最小,不用交换。
第一种思路是每次遇见比elem[i]小就交换。第二种,先找出最小的再交换。
优化:每次遍历找到最小的元素和最大的元素,将最小和最大的分别放在数组的头和尾。
4.2 实现
public static void selectSort(int[] array){
if(array == null){
return;
}
for(int i = 0;i<array.length;i++){
for(int j = i+1;j<array.length;j++){
if(array[i]>array[j]){ // 碰见比array[i]小的,就交换
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}
}
public static void selectSort2(int[] array){
if(array == null){
return;
}
for(int i= 0;i<array.length;i++){
int min = i;
for(int j =i+1;j<array.length;j++){ // 找到最小的元素,再交换
if(array[j]<array[min]){
min = j;
}
}
if(min != i){
int tmp = array[min];
array[min] = array[i];
array[i] = tmp;
}
}
}
//每一趟遍历找出最大的最小的,将其放在队列的尾部和头部
public static void selelctSort(int[] array){
if(array == null){
return;
}
for(int i = 0;i<=array.length/2;i++){
int min = i;
int max = i;
for(int j = i+1;j<=array.length-1-i;j++){//每次在array[i,array.length-1-i]中找出最大和最小
if(array[j]<array[min]){
min = j; //找最小
}
if(array[j]>array[max]){
max = j; // 找最大
}
}
if(max == i && min ==array.length-1-i){
swap(array,min,max); //恰好最大的元素在队头,最小的元素在队尾,则可以直接交换队头和队尾元素。
}else {
swap(array,i,min); // 最小的放队头
swap(array,array.length-1-i,max); // 最大的放队尾
}
}
}
public static void swap(int[] array,int left,int right){
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
4.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 不稳定 |
五、堆排序
5.1 原理
先建立一个大根堆,交换堆顶元素和最后一个元素的位置。交换完后,最大的元素已经在队尾,再次建立大根堆时不用再考虑最后一个元素。用 [0,end] 表示需要排序的元素,每把大根堆的堆顶元素和最后一个元素交换结束后,end减一,直到end=0,证明已经排序结束。
5.2 实现
public static void heapSort(int[] array){
if(array == null){
return;
}
int end = array.length-1; //标记结束位置的下标
while(end>=0){
int parent = (end-1)/2;
while(parent>=0){
shiftDown(array,parent,end);
parent--;
}
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
end--;
}
}
public static void shiftDown(int[] array,int parent,int end){
int child = parent*2+1;
while(child <= end){
if(child+1<=end && array[child]<array[child+1]){
child++;
}
if(array[child]>array[parent]){
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
}
parent = child;
child = 2*parent+1;
}
}
5.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 不稳定 |
六、冒泡排序
6.1 原理
如果第一个元素比第二个大,就交换这两个的位置。依次比较相邻位置的元素,
6.2 实现
public static void bubble(int[] array){
if(array == null){
return;
}
for(int i = 0;i<array.length-1;i++){
System.out.printf("第%d趟比较",i+1);
for(int j = 0;j<array.length-1-i;j++){
if(array[j] > array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
}
}
System.out.println(Arrays.toString(array));
}
}
// 优化后
public static void bubbleSort(int[] array){
if(array == null){
return;
}
for(int i = 0;i<array.length-1;i++){
boolean flag = false;
System.out.printf("第%d趟比较",i+1);
for(int j = 0;j<array.length-1-i;j++){
if(array[j]>array[j+1]){
int tmp = array[j];
array[j] = array[j+1];
array[j+1] = tmp;
flag = true;
}
}
System.out.println(Arrays.toString(array));
if(! flag){
break;
}
}
}
6.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 稳定 |
七、快速排序
7.1 快速排序的递归实现
7.1.1 原理
step1:从待排序区间中选择一个数,作为基准值(pivot).
step2:Partiton:遍历整个待排序区间,将比基准值pivot小的放到基准值的左边,将比基准值pivot大的放在基准值的右边。
step3:采用分治思想,对左右两个区间按照同样的方式处理,直到区间的长度小于等于1.
7.1.2 挖坑法
public static void quickSort(int[] array){
if(array == null){
return;
}
quick(array,0,array.length-1);
}
public static void quick(int[] array,int left,int right){
if(right-left<=0){
return;
}
int privot = partition(array,left,right);
quick(array,left,privot-1);
quick(array,privot+1,right);
}
public static int partition(int[] array,int left,int right){
int tmp = array[left];
while(left<right){
while (left<right && array[right]>= tmp){
right--;
}
array[left] = array[right];
while(left<right && array[left] <= tmp){
left++;
}
array[right] = array[left];
if(left == right){
array[left] = tmp;
}
}
return left;
}
7.1.3 hoare法
public static void quickSort(int[] array){
if(array == null){
return;
}
quick(array,0,array.length-1);
}
public static void quick(int[] array,int left,int right){
if(right-left<=0){
return;
}
int privot = hoarePartition(array,left,right);
quick(array,left,privot-1);
quick(array,privot+1,right);
}
public static int hoarePartition(int[] array,int left,int right){
int key = left;
while(left<right){
while(left<right && array[right]>=array[key]){
right--;
}
while(left<right && array[left]<=array[key]){
left++;
}
if(left != right){
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
if(left == right){
int tmp = array[left];
array[left] = array[key];
array[key] = tmp;
}
}
return left;
}
7.1.4 优化
优化分为三个方面:
①选择基准值的优化:主要是想将待排序序列均匀的分割,越均匀,速度越快。
选择左边或者右边:若最小的数在最左边,将其作为基准值,第一趟排序中数组里的元素几乎没交换,还处于无序状态。
随机选取:不是特别好,有可能产生的随机数是第二小的,第一趟排序下来就只交换了一下。
几数取中法:以三数取中为例,先调换元素位置使其满足array[left]<=array[mid]<=array[right],然后调换array[mid]和array[left]的位置。
②partiton过程中把和基准值相等的数也选择出来。
③当递归执行到一个区间时,进行直接插入排序。
public static void midtofirst(int[] array,int left,int right){
int mid = (left+right)/2;
while(true){
if(array[mid]<array[left]){
int tmp = array[mid];
array[mid] = array[left];
array[left] = tmp;
}
if(array[mid] > array[right]){
int tmp = array[mid];
array[mid] = array[right];
array[right] = tmp;
}
if(array[left]>array[right]){
int tmp =array[left];
array[left] = array[right];
array[right] = tmp;
}
if(array[left]<=array[mid] && array[mid]<=array[right]){
break;
}
// 满足array[left]<= array[mid]<=array[right]
// 交换array[left] 和array[mid]
int tmp = array[left];
array[left] = array[mid];
array[mid] = tmp;
}
// 使用
public static void quickdiedai(int[] array,int left,int right){
if(right-left<=0){
return;
}
midtofirst(array,left,right);
int privot = hoarepartiton(array,left,right);
quickdiedai(array,left,privot-1);
quickdiedai(array,privot+1,right);
}
7.2 非递归实现
采用栈将每次的left和right都存储起来。注意每次的pop和push时需要与left和right对应起来。
public static void quickNull(int[] array){
if(array == null){
return;
}
Stack<Integer> stack = new Stack<>();
stack.push(0);
stack.push(array.length-1);
while(! stack.isEmpty()){
int right = stack.pop();
int left = stack.pop();
int privot = partition(array,left,right);
if(privot-left>1){
stack.push(left);
stack.push(privot-1);
}
if(right-privot>1){
stack.push(privot+1);
stack.push(right);
}
}
}
7.3 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 不稳定 |
7.4 总结
step1:在待排序区间选择一个基准值
选择左边或右边、随机选取、几数取中法
step2:做partition,使得左边的数小于privot,右边的数大于privot。
hoare、挖坑
step3:分治处理左右两个小区间,直到小区间数目小于一个阈值,使用插入排序。
八、归并排序
8.1 原理
建立在归并操作上的一种有效的排序算法。先将每个子序列排序,再将有序子序列合并成一个有序表。若将两个有序表合并成一个有序表,称为二路归并。
8.2 归并排序的递归实现
public static void mergeSort(int[] array){
if(array == null){
return;
}
mergesortdigui(array,0,array.length-1);
}
public static void mergesortdigui(int[] array,int left,int right){
if(right - left<=0){
return;
}
int mid = (left+right)/2;
mergesortdigui(array,left,mid);
mergesortdigui(array,mid+1,right);
merge(array,left,mid,right);
}
public static void merge(int[] array,int left,int mid,int right){
int[] elem = new int[right-left+1];
int k = 0;
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
// 这种情况是s1和s2没有越界的情况
while(s1<=e1 && s2<=e2){
if(array[s1]<array[s2]){
elem[k++] = array[s1++];
}else{
elem[k++] = array[s2++];
}
}
// s1没有越界了
while(s1<=e1){
elem[k++] = array[s1++];
}
// s2没有越界
while(s2<=e2){
elem[k++] = array[s2++];
}
for (int i = 0; i < elem.length; i++) {
array[i+left] = elem[i];
}
}
8.3 归并排序的非递归实现
public static void mergeNoRecurrence(int[] array){
if(array == null){
return;
}
for(int gap =1;gap<array.length;gap*=2){
mergefeidiedai(array,gap);
}
}
public static void mergefeidiedai(int[] array,int gap){
int s1 = 0;
int e1 = s1+gap-1;
int s2 = e1+1;
int e2 = s2+gap-1 <=array.length-1?(s2+gap-1):array.length-1;
int[] elem = new int[array.length];
int k = 0;
while(s2 <= array.length-1){
while(s1<=e1 && s2<=e2){
if(array[s1]<=array[s2]){
elem[k++] = array[s1++];
}else{
elem[k++] = array[s2++];
}
}
while(s1<=e1){
elem[k++] = array[s1++];
}
while(s2<=e2){
elem[k++] = array[s2++];
}
// 结束后,s2和e2都指向s2+1,要不然都跳不出上面的循环
s1 = s2;
e1 = s1+gap-1;
s2 = e1+1;
e2 = s2+gap-1 <=array.length-1?(s2+gap-1):array.length-1;
}
// 第一段是肯定会有的,第二段有可能没有
// 如果第二段没有,就会跳出循环
// 此处也不能用e1,因为e1也有可能超过数组长度
while(s1<=array.length-1){
elem[k++] = elem[s1++];
}
for(int i =0;i<elem.length;i++){
array[i] = elem[i];
}
}
8.4 性能分析
时间复杂度 | |
空间复杂度 | |
稳定性 | 稳定 |
九、排序总结
排序算法 | 时间复杂度(最坏) | 空间复杂度 | 稳定性 |
冒泡排序 |
| 稳定 | |
插入排序 |
| 稳定 | |
选择排序 |
| 不稳定 | |
希尔排序 |
| 不稳定 | |
堆排序 | 不稳定 | ||
快速排序 | 不稳定 | ||
归并排序 |
| 稳定 |