上篇介绍了排序算法中的简单排序,这次我们来聊聊归并排序,快速排序 和 堆排序。
1归并排序:将2个有序的数组合并为一个有序的大数组的过程。核心为merge函数。使用了分治的思想:将一个大问题分解为互不相关的小问题,最后再将小问题进行合并得到结果。
下面我们看看merge函数怎么实现
private static void merge(int[] a, int lo, int mid, int hi){
int i=lo, j= mid+1;
for(int k=lo; k<=hi; k++){
aux[k] = a[k];
}
for(int k=lo; k<=hi; k++){
if(i > mid) a[k] = aux[j++]; //当左侧数组用尽,下个元素放置右侧数组中的数
else if(j > hi) a[k] = aux[i++]; //当右侧数组用尽,下个元素放置左侧数组中的数
else if(aux[j] < aux[i]) a[k] = aux[j++]; //当右侧元素小于左侧元素时,放置右侧元素,注意这里的顺序,先写这条语句的原因是当左右两元素相等时///使用左侧元素,保证了排序的稳定性。
else a[k] = aux[i++];
}
}
在merge函数中,使用了2个指针分别指向两个子数组的开头,并利用aux数组将2个数组进行原地排序。(使用了额外的空间)
并在主函数中递归的调用合并过程,在递归到最底层(最基本的情况),在返回之前调用merge,先将基本情况归并,最后直至归并整个数组。
下面我们看看主函数的实现:
private static void sort(int[] a, int lo, int hi){
if(lo >= hi) return;
int mid = lo + (hi - lo) / 2;
sort(a, lo ,mid);
sort(a, mid+1, hi);
merge(a, lo, mid, hi);
}
这里的基本情况就是当lo>=hi时,意味着这时只有1个元素,或者没有元素,所以并不需要调用merge进行归并。再将数组分解到最基本情况后,逐一进行归并。
下面看下归并函数的整体实现:
import java.util.*;
public class MergeSort
{
private static int[] aux;
public static int[] mergeSort(int[] a, int N)
{
aux = new int[N];
sort(a, 0, N-1);
return a;
}
private static void sort(int[] a, int lo, int hi){
if(lo >= hi) return;
int mid = lo + (hi - lo) / 2;
sort(a, lo ,mid);
sort(a, mid+1, hi);
merge(a, lo, mid, hi);
}
private static void merge(int[] a, int lo, int mid, int hi){
int i=lo, j= mid+1;
for(int k=lo; k<=hi; k++){
aux[k] = a[k];
}
for(int k=lo; k<=hi; k++){
if(i > mid) a[k] = aux[j++];
else if(j > hi) a[k] = aux[i++];
else if(aux[j] < aux[i]) a[k] = aux[j++];
else a[k] = aux[i++];
}
}
}
private static int partition(int[] a, int lo, int hi){
int i = lo, j = hi + 1;
int base = a[lo];
while(true){
while(a[++i] < base) if(i == hi) break;
while(a[--j] > base) if(j == lo) break;
if(i >= j) break;
swap(a, i, j);
}
swap(a, lo, j);
return j;
}
最后我们再看下主函数的实现:
private static void sort(int[] a, int lo, int hi) {
if(lo >= hi) return;
int j = partition(a, lo, hi);
sort(a, lo, j-1);
sort(a, j+1, hi);
}
就如前面所说,和归并排序的顺序正好相反。先进行切分操作,再进行嵌套。
总体实现:
import java.util.Arrays;
public class QuickSort {
public static int[] quickSort(int[] A, int n) {
sort(A, 0 ,n-1);
return A;
}
private static void sort(int[] a, int lo, int hi) {
if(lo >= hi) return;
int j = partition(a, lo, hi);
sort(a, lo, j-1);
sort(a, j+1, hi);
}
private static int partition(int[] a, int lo, int hi){
int i = lo, j = hi + 1;
int base = a[lo];
while(true){
while(a[++i] < base) if(i == hi) break;
while(a[--j] > base) if(j == lo) break;
if(i >= j) break;
swap(a, i, j);
}
swap(a, lo, j);
return j;
}
private static void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
快速排序的时间复杂度为O(nlgn),空间复杂度为O(lgn)(由于嵌套的原因)
3.堆排序:今天的最后我们聊聊堆排序,先说下堆的含义:堆分为大根堆和小根堆(我们说大根堆,小根堆同理)。大根堆代表着一颗完全二叉树,它的每颗子树的根节点都大于等于其两个子节点。符合这样的二叉树我们叫做堆有序。我们可以方便的用首个元素为空的数组来存储这个堆。这样的好处是可以非常方便的找到每个节点k的父节点k/2和两个子节点2k+1和2k。
堆有2个基本操作和2个常用操作:基本操作上浮swim和下沉sink.在堆排序中我们只需要sink操作。
我们先来看看两个操作的具体实现:
private static void swim(Comparable[] a,int k){
while(k > 1 && less(a, k/2, k)){
swap(a, k/2, k);
k = k/2;
}
}
private static void sink(Comparable[] a, int n, int N){ //当前节点与两个子节点中较大的进行比较,若子节点大于当前节点,交换。直到最底层。
while(2*n <= N){
int j = 2 * n; //存放最大子节点变量
if(j<N && less(a, j, j+1)){
j++;
}
if(!less(a, n, j)){ //若子节点比父节点小或相等,跳出循环(注意相等的边界)
break;
}
swap(a, n, j); //下沉
n = j;
}
}
有了这两个底层的操作,我们就可以定义堆的常用操作:添加元素和删除最大元素了。这两个操作的实现比较简单,就不放代码了。解释下实现原理:添加操作先将新元素放在数组的最后,然后进行swim操作就可以了。删除最大元素操作先将数组的最后一个元素(堆底元素)和被删除元素进行交换。在对新的堆顶进行sink操作。
回归正题,我们说了堆的意义,下面看看堆排序是怎么实现的。实现分为2部分:1.利用数组构建大根堆 2.逐一删除堆顶元素放在数组的最后
具体实现:
public static int[] heapSort(int[] a, int N) { //最大堆排序 : 1.构建堆 2.挨个取出最大值,放在堆后面(原地排序)
//1建大根堆,从N/2开始(最后一个有子节点的非叶子节点),
for(int i=N/2; i>=1; i--){
sink(a, i, N);
}
//2.每次从堆顶拿出最大元素并与堆中最后一个元素交换,而且缩小堆的大小
while(N > 1){
swap(a, 1, N);
sink(a, 1, --N);
}
return a;
}
堆排序的时间复杂度是O(nlgn),空间复杂度O(1),是不稳定的排序。考虑一个数组为 10, 6, 4, 5(1号), 5(2号), 5(3号),在构建堆时5(3号)就会和4进行交换而排在1号和2号的前面,所有此排序是不稳定的。
下面我们聊聊基于非比较的排序:计数排序和基数排序,今天就先到这里吧