本文已参与「新人创作礼」活动,一起开启掘金创作之路。
1. 三种低级排序
冒泡排序
选择排序
插入排序
(希尔排序)
2. 三种高级排序
- 快速排序 时间:o(nlogn) ~ o(n^2) 空间:o(logn)
分而治之,选取一个支点pivot(具体值),先整体有序,再局部有序 当一个序列中存在大量重复的元素,复杂度会趋近于O(n^2)
- 归并排序 时间:o(nlogn) 空间:o(n) 分而治之,,选取一个支点pivot(数组下标),先局部有序,再整体有序
相同点:
两个排序的基本思想都是分治--分而治之;具体实现都用递归。
不同点:
1、快速排序:边分解边排序。每次分解都实现整体上有序,即参照值左侧的数都小于参照值,右侧的大于参照值;是自上而下的排序; 归并排序:先分解再合并。先递归分解到最小区间,然后从小去区间开始合并排序,是自下而上的归并排序; 2、选取分界点时,快速排序选的是值,一半比这个值大,一半比这个值小。 归并排序选的是数组地址,即下标。不用交换处理,直接把数组一切两半。 3、快速排序是原地排序,原地排序指的是空间复杂度为O(1); 归并排序不是原地排序,因为两个有序数组的合并需要额外的空间协助才能合并; 4、快速排序是不稳定的,时间复杂度在O(nlogn)~O(n^2)之间 。归并排序是稳定的,时间复杂度是O(nlogn)。
- 堆排序 时间:o(nlogn) 空间:o(1)
进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法
3. 总结
当数据量,数据规模较大时,应该采用此3类排序算法,这样效率相比于之前的时间复杂度为O(n^2)的三种排序算法来说更高、更好些。 这三类排序算法的结论:
- 快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况,适合超大数据量。这两种排序都是不稳定的。
- 若要求排序稳定,则可选用归并排序。
三种常用的高级排序算法
方法一:快速排序
复杂度分析
- 时间复杂度:最坏是O(n^2),最好是O(nlogn),平均复杂度O(nlogn)
- 空间复杂度:因为没有额外的空间开销,O(1)
- 算法稳定性:快速排序是不稳定的排序算法。因为我们无法保证相等的数据按顺序被扫描到和按顺序存放。 当一个序列中存在大量重复的元素,复杂度会趋近于O(n^2)
public void quickSort(int[] nums,int low, int high){
int base = nums[low];
int start=low;
int end=high;
while(end>start){//注意start必须小于end
while(end>start && nums[end]>=base) end--;//这里是while,而不是if
if(end>start){
nums[start]=nums[end];
start++;
//nums[end]=base;
}
while(end>start && nums[start]<=base) start++;
if(end>start){
nums[end]=nums[start];
end--;
//nums[start]=base;
}
}
//把基数放到start==end的位置
nums[start]=base;
//对左边进行快排
if(start>low) quickSort(nums,low,start-1);
//对右边进行快排
if(end<high) quickSort(nums,end+1,high);
}
方法二:归并排序
复杂度分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n),需要开辟额外的空间。
- 算法稳定性:归并排序是稳定的排序算法
public void mergeSort(int[] arr, int low, int high){
if(low<high){
int mid = (low+high)/2;
mergeSort(arr,low,mid);
mergeSort(arr,mid+1,high);
merge(arr, low, mid, high);
}
}
private void merge(int[] arr, int low, int mid, int high){
int left_start = low, left_end = mid;
int right_start = mid+1, right_end = high;
int[] tmp = new int[high-low+1];
int i = 0;
while(left_start<=left_end && right_start<=right_end){
tmp[i++] = arr[left_start]<=arr[right_start] ? arr[left_start++] : arr[right_start++];
}
while(left_start<=left_end){
tmp[i++] = arr[left_start++];
}
while(right_start<=right_end){
tmp[i++]=arr[right_start++];
}
for(int j = 0;j<i; j++){
arr[low+j] = tmp[j];
}
}
方法三:堆排序
复杂度分析
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)。
- 算法稳定性:不稳定 进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法
public void HeapSort(int[] arr) {
/**
* 第一步,初始化堆,最大堆,从小到大。目的是对元素排序
* i从完全二叉树的第一个非叶子节点开始,也就是len/2-1开始(数组下标从0开始),从下往上调整堆
*/
for (int i = arr.length / 2 - 1; i >= 0; i--) {
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr, i, arr.length);
}
/**
* 第二步,交换堆顶(最大)元素和二叉堆的最后一个叶子节点元素。目的是交换元素
* i从二叉堆的最后一个叶子节点元素开始,也就是len-1开始(数组下标从0开始)
*/
for (int j = arr.length - 1; j > 0; j--) {
//将堆顶元素与末尾元素进行交换
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
//交换完之后需要重新调整二叉堆,从头开始调整,此时Index=0
adjustHeap(arr, 0, j);
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* 从上往下,从左往右调整
* @param arr
* @param index
* @param length
*/
public void adjustHeap(int[] arr, int index, int length) {//注意arr是数组
int temp = arr[index];//先取出当前元素i
int l_leaf = 2 * index + 1;
for (int i = l_leaf; i < length; i = 2 * i + 1) {//总是从结点的左子结点开始
int r_leaf = i + 1;
//如果左子结点小于右子结点,i指向右子结点
i = (r_leaf < length && arr[i] < arr[r_leaf]) ? r_leaf : i;
if (arr[i] > temp) {//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[index] = arr[i];//父节点已经调整完毕,不再变动,可以赋值
index = i;// 可能需要继续往下调整堆,只记录索引
} else {
break;
}
}
arr[index] = temp;//将temp值放到最终的位置
}