这是我参与更文挑战的第 8 天,活动详情查看: 更文挑战
-
Merge Sort
分治思想,可以用递归来实现,最后合并两个数组。
伪代码
//A 数组,n表示数组大小 merge_sort(A,n){ merge_sort_internal(A,0,n-1) } //递归调用函数 merge_sort_internal(A,p,r){ //递归终止条件 if(p >= r) then return //取 p 到 r 直接的中间位置 q q = p + ((r-p) >> 2) merge_sort_internal(p,q) merge_sort_internal(q+1,r) //合并两个数组,将A[p...q]和A[q+1...r]合并为A[p...r] merge(A,p,q,r) //merge(A[p,q],A[q+1,r]) }具体代码如下,尤其要注意合并两个数组时,哪个数组中还有值的判断条件。
调用 public static void main(String[] args) { int[] a = {9,9,10,11,12,1,3,5,6,8}; mergeSortA(a,a.length); System.out.println(Arrays.toString(a)); } public static void mergeSortA(int[] a,int length){ mergeInternal(a,0,length - 1); } private static void mergeInternal(int[] a,int p,int r){ if (p >= r){ return; } int q = p + ((r - p) >> 2); mergeInternal(a,p,q); mergeInternal(a,q+1,r); mergeArray(a,p,q,r); } private static void mergeArray(int[] a,int p,int q,int r){ int i = p; int j = q+1; int k = 0; //给temp数组使用 int[] temp = new int[r-p+1]; while (i <= q && j <= r){ if (a[i] <= a[j]){ temp[k++] = a[i++]; } else { temp[k++] = a[j++]; } } int start = i; int end = q; if (j <= r){ //因为前面使用的是j++,所以如果上面最后一次while循环是j=r,同时执行了temp[k++] = a[j++]; j就永远大于r了 start = j; end = r; } while (start <= end){ temp[k++] = a[start++]; } for (int m = 0;m <= r-p;m++){ a[p+m] = temp[m]; }归并排序并不是原地排序算法,因为涉及到合并数组,需要消耗额外的空间(致命弱点),是稳定的排序算法。
-
Quick Sort
从排序数组中下标从 p 到 r 之间的一组数据,从 p 到 r 之间选择任意一个数据作为 pivot(分区点q),小于 pivot 在前,大于 pivot 在后。分治思想,递归处理,直到 p
q-1,q+1r 这两个区间都缩小为1,就变成有序的了。快排的核心思想就是分治和分区。
伪代码:
quick_sort(A,n){ //n 是 数组大小 quick_sort_internal(A,0,n-1) } quick_sort_internal(A,p,r){ if(p >= r) return //终止条件 q = partitionn(A,p,r) //获取分区点 quick_sort_internal(A,p,q-1) quick_sort_internal(A,q+1,r) } //分区函数 partition(A,p,r){ return 分区点 }如果希望快排是原地排序算法,那么它的空间复杂度得是 ,不需要额外空间。快排不是稳定的排序算法(涉及值的交换)。
具体代码如下:
private static void quickSortA(int[] a,int length){ quickSortInternal(a,0,length - 1); } private static void quickSortInternal(int[] a,int left,int right){ if (left >= right) return; int q = division(a,left,right); quickSortInternal(a,left,q-1); quickSortInternal(a,q+1,right); } private static int division(int[] a,int left,int right){ int base = a[left]; //以左边为基准点 while (left < right) { //从序列右端开始,往左遍历,找到小于 base 的值 while (left < right && a[right] >= base) { right--; } a[left] = a[right]; //从序列左端开始,往右遍历,找到大于 base 的值 while (left < right && a[left] <= base){ left++; } a[right] = a[left]; } a[left] = base; return left; }快排和归并排序的对比,归并排序是从下到上的,先处理子问题,然后再合并,而快排是从上到下,先分区,然后再处理子问题。
-
Heap Sort
堆排序可以分解成两个大的步骤,建堆和排序。建堆的时间复杂度是 O(n),建堆时我们是按照大顶堆的特性来组织的。建堆可以按照从上到下、或者从下到上的方式插入。排序是有点类似删除堆顶部元素,首先将 0 和 n 的位置交换,然后将 0
k-1 进行堆化,继续交换 0 和 n-1 的位置,将 0k-2 进行堆化,依次类推。堆排序是原地排序,时间复杂度 O(nlogn),不是稳定的排序算法,因为在排序的过程中存在最后一个节点更堆定点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
public static void heapSort(int[] a, int length) { //建堆 buildHead(a, length); //排序 int k = length - 1; while (k > 0) { swap(a, 0, k); --k; heapify(a, k, 0); } } private static void heapify(int[] a, int n, int i) { while (true) { int maxPos = i; if (i * 2 + 1 <= n && a[i] < a[i * 2 + 1]) { maxPos = i * 2 + 1; } if (i * 2 + 2 <= n && a[maxPos] < a[i * 2 + 2]) { maxPos = i * 2 + 2; } if (maxPos == i) { break; } swap(a, i, maxPos); i = maxPos; } } private static void buildHead(int[] a, int length) { for (int i = length / 2 - 1; i >= 0; i--) { heapifyA(a, length - 1, i); } }
为什么快速排序要比堆排序性能好?
- 堆排序数据访问的方式没有快速排序友好。堆化时访问数据的下标可能是1,2,4,8,而不是像快速排序那样,局部顺序访问,所以对 CPU 缓存是不友好的。
- 对于同样的数据,在排序过程中,堆排序算法的数据交换次数要多于快速排序。
为什么插入排序比冒泡排序更受欢迎呢?
因为冒泡排序不管怎么优化,元素交换的次数是一个固定值,是原始数据的逆序度。插入排序是同样的,不管怎么优化,元素移动的次数也等于原始数据的逆序度。冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要 3 个赋值操作,而插入排序只需要 1 个。
冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中数据的移动操作:
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}