六种比较排序算法的JAVA实现

  • 序言

这里说的六种比较排序算法分别是:冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序。如果只记忆代码,我感觉这几种排序很容易就会搞混,然后忘记,毕竟我们关心的是能否出现正确的排序结果,而如何实现的——不好意思,等我有时间再看吧。所以,这里还是希望能够记住排序的思想,那么今后无论使用何种语言,比较排序就不是问题了。

代码之后的时间复杂度、空间复杂度及稳定性分析可能有出入,请批判的的去阅读。

  • 冒泡排序

冒泡排序应该是我们接触的最简单的排序,如果说世界上还是一种适用于记忆的排序,那么我会选择冒泡。算法的原理只是就是构造一种“上浮”机制。我们以将一个无序数组按从小到大排序为例进行说明。那么我们的“上浮”机制是什么呢?就是将数组中最大的数排到数组最后,那么我们就实现了所有比这个数小的数组元素的整体“上浮”。这就相当于正常情况下将木块和铁块一起丢到水里,然后结果可想而知。算法的具体实现如下:

package chapter1.sort;

/**
 * 冒泡排序,是通过嵌套循环,达到数字按大小顺序依次沉淀,使符合比较规则的元素逐渐上浮
 */
public class BubbleSort {
    /**
     * 元素交换的过程
     */
    public void swap(int[] A, int i, int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

    /**
     * 元素排序的过程
     */
    public void sort(int[] A, int n){
        for(int j = 0; j < n - 1; j++){
            for(int i = 0; i < n - j - 1; i++){
                if(A[i] > A[i + 1]){
                    swap(A, i, i+1);
                }
            }
        }
    }

    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        BubbleSort bs = new BubbleSort();
        bs.sort(A, A.length);
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
    }
}

我们看到了,冒泡排序的关键步骤就是比较排序。元素的排序过程中,外层循环起控制作用,负责决定内层排序执行排序的范围(i < n - j -1),而内层循环是则执行“比较交换”动作 ,达到元素上浮的目的。

空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。

时间复杂度计算:首先明确比较排序的原子操作为比较。外层循环执行了n-1次,内层循环最多执行 n-1次,最少执行1次,那么其时间复杂度函数为(n-1)*[(n-1 + 1)/2] = (n^2 - n)/2, 按照时间复杂度计算原则,去掉常数,去掉最高此项系数,结果为O(n^2)。

算法稳定性:相对稳定。之所以说的是相对稳定,是因为如果将sort函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。

  • 选择排序

我一直想用一种想思考方式去理解选择排序和插入排序的区别,因为不知道是不是我语文不好的原因,总将这两种排序混淆。如何记忆选择排序,我们来回顾下小学生站队。首先确定排队的规则身高由低到高,那么选择排序的方式是什么呢?我先选择站在第一位的学生,和后面的同学比较身高,如果发现了身高比这名学生还要低的学生,那么让原来的学生回去,比较身高的换成这名身高更低的学生,如此进行直到结束,然后将身高最低的学生与第一名学生互换位置,之后第二名学生进行比较,直到所有位置的学生都比较了身高。算法的具体实现如下:

package sort;

/**
 * 简单选择排序 寻找最大(小)的数,然后在之后的数列中重复该操作
 */
public class SelectionSort {
    /**
     * 元素交换的过程
     */
    public void swap(int[] A, int i, int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

    /**
     * 元素排序的过程
     */
    public void sort(int[] A, int n){
        for (int i = 0; i < n-1; i++){
            int min = i;
            for (int j = i+1; j < n; j++){
                if(A[j] < A[min]){
                    min = j;
                }
            }
            if(min != i){
                swap(A, i, min);
            }
        }
    }

    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        SelectionSort ss = new SelectionSort();
        ss.sort(A, A.length);
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
    }
}

 空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。

时间复杂度计算:O(n^2)。

算法稳定性:相对不稳定。不稳定的情况出现在选择的过程中,如果出现较小的元素,在两个相同的较大元素之后,那么排序的时候,两个较大的元素可能出现相对位置互换,例如{6, 7, 8, 7, 3}。

  • 插入排序

之前说了选择排序和插入排序容易混淆,那么我再说下我怎么记忆插入排序的吧。还是小学生排队的问题,这次的比较是先第一名和第二名比较身高,排出高低顺序,之后第三名和前两名比较,这时,如果第三名比前两名身高都低,那么前两名各向后移动一位,然后第三名站到前两名之前。之后的排序方式都是新的学生与前面的已经排好序的学生比较身高,当找到合适位置的时候,这个位置及其后面的学生向后移动一位,然后比较的学生站到这个位置,直到所有位置的学生都比较了身高。算法的具体实现如下:

package sort;

/**
 * 简单插入排序 新元素与之前的有序元素进行比较,找到位置后,该位置及其后面元素向后移一位
 */
public class InsertionSort {
    /**
     * 元素排序的过程
     */
    public void sort(int[] A, int n){
        for (int i = 1; i < n; i++){
            int temp = i;
            int j = i - 1;
            while (j >= 0 && A[j] > temp){
                A[j+1] = A[j];
                j --;
            }
            A[j+1] = temp;
        }
    }

    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        InsertionSort is = new InsertionSort();
        is.sort(A, A.length);
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
    }
}

空间复杂度计算:因为中比较中需要的temp是额外空间,所以其空间复杂度为O(1)。

时间复杂度计算:O(n^2)。

稳定性:相对稳定。之所以说的是相对稳定,是因为如果将sort函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。

  • 快速排序

快速排序应该是面试中提问频率最高的的排序算法了,而JAVA的Arrays类中的排序方法,也是以快速排序为基础的双轴快速排序。所以掌握快速排序是很有必要的。排序思想呢,我们还是用上面的小学生排队来举例。首先,我们选出一名学生,原队列中比这名学生高的学生站在他的后面,而比他矮的站在他的前面,之后对前后两个队列分别重复上面的操作,直到整个队列身高有序。算法的具体实现如下:

package sort;

/**
 * 快速排序,以二分的思想不停的比较并细化队列,直到队列有序
 */
public class QuickSort {
    /**
     * 元素交换的过程
     */
    public void swap(int[] A, int i, int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }

    /**
     * 快排分队过程
     */
    private int partition(int[] A, int left, int right){
        int pivot = A[right];
        int tail = left - 1;
        for (int i = left; i < right; i++){
            if(A[i] <= pivot){
                swap(A, ++tail, i);
            }
        }
        swap(A, tail + 1, right);
        return tail + 1;
    }

    /**
     * 元素排序的过程
     */
    public void sort(int[] A, int left, int right){
        if (left >= right) return;
        int pivot_index = partition(A, left, right);
        sort(A, left, pivot_index - 1);
        sort(A, pivot_index + 1, right);
    }

    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        QuickSort qs = new QuickSort();
        qs.sort(A, 0, A.length - 1);
        System.out.println("这是快速排序:");
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
        System.out.println();
    }
}

空间复杂度计算:取决于递归过程中递归树的深度,最差为O(n),最优为O(logn)。

时间复杂度计算:介于O(n^2)与O(logn)之间。

稳定性:相对不稳定。不稳定的情况出现在快速分队的过程中,如果出现较小的元素,在两个相同的较大元素之后,那么排序的时候,如果以这个较小的元素为基准(或者说轴),那么两个较大的元素可能出现相对位置互换,例如{6, 7, 8, 7, 3},以3为基准。

  • 归并排序

归并排序也是一种以二分法为思想基础去理解的排序,其最终的目的是先将数组分为最小的二元组或一元组进行分别排序,然后在对各个元组进行排序,最后递归直至所有队列有序。还是以小学生排队为例,先将队列从前到后两两分队,如果是单数,那么单人是一个队,然后这些分队内部先排序,之后相邻队的学生两两进行一次排序,如此重复,直至最后是两个大队的学生排序完成,整个队列就排序完成。算法的具体实现如下:

package sort;

/**
 * 归并排序,将排序先局部,再整体
 */
public class MergeSort {
    /**
     * 元素交换的过程
     */
    public void merge(int A[], int left, int mid, int right){
        int tempLength = right -  left + 1;
        int[] temp = new int[tempLength];
        int index = 0;
        int i = left;
        int j = mid + 1;
        while (i <= mid && j <= right){
            temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];
        }

        while (i <= mid){
            temp[index++] = A[i++];
        }

        while (j <= right){
            temp[index++] = A[j++];
        }
        for (int k = 0; k < tempLength; k++){
            A[left++] = temp[k];
        }
    }
    /**
     * 元素排序的过程
     */
    public void sortRecursion(int[] A, int left, int right){
        if(left >= right){
            return;
        }
        int mid = (left + right) / 2;
        sortRecursion(A, left, mid);
        sortRecursion(A, mid + 1, right);
        merge(A, left, mid, right);
    }

    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        int[] B = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        MergeSort ms = new MergeSort();
        ms.sortRecursion(A,0, A.length - 1);
        System.out.println("这是递归的归并排序:");
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
    }
}

空间复杂度计算:归并需要额外的空间存放排序结果,即需要数组等长的空间,所以空间复杂度为O(n)。

时间复杂度计算:O(nlogn)。

稳定性:相对稳定。之所以说的是相对稳定,是因为如果将merge函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。

  • 堆排序

堆排序是用“堆”这种数据结构来实现排序的过程。下面我们用大根堆来讨论。先用数组生成大根堆,我们知道大根堆的最大特点是将数组中的最大数作为堆的堆顶,这样堆顶(即数组首项)为最大,那么我们想要由小到大排列,只需将数组的首项和尾项互换即可,然后将数据的前n-1项继续重复上述操作,直到堆内只有一个元素,那么这个就是数组的最小项,排序完成。算法的具体实现如下:

package sort;

/**
 * 堆排序,下面为大根堆排序,堆顶(首项)与数组尾项互换,数据前n-1项继续重复上述操作
 */
public class HeapSort {
    /**
     * 元素交换的过程
     */
    public void swap(int[] A, int i, int j){
        int temp = A[i];
        A[i] = A[j];
        A[j] = temp;
    }
    /**
     * 大元素前移的过程
     */
    private void heapify(int[] A, int i, int size){
        int left_child = 2 * i + 1;
        int right_child = 2 * i + 2;
        int max = i;
        if(left_child < size && A[left_child] > A[max]){
            max = left_child;
        }
        if(right_child < size && A[right_child] > A[max]){
            max = right_child;
        }
        if(max != i){
            swap(A, max, i);
            heapify(A, max, size);
        }
    }
    /**
     * 建堆的过程,从堆底开始上浮大元素,注意初始值和i--
     */
    private int buildHeap(int[] A, int n){
        int heap_size = n;
        for (int i = heap_size /2 - 1; i >= 0; i--){
            heapify(A, i, heap_size);
        }
        return heap_size;
    }
    /**
     * 元素排序的过程
     */
    public void sort(int[] A, int n){
        int heap_size = buildHeap(A, n);
        while (heap_size > 1){
            swap(A, 0, --heap_size);
            heapify(A, 0, heap_size);
        }
    }
    /**
     * 测试用例
     */
    public static void main(String[] args){
        int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
        HeapSort hs = new HeapSort();
        hs.sort(A, A.length);
        System.out.println("这是堆排序:");
        for (int i = 0; i < A.length; i++){
            System.out.print(A[i]);
        }
        System.out.println();
    }
}

空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。

时间复杂度计算:O(nlogn)。

稳定性:不稳定。不稳定发生在堆顶和数组尾项交换的时候,这次可能会打乱数组中等值元素的相对位置,例如{9, 7, 6, 5, 7}。

  • 结束语 

以上就是我对这几种排序算法的理解,上面的例子可能不是很好,但是希望对大家有所帮助,如果想了解更详细的,请查看这篇文章

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值