排序算法实现及比较

排序算法比较

在这里插入图片描述

一、数组形式排序

1、快速排序

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换,因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2)。它的平均时间复杂度为O(nlogn),是不稳定的排序。其实快速排序是基于一种叫做“二分”的思想。
特点:

每次循环只能确定基准位元素的位置

示例:

public class QuickSort {
    public static void quickSort(int[] arr, int low, int high){
        int i,j,temp,t;
        // 当 low>high 时一次循环就结束
        if(low>high){
            return;
        }
        i=low;
        j=high;
        // temp 就是基准位
        temp = arr[low];
 
        while (i<j) {
            // 先看右边,依次往左遍历,直到找到小于 temp 的值或者 i=j 时停止遍历
            while (temp<=arr[j]&&i<j) {
                j--;
            }
            // 再看左边,依次往右遍历,直到找到大于 temp 的值或者 i=j 时停止遍历
            while (temp>=arr[i]&&i<j) {
                i++;
            }
            // 如果满足条件则交换
            if (i<j) {
                t = arr[j];
                arr[j] = arr[i];
                arr[i] = t;
            }
 
        }
        // 最后将基准为与 i 和 j 相等位置的数字交换
         arr[low] = arr[i];
         arr[i] = temp;
        // 递归调用左半数组
        quickSort(arr, low, j-1);
        // 递归调用右半数组
        quickSort(arr, j+1, high);
    }
 
 
    public static void main(String[] args){
        int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19};
        quickSort(arr, 0, arr.length-1);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}

2、堆排序(大根堆)

堆排序是一种树形选择排序方法,它的特点是:在排序的过程中,将 array[0,…,n-1] 看成是一颗完全二叉树的顺序存储结构,利用完全二叉树中双亲节点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(最小)的元素。

若 array[0,…,n-1] 表示一颗完全二叉树的顺序存储模式,则双亲节点指针和孩子结点指针之间的内在关系如下:

任意一节点指针 i:父节点:i==0 ? null : (i-1)/2

左孩子:2*i + 1

右孩子:2*i + 2

堆的定义:

n 个关键字序列 array[0,…,n-1],当且仅当满足下列要求:(0 <= i <= (n-1)/2)

array[i] <= array[2*i + 1] 且 array[i] <= array[2*i + 2]:称为小根堆

array[i] >= array[2*i + 1] 且 array[i] >= array[2*i + 2]:称为大根堆

建立大根堆:

n 个节点的完全二叉树 array[0,…,n-1],最后一个节点 n-1 是第 (n-1-1)/2 个节点的孩子。对第 (n-1-1)/2 个节点为根的子树调整,使该子树称为堆。

对于大根堆,调整方法为:若【节点的关键字】<【左右子女中关键字较大者】,则交换之后向前依次对各节点((n-2)/2 - 1)~ 0 为根的子树进行调整,看该节点值是否大于其左右子节点的值,若不是,将左右子节点中较大值与之交换,交换后可能会破坏下一级堆,于是继续采用上述方法构建下一级的堆,直到以该节点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根节点。

堆排序(大根堆):

① 将存放在 array[0,…,n-1] 中的 n 个元素建成初始堆;

② 将堆顶元素与堆底元素进行交换,则序列的最大值即已放到正确的位置;

③ 但此时堆被破坏,将堆顶元素向下调整使其继续保持大根堆的性质,再重复第②③步,直到堆中仅剩下一个元素为止。

堆排序算法的性能分析:

空间复杂度:o(1);

时间复杂度:建堆:o(n),每次调整 o(log n),故最好、最坏、平均情况下:o(n*logn)

稳定性:不稳定

建立大根堆

	//构建大根堆:将array看成完全二叉树的顺序存储结构
    private int[] buildMaxHeap(int[] array){
        //从最后一个节点array.length-1的父节点(array.length-1-1)/2开始,直到根节点0,反复调整堆
        for(int i=(array.length-2)/2;i>=0;i--){
            adjustDownToUp(array, i,array.length);
        }
        return array;
    }

    //将元素array[k]自下往上逐步调整树形结构
    private void adjustDownToUp(int[] array,int k,int length){
        int temp = array[k];
        for(int i=2*k+1; i<length-1; i=2*i+1){    // i 初始化为当前节点的左孩子,每次沿节点较大的子节点向下调整
            if(i<length && array[i]<array[i+1]){  // 取节点较大的子节点的下标
                i++;   // 如果节点的右孩子>左孩子,则取右孩子节点的下标
            }
            if(temp>=array[i]){  //根节点 >=左右子女中关键字较大者,调整结束
                break;
            }else{   //根节点 <左右子女中关键字较大者
                array[k] = array[i];  //将左右子结点中较大值array[i]调整到双亲节点上
                k = i; //【关键】修改k值,以便继续向下调整
            }
        }
        array[k] = temp;  //被调整的结点的值放人最终位置
    }

堆排序

	//堆排序
    public int[] heapSort(int[] array){
        array = buildMaxHeap(array); //初始建堆,array[0]为第一趟值最大的元素
        for(int i=array.length-1;i>1;i--){
            int temp = array[0];  //将堆顶元素和堆低元素交换,即得到当前最大元素正确的排序位置
            array[0] = array[i];
            array[i] = temp;
            adjustDownToUp(array, 0,i);  //整理,将剩余的元素整理成堆
        }
        return array;
    }

3、归并排序

归并排序(Merge)是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4)重复步骤3直到某一指针达到序列尾
5)将另一序列剩下的所有元素直接复制到合并序列尾
在这里插入图片描述

(1)自顶向下

自顶向下使用递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是 O(logn)。思路如下:

1)找到序列的中点,以中点为分界,将序列拆分成两个子序列。
2)对两个子序列分别排序。
3)将两个排序后的子序列合并,得到完整的排序后的序列。
4)上述过程可以通过递归实现。递归的终止条件是子序列的节点个数小于或等于 1,即当序列为空或者序列只包含 1 个节点时,不需要对序列进行拆分和排序

代码

public class Main6 {

    public static void main(String[] args) {
        int[] arr = {11,44,23,67,88,65,34,48,9,12};
        int[] tmp = new int[arr.length];    //新建一个临时数组存放
        mergeSort(arr,0,arr.length-1);
        for(int i=0;i<arr.length;i++){
            System.out.print(arr[i]+" ");
        }
    }

    public static void merge(int[] arr, int low, int high){
        int mid = (high-low)/2+low;
        int[] tmp = new int[arr.length];
        int i = 0, j = low, k = mid+1;  //左边序列和右边序列起始索引
        while(j <= mid && k <= high){
            if(arr[j] < arr[k]){
                tmp[i++] = arr[j++];
            }else{
                tmp[i++] = arr[k++];
            }
        }
        //若左边序列还有剩余,则将其全部拷贝进tmp[]中
        while(j <= mid){
            tmp[i++] = arr[j++];
        }

        while(k <= high){
            tmp[i++] = arr[k++];
        }

        for(int t=0;t<i;t++){
            arr[low+t] = tmp[t];
        }
    }

    public static void mergeSort(int[] arr, int low, int high){
        if(low<high){
            int mid = (low+high)/2;
            mergeSort(arr, low, mid); //对左边序列进行归并排序
            mergeSort(arr,mid+1, high);  //对右边序列进行归并排序
            merge(arr, low, high);    //合并两个有序序列
        }
    }

}

(2)自底向上

使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。 思路如下:

1)用 subLength 表示每次需要排序的子序列的长度,初始时 subLength=1。
2)每次将序列拆分成若干个长度为 subLength 的子序列(最后一个子序列的长度可以小于 subLength),按照每两个子序列一组进行合并,合并后即可得到若干个长度为
subLength×2 的有序子序列(最后一个子序列的长度可以小于 subLength×2)。
3)将 subLength
的值加倍,重复第 2 步,对更长的有序子链表进行合并操作,直到有序子链表的长度大于或等于 length,整个链表排序完毕。

二、链表形式的排序

三、常用的排序 API 与数据结构

1、Arrays.sort()

(1)API
在这里插入图片描述
(2)源码

public static void sort(int[] a, int fromIndex, int toIndex) {
        rangeCheck(a.length, fromIndex, toIndex);
        DualPivotQuicksort.sort(a, fromIndex, toIndex - 1, null, 0, 0);
    }

(3)执行流程
在这里插入图片描述

1)如何判断具备不具备结构:

    // Check if the array is nearly sorted
    for (int k = left; k < right; run[count] = k) {
        if (a[k] < a[k + 1]) { // ascending
            while (++k <= right && a[k - 1] <= a[k]);
        } else if (a[k] > a[k + 1]) { // descending
            while (++k <= right && a[k - 1] >= a[k]);
            for (int lo = run[count] - 1, hi = k; ++lo < --hi; ) {
                int t = a[lo]; a[lo] = a[hi]; a[hi] = t;
            }
        } else { // equal
            for (int m = MAX_RUN_LENGTH; ++k <= right && a[k - 1] == a[k]; ) {
                if (--m == 0) {
                    sort(a, left, right, true);
                    return;
                }
            }
        }

        /*
         * The array is not highly structured,
         * use Quicksort instead of merge sort.
         */
        if (++count == MAX_RUN_COUNT) {
            sort(a, left, right, true);
            return;
        }
    } 

实际逻辑是分组排序,每降序为一个组,像1、9、8、7、6、8,9 到 6 是降序,为一个降序组,然后从 8 后面继续往后面找。每遇到这样一个降序组,++count,当 count 大于 MAX_RUN_COUNT(67),被判断为这个数组不具备结构(也就是这数据时而升时而降),然后送给之前的 sort(里面的快速排序)的方法(The array is not highly structured,use Quicksort instead of merge sort.)。如果 count 少于 MAX_RUN_COUNT(67)的,说明这个数组还有点结构,就继续往下走下面的归并排序。

2)为什么要判断结构

当逆序较多时快速排序效率会更高,因此数组没有结构时用快速排序;
当顺序较多时归并排序效率会更高,因此数组有结构时用归并排序;

2、Collections

3、优先级队列

队列是一种先进先出(FIFO)的数据结构,但是有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列。Java 集合框架中提供了 PriorityQueue 和 PriorityBlockingQueue 两种类型的优先级队列,PriorityQueue 是线程不安全的,PriorityBlockingQueue 是线程安全的,这里主要使用 PriorityQueue。PriorityQueue 的底层是堆,堆的底层是数组

(1)PriorityQueue 使用注意点

1)PriorityQueue 中放置的元素必须要能够比较大小 (只有实现了 Comparable 和 Comparator 接口的类才能比较大小),不能插入无法比较大小的对象,否则会抛出 ClassCastException 异常;
2)不能插入 null 对象,否则会抛出 NullPointerException 异常
3)没有容量限制,可以插入任意多个元素,其内部可以自动扩容
4)插入和删除元素的时间复杂度均为 O(log2N)
5)PriorityQueue 底层使用了堆数据结构

(2)PriorityQueue 的扩容方式

	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = queue.length;
        
        int newCapacity = oldCapacity + ((oldCapacity < 64) ? (oldCapacity + 2): (oldCapacity >> 1));

        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            newCapacity = hugeCapacity(minCapacity);
        }

        queue = Arrays.copyOf(queue,newCapacity);
    }

在这里插入图片描述

(3)构造函数
在这里插入图片描述

PriorityQueue 默认是小根堆,若要要改为大根堆,需要传入重写的 Comparator 实例

(4)插入/删除/获取优先级最高的元素
在这里插入图片描述

补充:
在这里插入图片描述

4、TreeSet

5、TreeMap


版权声明:本文为IT__learning原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。