前面所讲的冒泡、插入和选择排序,因为平均时间复杂度为o(n^2),所以仅仅适合小数据量的排序工作。在数据量比较大时,平均时间复杂度为o(nlogn)的归并和快排就派上用场了,下面分别介绍归并排序和快速排序。
1. 归并排序
- 最好、最坏、平均时间复杂度都为o(nlogn)
- 不属于原地排序算法,其空间复杂度为o(n),因为merge函数中需要使用额外的空间
- 其排序的关键在于merge函数,merge函数中可以实现稳定排序,所以整个归并排序过程是稳定的
class solution{
public void mergeSort(int[]a, n){
mergeSortC(a, 0, n-1)
};
public void mergeSortC(int[]a, int p, int r){
if(p>=r) return;
int q = (p+r)/2;
//递归归并排序
mergeSortC(a, p, q);
mergeSortC(a, q+1, r);
merge(a,p,q,r);
};
public void merge(int[]a, int p, int q,int r){
int[] temp = new int[r-p];
int i=p, j=q+1,k=0;
while(i<=q && j<=r){
if(a[i]<=a[j]){
temp[k++] = a[i++];
}else{
temp[k++] = a[j++];
}
}
//判断那个子数组有剩余数据
int start = i, end = q;
if(j<=r){
start = j;
end = r;
}
//将剩余的数据拷贝到临时数组
while(start<=end){
temp[k++] = a[start++];
}
for(int m=0; m<r-p; m++){
a[m] = temp[m];
}
};
}
2. 快速排序
- 最好时间复杂度,平均时间复杂度都为o(nlogn),最坏时间复杂度为o(n^2)
- 快速排序与归并排序相比,巧妙的在partition函数中进行原地分区,所以快速排序是原地排序算法
- 快速排序partition中的原地分区使得快速排序不稳定
法一:不加哨兵
class Solution{
public void quickSort(int[]a, int n){
quickSortC(a, 0, n-1);
};
//实现快排的主函数
public void quickSortC(int[]a, int p, int r){
if(p>=r) return; //递归终止条件
q = partition(a, p, r);
quickSortC(a, p, q-1);
quickSortC(a, q+1, r);
};
//partition分割函数
public int partition(int[]a, int p, int r){
int pivot = a[r];
//将整个数组分为两个部分,左半部分小于pivot,右半部分大于pivot
int i = p;
for(int j=p; j<r; j++){
if(a[j]<pivot){
int temp=a[j];
a[j]=a[i];
a[i]=temp;
i++;
}
}
//交换a[i]和a[r]的值
int temp1 = a[r];
a[r] = a[i];
a[i] = temp1;
return i;
};
}
法二:添加哨兵,可以减少一些交换步骤,提高程序效率
class Solution{
public void quickSort(int[]a, int n){
quickSortC(a, 0, n-1);
};
public void quickSortC(int[]a, int p, int r){
int low = p;
int high = r;
int pivot = a[low]; //选择最左端点的值为轴枢值
while(low<high){
//从后往前找到值比轴枢值小的值
while(low<high && a[high]>=pivot){
high--;
}
a[low] = a[high];
while(low<high && a[low]<=pivot){
low++;
}
a[high] = a[low];
}
a[low] = pivot; //找到了轴枢值应该在的位置
if(low > p+1){
quickSortC(a, p, low-1);
}
if(high < r-1){
quickSortC(a, high+1, r);
}
};
}
实战一:求无序数组的第k大元素(leetcode215)
- 思路:我们将问题转换为求无序数组的第k大的元素的索引,其索引值为len-k,其中len为数组的长度,我们选择数组区间 nums[0...n-1]的最后一个元素 nums[n-1]作为 pivot,对数组 nums[0...n-1]原地分区,这样数组就分成了三部分,nums[0...p-1]、nums[p]、nums[p+1...n-1]。如果 p=len-k,那么nums[p]就是要求解的元素;如果 len-k>p, 说明第 k 大元素出现在 nums[p+1...n-1]区间,我们再按照上面的思路递归地在 nums[p+1...n-1]这个区间内查找。同理,如果 len-k<p,那我们就在 nums[0...p-1]区间查找。
- 时间复杂度:o(n)。时间复杂度为一个等比数列,n, n/2,n/4,n/8,...,1,最后求和为值2n-1。
- 空间复杂度为o(1)
class Solution {
//基于快速排序partition方法的实现
public int findKthLargest(int[] nums, int k) {
int len = nums.length; //得出数组的长度
int left = 0;
int right = len -1;
//将求第k大的元素转换为第k大元素的索引是len-k
int target = len-k;
while(true){
int index = partition(nums, left, right);
if(target == index){
return nums[index];
}
else if(target<index){
right = index-1;
}else{
left = index+1;
}
}
};
public int partition(int[]nums, int p, int q){
int pivot = nums[q];
int i = p;
for(int j=p; j<q; j++){
if(nums[j]<pivot){
swap(nums,i,j);
i++;
}
}
swap(nums, i, q);
return i;
};
public void swap(int[]nums,int p, int q){
int temp = nums[p];
nums[p] = nums[q];
nums[q] = temp;
};
}