目录
一、插入排序
1、直接插入排序
每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,全部记录插入完成为止。
java
public static void insertSort(int[] arr){
for (int i = 1; i < arr.length; i++) {
int tmp=arr[i];
int j = i-1;
for (; j >=0; j--) {
if (arr[j]>tmp){
arr[j+1]=arr[j];
}else {
break;
}
}
arr[j+1]=tmp;
}
}
- 元素集合越接近有序则时间效率越高
|-------|---------------------------------------------------------------------------------------------------------------------------------------------------------|
| 空间复杂度 | O(1) |
| 时间复杂度 | 最好情况下:O(N)(数据完全有序,此时只有i在动) 最坏情况下:O() (数据完全逆序,此时全部元素移动次数之和为 ) |
| 稳定性 | 稳定 |
2、希尔排序(缩小增量法)
先将整个待排序的记录序列分割成若干个子序列分别进行直接插入排序,待整个序列的记录"基本有序"时,再对全体记录进行一次直接插入排序。
- gap:将待排序序列划分成多个子序列时元素之间的间隔距离
java
public static void shellSort(int[] arr){
int gap=arr.length/2;
while (gap>1){
shell(arr,gap);
gap/=2;
}
}
private static void shell(int[] arr, int gap) {
for (int i = gap; i < arr.length; i++) {
int tmp=arr[i];
int j=i-gap;
for (;j>=0;j--){
if (arr[j]>tmp){
arr[j+1]=arr[j];
}else {
break;
}
}
arr[j+1]=tmp;
}
}
- 希尔排序是对直接插入排序的优化
- 当gap>1时都是预排序,目的是让数组更接近于有序。当gap==1时,数组已经接近有序了,这样就会很快,这样整体而言,可以达到优化的效果。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定。而我们的gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O() 到 O() 来计算
- 空间复杂度:O(1)
- 稳定性:不稳定
二、选择排序
1、直接选择排序
每一次从待排序的数据元素中选出最小值,存放在待排序序列的起始位置,直到全部待排序的数据元素排完
java
public static void selectSort(int[] arr){
for (int i = 0; i < arr.length; i++) {
//1、找到待排序序列中的最小值
int minIndex=i;
for (int j = i+1; j < arr.length; j++) {
if (arr[j]<arr[minIndex]){
minIndex=j;
}
}
//2、将待排序序列中的最小值放到待排序序列起始位置
swap(arr,minIndex,i);
}
}
- 很好理解,但是效率不高
- 时间复杂度:O(N^2)(总元素总比较次数 )
- 空间复杂度:O(1)
- 稳定性:不稳定
改进版(时间复杂度和上面是一样的)
java
public static void selectSort2(int[] arr){
int left=0;
int right=arr.length-1;
while (left<right){
int minIndex=left;
int maxIndex=left;
for (int i = left+1; i <=right; i++) {
if (arr[i]<arr[minIndex]){
minIndex=i;
}
if (arr[i]>arr[maxIndex]){
maxIndex=i;
}
}
swap(arr,left,minIndex);
//最大值刚好在left位置,但是已经被交换到minIndex位置
if (maxIndex==left){
maxIndex=minIndex;
}
swap(arr,right,maxIndex);
left++;
right--;
}
}
2、堆排序
堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法,通过堆来选择数据。
- 排升序要建大堆,排降序建小堆
java
public static void heapSort(int[] array){
//1、建立大根堆
createBigHeap(array);
//2、在大根堆上进行堆排序
int end= array.length-1;
while (end>0){
swap(array,0,end);
shiftDown(array,0,end);
end--;
}
}
private static void createBigHeap(int[] array) {
for (int parent = (array.length-1)/2; parent >= 0; parent--) {
shiftDown(array,parent,array.length);
}
}
private static void shiftDown(int[] array, int parent, int length) {
int child=2*parent+1;
while (child<array.length){
if (child+1<array.length && array[child+1]>array[child]){
child++;
}
if (array[child]>array[parent]){
swap(array,child,parent);
parent=child;
child=2*parent+1;
}else {
break;
}
}
}
|-------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 时间复杂度 | ) ①N个结点建堆时间复杂度 ②N个节点排序,先把最大元素就位后再调整n-1个元素,每个元素进行一次O()此调整操作,也就是) ③总的时间复杂度就为(取高阶部分) |
| 空间复杂度 | O(1) |
| 稳定性 | 不稳定 |
三、交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置
特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
1、冒泡排序
java
public static void bubbleSort(int[] arr){
for (int i = 0; i < arr.length-1; i++) {//i控制排序轮数 n个元素进行n-1轮就能完成排序,每进行一轮就会把当前未排序部分的最大元素移动到length-1-i位置
boolean flag=false;
for (int j = 0; j < arr.length-1-i; j++) {//j控制每一轮中相邻元素的比较操作
if (arr[j+1]<arr[j]){
swap(arr,arr[j+1],arr[j]);
flag=true;
}
}
//每一趟走完都判断flag的值,若仍为false,则说明数组已经处于有序状态。则直接返回,不再进行后续多余的轮次比较
if (flag==false){
return;
}
}
}
- 时间复杂度:,加优化之后最好情况则为(走完第一轮就发现已有序)
- 时间复杂度:
- 稳定性:稳定
2、快速排序
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中的所有元素均大于基准值。然后左右子序列重复该过程,直到所有元素都排列在相应的位置上为止
java
public static void quickSort(int[] arr){
quick(arr,0,arr.length-1);
}
private static void quick(int[] arr, int start, int end) {
//判断:start==end表示子序列只有1个元素,start>end表示子序列无元素
//此时就表示子序列已经有序,直接返回
if (start>=end) return;
//1、选择基准值进行划分
int pivot=partion(arr,start,end);
//2、递归排序,分别对基准值左边的子数组和右子数组重复以上步骤(直到子数组元素个数为1(left==right)或者为0(left>right),此时整个数组就完成了排序)
quick(arr,start,pivot-1);
quick(arr,pivot+1,end);
}
上述为实现快速排序的主框架,而将区间按照基准值划分左右两部分partion( )的常见方式有:
(1)Hoare版
首先选取序列首元素作为基准值
通过双指针从序列两端开始向中间扫描,移动过程中,先右指针找到小于基准值的值,再等左指针找到大于基准值的值,交换左右指针元素的值
持续这个过程直到左右指针相遇,此时相遇位置就是基准元素的最终正确位置,而此时左右指针相遇位置的值也必然小于基准值,交换基准值与相遇值
经过这一步,序列以基准元素为界被划分成了两部分,左边部分的元素都小于基准值,右边部分的值都大于基准值
java
private static int partionHoare(int[] arr, int start, int end) {
int left=start;
int right=end;
int pivot=arr[start];
while (left<right){
while (right>left && arr[right]>=pivot) right--;
while (left<right && arr[left]<=pivot) left++;
swap(arr,left,right);
}
swap(arr,start,left);
return left;
}
(2)挖坑法
首先选取首元素作为基准值,将这个基准值看作一个坑
通过双指针从序列两端开始向中间扫描,移动过程中,先右指针找到小于基准值的值填入坑中,此时右指针所在的位置就变成了新的坑
接着让左指针找到大于基准值的值填入坑中,此时左指针所在的位置就变成了新的坑
如此反复,左右指针交替移动并填坑交换,直到两指针相遇,就把基准值填入此坑中
经过这一系列操作,就实现了以基准元素为界将序列划分成了左右两部分。左边的元素都小于基准值,右边的元素都大于基准值
java
private static int partionHug(int[] arr, int start, int end) {
int left=start;
int right=end;
int pivot=arr[start];
while (left<right){
while (right>left && arr[right]>pivot) right--;
arr[left]=arr[right];
while (left<right && arr[left]<pivot) left++;
arr[right]=arr[left];
}
arr[left]=pivot;
return left;
}
(3)前后指针
- 前指针prev负责标记已经处理过的、符合小于基准值的元素的区域边界
- 后指针cur负责遍历序列找到大于基准值的元素
(1)当cur所指向的元素大于等于基准值,则cur直接向后移动
(2)当cur找到小于基准值的值,就把prev向后移动一位。判断此时prev与cur是否有间隔:
- 若有,则表示此时[prev,cur)这些下标的元素都是大于基准值的元素,那就需要先交换cur和prev元素的位置。保证prev仍是标记已经处理过的、符合小于基准值的元素的区域边界
- 若没有,则此时prev与cur所指向的都是同一个元素,这个元素就是小于基准值的,则不用交换。prev仍是标记已经处理过的、符合小于基准值的元素的区域边界
- 以上判断处理后,cur才能继续向后移动遍历序列
(3)循环以上操作直到cur遍历完整个序列
java
private static int partion(int[] arr, int start, int end) {
int prev=start;
int cur= start+1;
while (cur <= end){
//cur找到小于基准值的值,就把prev向后移动一位。如果此时cur与prev指针之间有间隔,则交换prev与cur所指向的元素,之后cur继续向后移动
//若cur所指向的元素大于等于基准值,则cur直接向后移动
if (arr[cur]<arr[start] && arr[++prev]!=arr[cur])
swap(arr,cur,prev);
cur++;
}
//持续这个过程,直到cur遍历完整个序列
//当cur遍历完整个序列,就把基准值元素与prev所指向的元素进行交换,此时这个prev所指向的位置就是基准元素的最终正确位置
swap(arr,prev, start);
return prev;
}
性能分析
时间复杂度
- 最好情况:
当每次划分时选取的基准元素恰好能将待排序均匀的分为两个子序列,此时其递归树高度为;
每次划分操作都需要对当前层的子序列中的元素遍历一遍,不过每一层所有的子序列元素个数总和其实就是最初整个待排序序列的元素个数N。
由于递归树高度为,每一层操作时间复杂度为,根据乘法原理,整个快速排序在最好情况下的时间复杂度就为
- 最坏情况:
当每次划分时,选取的基准元素都是当前序列中的最大或最小值
比如待排序序列N个元素本身已经有序或逆序时,选取的基准元素又是两边的最大值或是最小值,则划分出来的两个子序列一个为空,另一个包含N-1个元素,这样就会导致递归树高度变为N(单分支树);每次划分操作都需要对当前层的子序列中的元素遍历一遍,每一层元素个数为N-1、N-2、N-3、N-4、......1、0(N层)
时间复杂度计算(N-1)+(N-2)+(N-3)+(N-4)+......+1+0=
关注趋势而非精确时间,去掉系数得时间复杂度为
空间复杂度:
- 最好情况:
- 最坏情况:
稳定性:不稳定
比如用Hoare版进行划分的话,【7,5,4,7*,3,2】以7为基准,经过一次划分后:【2,5,4,7*,3,2】,两个7的相对位置这就发生了变化。
优化
1、三数取中法选基准值
旨在避免快速排序的最坏情况:每次选择的基准值都是序列中的最大值或最小值
具体做法是从数组的开头、中间和结尾分别选取一个元素,然后对这三个元素进行排序,选择中间的元素作为基准值。这样就可以确保基准值大致位于序列的中间位置,从而减少递归的深度和比较次数,提高排序效率
2、递归到小的区间时可以考虑使用插入排序
快速排序在小区间效率较低,因为其递归调用和子序列划分的开销较大,而插入排序在小规模数据上的性能更好,因此通过优化快速排序,在小规模数据时使用插入排序可以提高效率。
java
public static void quickSort(int[] array){
quick(array,0,array.length-1);
}
private static void quick(int[] array,int start,int end){
if(start >= end) return;//左边是一个节点,或者 一个节点都没有
//小区间用插排减少递归次数
if(end-start+1 <=15){
insertSortRange(array,start,end);
return;
}
//三数求中
int index=midOfThree(array,start,end);
swap(array,index,start); //保证 start下标是中间值,以中间大小的值作为基准,减少空间
int pivot=partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
//区间内 进行插入排序
private static void insertSortRange(int[] array,int begin,int end){
for (int i = begin+1; i <= end ; i++) {
int tmp=array[i];
int j=i-1;
for (; j >=begin ; j--) {
if (array[j]>tmp){
array[j+1]=array[j];
}else {
break;
}
}
array[j+1]=tmp;
}
}
//三数求中
private static int midOfThree(int[] array,int left,int right){
//1、先找到 mid 位置
int mid=(left+right)/2;
//2、找到 3 个数中 中间大的数字
if(array[left]<array[right]){
if(array[mid]<array[left]){
return left;
}else if(array[mid]>array[right]){
return right;
}else {
return mid;
}
}else {
if (array[mid]>array[left]){
return left;
}else if(array[right]>array[mid]){
return right;
}else {
return mid;
}
}
}
非递归实现
java
public static void quickSortNor(int[] array){
Stack<Integer> stack=new Stack<>();
int left=0;
int right=array.length-1;
int pivot=partitionDig2(array,left,right);
if (pivot-1>left){
//左边一定有两个元素
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
while (!stack.isEmpty()){
right=stack.pop();
left=stack.pop();
pivot=partitionDig2(array,left,right);
if (pivot-1>left){
//左边一定有两个元素
stack.push(left);
stack.push(pivot-1);
}
if(pivot+1<right){
stack.push(pivot+1);
stack.push(right);
}
}
}
四、归并排序
先将数组不断分解为单个的子数组,再将相邻子数组合并为有序数组,重复合并操作,直至得到完整的有序数组
1、递归法
java
public static void mergeSort(int[] array){
mergeSortFunc(array,0,array.length-1);
}
private static void mergeSortFunc(int[] array, int left, int right) {
if(left>=right) return;
int mid=(left+right)/2;
mergeSortFunc(array,left,mid);
mergeSortFunc(array,mid+1,right);
merge(array,left,right,mid);
}
private static void merge(int[] array, int left, int right, int mid) {
int s1=left;
int s2=mid+1;
int[] tmpArr=new int[right-left+1];
int k=0;
//两个区间都同时是有数据的
while (s1 <=mid && s2<=right){
if(array[s2] <= array[s1]){
tmpArr[k++]=array[s2++];
}else{
tmpArr[k++]=array[s1++];
}
}
while(s1<=mid){
//第一个段还有数据
tmpArr[k++]=array[s1++];
}
while(s2<=right){
//第二个段还有数据
tmpArr[k++]=array[s2++];
}
//此时tmpArr一定是这个区间里面有序的数据了
for (int i = 0; i < tmpArr.length; i++) {
array[i+left]= tmpArr[i];
}
}
- 空间复杂度:
- 时间复杂度:
- 稳定性:稳定
2、非递归法
java
//非递归 归并排序
public static void mergeSortNor(int[] array) {
int gap = 1;
while (gap < array.length) {//还没归并完
for (int i = 0; i < array.length; i += 2 * gap) {
int left=i;
int mid=left+gap-1;
int right=mid+gap;
if (mid>=array.length){
mid=array.length-1;
}
if (right>=array.length){
right=array.length-1;
}
merge(array,left,right,mid);
}
gap*=2;
}
}
3、海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中无法把数据全部放下,所以需要外部排序,而归并排序是最常用的
- 把文件切分成200份,每个512M
- 分别对512M排序,因为内存可以放得下,所以任意排序方式都可以
- 进行二路归并,同时对200有序文件做归并过程,最终结果就有序了
五、排序算法及稳定性分析
排序算法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | 稳定 | ||||
插入排序 | 稳定 | ||||
选择排序 | 不稳定 | ||||
希尔排序 | 不稳定 | ||||
堆排序 | 不稳定 | ||||
快速排序 | ~ | 不稳定 | |||
归并排序 | 稳定 |
六、其他非基于比较排序
1、计数排序
先统计待排序数组中各元素出现次数,记录于计数数组;再依据计数数组依次将元素按序返回原数组,实现排序
- 适用于一定范围整数排序
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
java
public static void countSort(int[] array){
int minVal=array[0];
int maxVal=array[0];
//1、求当前数组的最大值 和 最小值
for (int i = 1; i < array.length; i++) {
if (array[i] <minVal){
minVal=array[i];
}
if (array[i]>maxVal){
maxVal=array[i];
}
}
//2、跟进最大值 和 最小值 来确定数组的大小
int[] count=new int[maxVal-minVal+1];
//3、遍历原来的数组开始计数
for (int i = 0; i < array.length; i++) {
count[array[i]-minVal]++;
}
//4、遍历计数数组 count,把当前元素 写回 array
int index=0;//重新表示array数组的下标
for (int i = 0; i < count.length; i++) {
while(count[i]>0){
array[index]=i+minVal;
index++;
count[i]--;
}
}
}
2、基数排序
3、桶排序