一、归并排序
基本思想
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。如图所示:
代码实现:
public static void mergeSort(int[] array){
mergeSortFun(array,0,array.length-1);
}
public static void mergeSortFun(int[] array,int left,int right){
if(left==right){
return;
}
//拆分
int mid = (right+left)/2;
mergeSortFun(array,left,mid);
mergeSortFun(array,mid+1,right);
//合并
merge(array,left,mid,right);
}
private static void merge(int[] array,int left,int mid,int right){
int[] tmp = new int[right-left-1];
int k = 0;
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
while(s1<=e1&&s2<=e2){
if(array[s1]<=array[s2]){//加=是稳定排序
tmp[k++] = array[s1++];
}else{
tmp[k++] = array[s2++];
}
}
while(s1<=e1){
tmp[k++] = array[s1++];
}
while(s2<=e2){
tmp[k++] = array[s2++];
}
for(int i = 0;i<k;i++){
array[i+left] = tmp[i];
}
}
归并排序总结
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定,取决于-->
归并排序的非递归
思想图解:
public static void mergeSortNor(int[] array){
int gap = 1;
while(gap<array.length){
for (int i = 0; i < array.length; i = i+2*gap) {
int left = i;
int mid = left+gap-1;
//如果是奇数的话可能会出现越界情况,left的下一个没有数了
if(mid>=array.length){
mid = array.length-1;//直接让mid指向最后一个数
}
int right = mid+gap;
//right也会出现和mid一样的情况
if(right>=array.length){
right = array.length-1;
}
merge(array,left,mid,right);
}
gap*=2;
}
}
归并排序的应用
海量数据的排序问题
外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1. 先把文件切分成 200 份,每个 512 M
2. 分别对512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3. 进行2路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
图解:
二、 排序算法总结
排序算法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
冒泡排序 | O(n)优化下 | O(n^2) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(n^1.3)~O(n^1.5) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(1) | 不稳定 |
快速排序 | O(n*log(n)) | O(n*log(n)) | O(n^2) | O(log(n))~O(n) | 不稳定 |
归并排序 | O(n*log(n)) | O(n*log(n)) | O(n*log(n)) | O(n) | 稳定 |
三、其他非基于比较排序
1、计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
如果数据比较大,可以找到最大的数据和最小的数据,差值+1就是计数数组的大小,待排数组的值 - 最小值就是计数数组的下标,所以取得时候要将下标值+最小值
//适合数据集中的排序
public static void countSort(int[] array){
//找到最小和最大的数
int maxVal = array[0];
int minVal = array[0];
for(int i = 0;i<array.length;i++){
if(maxVal<array[i]){
maxVal=array[i];
}
if(minVal>array[i]){
minVal = array[i];
}
}
//定义count数组
int[] count = new int[maxVal-minVal+1];
//遍历array数组,将count与array联系起来
for (int i = 0;i<array.length;i++){
int val = array[i];
count[val-minVal]++;
}
int index = 0;
for(int i=0;i<count.length;i++){
while(count[i]>0){
array[index] = i+minVal;
index++;
count[i]--;
}
}
}
计数排序总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
2. 时间复杂度:O(N+数据范围)
3. 空间复杂度:O(数据范围)
4. 稳定性:稳定
2、基数排序
//基数排序
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}
//找到最大值
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
//找到最大值得最高有几位
protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int length = 0;
for (long temp = num; temp != 0; temp /= 10) {
length++;
}
return length;
}
public void radixSort(int[] arr) {
int maxDigit = getMaxDigit(arr);//最大值得最高的位数
int mod = 10;
int dev = 1;
for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];
for (int j = 0; j < arr.length; j++) {
int bucket = ((arr[j] % mod) / dev) + mod;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}
//依次将bucket里的值放入arr中
int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
}
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}