八大排序算法[二]

145 阅读3分钟

这是我参与更文挑战的第 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 在后。分治思想,递归处理,直到 pq-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 分区点
    } 
    

    如果希望快排是原地排序算法,那么它的空间复杂度得是 O(1){O(1)},不需要额外空间。快排不是稳定的排序算法(涉及值的交换)。

    具体代码如下:

        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 的位置交换,然后将 0k-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;
}