排序算法之交换排序(冒泡排序、快速排序)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/swpu_ocean/article/details/83990804

前言

在前面几篇博客中总结了插入排序(直接插入和希尔排序)、选择排序(直接选择和堆排序)以及归并排序,这里将讲下两种选择排序算法——冒泡排序和快速排序。

冒泡排序

基本概念

冒泡排序相对快速排序而言相对简单。冒泡就如同水里的鱼吐泡泡一样,刚开始时泡泡很小,但随着上浮离水面越来越近,泡泡也逐渐变大。冒泡排序也是因此而来,在每一趟排序中,依次比较相邻的两个数,选出最大的数将它移到一端,最终将得到一个有序的序列。

实现思路

通过双重循环,外层循环控制循环次数,内层循环控制比较次数。假如有N个数需要进行排序,在第一趟排序过程中,依次比较相邻两个数,可通过a[j]和a[j+1]标识,选出最大的数后将其移动至最右端,结束第一次循环。开始第二趟排序,此时内层循环只需要比较前N-1个元素,因为在上一轮排序中已经选了整个序列中最大的元素放置在最后一位上,直到外层循环结束,排序过程完成。

举例说明

待排序的数组:int[] array = {6,3,8,2,9,1};

第一趟排序:
    第一次排序:6和3比较,6大于3,交换位置:3 6 8 2 9 1
    
    第二次排序:6和8比较,6小于8,不交换位置:3 6 8 2 9 1
    
    第三次排序:8和2比较,8大于2,交换位置:3 6 2 8 9 1
    
    第四次排序:8和9比较,8小于9,不交换位置:3 6 2 8 9 1
    
    第五次排序:9和1比较:9大于1,交换位置:3 6 2 8 1 9
    
    第一趟总共进行了5次比较,选出最大元素9,排序结果:3 6 2 8 1 9


第二趟排序:
    第一次排序:3和6比较,3小于6,不交换位置:3 6 2 8 1 9
    
    第二次排序:6和2比较,6大于2,交换位置:3 2 6 8 1 9
    
    第三次排序:6和8比较,6大于8,不交换位置:3 2 6 8 1 9
    
    第四次排序:8和1比较,8大于1,交换位置:3 2 6 1 8 9
    
    第二趟总共进行了4次比较,选出最大元素8,排序结果:3 2 6 1 8 9


第三趟排序:
    第一次排序:3和2比较,3大于2,交换位置:2 3 6 1 8 9
    
    第二次排序:3和6比较,3小于6,不交换位置:2 3 6 1 8 9
    
    第三次排序:6和1比较,6大于1,交换位置:2 3 1 6 8 9
    
    第三趟总共进行了3次比较,选出最大元素6,排序结果:2 3 1 6 8 9


第四趟排序:
    第一次排序:2和3比较,2小于3,不交换位置:2 3 1 6 8 9
    
    第二次排序:3和1比较,3大于1,交换位置:2 1 3 6 8 9
    
    第四趟总共进行了2次比较,选出最大元素3,排序结果:2 1 3 6 8 9


第五趟排序:
    第一次排序:2和1比较,2大于1,交换位置:1 2 3 6 8 9
    
    第五趟总共进行了1次比较,选出最大元素2,排序结果:1 2 3 6 8 9

可以看出,在冒泡排序中,如果有N个元素需要进行排序,则外层循环需要进行N-1次,而内层循环由于每次会选出一个最大的数,则每次的内层循环次数为N-i-1,i为外层循环次数。

代码解析

/**
 * @author: zhangocean
 * @Date: 2018/11/10 12:51
 */
public class BubbleSort {

    public void bubbleSort(int[] array){
        System.out.println("排序前:" + Arrays.toString(array));
        int temp;
        for(int i=0;i<array.length-1;i++){
            for(int j = 0;j<array.length-i-1;j++){
				//依次比较相邻元素,并将较大者右移
                if(array[j+1] < array[j]){
                    temp = array[j+1];
                    array[j+1] = array[j];
                    array[j] = temp;
                }
            }
        }
        System.out.println("排序后:" + Arrays.toString(array));
    }

    public static void main(String[] args) {
        int[] arr = new int[10];
        Random random = new Random();
        for(int i=0;i<10;i++){
            arr[i] = random.nextInt(100);
        }
        BubbleSort bubbleSort = new BubbleSort();
        bubbleSort.bubbleSort(arr);

    }
}

输出结果:

排序前:[83, 79, 16, 44, 41, 0, 1, 22, 19, 44]
排序后:[0, 1, 16, 19, 22, 41, 44, 44, 79, 83]

时间复杂度:O(N²)
空间复杂度:O(1)

快速排序

基本概念

快速排序,也称为快排。顾名思义,是实践中的一种快速的排序算法。该算法之所以特别快,主要是由于非常精练和高度优化的内部循环。在快排中我们首先需要选取一个元素作为枢纽元,然后将待排序的序列中比枢纽元大的数放在一个集合中,再将比枢纽元小的元素放在一个集合中,通过这种方式递归的在各个集合中再进行快排,最后将得到一个有序的序列集合。

画图分析

首先需要选取一个枢纽元,这里我们随机选取65作为枢纽元。

将比枢纽元大的元素以及比枢纽元小的元素各划分为一个集合,再在集合中重复选取枢纽元以及以上操作,这样最终将获得一个有序的序列。

选取枢纽元

在快速排序中,枢纽元的选取十分重要,虽然上面描述的算法无论选择哪个元素作为枢纽元都能完成排序工作,但是有些选择显然优于其他选择。

在一些快排算法中将数组的第一个元素作为枢纽元,然而在《数据结构与算法分析for Java》中将这定义为一种错误的方法。如果输入是随机的,那么可以接受这种选取方法,而如果输入是预排序的或是反序的,那么这样的枢纽元就产生一个劣质的分割。因为这样的数组第一个元素不是最大就是最小的数,那么必定会导致其他的所有元素被分到同一个集合里,这样也失去了快排分割的意义。

这里使用三数中值分割法来选取枢纽元。在一段待排序的数组中,我们通常使用左端、右端和中心位置上的三个元素的中值作为枢纽元。例如,输入为8,1,4,9,0,3,5,2,7,6,它的左边元素为8,右边元素为6,中间位置(left+right)/2的元素为0,于是枢纽元为这三个元素的中值,即6。使用三数中值分割法消除了预排序输入的坏情形,并且实际减少了14%的比较次数。

分割策略

快速排序如归并排序一样,都采用分治的递归算法。在快排的分割阶段要做的就是把所有小元素移到数组的左边而把大元素移到数组的右边,当然,“小”和“大”是相对于枢纽元而言的。

由于我们采用三数中值分割法来选取枢纽元,对于一个数组,我们在a[left]、a[right]和a[center]中选取中值作为枢纽元,并将三者中最小的移至a[left],最大的移至a[right],这样子有额外的好处,因为a[left]本来就是在分割阶段需要放置比枢纽元小的元素,a[right]也是如此。之后我们需要将枢纽元与a[right-1]位置的元素交换位置,并将i和j初始化为left+1和right-2。

初始化完成之后就可以进行移动并与枢纽元进行比较,i向右移动,当遇到比枢纽元大的元素时停止,然后向左移动j,当j遇到比枢纽元小的元素时停止,此刻比较i和j的位置,如果i仍在j的左边,则交换i、j位置处的元素。交换完后i与j继续移动,直到i移动到j的右边时停止两者的移动。并将i停止移动时位置上的元素与枢纽元(a[right-1]位置)交换。此时枢纽元左边的所有元素都小于枢纽元,右边的所有元素都大于枢纽元。

分割过程解析

假设现有这样一个待排序数组:8 1 4 9 0 3 5 2 7 6。我们首先使用三数中值分割法选取出枢纽元,并将三元素中的最小者0移至a[left],最大者8移至a[right],以及作为中值的枢纽元6移至a[right-1]

将i和j初始化为left+1和right-2,比较a[left+1]的元素1,小于枢纽元,右移i,发现下一个元素4仍然小于枢纽元,继续右移,9大于枢纽元,停止移动i。开始对j进行移动,但是我们发现a[right-2]的元素2小于枢纽元,于是停止对j的移动。此时i在j的左边,于是交换i和j位置上的元素。

交换完后继续从i开始移动,但是很遗憾下一个元素7大于枢纽元,i只好停止移动,j遇到的下一个元素5小于枢纽元,停止j,交换i和j的位置

继续移动i和j,这时i停在7位置处,j停在3位置处,我们发现i此时跑到了j的右边,于是这一轮i和j的移动结束,将i位置处的元素7与枢纽元6交换位置,此时我们的分割策略就结束了,可以发现,枢纽元6的左边所有元素均小于它,右边的元素都大于枢纽元

通过一次移动我们将整个数组分成了两个集合,接下来我们只需要在两个集合中继续使用快排,最终就能获得一段有序的序列。

代码解析

/**
 * @author: zhangocean
 * @Date: 2018/11/11 14:49
 */
public class QuickSort {

    /**
     * 三数中指分割法
     */
    private int median3(int[] arr, int left, int right){
        int mid = (left+right)/2;
        if(arr[left] > arr[mid]){
            swapReferences(arr, left, mid);
        }
        if(arr[left] > arr[right]){
            swapReferences(arr, left, right);
        }
        if (arr[mid] > arr[right]){
            swapReferences(arr, mid, right);
        }
        swapReferences(arr, mid, right-1);
        return arr[right-1];
    }

    /**
     * 快排核心代码
     */
    private void quickSort(int[] arr, int left, int right) {
        if(left < right){
            //选出枢纽元,并将枢纽元放置在right-1位置处
            int pivot = median3(arr, left, right);
            int i = left+1;
            int j = right-2;
            if(i <= j){
                for(;;){
					//当i或j位置处的元素等于枢纽元时,也需要停止i或j的移动
                    while (arr[i] < pivot) {
                        i++;
                    }
                    while (arr[j] > pivot){
                        j--;
                    }
                    if(i < j){
                        swapReferences(arr, i, j);
                    } else {
                        break;
                    }
                }
                swapReferences(arr, i, right-1);
                quickSort(arr, left, i-1);
                quickSort(arr, i+1, right);
            }

        }
    }

    /**
     * 交换元素
     */
    private void swapReferences(int[] arr, int left, int right){
        int temp = arr[left];
        arr[left] = arr[right];
        arr[right] = temp;
    }

    public static void main(String[] args) {
        int[] arr = new int[10];
        Random random = new Random();
        for(int i=0;i<10;i++){
            arr[i] = random.nextInt(10);
        }
        System.out.println("排序前:" + Arrays.toString(arr));
        QuickSort sort = new QuickSort();
        sort.quickSort(arr, 0, arr.length-1);
        System.out.println("排序后:" + Arrays.toString(arr));
    }
}

输出结果:

排序前:[76, 19, 27, 81, 80, 82, 23, 86, 94, 16]
排序后:[16, 19, 23, 27, 76, 80, 81, 82, 86, 94]

时间复杂度:O(N²)
空间复杂度:O(NlogN)

代码梳理

对于快排算法的代码比较复杂,首先median3(int[],int,int)方法为三数中值分割法,在该方法中选取出了枢纽元,并将枢纽元放置在a[right-1]位置处,然后返回枢纽元以便后面移动中的比较。

quickSort(int[],int,int)为快排的核心代码,在代码的32、33行处将i和j分别初始化在left+1和right-2位置处。代码36-41行则是对i和j进行移动操作,当i和j停止移动时,会在42行处判断i和j的相对位置,如果i没有跑到j的右边,则交换它俩此刻位置上的元素,否则,结束此轮i和j的移动。

代码48-50行则是将i最后停止的位置与枢纽元进行交换,并在枢纽元的左右集合中继续递归的使用快排。为了避免初始化时将i初始化到j的右边,这里我在34行处对i和j的初始位置进行判断,当递归集合中的元素小于等于3个时,就不要i和j的元素移动了,因为在三数中值分割法中就已经排好这三个元素的位置了。

总结

从代码上我们也可以很清楚的看到冒泡排序对于快排的简单性,但是快排在大多数情况下对于运算时间有极大的优化。快速排序的最坏时间复杂度达到了O(N²),但是在平均情况下它的时间复杂度为O(NlogN)。对于排序算法的选择也是有技巧的,我也会在后面的博客中对每一种排序算法进行总的比较。

更多了解,还请关注我的个人博客:www.zhyocean.cn

展开阅读全文

没有更多推荐了,返回首页