数据结构与算法总结——排序(二)归并排序,快速排序 和 堆排序

上篇介绍了排序算法中的简单排序,这次我们来聊聊归并排序,快速排序 和 堆排序。

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++];
        }
    }
    

}
归并排序的时间复杂度为 O(nlgn) ,空间复杂度为O(n), 是稳定排序。

2.快速排序:快速排序和归并排序就像时两个兄弟,结构相似但是风格迥异。核心的不同是快速排序是先进行其核心操作切分,在进行递归。而归并是先进行递归,再进行归并。这就是他们的不同。所以快速排序的思想是:当每个位置的元素都是有序的时候,整个数组自然就有序了。
我们首先看看快速排序的核心操作:切分patition操作,它的思想是选择数组的第一个元素作为切分元素,将整个数组进行切分,使整个数组切分后左侧元素小于等于切分元素,右侧元素大于等于切分元素。我们来看下它的实现:
	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;
    	
    }
切分操作的思想是:分别从头到尾和从尾到头对数组进行扫描,每当遇到不满足切分区间的元素就停下来,将2个元素进行交换,使其符合切分规则。直到两个指针相遇或错过为止。
这里有3点是需要注意的:1.当遇到于切分元素相等的元素时,指针也会停下来进行交换。这样做看似多进行了几次交换操作,但是它的好处是更巨大的:它可以防止当相同于元素过多的时候,切分不均匀导致的嵌套层数过深从而性能恶化。(最理想的切分当然是平均的把数组分成相等的两份)。
2.接着上面一点说,那么每次都取子数组的第一个元素作为切分元素的话,是怎么保证每次切分都是平均切分的呢? 这里其实有一个假设:数组中的数据是平均分布的。再加上在排序前要对数组元素进行随机话打乱,从而在概率上保证了每次是平均分割数组。
3.为什么patition操作的返回值是j呢? 其实这里有2种情况:1.i 和j相等,这时要么是相遇在了数组的最右侧,要么是遇到了与切分元素相等的元素相遇到了数组的中间位置。2.i和j不相等时,j在i的左侧,代表了左侧数组的尾部。在返回之前将切分元素和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号的前面,所有此排序是不稳定的。

下面我们聊聊基于非比较的排序:计数排序和基数排序,今天就先到这里吧






  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值