了解一个排序算法一般要从排序算法的实现原理,代码实现,时间复杂度,空间复杂度,稳定性,适合场景,哪些缺陷或者有哪些优化措施。
-
插入排序
-
实现原理:可以将待排序数组分为有序区间和无序区间,每次都从无序区间选取第一个数据放到有序区间的合适位置。
-
代码实现:
private void insertSort(int[] nums){
for(int i=1;i<nums.length;i++){
int tmp=nums[i];
int d=i-1;
//不写nums[d]>=tmp是保证稳定性
while(d>=0&&nums[d]>tmp){
nums[d+1]=nums[d];
d--;
}
nums[d+1]=tmp;
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
- 比较适合待排序数据比较有序,数据数量比较小的场景
- 折半插入排序
既然每次都是在有序区间查找合适的位置,所以可以用二分查找来搜索插入位置
private void insertBinarySort(int[] nums){
for(int i=1;i<nums.length;i++){
int tmp=nums[i];
int left=0;
int right=i;
while(left<right){
int mid=(left+right)>>1;
if(nums[mid]<=tmp){
left=mid+1;
}else{
right=mid;
}
}
System.arraycopy(nums,left,nums,left+1,i-left);
nums[left]=tmp;
}
}
-
希尔排序
-
实现原理:希尔排序又叫缩小增量法。每次选定一个间隔gap,间隔相同的为一组,对同一组的数据进行排序,然后不断的缩小gap,gap>1时候的排序都是预排序,当gap等于1的时候,数组已经接近有序了,gap等于1排完后就有序了。
-
代码实现:
private void shellSort(int[] nums){
int gap=nums.length;
while(gap>1){
shell(nums,gap);
gap=gap/3+1;
}
shell(nums,1);
}
private void shell(int[] nums,int gap){
for(int i=gap;i<nums.length;i++){
int tmp=nums[i];
int d=i-gap;
while(d>=0&&nums[d]>tmp){
nums[d+gap]=nums[d];
d-=gap;
}
nums[d+gap]=tmp;
}
}
-
时间复杂度:跟gap的取值有关,平均:O(N^1.3)
-
空间复杂度:O(1)
-
稳定性:不稳定
-
选择排序
-
实现原理:每次都在无序区间里面选出一个最大或最小的元素放在有序区间的最前或最后。
-
代码实现:
private void selectSort(int[] nums){
for(int i=0;i<nums.length-1;i++){
int max=0;
for(int j=1;j<nums.length-i;j++){
if(nums[j]>nums[max]){
max=j;
}
}
swap(nums,max,nums.length-1-i);
}
}
- 时间复杂度:O(N^2);
- 空间复杂度:O(1);
- 稳定性:不稳定
- 双向选择排序:既然每次都要遍历一次无序区间来选出最大或最小的元素,那还不如一次把最大和最小的元素都选出来。
private void selectDouble(int[] nums){
int left=0;
int right=nums.length-1;
while(left<right){
int max=left;
int min=left;
for(int i=left+1;i<=right;i++){
if(nums[i]>nums[max]){
max=i;
}
if(nums[i]<nums[min]){
min=i;
}
}
swap(nums,max,right);
//可能right的位置恰好为min的位置
if(min==right){
min=max;
}
swap(nums,min,left);
left++;
right--;
}
}
- 堆排序
- 实现原理:基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。注意: 排升序要建大堆;排降序要建小堆。
- 代码实现:
public void heapSort(int[] nums){
createHeap(nums);
for(int i=0;i<nums.length-1;i++){
swap(nums,0,nums.length-1-i);
shiftDown(nums,0,nums.length-i-1);
}
}
private void createHeap(int[] nums){
for(int i=(nums.length-2)/2;i>=0;i--){
shiftDown(nums,i,nums.length);
}
}
private void shiftDown(int[] nums,int parent,int size){
int left=2*parent+1;
while(left<size){
int max=left;
int right=2*parent+2;
if(right<size&&nums[right]>nums[max]){
max=right;
}
if(nums[parent]>nums[max]){
break;
}
swap(nums,max,parent);
parent=max;
left=2*parent+1;
}
}
- 时间复杂度:N*O(logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
- 冒泡排序
- 实现原理:每次相邻的元素比较,如果前面的元素比后面的大就交换两元素,每一趟都能把最大的元素排到最后。
- 代码实现:
private void bubbleSort(int[] nums){
boolean isSort=false;
for(int i=0;i<nums.length-1;i++){
isSort=true;
for(int j=0;j<nums.length-1-i;j++){
if(nums[j]>nums[j+1]){
isSort=false;
swap(nums,j,j+1);
}
}
if(isSort){
break;
}
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
- 快速排序
- 实现原理:每次都选出一个基准值,把比基准值小的放在一侧,比基准值大大元素放在另一侧,然后采用同样的方法递归的处理两侧的元素,直到区间就一个元素或者没有元素就排好了。
- 基准值的选择:hoare方法,挖坑法,前后索引法
- 代码实现:
public void quickSort(int[] nums,int left,int right){
if(left>=right){
return;
}
int p=partition3(nums,left,right);
quickSort(nums,left,p-1);
quickSort(nums,p+1,right);
}
//hoare法
private int partition1(int[] nums,int left,int right){
int pivot=nums[left];
int i=left;
int j=right;
while(i<j){
while(i<j&&nums[j]>=pivot){
j--;
}
while(i<j&&nums[i]<=pivot){
i++;
}
swap(nums,i,j);
}
swap(nums,i,left);
return i;
}
//挖坑法
private int partition2(int[] nums,int left,int right){
int pivot=nums[left];
int i=left;
int j=right;
while(i<j){
while(i<j&&nums[j]>=pivot){
j--;
}
nums[i]=nums[j];
while(i<j&&nums[i]<=pivot){
i++;
}
nums[j]=nums[i];
}
nums[i]=pivot;
return i;
}
//前后索引
private int partition3(int[] nums,int left,int right){
int pivot=nums[left];
int d=left+1;
for(int i=left+1;i<=right;i++){
if(nums[i]<pivot){
swap(nums,d,i);
d++;
}
}
swap(nums,d-1,left);
return d-1;
}
-
优化:为了避免在选取基准值的时候选到最值(如果选到最值,那所有元素都会在一侧,如果每次都选到最值,就好像一棵单支树,那就会严重影响到效率),每次在选取基准值的时候采用三数取中法(left,right,mid);
还可以在选取基准值的时候将跟基准值相等的元素也选出来;
当区间内的元素个数小于某一个阈值的时候采用插入法排序,不再递归 -
时间复杂度:O(N*logN)
-
空间复杂度:O(logN)
-
稳定性:不稳定
-
非递归:当数据特别多的时候,递归可能会溢出
private void quickSortNor(int[] nums){
Stack<Integer> stack=new Stack<>();
stack.push(nums.length-1);
stack.push(0);
while(!stack.isEmpty()){
int left=stack.pop();
int right=stack.pop();
if(left>=right){
continue;
}
int p=partition3(nums,left,right);
stack.push(right);
stack.push(p+1);
stack.push(p-1);
stack.push(left);
}
}
- 归并排序
- 实现原理:每次都将待排序区间均分,当区间元素个数为0或者1的时候就开始归并。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
- 代码实现:
public void mergeSort(int[] nums,int left,int right){
if(left>=right){
return;
}
int mid=(left+right)>>1;
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
merge(nums,left,mid,right);
}
private void merge(int[] nums,int left,int mid,int right){
int i=left;
int j=mid+1;
int len=right-left+1;
int k=0;
int[] tmp=new int[len];
while(i<=mid&&j<=right){
if(nums[i]<=nums[j]){
tmp[k++]=nums[i++];
}else{
tmp[k++]=nums[j++];
}
}
while(i<=mid){
tmp[k++]=nums[i++];
}
while(j<=right){
tmp[k++]=nums[j++];
}
System.arraycopy(tmp,0,nums,left,len);
}
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
- 非递归:
public void mergeSortNor(int[] nums){
for(int i=1;i<nums.length;i*=2){
for(int j=0;j<nums.length;j+=2*i){
int left=j;
int mid=j+i;
int right=j+2*i;
if(mid>nums.length){
mid=nums.length;
}
if(right>nums.length){
right=nums.length;
}
merge(nums,left,mid-1,right-1);
}
}
}
- 归并排序也属于外部排序(待排数据不能一次性全部加载到内存中)
比如:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
- 先把文件切分成 200 份,每个 512 M
- 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
- 进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了