程序猿必备技能-->七大排序的思想及实现

本文详细介绍了排序算法的种类和基本概念,包括直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序和快速排序的实现原理和代码示例。讨论了各种排序算法的时间复杂度、空间复杂度以及稳定性,并提供了非递归实现的快速排序和归并排序方法。
摘要由CSDN通过智能技术生成

一. 排序

1.1 排序的基本概念

排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。

下面列举一些关于排序的其他概念:

稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持 不变,即在原序列中,r[i]=r[j] ,且 r[i] r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳 定的;否则称为不稳定的。

内部排序 :数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序

常见的排序方法一般只能用于内部排序,即数据量不算大的情况下,可以用于外部排序的算法比较少~~

1.2 排序的应用

生活中排序的例子有很多:从小到大一直很折磨人的成绩排名,世界五百强企业...还有朝思暮想的富婆

1.3 常见的七大排序

二. 七大排序的实现

2.1 直接插入排序

2.1.1 基本思想

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列

实际上我们在玩扑克牌的时候,每抹一张牌,都会不自觉地插入排序~~

 

如上图,假设 i 下标的元素28为要插入的元素,i 前面的元素已经有序,那么28就应该插到30的位置上,比28大的元素都要向后移动

让下标 i 从后往前遍历, 可以把情况分为三种:

1. arr[i]<arr[i-1],让下标为i-1的元素向后移动

2.arr[i]>arr[i-1],因为 i 之前的元素都已经有序,所以下标为i的元素已经到了合适的位置,终止循环

3.arr[i]==arr[i-1],看下图

 

如果此时还要将i-1的元素向后移动,就不能保证排序的稳定性

但是如果对排序稳定性没有要求的话,是可以将后面的20插入到第一个20前面的

2.1.2 代码实现

我们只要让 i 下标从1向后遍历,就可以让 i 前面的元素有序,并且将 i 插入到合适的位置 

下面给出具体实现的代码

public static void InsertSort(int[] arr){
        for(int i=1;i< arr.length;i++){
            int key=arr[i];//保存arr[i]的值,因为arr[i-1]向后移动会覆盖掉arr[i]
            int j=i-1;
            while(j>=0&&arr[j]>key){
                arr[j+1]=arr[j];
                j--;
            }
            arr[j+1]=key;
        }
    }

来分析一下直接插入排序的的性能:
时间复杂度:

最坏情况下,即当数组逆序时,每个下标为 i (i>=0&&i<n) 的元素都要插入到下标为0的位置,O(T)=O(N^2)

最好情况下,即数组本来已经有序,只需要遍历一遍数组即可,O(T)=O(N)

所以时间复杂度是O(N^2),但是我们可以判断出来,直接插入排序适合数组接近有序的情况

空间复杂度:

只用了一个变量key存储每次要插入的元素,所以空间复杂度是O(1)

稳定性:

虽然直接插入排序可以通过代码来使它变的不稳定,但是我们还是认为直接插入排序是稳定的

2.2 希尔排序

2.2.1 基本思想

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

 

 如上图,假设一开始 gap==5,从i=0 开始,每隔4个元素分成一组,组内元素插入排序

再让gap==2,从i=0 开始继续分组,重复刚才的步骤

最后让gap==1,实际上这次的排序就是直接插入排序

  

2.2.2 代码实现

理解完希尔排序的思想,你肯定有个问题:

 gap是怎么取值的?

关于gap怎么取值,实际上并没有一个合适的答案,但是不论怎样,最后一次排序时gap一定为1

下面代码让gap=n/2,n/4...

下面给出实现代码

public static void shell(int[]array,int gap){
        for(int i=gap;i<array.length;i++){
            int j=i-gap;
            int tmp=array[i];
            for(;j>=0;j-=gap){
                if(array[j]>tmp){
                    array[j+gap]=array[j];
                }else {
                    break;
                }
            }
            array[j+gap]=tmp;
        }
    }

    public static void shellSort(int[] array) {
         int gap= array.length;
         while(gap>1){
             gap/=2;
             shell(array,gap);
         }
    }

上面的代码有些难理解,为啥外层循环 i不直接+=gap?

拿gap==2举例,如果每次循环 i +=gap,就只能对红色组进行排序

如果让i++就不同了, i=0,2,...时对红色组排序,i=1,3,5,...时对绿色组排序,两个组交替进行

来分析一下希尔排序的性能:

 参考上述资料,希尔排序的时间复杂度并没有一个准确值,我们就按O(N^1.3)来算

空间复杂度:O(1),原理和直接插入排序是一样的

稳定性:不稳定

见下例

2.3 直接选择排序

 2.3.1 基本思想

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。 

 假设i==0,让min从i开始向后遍历,找到最小元素

交换下标为 i 和 min 的元素.i 向后遍历

2.3.2 代码实现

因为直接选择排序的思想比较简单,下面直接给出代码

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

    public static void selectSort(int[] array) {
        for (int i = 0; i < array.length; i++) {
            int min=i;
            for(int j=i;j< array.length;j++){
                if(array[j]<array[min])
                    min=j;
            }
            swap(array,i,min);
        }
    }

 直接选择排序还有另一种实现方法:

每一次从待排序的数据元素中选出最小和最大的一个元素,把最小元素存到起点位置,最大元素存到终点位置,直到起点和终点之间只有一个元素

start==0,end==n-1,让 i 从start向后遍历到end,找出[start,end]区间内的最大元素和最小元素

 交换max和end,min和start

 下面给出代码

public static void selectSort2(int[] array){
        int start=0;
        int end=array.length-1;
        while(start<end){
            int min=start;
            int max=start;
            for (int i = start; i <=end ; i++) {
                if(array[i]>array[max])
                    max=i;
                if(array[i]<array[min])
                    min=i;
            }
            swap(array,start,min);
            if(max==start)//最大元素是下标为start的元素,该元素已经被交换到min下标那里了
                max=min;
            swap(array,max,end);
            start++;
            end--;
        }
    }

 性能分析:

时间复杂度:O(N^2),并且直接选择排序对数据是不敏感的,不论数据是否有序,时间复杂度都是O(N^2)

空间复杂度:O(1)

稳定性:不稳定

如下例:

2.4 堆排序

 2.4.1 基本思想

堆排序 (Heapsort) 是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

如下图,想要让Array数组变成从小到大的顺序,先建立一个大根堆

 

 让堆顶元素和最后一个元素交换

 堆顶元素向下调整,重复刚才的过程

2.4.2 代码实现

 private static void shiftDown(int[] array,int parent,int end) {
        int child=parent*2+1;
        while(child<end){
            if(child+1<end&&array[child]<array[child+1])
                child++;
            if(array[parent]<array[child]){
                swap(array,parent,child);
                parent=child;
                child=parent*2+1;
            }else {
                break;
            }
        }
    }


    public static void heapSort(int[] array) {
 
        for (int i = (array.length-2)>>1; i >=0 ; i--) {//建堆
            shiftDown(array,i,array.length);
        }

        int end=array.length-1;
        while(end>0){
            swap(array,0,end);
            shiftDown(array,0,end);
            end--;
        }
    }

性能分析:

时间复杂度:O(N*logN)

前面的文章已经提到过,建堆的时间复杂度是O(N),每次把堆顶元素和最后一个元素交换,然后向下调整,时间复杂度是O(N*logN)

空间复杂度:O(1)

稳定性:不稳定,见下例

  

 2.5 冒泡排序

2.5.1 冒泡排序的思想

所谓冒泡,就是每次循环都让最大的元素冒出来,变成最后一个元素.如何实现呢?让j从0开始向后遍历,如果 array[j]>array[j+1],就让它们交换,这样就可以保证较大的元素一直在后面

2.5.2 代码实现

因为冒泡排序是我们的老朋友了,所以不再画图进行演示,直接上代码

public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            for(int j=0;j<array.length-1-i;j++){
                if(array[j]>array[j+1]){
                    swap(array,j,j+1);
                }
            }
        }
    }

冒泡排序还有一个优化版--设置一个flag判断这趟循环是否交换了元素,如果没有发生元素交换,说明这个数组已经有序了

public static void bubbleSort(int[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            boolean flg=true;
            for(int j=0;j<array.length-1-i;j++){
                if(array[j]>array[j+1]){
                    swap(array,j,j+1);
                    flg=false;
                }
            }
            if(flg) break;
        }
    }

性能分析:

时间复杂度:O(N^2),且对数据不敏感

空间复杂度:O(1)

稳定性:稳定

2.6 快速排序

2.6.1 快速排序的思想

 快速排序是Hoare1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有 元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

该怎么查找基准元素的正确位置呢?

思路很简单,比基准元素小的元素都扔到基准元素的左边,比基准元素大的都扔到基准元素的右边

有三种实现方法,我们先来看hoare法

如上图,是一个待排序的数组,以第一个元素作为基准元素

第一步: 让R从右往左走,遇到比key小或等于key的数字就停下来

第二步: 让L从左往右走,遇到比key大或等于key的数字就停下来

第三步: R下标的元素和L下标的元素交换

重复刚才的步骤,直到L == R

这时候我们可以看到,R和L下标所在的元素比key小,也就是说R/L所在的位置就是key的正确位置,这时候交换key和array[R],递归排序key左边和右边的元素就可以了

这里有俩问题需要强调一下:

问: 当array[R]==key和array[L]==key的时候可不可以让R和L停下来?

下面给个数组来模拟一下

 

 然后他俩就像扔烫手山芋一样互相交换6,这可苦了CPU了,所以这个程序就会一直执行下去

 

问2: 为啥要让R先走,让L先走可以吗

再给一个数组

 很不幸,L出师不利,走了一步就停下了,再让R从后往前遍历,找<=key的元素(但是不能找到L前面,因为L前面的元素已经<=key了)

 然后我们交换key和R下标的元素~

这时候问题就出来了,key==1本来就应该在首位,所以让L先走的逻辑是不对的

 2.6.2 代码实现

为了实现接口的统一,这里使用quickSort方法对_quickSort方法包装一下

public static int partition1(int[]array,int begin,int end){

        int left=begin;//为了方便使用swap方法交换指定下标的元素,先把key的下标保存起来
        int key=array[left];//用key保存基准元素

        while(begin<end){
            while(begin<end&&array[end]>=key){
                end--;
            }
            while(begin<end&&array[begin]<=key){
                begin++;
            }
            swap(array,begin,end);
        }
        swap(array,left,begin);
        return begin;
    }
    public static void _quickSort(int[] array,int begin,int end){
        if(begin>=end){
            return;
        }
        int pivot=partition1(array,begin,end);

        _quickSort(array,begin,pivot-1);//递归排序key的左边和右边
        _quickSort(array,pivot+1,end);
    }

    public static void quickSort(int[] array){//为了保证接口的统一性--封装一下快排
        _quickSort(array,0,array.length-1);
    }

怎么定位基准元素的正确位置,还有两种常用方法 

法二:挖坑法

第一步:存储基准元素key,把key所在的下标设置成一个坑

 第二步:让R从后往前遍历,找到<=key的元素,把它扔到pivot里,把pivot更新成R的值

 第三步:让L从前往后遍历,找到>=key的元素,把它扔到pivot里,让pivot=L

 重复刚才的步骤,直到L==R,然后把key赋给R/L所在下标元素就可以啦

public static int partition2(int[]array,int begin,int end){

        int key=array[begin];
        int pivot=begin;
        while(begin<end){
            while(begin<end&&array[end]>=key){
                end--;
            }
            array[pivot]=array[end];
            pivot=end;
            while(begin<end&&array[begin]<=key){
                begin++;
            }
            array[pivot]=array[begin];
            pivot=begin;
        }
        array[begin]=key;
        return begin;
    }

法三:前后指针法

定义一个前指针prev,用来保存<=key的最后一个元素的下标

让prev指向基准元素,cur指向后一个元素

 如果cur指向的元素<=key,说明当前prev的下一个元素应该是cur指向的元素,prev++,交换下标为prev和cur的元素(这样prev仍然指向最后一个<=key的元素)

如果cur指向的元素>key,说明prev的下一个元素不应该是cur指向的元素,cur接着向后遍历就可以了

 直到cur==array.length时,prev下标就是基准元素key的正确下标

static int partition3(int[] array,int begin,int end){
        int key=array[begin];
        int prev=begin;
        int cur=prev+1;
        while(cur<array.length){
            if(array[cur]<key){
                ++prev;
                swap(array,prev,cur);
            }
            cur++;
        }
        swap(array,begin,prev);
        return prev;
    }

还可以对这串代码做一个小小的优化---当array[cur]<key时,如果prev后面的元素就是cur,不需要让cur自己与自己交换了

 下面是优化版

static int partition3(int[] array,int begin,int end){
        int key=array[begin];
        int prev=begin;
        int cur=prev+1;
        while(cur<array.length){
            if(array[cur]<key&&++prev!=cur){
                swap(array,prev,cur);
            }
            cur++;
        }
        swap(array,begin,prev);
        return prev;
    }

现在来分析一下快速排序的性能:

时间复杂度:

每次查找基准元素的正确位置时间复杂度为O(N)

在最好的情况下,基准元素是当前排序数组的中间值,就可以把这个数组平分成两份,这种情况下

需要递归O(logN)次,时间复杂度是O(N*logN)

最坏情况下,基准元素左边或者右边的区间没有元素(基准元素为待排序数组的最大值或最小值),需要递归N次,时间复杂度是O(N^2)

 但是,快速排序的时间复杂度我们仍然按O(N*logN)来算!!!

 为了防止最坏情况的发生,先用三数取中法设置基准元素

三数取中法:取begin,mid,end的中间值作为基准元素

static private int getMidIndex(int[]array,int begin,int end){
        int mid=(begin+end)>>1;
        if(array[begin]>array[end]){
            if(array[end]>array[mid]){//下标为end的元素是中间值
                return end;
            }else {
                return array[mid]<array[begin]?mid:begin;//下标为begin和mid的元素都比end大,返回begin和mid的较小值
            }
        }else {
            if(array[begin]>array[mid]){//逻辑同上
                return end;
            }else {
                return array[mid]<array[end]?mid:end;
            }
        }
    }
    public static void _quickSort(int[] array,int begin,int end){
        if(begin>=end){
            return;
        }
        int midIndex=getMidIndex(array,begin,end);
        swap(array,begin,midIndex);//将中间值和原来的基准值交换
        
        int pivot=partition3(array,begin,end);
        _quickSort(array,begin,pivot-1);
        _quickSort(array,pivot+1,end);
    }
    public static void quickSort(int[] array){
        _quickSort(array,0,array.length-1);
    }

空间复杂度:

最好情况下,需要开辟O(logN)个栈帧

最坏情况下,需要开辟O(N)个栈帧

空间复杂度仍然按最好情况O(logN)来计算

稳定性:不稳定

比如下面这个数组,R需要继续往前走,否则这个程序会一直运行~

 

2.6.3 快速排序的非递归实现

实现思路很简单:把每次待排序数组的起始下标和终点下标存在栈里,每次排序时取出栈里的元素就可以了

public static void non_quickSort(int[] array) {
        Stack<Integer> stack = new Stack<>();
        int start = 0;
        int end = array.length-1;
        stack.push(start);//起始下标先入栈
        stack.push(end);//终点下标后入栈
        while(!stack.empty()){
            int pivot = partition1(array,start,end);
            if(pivot > start+1) {//如果左区间[start,pivot)只有一个元素start,不需要继续排序
                stack.push(start);
                stack.push(pivot-1);
            }
            if(pivot < end-1) {//如果右区间(pivot,end]只有一个元素end,不需要继续排序
                stack.push(pivot+1);
                stack.push(end);
            }
            end=stack.pop();//终点下标先出栈
            start=stack.pop();//起始下标再出栈
        }
    }

非递归快排里面也可以使用三数取中法进行优化

2.7 归并排序

2.7.1 基本思想

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

 

 2.7.2 递归代码实现

static private void merge(int[] array,int begin,int mid,int end){
        int begin1=begin,begin2=mid+1;
        int end1=mid,end2=end;
        int[] tmp=new int[end-begin+1];//需要设置一个额外的数组记录两个有序数组排序后的结果
        int k=0;
        while(begin1<=end1&&begin2<=end2){
            if(array[begin1]<=array[begin2]){//如果没有=,就是不稳定排序
                tmp[k++]=array[begin1++];
            }else {
                tmp[k++]=array[begin2++];
            }
        }
        while(begin1<=end1){
            tmp[k++]=array[begin1++];
        }
        while(begin2<=end2){
            tmp[k++]=array[begin2++];
        }
        for(int i=0;i<k;i++){//把排序好的数组拷贝到原数组里面
            array[i+begin]=tmp[i];
        }
    }
    static private void _mergeSort(int[]array,int begin,int end){
        if(begin>=end){
            return;
        }
        int mid=(begin+end)>>1;
        _mergeSort(array,begin,mid);
        _mergeSort(array,mid+1,end);
        merge(array,begin,mid,end);
    }
    public static void mergeSort(int[] array){
        _mergeSort(array,0,array.length-1);
    }

2.7.3 归并排序的非递归实现

归并排序的非递归是用循环实现的

定义一个整数gap,每次合并的数组元素个数都为gap

 

 下面给出实现代码


    public static void non_mergeSort(int[] array){

        int gap=1;
        while(gap<array.length){
            for(int i=0;i<array.length;i+=2*gap){

                int mid=i+gap-1;
                int end=mid+gap;

                //mid和end可能会越界
                if(mid>=array.length){
                    mid=array.length-1;
                }
                if(end>=array.length){
                    end=array.length-1;
                }
                merge(array,i,mid,end);//合并两个有序数组
            }
            gap*=2;
        }
    }

 性能分析:

时间复杂度:O(N*logN),并且对数据不敏感

 空间复杂度:O(N)

每次递归申请区间大小的数组,一共要申请O(N)个空间

稳定性:稳定排序

还有一些排序是比较常见的,计数排序,基数排序,桶排序...请各位读者自己移步去看,本文不再赘述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不 会敲代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值