数据结构——【排序】详解

目录

一. 排序

1.1 基本概念

1.2 稳定性

1.3 七大基于比较的排序-总览

二. 插入排序

2.1 直接插入排序

2.2 希尔排序

三. 选择排序⛅

3.1 直接选择排序

3.2 堆排序

四. 交换排序

4.1 冒泡排序

4.2 快速排序

五. 归并排序

5.1 归并排序


一. 排序

1.1 基本概念

排序(sorting)又称分类,就是将一组任意序列得数据元素按一定得规律进行排列(按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作),使之成为有序序列。

平时的上下文中,如果提到排序,通常指的是排升序(非降序)。

通常意义上的排序,都是指的原地排序(in place sort)。

1.2 稳定性

定义:数组 arr 中有若干元素,其中 A元素和 B元素相等,并且 A元素在 B元素前面,如果使用某种排序算法排序后,能够保证 A元素依然在 B元素的前面,可以说这个该算法是稳定的。

数据结构——【排序】详解_第1张图片

稳定性的意义:如果一组数据只需要一次排序,则稳定性一般是没有意义的,如果一组数据需要多次排序,稳定性是有意义的。例如要排序的内容是一组商品对象,第一次排序按照价格由低到高排序,第二次排序按照销量由高到低排序,如果第二次排序使用稳定性算法,就可以使得相同销量的对象依旧保持着价格高低的顺序展现,只有销量不同的对象才需要重新排序。这样既可以保持第一次排序的原有意义,而且可以减少系统开销。

1.3 七大基于比较的排序-总览

数据结构——【排序】详解_第2张图片

二. 插入排序

2.1 直接插入排序

整个区间被分为 :有序区间  无序区间;

每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。

数据结构——【排序】详解_第3张图片

 代码实现:

   public static void insertSort(long[] array) {
        // 一共要取多少个元素来进行插入过程(无序区间里有多少个元素)
        for (int i = 0; i < array.length - 1; i++) {
            // 有序区间 [0, i]  至少在 i == 0 的时候得有一个元素
            // 无序区间 [i + 1, n)

            // 先取出无序区间的第一个元素,记为 k
            long k = array[i + 1];

            // 从后往前,遍历有序区间
            // 找到合适的位置退出
            // 所谓合适的位置,就是第一次 k >= array[j] 的位置
            int j;
            for (j = i; j >= 0 && k < array[j]; j--) {
                array[j + 1] = array[j];        // 将不符合条件的数据往后般一格
            }

            array[j + 1] = k;
        }
    }

性能分析:

时间复杂度

最坏情况:O(n^2)----数组逆序的情况下

最好情况:O(n)----数组有序的情况下

特点:越有序越快

空间复杂度:O(1)

稳定性:比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么把要插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。

2.2 希尔排序

希尔排序法(Shell Sort)又称缩小增量法。

希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时, 所有记录在统一组内排好序。

当 gap > 1 时都是预排序,目的是让数组更接近于有序。当 gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。希尔排序本身是直接插入排序的一种优化。

数据结构——【排序】详解_第4张图片

 代码实现:

public static void shellSort(int[] array) {
    int gap = array.length;
    while (gap > 1) {
       insertSortGap(array, gap);
       gap = (gap / 3) + 1; // OR gap = gap / 2;
    }
    insertSortGap(array, 1);
}
private static void insertSortGap(int[] array, int gap) {
    for (int i = 1; i < array.length; i++) {
       int v = array[i];
       int j = i - gap;
       for (; j >= 0 && array[j] > v; j -= gap) {
           array[j + gap] = array[j];
       }
       array[j + gap] = v;
   }
}

性能分析:

时间复杂度

最坏情况:O(n^2)----比较难构造

最好情况:O(n)----数组有序的情况下

空间复杂度:O(1)

稳定性:不稳定

三. 选择排序⛅

3.1 直接选择排序

每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素 排完 。

数据结构——【排序】详解_第5张图片

  代码实现:

public static void selectSort(int[] array){
   for(int i = 0;i < array.length - 1;i++){
       //无序区间:[0,array.length - i)
       //有序区间:[array.length  i,array.length)
       int max = 0;
       for (int j = 1;j < array.length - i;j++){
           if(array[j] > array[max]){
              max = j;
           }
       }
       int t = array[max];
       array[max] = array[array.length - i - 1];
       array[array.length - i - 1] = t;
    }
}

性能分析:

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

空间复杂度:O(1)

稳定性:不稳定

3.2 堆排序

基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大(或最小)的数,而是通过堆来选择无序区间的最大(或最小)的数。

注意: 排升序要建大堆;排降序要建小堆。

数据结构——【排序】详解_第6张图片

  代码实现:

public class HeapSort {
    public static void sort(int []arr){
        //1.构建大顶堆
        for(int i=arr.length/2-1;i>=0;i--){
            //从第一个非叶子结点从下至上,从右至左调整结构
            adjustHeap(arr,i,arr.length);
        }
        //2.调整堆结构+交换堆顶元素与末尾元素
        for(int j=arr.length-1;j>0;j--){
            swap(arr,0,j);//将堆顶元素与末尾元素进行交换
            adjustHeap(arr,0,j);//重新对堆进行调整
        }

    }

    /**
     * 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
     * @param arr
     * @param i
     * @param length
     */
    public static void adjustHeap(int []arr,int i,int length){
        int temp = arr[i];//先取出当前元素i
        for(int k=i*2+1;ktemp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
                arr[i] = arr[k];
                i = k;
            }else{
                break;
            }
        }
        arr[i] = temp;//将temp值放到最终的位置
    }

    /**
     * 交换元素
     * @param arr
     * @param a
     * @param b
     */
    public static void swap(int []arr,int a ,int b){
        int temp=arr[a];
        arr[a] = arr[b];
        arr[b] = temp;
    }

    public static void main(String []args){
        int []arr = {9,8,7,6,5,4,3,2,1};
        sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

性能分析:

时间复杂度:O(n * log(n))

空间复杂度:O(1)

稳定性:不稳定

四. 交换排序

4.1 冒泡排序

在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。

数据结构——【排序】详解_第7张图片

 代码实现:

public class bubbleSort {
    public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean isSorted = true;
            for (int j = 0; j < array.length - i - 1; j++) {
                // 相等不交换,保证稳定性
                if (array[j] > array[j + 1]) {
                    heapSort.swap(array, j, j + 1);
                    isSorted = false;
                }
            }
            if (isSorted) {
                break;
            }
        }
    }
}

性能分析:

时间复杂度

最坏情况:O(n^2)----数据逆序

最好情况:O(n)----数据有序

空间复杂度:O(1)

稳定性:稳定

4.2 快速排序

1. 从待排序区间选择一个数,作为基准值(pivot);

2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;

3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序, 或者小区间的长度 == 0,代表没有数据。

 代码实现:

public class quickSort {
    public static void quickSort(long[] array) {
        quickSortRange(array, 0, array.length - 1);
    }

    // 为了代码书写方便,我们选择使用左闭右闭的区间表示形式
    // 让我们对 array 中的从 from 到 to 的位置进行排序,其他地方不用管
    // 其中,from,to 下标的元素都算在区间的元素中
    // 左闭右闭的情况下,区间内的元素个数 = to - from + 1;
    private static void quickSortRange(long[] array, int from, int to) {
        if (to - from + 1 <= 1) {
            // 区间中元素个数 <= 1 个
            return;
        }

        // 挑选中区间最右边的元素 array[to]
        // array[to] 还是 array[to - 1] 还是 array[array.length] 还是 array[array.length - 1] 呢?
        int pi = partitionMethodA(array, from, to);
        // 小于等于 pivot 的元素所在的区间如何表示 array, from, pi - 1
        // 大于等于 pivot 的元素所在的区间如何表示 array, pi + 1, to

        // 按照分治算法的思路,使用相同的方式,处理相同性质的问题,只是问题的规模在变小
        quickSortRange(array, from, pi - 1);    // 针对小于等于 pivot 的区间做处理
        quickSortRange(array, pi + 1, to);   // 针对大于等于 pivot 的区间做处理
    }

    /**
     * 以区间最右边的元素 array[to] 最为 pivot,遍历整个区间,从 from 到 to,移动必要的元素
     * 进行分区
     * @param array
     * @param from
     * @param to
     * @return 最终 pivot 所在的下标
     */
    private static int partitionMethodA(long[] array, int from, int to) {
        // 1. 先把 pivot 找出来
        long pivot = array[to];
        // 2. 通过定义 left 和 right 两个下标,将区间划分出来
        int left = from;
        int right = to;
        // [from, left)   都是 <= pivot 的
        // [left, right)  都是未参与比较的
        // [right, to]    都是 >= pivot 的

        // 循环,保证每个元素都参与了和 pivot 的比较
        // 也就是,只要 [left, right) 区间内还有元素,循环就应该继续
        while (left < right) {
//        while (right - left > 0) {
            // 先让左边进行比较

            // 随着 left 在循环过程中一直在 left++,请问 left < right 的条件能一定保证么
            // 不一定,所以,我们时刻进行 left < right 条件的保证
            // 并且,只有在 left < right 成立的情况下,array[left] 和 pivot 的比较才有意义
            // left < right && array[left] <= pivot 的顺序不能交换
            while (left < right && array[left] <= pivot) {
                left++;
            }
            // 循环停止时,说明 array[left] > pivot

            while (left < right && array[right] >= pivot) {
                right--;
            }
            // 循环停止时,说明 array[right] < pivot

            // 两边都卡住时,交换 [left] 和 [right] 位置的元素
            long t = array[left];
            array[left] = array[right];
            array[right] = t;
        }

        // 说明 left == right,说明 [left, right) 区间内一个元素都没有了
        // 所有元素都和 pivot 进行过比较了,然后都在各自应该的位置上了
        // 并且 array[left] 一定是 >= pivot 的第一个元素(不给大家证明了)
        long t = array[to];
        array[to] = array[left];
        array[left] = t;

        // 返回 pivot 最终所在下标
        return left;
    }

    public static void main(String[] args) {
        long[] array = {-1, -1, -1, -1, 8, 7, 6, 5, 4, 3, 2, 1, -1, -1, -1 };

        int pi = partitionMethodA(array, 4, 11);
        System.out.println(pi);
    }
}

性能分析:

时间复杂度

最好情况:O(n * log(n))

平均情况:O(n * log(n))

最坏情况:O(n^2)

空间复杂度:最好 = 平均 = O(log(n));最坏 = O(n)

稳定性:不稳定

五. 归并排序

5.1 归并排序

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

数据结构——【排序】详解_第8张图片

可以看到这种结构很像一棵完全二叉树,本文的归并排序我们采用递归去实现(也可采用迭代的方式去实现)。分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n

再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤。

数据结构——【排序】详解_第9张图片

  代码实现:

public class mergeSort {
    private static void merge(int[] array, int low, int mid, int high) {
        int i = low;
        int j = mid;
        int length = high - low;
        int[] extra = new int[length];
        int k = 0;

        // 选择小的放入 extra
        while (i < mid && j < high) {
            // 加入等于,保证稳定性
            if (array[i] <= array[j]) {
                extra[k++] = array[i++];
            } else {
                extra[k++] = array[j++];
            }
        }

        // 将属于元素放入 extra
        while (i < mid) {
            extra[k++] = array[i++];
        }

        while (j < high) {
            extra[k++] = array[j++];
        }

        // 从 extra 搬移回 array
        for (int t = 0; t < length; t++) {
            // 需要搬移回原位置,从 low 开始
            array[low + t] = extra[t];
        }
    }

    public static void mergeSort(int[] array) {
        mergeSortInternal(array, 0, array.length);
    }
    // 待排序区间为 [low, high)
    private static void mergeSortInternal(int[] array, int low, int high) {
        if (low >= high - 1) {
            return;
        }

        int mid = (low + high) / 2;
        mergeSortInternal(array, low, mid);
        mergeSortInternal(array, mid, high);

        merge(array, low, mid, high);
    }
}

性能分析:

时间复杂度:O(n * log(n))

空间复杂度:O(n)

稳定性:稳定

你可能感兴趣的:(数据结构,排序算法,数据结构)