数据结构学习笔记-七大排序问题

目录

1.稳定性

2.分类

3.插入排序

3.1 直接插入排序

3.2 性能分析

 3.3 折半插入排序

4.希尔排序

4.1 原理

4.2 实现

5. 选择排序

5.1 直接选择排序-原理

5.2 实现

5.3 性能分析

5.4 双向选择排序

6、堆排序

6.1 原理

6.2 实现 

6.3 性能分析

7. 冒泡排序

7.1 原理

7.2 实现 

7.3 性能分析

8. 快速排序 

8.1 原理

8.2 实现

8.3 性能分析

9. 归并排序

9.1 原理

9.2  实现 

10. 七大排序总结


1.稳定性

两个相等的数据,如果经过排序 之后,排序算法 能够保证其相对位置不发生变化,则称该算法是具备稳定性的算法。

2.分类

1.基于比较(直接把两个元素进行大小比较)的排序,也叫内部排序,将数据都在内存中操作

2.需要借助硬盘等辅助介质进行的排序操作(大数据排序,数据大到内存放不下):桶排序、基数排序、计数排序。

 

3.插入排序

3.1 直接插入排序

每次选择无序区间的第一个数插入到有序区间的合适位置,不断重复此流程知道数组有序。

 /**
     * 直接插入排序
     * @param arr
     */
    public static void insertSort(int[] arr){
        //有序数组[0,i)
        //默认第一个就是有序
        for (int i = 1; i <arr.length ; i++) {
            //每次都从无序区间中选择第一个元素插入到有序区间的合适位置
            for (int j = i; j >0 &&arr[j-1]>arr[j] ; j--) {
                swap(arr,j,j-1);
            }
        }
    }

3.2 性能分析

时间复杂度空间复杂度
最好平均最坏
O(n)O(n^2)O(n^2)O(1)
数据有序数据逆序

稳定性:稳定

总结:在插入排序中,初始数据越接有序,时间效率越高,因此 ,插入 排序经常作为高级排序的优化手段,在小数据规模上的性能非常好。

 3.3 折半插入排序

由于插入是在有序区间进行插入,因此我们可以使用二分查找法来快速定位插入的位置(之前是通过遍历有序区间插入)。再将要插入元素的位置及其之后位置上的元素进行数据搬移。

     /**
     * 折半插入排序
     */
    public static void bsInsertSort(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int val=arr[i];
            int low=0;
            int high=i;
            while (low<high) {
                int mid = (low + high) / 2;
                if (val >=arr[mid]) {//保证了稳定性
                    low=mid+1;
                } else {
                    high=mid;
                }
            }
            //数据搬移
            for (int j = i; j > low; j--) {
                arr[j]=arr[j-1];
            }
            //low就是元素插入位置
            arr[low]=val;

        }
    }

时间复杂度O(n^2)

4.希尔排序

4.1 原理

希尔排序又称缩小增量法。基本原理是:选定一个整数gap,将待排序的数据中所有记录按gap分组。所有距离为gap的放在同一组,将组内元素进行排序。然后不断缩小gap的大小,直到gap=1,当gap=1时数组已经近乎有序,调用普通插入排序统一排序即可(直接插入排序最好)。

gap一般就是数组长度一直除以2或除以3,直到gap=1。

4.2 实现

/**
     * 希尔排序
     * @param arr
     */
    public static void shellSort(int[] arr){
        int gap=arr.length/2;
        while (gap>=1){
            insertionSortGap(arr,gap);
            gap = gap / 2;
        }
    }

    private static void insertionSortGap(int[] arr, int gap) {
        //不断从gap开始走到数组末尾
        for (int i = gap; i <arr.length ; i++) {
            //最内层从gap索引开始向前看,看的元素就是距离gap长度的元素
            // 不断比较当前元素和前面gap元素大小
            // j - gap >= 0说明前面数组还要相同距离的元素,比较arr[j] 和 arr[j - gap]
            for (int j = i; j - gap >= 0 &&arr[j] <arr[j-gap]; j=j-gap) {
                swap(arr, j, j-gap);
            }
        }
    }

为什么要从gap的位置从后往前去遍历呢 ?

因为从后往前遍历,交换j和j-gap的值,并且直到j-gap<0结束,这样就保证了能将最小值放在最前面。

4.3 性能分析

时间复杂度空间复杂度
最好平均最坏
O(n)O(n^1.3)O(n^2)O(1)
数据有序比较难构造

5. 选择排序

5.1 直接选择排序-原理

每次从无序区间中选择最小值放在无序区间的最开始位置,每当进行一次无序循环(选择无序区间的最小值),无序区间的个数减一,有序区间的个数加一。

有序区间:[0,i)   无序区间:[i+1,n)

5.2 实现

/**
     * 直接选择排序
     * @param arr
     */
    public static void selectSort(int[] arr) {
        // 每次从待排序数组中选择最小值放在待排序数组的最前面。
        // 最外层的for循环表示要执行的总次数,类似于冒泡,当剩下最后一趟时,整个数组已经有序
        // 默认第一个元素就是有序的
        // 已经有序的集合[0,i)
        // 待排序的集合[i + 1,n)
        // 每次进行一趟排序,最小值就放在了数组的最前面,已经有序的集合元素个数 + 1
        // 待排序集合元素个数 - 1
        for (int i = 0; i < arr.length - 1; i++) {
            // min变量存储了最小值元素的下标
            int min = i;
            // 每次从无序区间中选择最小值
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }
            // 此时min就存储了最小值的元素下标,就把min对应的元素换到无序区间的最前面
            swap(arr,i,min);
        }
    }

5.3 性能分析

时间复杂度空间复杂度
O(n^2)O(1)
数据不敏感数据不敏感

稳定性:不稳定

5.4 双向选择排序

之前的直接选择排序时是每次从无序区间中选出一个最小值放在无序区间的最前面,一趟下来只有一个元素到达最终位置。

而双向选择排序则是每次从无序区间中选取一个最大值和最小值,分别放在无序区间的最前面和最后面,一趟下来 就有两个元素到达了最终位置。

  /**
     * 双向选择排序,每次选出最小值放前面,最大值放后面
     * @param arr
     */
    public static void selectSortOP(int[] arr) {
        int low = 0,high = arr.length - 1;
        // 有序区间[0,low + 1)
        while (low < high) {
            int min = low,max = low;
            for (int i = low + 1; i <= high; i++) {
                if (arr[i] > arr[max]) {
                    max = i;
                }
                if (arr[i] < arr[min]) {
                    min = i;
                }
            }
            // min存储了无序区间的最小值,max存储了无序区间的最大值
            swap(arr,low,min);
            if (max == low) {
                // 当max就处在low位置,由于swap(arr,low,min),low对应的元素值修改了,修改到min对应的下标
                max = min;
            }
            swap(arr,max,high);
            low += 1;
            high -= 1;
        }
    }

6、堆排序

6.1 原理

1.将任意数组进行堆化(升序调整为最大堆,降序调整为最小堆),再依次进行extractMax(取出堆顶元素)操作,就得到了一个排序的数组。

这种方法的时间复杂度O(logn),空间复杂度O(n),需要创建一个和原数组大小一致的临时空间。

2.原地堆排序:将任意数组进行堆化(升序调整为最大堆,降序调整为最小堆),此处假设得到一个升序集合 ,需要建大堆。此时堆顶元素就是整个数组中的最大值,排序后它应该处在数组的末尾,因此交换它和当前堆的最后一个元素,这样最大值就到了最终位置,再对交换后的堆顶元素进行下沉操作(每当有一个元素到达最终位置时,此时的下沉操作就不能再考虑这个元素),如此迭代到最后一个元素时,数组已经有序。

6.2 实现 

 /**
     * 将任意数组进行原地堆排序
     * @param arr
     */
    public static void heapSort(int[] arr){
        //将任意数组调整为最大堆
        //从最后一个非叶子节点开始
        for (int i = (arr.length-1-1)/2; i >=0 ; i--) {
            siftDown(arr,i,arr.length);
        }
        //依次取出堆顶元素和最后位置元素交换
        //最开始待排序[0,arr.length-1] 已排序[]
        //第一次排序 待排序[0,arr.length-2] 已排序[arr.length-1]
        //第二次排序 待排序[0,arr.length-3]  已排序[arr.length-2,arr.length-1]
        for (int i = arr.length-1; i >0 ; i--) {
            swap(arr,0,i);
            siftDown(arr,0,i);

        }
    }
 /**
     * 元素的下沉操作
     * @param arr
     * @param i
     * @param n 当前arr中有效的元素个数
     */
    private static void siftDown(int[] arr, int i, int n) {
        while((i*2)+1<n){
            int j=(i*2+1);
            if(j+1<n && arr[j+1]>arr[j]){
                j=j+1;
            }
            if(arr[i]>arr[j]){
                break;
            }else {
                swap(arr,i,j);
                i=j;
            }
        }
    }

    private static void swap(int[] arr, int i, int j) {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }
}

6.3 性能分析

时间复杂度空间复杂度
O(n*log(n))O(1)
数据不敏感数据不敏感

稳定性:不稳定

7. 冒泡排序

7.1 原理

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

7.2 实现 

/**
     * 冒泡排序
     * @param arr
     */
    public static void bubbleSort(int[] arr){
        //最外层表示要比较的趟数,此处-1是因为,整个待排序数组剩一个元素时,整个数组已经有序
        for (int i = 0; i < arr.length-1; i++) {
            boolean isSwapped=false;
            for (int j = 0; j < arr.length-1-i; j++) {
                if(arr[j]>arr[j+1]){
                    isSwapped=true;
                    swap(arr,j,j+1);
                }
            }
            if(!isSwapped){
                //内层循环没有元素交换,整个数组有序
                break;
            }
        }
    }
 private static void swap(int[] arr, int i, int j) {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }

7.3 性能分析

 

稳定性:稳定

8. 快速排序 

8.1 原理

选取一个分区点(基准值),将数组分为三部分,基准值之前的数组都小于基准值,基准值之后的数组的值都大于基准值,重复这个过程,就得到了一个排序数组。

8.2 实现

 /**
     * 快排的基础实现
     * @param arr
     */
    public static void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length-1);
    }
/**
     * 在l...r进行快排
     * @param arr
     * @param l
     * @param r
     */
    private static void quickSortInternal(int[] arr, int l, int r) {
        if(r-l<=15){
            insertBase(arr,l,r);
            return;
        }
        //求基准值
        int p=partition(arr,l,r);
        quickSortInternal(arr,l,p-1);
        quickSortInternal(arr,p+1,r);
    }

    private static int partition(int[] arr, int l, int r) {
        //在当前数组中随机选择一个元素作为基准值
        int randomIndex=random.nextInt(l,r);
        swap(arr,l,randomIndex);
        int v=arr[l];
        int j=l;
        //i是当前处理的元素下标
        //arr[l+1...j]<v
        //arr[j+1...i]>=v
        for (int i = l+1; i <=r ; i++) {
            if(arr[i]<v){
                swap(arr,j+1,i);
                //小于v的元素+1
                j++;
            }
        }
        swap(arr,j,l);
        return j;
    }
 private static void swap(int[] arr, int i, int j) {
        int tmp=arr[i];
        arr[i]=arr[j];
        arr[j]=tmp;
    }

8.3 性能分析

 稳定性:不稳定

性能衰减:1.当待排序元素接近有序时,快速排序退化为单支树,时间复杂度为O(n^2)。

2.当待排序数组中包含大量重复元素时,会导致大于等于基准值部分的元素远大于小于基准值的部分,使递归树产生倾斜。

9. 归并排序

9.1 原理

将集合排序时分为两大部分:1.拆分:将原数组不断拆分,拆分到每个小数组只剩下一个元素时,拆分过程结束。2.合并 :将拆分后的数组不断合并,直到合并到整个数组,这是整个数组有序 。

9.2  实现 

 /**
     * 在arr上进行归并排序
     * @param arr
     */
    public static void mergeSort(int[] arr){
        mergeSortInternal(arr,0,arr.length-1);
    }

    /**
     * 递归
     * 在arr[l...r]上进行归并排序
     * @param arr
     * @param l
     * @param r
     */
    private static void mergeSortInternal(int[] arr, int l, int r) {
        if(r-l<=15){
            insertBase(arr,l,r);
            return;
        }
        int mid=l+((r-l)>>1);
        mergeSortInternal(arr,l,mid);//先对左半部分进行归并排序
        mergeSortInternal(arr,mid+1,r);//再对右半部分进行归并排序
        if(arr[mid]>arr[mid+1]){
            merge(arr,l,mid,r);
        }
    }

    /**
     * 将已经有序的arr[l..mid]和[mid+1...r]合并为一个大的有序数组
     * @param arr
     * @param l
     * @param mid
     * @param r
     */
    private static void merge(int[] arr, int l, int mid, int r) {
        //开辟一个大小和合并后数组大小相同的数组
        int[] temp=new int[r-l+1];
        //将原数组的内容拷贝到新数组
        for (int i = l; i <=r; i++) {
            temp[i-l]=arr[i];
        }
        int i=l;//左半有序数组的第一个元素
        int j=mid+1;//右半有序数组第一个元素
        //遍历原数组,选择左半区间和右半区间的最小值写回原数组
        //k表示当前处理到原数组的哪个位置
        for (int k = l; k <=r ; k++) {
            if(i>mid){
                //此时左半区间处理完毕,将右区间剩下的元素写回原数组
                arr[k]=temp[j-l];
                j++;
            }else if(j>r){
                //此时有右半区间处理完毕,将左区间剩下的元素写回原数组
                arr[k]=temp[i-l];
                i++;
            }else if(temp[i-l]<temp[j-l]){
                arr[k]=temp[i-l];
                i++;
            }else {
                arr[k]=temp[j-l];
                j++;
            }
        }
    }

    /**
     * 归并排序的非递归写法
     * @param arr
     */
    public static void mergeSortNonRecursion(int[] arr){
        //sz表示每次合并的个数,最开始从1个元素开始合并,以此累乘
        for (int sz = 1; sz <= arr.length ; sz=sz*2) {
            //merge过程,i表示每次merge开始的索引下标
            for (int i = 0; i+sz <arr.length ; i+=sz*2) {
                merge(arr,i,i+sz-1,Math.min(i+2*sz-1, arr.length-1));
            }
        }
    }

9.3 性能分析

时间复杂度空间复杂度
O(n * log(n))O(n)
数据不敏感数据不敏感

归并排序和堆排序一样,都是非常稳定的O(nlogn)的排序算法。

10. 七大排序总结

排序方法最好平均最坏空间复杂度稳定性
冒泡排序O(n)O(n^2)O(n^2)O(1)稳定
插入排序O(n)O(n^2)O(n^2)O(1)稳定
选择排序O(n^2)O(n^2)O(n^2)O(1)不稳定
希尔排序O(n)O(n^1.3)O(n^2)O(1)不稳定
堆排序O(n * log(n))O(n * log(n))O(n * log(n))O(1)不稳定
快速排序O(n * log(n))O(n * log(n))O(n^2)O(log(n)) ~ O(n)不稳定
归并排序O(n * log(n))O(n * log(n))O(n * log(n))O(n)稳定

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值