数据结构-------排序

1. 排序的概念

排序:就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减地排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录。若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则称为不稳定的。
简单来说,就是看排序的时候有没有隔着区间进行交换或插入
内部排序:数据元素全部存放在内存中的排序。
外部排序:数据元素太多不能同时存放在内存中,根据排序过程的要求不断在内存之间移动数据的排序。

2. 常见排序算法的实现

2.1 插入排序

2.1.1 基本思想

将待排序的记录按其关键码的值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。实际中我们玩扑克牌时,就用了插入排序的思想。

2.1.2 直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

public class Sort {
    public static void insertSort(int[] array){
        //外层循环目的:取到数组中的每个元素
        for(int i=1; i< array.length;++i){
            //将该元素插入到序列中
            //array 数组中:[0,i)的元素已经排好了

            //i位置的数据是本次要插入的数据
            int key=array[i];
            int end=i-1;

            while(end>=0&& key<array[end]){
                array[end+1]=array[end];
                end--;
            }
            array[end+1]=key;
        }
    }

}

直接插入排序的特性总结:

  1. 元素集合越接近有序,直接插入排序算法的时间效率最优(最优的时间复杂度为O(N))
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定
  5. 接近有序:小的尽量靠前,不大不小的尽量居中,大的尽量靠后
  6. 应用场景:数据量少或者数据接近有序

2.1.3 希尔排序(缩小增量排序)

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

public static void shellSort(int[] array){
        int gap=3;
        while(gap>0){
        
        for(int i=gap; i< array.length;++i){
            //将该元素插入到序列中
            //array 数组中:[0,i)的元素已经排好了

            //i位置的数据是本次要插入的数据
            int key=array[i];
            int end=i-gap;

            while(end>=0&& key<array[end]){
                array[end+gap]=array[end];
                end-=gap;
            }
            array[end+gap]=key;
        }
        gap-=1;
        }
    }

希尔排序总结

  • 希尔排序是对直接插入排序的优化。
  • 当gap>1时都是预排序,目的是让数组更接近有序。当gap=1时,数组已经接近有序了,这样就会很快,整体而言可以达到一个优化的效果
  • 希尔排序的时间复杂度不好计算,因为gap的取值很多,导致很难计算。
    我们暂且按照Knuth提出的方式来记,时间复杂度: O ( n 1.25 ) ( 1.6 ∗ n 1.25 ) O(n^{1.25}) (1.6*n^{1.25}) O(n1.25)(1.6n1.25)

2.2 选择排序

2.2.1 基本思想

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

2.2.2 直接选择排序

  • 在元素集合array[i]-----array[n-1]中选择关键码最大(最小)的数据元素
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素的最后一个(第一个)元素交换。
  • 在剩余的array[i]------array[n-2] (array[i+1]-----array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

代码实现

 public static void selectSort(int[] array){
        int size= array.length;
        for(int i=0;i<size-1;++i){//控制选择的趟数
            //具体选择的方式

            int pos=0;
            //找最大元素的位置
            for(int j=0; j<size-i ;++j){
                if(array[j]>array[pos]){
                    pos=j;
                }
            }

            //用pos标记的最大元素与区间最后一个位置上的元素进行交换
            if(pos!=size-i-1){
                swap(array,pos,size-1-i);
            }
        }
    }

优化的选择排序

public static void selectSortOP(int[] array){
        int begin=0;
        int end= array.length-1;
        while(begin<end){
            int minPos=begin;//标记最小元素
            int maxPos=begin;//标记最大元素
            int index=begin+1;
            while(index<=end){
                if(array[index]>array[maxPos]){
                    maxPos=index;
                }
                if(array[index]<array[minPos]){
                    minPos=index;
                }

                index++;
            }
            //在[begin,end]区间中,已经找到了最大和最小元素
            if(maxPos!=end){
                swap(array,maxPos,end);
            }

            if(minPos==end){
                minPos=maxPos;
            }
            if(minPos!=begin){
                swap(array,begin,minPos);
            }

        }

    }

选择排序特性总结

  1. 直接选择排序思想非常好理解,但是效率不高。实际中很少使用
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定
  5. 选择排序的缺陷:会出现重复比较

2.2.3 堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
注意排升序建大堆,排降序建小堆
代码实现

public static void shiftDown(int[] array,int size,int parent){
        //child标记parent的左孩子

        int child=parent*2+1;

        //循环条件可以保证左孩子存在,
        while(child<size) {
            //找到parent的最大孩子
            //右孩子存在的情况下找左右孩子中最大的孩子
            if (child+1<size && array[child + 1] > array[child]) {
                child += 1;
            }

            //根结点如果比最大孩子小,与之交换
            if (array[parent] < array[child]) {
                swap(array, parent, child);
                parent = child;
                child = parent * 2 + 1;
            }else{
                return;
            }
        }
    }
    public static void heapSort(int[] array){
        //1.建堆
        //找倒数第一个非叶子结点
        int size= array.length;
        int lastLeaf=((size-2)>>1);
        for(int root=lastLeaf;root>=0;root--){
            shiftDown(array, size, root);
        }

        //2.利用堆删除的思想排序

        int end=size-1;
        while(end>0){
            //用堆顶元素与堆中最后一个元素交换
            swap(array,0,end);

            //将堆中有效元素个数减少一个

            //将堆顶元素向下调整
            shiftDown(array,end,0);
            end--;
        }
    }

应用场景:需要一个序列中前k个最大或者最小

2.3 交换排序

基本思想:交换就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置。
交换排序的特点:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

2.3.1 冒泡排序

代码实现

 public static void bubbleSort(int[] array){
        int size= array.length;
        for(int i=0;i<size-1;++i){
            //用相邻的两个元素进行交换
            //如果满足条件则交换
            for(int j=1; j<size; j++){
                if(array[j-1]>array[j]){
                    swap(array,j-1, j);
                }
            }
        }
    }

优化的冒泡排序(如果没有交换 ,就说明排好了,不需要再比了):

 public static void bubbleSort(int[] array){
        int size= array.length;
        boolean isChange=false;
        for(int i=0;i<size-1;++i){
            //用相邻的两个元素进行交换
            //如果满足条件则交换
            for(int j=1; j<size; j++){
                if(array[j-1]>array[j]){
                    swap(array,j-1, j);
                    isChange=true;
                }
            }
            
            if(!isChange){
                return;
            }
        }
    }

冒泡排序的特性总结

  1. 冒泡排序是一种非常容易理解的排序
  2. 时间复杂度:O(N^2)
  3. 空间复杂度:O(1)
  4. 稳定性:稳定

2.3.2 快速排序

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法。
基本思想:任意取待排序元素序列中的某元素作为基准值,根据基准值将区间分割成两个部分,左侧部分比基准值小,右侧部分比基准值大。重复此过程,直到所有元素排完。
基准值:没有规定怎么取,区间任意一个数字都可以作为基准值。
代码的可行性上来说:一般取得是区间两侧的数据

public static int partition(int[] array,int left,int right){
        
        return 0;
    }
    public static void quickSort(int[] array,int left,int right){
        //如果排升序
        //找一个基准值将区间分割成两个部分
        int div=partation(array,left,right);

        //左侧部分比基准值小
        quickSort(array,left,div);

        //右侧部分比基准值大
        quickSort(array,div+1,right);

    }

上述代码为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像。
将区间按照基准值划分为左右两半部分的常见方式有:

  1. Hoare版

代码实现

public static int partition1(int[] array,int left,int right){

        int key=array[right-1];
        int begin=left;
        int end=right-1;

        while(begin<end){
            //让begin从前往后找比key大的元素,找到之后停下来
            while(begin<end && array[begin]<=key){
                begin++;
            }
          //让end从后往前找比key小的元素,找到之后停下来
            while(begin<end && array[end]>=key){
                end--;
            }

            if(begin!=end){
                swap(array,begin,end);
            }
        }
        if(begin!=right-1){
            swap(array,begin,right-1);
        }
        return 0;
    }
  1. 挖坑法

代码实现

//挖坑法
    public static int partition2(int[] array,int left,int right){
        
        int key=array[right-1];
        int begin=left;
        int end=right-1;

        while(begin<end){
            //让begin从前往后找比基准值大的元素
            while(begin<end && array[begin]<=key){
                begin++;
            }

            if(begin<end){
                //begin位置找到了一个比key大的元素,填end位置的坑
                array[end]=array[begin];

                //begin位置形成了一个新的坑位
            }

            //让end从后往前找比基准值小的元素
            while(begin < end && array[end]>= key){
                end--;
            }

            if(begin<end){
                //end位置找到了一个比key小的元素,填begin位置的坑
                array[begin]=array[end];
                //end位置就形成了一个新的坑位
            }
        }

        //begin和end位置是最后的一个坑位
        //这个坑位使用基准值填充
        array[begin]=key;
        return begin;
    }

  1. 前后标记

代码实现

public static int partition3(int[] array,int left,int right){
        int cur=left;
        int prev=cur-1;
        int key=array[right-1];

        //让cur从前往后找比key小的元素
        while(cur<right){
            if(array[cur]<key&& ++prev!=cur){
                 swap(array,cur,prev);
            }
            ++cur;
        }

        //将基准值的位置放好
        if(++prev!=right-1){
            swap(array,prev,right-1);
        }
        return prev;
    }

2.3.3 快速排序优化

  1. 三数取中法选key
    为了尽可能避免取到极值,一次性找三个数据:左侧取一个,右侧取一个,中间取一个,以这三个数据最中间的数据作为基准值。

代码实现

public static int getIndexOfMiddle(int[] array,int left,int right){
        //left
        //mid:left+(right-left)>>1
        //right-1
        int mid=left+((right-left)>>1);
        //求三个数据中的中间值
        if(array[left]<array[right-1]){
            if(array[mid]<array[left]){
                return left;
            }else if(array[mid]>array[right-1]){
                return right-1;
            }else{
                return mid;
            }
        }else{
            if(array[mid]>array[left]){
                return left;
            }else if(array[right-1]>array[mid]){
                return right-1;
            }else{
                return mid;
            }

        }
    }

public static int partition1(int[] array,int left,int right){

        int index=getIndexOfMiddle(array, left, right);
        if(index!=right){
            swap(array,index,right-1);
        }
        int key=array[right-1];
        int begin=left;
        int end=right-1;

        while(begin<end){
            //让begin从前往后找比key大的元素,找到之后停下来
            while(begin<end && array[begin]<=key){
                begin++;
            }
          //让end从后往前找比key小的元素,找到之后停下来
            while(begin<end && array[end]>=key){
                end--;
            }

            if(begin!=end){
                swap(array,begin,end);
            }
        }
        if(begin!=right-1){
            swap(array,begin,right-1);
        }
        return 0;
    }
  1. 递归到小的子区间,可以考虑使用插入排序

代码实现

//快速排序中的插入排序
    public static void insertSortQuick(int[] array,int left,int right){
        //外层循环目的:取到数组中的每个元素
        for(int i=left+1; i< right;++i){
            //将该元素插入到序列中
            //array 数组中:[0,i)的元素已经排好了

            //i位置的数据是本次要插入的数据
            int key=array[i];
            int end=i-1;

            while(end>=0&& key<array[end]){
                array[end+1]=array[end];
                end--;
            }
            array[end+1]=key;
        }
    }
    public static void quickSort(int[] array,int left,int right){
        //优化,递归到小的子区间时,考虑使用插入排序
        if(right-left<16){
            insertSortQuick(array,left,right);
        }else{
            //如果排升序
            //找一个基准值将区间分割成两个部分
            int div=partition(array,left,right);

            //左侧部分比基准值小
            quickSort(array,left,div);

            //右侧部分比基准值大
            quickSort(array,div+1,right);
        }
    }

2.3.4 快速排序非递归

借助栈来实现

public static void quickSort(int[] array){
        Stack<Integer> s=new Stack<>();
        s.push(array.length);
        s.push(0);

        while(!s.empty()){
            int left=s.pop();
            int right=s.pop();
            if(right-left<47){
                insertSortQuick(array,left,right);
            }else{
                int div=partition(array,left,right);

                //先压入基准值右侧部分
                // [div+1,right)
                s.push(right);
                s.push(div+1);

                //后压入基准值左侧部分
                //[left,div)
                s.push(div);
                s.push(left);
            }
        }
    }

2.3.5 快速排序总结

  1. 快速排序整体的综合性能和使用场景都是比较好的,所以才叫快速排序
  2. 时间复杂度 O ( N ∗ l o g 2 N ) O(N*log_2N) O(Nlog2N)
    在这里插入图片描述
  3. 空间复杂度 O ( l o g 2 N ) O(log_2N) O(log2N)
  4. 稳定性:不稳定

2.4 归并排序

2.4.1 基本思想

归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有的子序列合并,得到完全有序的序列。即先使每个子序列有序,再使子序列间有序,若将两个有序表合并成一个有序表,称为二路归并。
代码实现

public static void mergeDate(int[] array,int left,int mid,int right,int[] temp){
        //归并
        int begin1=left;
        int end1=mid;
        int begin2=mid;
        int end2=right;

        int index=left;
        while(begin1<end1 && begin2<end2){
            if(array[begin1]<=array[begin2]){
                temp[index++]=array[begin1++];
            }else{
                temp[index++]=array[begin2++];
            }
        }

        while(begin1<end1){
            temp[index++]=array[begin1++];
        }

        while(begin2<end2){
            temp[index++]=array[begin2++];
        }
    }
public static void mergeSort(int[] array,int left,int right,int[] temp){
        if(right-left>1){
            //先对[left,right)区间中的元素进行均分
            int mid=left+((right-left)>>1);

            //[left,mid)
            mergeSort(array,left,mid,temp);
            //[mid,right)
            mergeSort(array,mid,right,temp);

            //进行归并
            mergeDate(array,left,mid,right,temp);
            System.arraycopy(temp,left,array,left,right-left);
        }
    }

    public static void mergeSort(int[] array){
        int[] temp=new int[array.length];
        mergeSort(array,0, array.length,temp);
    }

非递归实现

public static void mergeSortNor(int[] array){
        int size= array.length;
        int[] temp=new int[size];

        int gap=1;
        while(gap<size){
            for(int i=0; i<size; i+=gap*2){
                int left=i;
                int mid=left+gap;
                int right=mid+gap;
                if(mid>size){
                    mid=size;
                }
                if(right > size){
                    right = size;
                }

                mergeDate(array,left,mid,right,temp);
            }

            System.arraycopy(temp,0,array,0,size);

            gap<<=1;
        }
    }

2.4.2 归并排序总结

  1. 归并的缺点在于需要O(N)的时间复杂度
  2. 时间复杂度 O ( N ∗ l o g 2 N ) O(N*log_2N) O(Nlog2N)
  3. 空间复杂度:O(N)
  4. 稳定性稳定

2.4.3 海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序

  1. 先把文件切分成200份,每个512M
  2. 分别对512M排序,因为内存中已经可以放得下了,所以任意排序方式都可以
  3. 进行200路归并,同时对200份有序文件做归并过程,最终结果就有序了

2.5 非比较排序

2.5.1 计数排序

思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤

  1. 统计相同元素出现的次数
  2. 根据统计的结果将序列回收到原来的序列中

计数排序适用场景:
例子:
数据: 4 2 3 9 4 3 7 0 9 1 1 3 5 7 6 7 8 9 2 5 4 0
3. 先统计每个数字出现的次数,发现数据集中在0-9之间
用一个下标是0-9的数组来进行计数:
在这里插入图片描述

  1. 根据计数结果对数据进行回收,对于count数组:按照下标从小到大进行回收

代码实现

    //计数排序
    public static void countSort(int[] array){
        //找数据范围
        int maxValue=array[0];
        int minValue=array[0];
        for(int i=0; i<array.length;++i){
            if(array[i]>maxValue){
                maxValue=array[i];
            }

            if(array[i]<minValue){
                minValue=array[i];
            }
        }

        //计算计数空间的大小
        int range=maxValue-minValue+1;
        int[] count=new int[range];

        //1.统计array中每个数据出现的次数
        for(int i=0;i<array.length;++i){
            count[array[i]-minValue]++;
        }

        //2.根据统计结果回收
        int index=0;
        for(int i=0;i<range;++i){
            while(count[i]>0){
                array[index++]=i+minValue;
                count[i]--;
            }
        }

    }

2.5.2 计数排序的特性总结

  1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限
  2. 时间复杂度:O(max(N,范围))
  3. 空间复杂度:O(范围)
  4. 稳定性:稳定

3. 排序算法复杂度及稳定性分析

排序方法最好平均最坏空间复杂度稳定性
冒泡排序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*logN)O(N*logN)O(N*logN)O(1)不稳定
快速排序O(N*logN)O(N*logN)O(N^2)O(logN)~O(N)不稳定
归并排序O(N*logN)O(N*logN)O(N*logN)O(N)稳定
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值