排序算法全面解析!!

排序的基本概念

在本章学习中,我们默认为从小到大的排序,如果想要从大到小的排序,我们改变符号即可
(1)排序的稳定性
在这里插入图片描述
谁先在系统中先提交,谁就应该在排序中排在前面;像上图中的12,要达到稳定的排序就应该把红色的12放在前面
(2)内部排序:数据元素全都放在内存中的排序
(3)外部排序:就是当我们的数据元素非常多的时候,内存上放不下,我们就放在磁盘上,然后在磁盘上进行排序

常见的排序算法

在这里插入图片描述

插入排序

直接插入排序

【1】时间复杂度:
==最好情况:O(N)==数据完全有序的时候1,2,3,4,5;相当于i走而j不走
==最坏情况:O(N^2)==数据完全逆序的时候 5,4,3,2,1
【2】==空间复杂度:O(1)==因为它没有另外开辟空间
【3】稳定性:稳定的排序
PS:一个本身就是稳定的排序,是可以实现为不稳定的排序的;但是相反一个本身就是不稳定的排序,是不可能实现为稳定的排序

import java.util.Arrays;

public class Sort {
public static  void insertSort(int[] array){
    for (int i = 1; i < array.length ; i++) {
        int temp=array[i];//先拿出来,否则就会被覆盖掉
        int j=i-1;
        for (; j >=0 ; j--) {
           if(temp<array[j]){
               array[j+1]=array[j];
           }else{
               break;
           }
        }
        array[j+1]=temp;//跳出整个循环之后执行这一条代码
    }
}

    public static void main(String[] args) {
        Sort so=new Sort();
        int[] array={2,5,7,1,8,9};
        so.insertSort(array);
        System.out.println(Arrays.toString(array));//此处的array是上面的array重新排序之后输出的
    }
}

注意:break是跳出整个循环,而continue只是跳出当前循环,继续进行下一个循环
下面我们构建一个Test来测试顺序,倒序,随机顺序三种array的直接插入排序的耗时

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Random;

//我们分别给顺序,逆序还有无序的进行排序时间测试
public class Test {
    public static void orderArray(int[] array) {
        for (int i = 0; i < array.length; i++) {
           array[i]=i;
        }
    }
    public static void notOrderArray(int[] array){
        for (int i = 0; i <array.length ; i++) {
          array[i]= array.length-i;
        }
    }
    public static void notOrderArrayRandom(int[] array){
        Random random=new Random();
        for (int i = 0; i < array.length; i++) {
            array[i]=random.nextInt(10_0000);
        }
    }
    public static void testInsertSort(int[] array){
        int[] tmpArray= Arrays.copyOf(array,array.length);
        long startTime=System.currentTimeMillis();
        Sort.insertSort(tmpArray);
        long endTime=System.currentTimeMillis();
        System.out.println("插入排序的耗时:"+(endTime-startTime));
    }

    public static void main(String[] args) {
        int[] array=new int[10_0000];
        orderArray(array);//一个顺序的数组
        //notOrderArray(array);一个倒序的数组
        //notOrderArrayRandom(array);随机生成数组中元素的顺序
        testInsertSort(array);

    }
}

PS:为什么要在testInsertSort方法中进行数组的拷贝?
因为如果你不进行拷贝的话,那么你后面再测试倒序数组的耗时的时候,你用的还是顺序数组的已经排好顺序的数组,不能达到测试目的

希尔排序

希尔排序是对直接插入排序的优化,它采用跳跃式分组,可能会将更小的元素尽可能的往前放
【1】时间复杂度:n^1.3 ^- n^1.5
希尔排序的时间复杂度不是一个精确的值,我们目前只能得到一个范围
【2】空间复杂度:O(1)
【3】稳定性:不稳定排序

public static void shellSort(int[] array){
    int gap= array.length;//gap的值应该为数组长度
    while(gap>1){//因为当gap=1的时候我们就直接进行排序了
        gap/=2;
        shell(array,gap);
    }
    //shell(array,gap)为什么不用写这个呢,是因为我们把while循环里面走一遍
    // 我们就会发现在循环里面已经走了gap=1的代码
}
public static void shell(int[] array,int gap) {
    for (int i = gap; i < array.length; i++) {
        int temp = array[i];
        int j = i - gap;
        for (; j >= 0; j -= gap) {
            if (array[j] > temp) {
                array[j + gap] = array[j];
            } else {
                break;
            }
        }
        array[j + gap] = temp;
    }
}

我们希尔排序是直接插入排序的优化,所以我们在写代码的时候就把直接插入排序中+1或者-1的地方,都改成+gap或者-gap
希尔排序的测试

public  static void testShellSort(int[] array){
        int[] tmpArray=Arrays.copyOf(array,array.length);
        long startTime=System.currentTimeMillis();
        Sort.shellSort(array);
        long endTime=System.currentTimeMillis();
        System.out.println("希尔排序的耗时:"+(endTime-startTime));
    }

    public static void main(String[] args) {
        int[] array=new int[10_0000];
        //orderArray(array);//一个顺序的数组
        notOrderArray(array);//一个倒序的数组
        //notOrderArrayRandom(array);随机生成数组中元素的顺序
        testInsertSort(array);
        testShellSort(array);

    }

在这里插入图片描述
PS:希尔排序一般考的很少,但是也有概率会被考到

选择排序

选择排序

【1】时间复杂度:不管是最好还是最坏都是O(N)
【2】空间复杂度:O(1)
【3】稳定性:不稳定排序
以下这两种方法的时间复杂度是一样的,但是选择排序对于我们来说不太实用,所以不经常使用
选择排序代码1——方法1

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

选择排序代码2——方法2

public static void selectSort2(int[] array){
    int left=0;
    int right= array.length-1;
    while(left<right){
        int minIndex=left;
        int maxIndex=left;
        for (int i = left+1; i <=right ; i++) {
            if(array[i]<array[minIndex]){
                minIndex=i;
            }
            if(array[i]>array[maxIndex]){
                maxIndex=i;
            }
        }
        swap(array,left,minIndex);
        //最大值就在第一个的位置,最小值的left和minIndex已经交换了
        //所以最大值就被换走了,为了保证maxIndex一直指向最大值,所以让maxIndex也挪过去,即 maxIndex=minIndex;
        if(maxIndex==left){
            maxIndex=minIndex;
        }
        swap(array,maxIndex,right);
        left++;
        right--;
    }
    }

**题目练习:**使用选择排序对长度为100的数组进行排序,则比较的次数为4950
**解析:**如果有n个元素,则第一次比较次数: n - 1;第二次比较次数: n - 2…
第n - 1次比较次数: 1
所有如果n = 100,则比较次数的总和:99 + 98 + … + 1=4950次。

堆排序

【1】时间复杂度:O(N*logN)
【2】空间复杂度:O(1)
【3】稳定性:不稳定性
堆排序是我们目前说这几种排序方式中,时间复杂度最快的
代码展示:

 public static void createBigHeap(int[] array){
        for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
            shiftDown(array,parent,array.length);
        }
    }
    public 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[child] > array[parent]) {
            swap(array,child,parent);
            parent=child;
            child=parent*2+1;
        }else{
            break;
        }
    }
    }
    public static void heapSort(int[] array){
    createBigHeap(array);
    int end= array.length-1;
    while(end>0){//只要没有到根节点的时候,我们都可以进行循环
        swap(array,0,end);
        shiftDown(array,0,end);//为什么这块取end,是因为在shiftDown方法中(child<end)
        end--;
    }
    }

交换排序

冒泡排序

【1】时间复杂度:O(n^2),如果加了优化,最好情况为O(N)
【2】空间复杂度:O(1)
【3】稳定性:稳定

public static void bubbleSort(int[] array){
        for (int i = 0; i < array.length-1 ; i++) {
            boolean flg=false;
            for (int j =0; j < array.length-1-i ; j++) {
            if(array[j+1]<array[j]){
                swap(array,j,j+1);
                flg=true;
            }
            }
            if(!flg){//这样如果你在前几次就排好序了,后面就不用重复进行了
                return;
            }
        }
    }

快速排序

【1】时间复杂度:一般情况下你就答这个:最好情况:O(N*logN) 满二叉树或者完全二叉树
最坏情况:O(N^2) 单分支的树
【2】空间复杂度:==最好情况:O(logN) == 满二叉树或者完全二叉树,相当于树的高度
最坏情况:O(N) 单分支的树
【3】稳定性:不稳定
【4】快速排序算法是基于分治法的一个排序算法。

1.Hoare法快速排序

代码展示:

 public static void quickSort(int[] array){
    quick(array,0,array.length-1);
    }
    private static void quick(int[] array,int start,int end){
    if(start>=end) return;//左边一个结点或者一个结点也没有,说明已经到最后了
        int pivot=partition(array,start,end);//找到那个中间的那个,左边的都比它小,右边的都比它大
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
    private static int partition(int[] array,int left,int right){
    int key=array[left];
    int i=left;
    while(left<right){
        while(left<right&&array[right]>=key){
            right--;
        }
        while(left<right&&array[left]<=key){
            left++;
        }
        swap(array,left,right);
    }
    //当left和right相遇的时候,我们就结束 while循环,所以我们要将找到的这个key换到序列中间的位置
        swap(array,i,left);  // 换的是元素,不是下标
     return left;
    }

PS:Q1:为什么要先循环right,再循环left?
在这里插入图片描述
如图所示,要是我们要让left先循环,right后循环,那么到最后达pivot的时候,即left和right相遇的地方,我们退出循环,将6和9交换,但是我们就发现了问题,此时pivot=6,但是6的左边有了比它大的9,所以说在进行Hoare快排找pivot的时候,我们只能让right先走循环,然后left再走

Q2:为什么要在循环的时候对key取等号?
等号是一定要取的,因为要是不取等号的话,left和right的值要是等于key的话,它就会一直进不去循环,left和right的值一直改变不了

2. 挖坑法快速排序

我们一般使用快排的时候,一般使用挖坑法而不是Heare法

public static void quickSort2(int[] array){
        quick(array,0,array.length-1);
    }
    private static void quick2(int[] array,int start,int end){
        if(start>=end) return;//左边一个结点或者一个结点也没有,说明已经到最后了
        int pivot=partition2(array,start,end);//找到那个中间的那个,左边的都比它小,右边的都比它大
        quick(array,start,pivot-1);
        quick(array,pivot+1,end);
    }
    private static int partition2(int[] array,int left,int right) {
        int key = array[left];
        while (left < right) {
            while (left < right && array[right] >= key) {
                right--;
            }
            array[left] = array[right];
            while (left < right && array[left] <= key) {
                left++ ;
            }
            array[right] = array[left];
        }
        array[left] = key;
        return left;
    }
3.前后指针法(拓展)

一般不会考到,我们了解即可

public static void quickSort3(int[] array){
    quick3(array,0,array.length-1);
    }
    private static void quick3(int[] array,int start,int end){
    if(start>=end) return;
    int pivot=partition3(array,start,end);
    partition3(array,start,pivot-1);
    partition3(array,pivot+1,end);

    }
    private static int partition3(int[] array,int left,int right){
    int prev=left;
    int cur=left+1;
    while(cur<=right){
        if(array[cur]<array[left]&&array[++prev]!=array[cur]){
            swap(array,cur,prev);
        }
        cur++;
    }
    swap(array,prev,left);
    return prev;
    }

因为这三种方法它得到的基准都不一样,所以在选择题中考到相关题目,我们就需要一个一个去测试,我们建议的测试顺序是:1.挖坑法 2.Heare法 3.前后指针法

4.快速排序优化

1.用三数取中法获得key
2.在小范围内使用插入排序,减少递归的次数

public static void quickSort4(int[] array){
        quick3(array,0,array.length-1);
    }
    private static void quick4(int[] array,int start,int end){
        if(start>=end) return;
        //三数取中——可以降低空间复杂度
        int index=midOfTree(array,start,end);
        swap(array,index,start);//这样就可以保证start下标所指的元素是中间大的数
        int pivot=partition3(array,start,end);
        partition3(array,start,pivot-1);
        partition3(array,pivot+1,end);

    }
    public static int midOfTree(int[] array,int left,int right){
    int mid=(left+right)/2;
    if(array[left]<array[right]){
        if(array[mid]<array[left]){
            return left;
        }else if(array[mid]>array[right]){
            return  right;
        }else{
            return mid;
        }
    }else{
        if(array[mid]>array[left]){
            return left;
        }else if(array[mid]<array[right]){
            return  right;
        }else{
            return mid;
        }
    }
    }
    //在小范围里面我们就可以采用直接插入排序
    public static void insertSortRange(int[] array,int begin,int end){
        for (int i = begin+1; i <=end ; i++) {
            int temp=array[i];
            int j=i-1;
            for (; j>=0 ;j--) {
                if(array[j]>temp){
                    array[j+1]=array[j];
                }else{
                    break;//跳出该次循环
                }
            }
            array[j+1]=temp;
        }
    }
快速排序非递归

快排的非递归是在模拟递归的过程,所以时间复杂度并没有本质的变化,但是没有递归,可以减少栈空间的开销。栈和队列都可以实现

//快速排序非递归_使用一个栈来实现非递归
 private static int partition3(int[] array,int left,int right){
    int prev=left;
    int cur=left+1;
    while(cur<=right){
        if(array[cur]<array[left]&&array[++prev]!=array[cur]){
            swap(array,cur,prev);
        }
        cur++;
    }
    swap(array,prev,left);
    return prev;
    }
    public static void quickSortNor(int[] array){
        Stack<Integer> stack=new Stack<>();
        int left=0;
        int right=array.length-1;
        int pivot=partition3(array,left,right);
        if(pivot-1>left){//说明它左边还有元素
            stack.push(left);
            stack.push(pivot-1);
        }
        if(pivot+1<right){
            stack.push(pivot+1);
            stack.push(right);
        }
        while(!stack.isEmpty()){
            right=stack.pop();
            left=stack.pop();
            pivot=partition3(array,left,right);
            if(pivot-1>left){//说明它左边还有元素
                stack.push(left);
                stack.push(pivot-1);
            }
            if(pivot+1<right){
                stack.push(pivot+1);
                stack.push(right);
            }
        }
    }

归并排序

【1】时间复杂度:O(N*logN)
【2】空间复杂度:O(N)
【3】稳定性:稳定
归并排序的缺点是空间复杂度大,始终是O(n)

归并排序的普通实现(递归实现)

 //递归实现
    public static void mergeSortFunc(int[] array, int left,int right){
    int mid=(left+right)/2;
    if(left>=right) return;
    mergeSortFunc(array,0,mid);
    mergeSortFunc(array,mid+1,right);
    //分解之后进行合并
        merge(array,left,right,mid);
    }
    public static void merge(int[] array,int left,int right,int mid){
    int s1=left;
    int s2=mid+1;
    int[] tmpArr=new int[right-left+1];
    int k=0;
    //确保两个区间都有数据
    while(s1<=mid&&s2<=right){
        if(array[s2]<=array[s1]){
            tmpArr[k]=array[s2];
            k++;
            s2++;
        }else{
            tmpArr[k]=array[s1];
            k++;
            s1++;
        }
    }
    //如果有一个区间的s走完了,但是另一个区间的还没走完,我们需要进行新一轮的循环来走其中的数据
        while(s1<=mid){
            tmpArr[k]=array[s1];
            k++;
            s1++;
        }
        while(s2<=right){
            tmpArr[k]=array[s2];
            k++;
            s2++;
        }
        //把所有元素按从小到大的顺序走完以后,我们要进行合并了,我们利用一个新的数组中完成合并
        for (int i = 0; i <tmpArr.length ; i++) {
            array[i+left]=tmpArr[i];
        }
    }

递归排序非递归实现

public static void mergeSortNor(int[] array){
    int gap=1;
    while(gap<array.length){
        for (int i = 0; i <array.length ; i+=2*gap) {
            int left=i;
            int mid=left+gap-1;
            int right=mid+gap;
            //为了防止mid和right越界
            if(mid>= array.length){
               mid=array.length-1;
            }
            if(right>=array.length-1){
               right=array.length-1;
            }
            merge2(array,left,right,mid);
        }
        gap*=2;
    }
    }
    public static int[] merge2(int[] array,int left,int right,int mid){
        int s1=left;
        int s2=mid+1;
        int[] tmpArr=new int[right-left+1];
        int k=0;
        //确保两个区间都有数据
        while(s1<=mid&&s2<=right){
            if(array[s2]<=array[s1]){
                tmpArr[k]=array[s2];
                k++;
                s2++;
            }else{
                tmpArr[k]=array[s1];
                k++;
                s1++;
            }
        }
        while(s1<=mid){
            tmpArr[k]=array[s1];
            k++;
            s1++;
        }
        while(s2<=right){
            tmpArr[k]=array[s2];
            k++;
            s2++;
        }
        for (int i = 0; i <tmpArr.length ; i++) {
            array[i+left]=tmpArr[i];
        }
        return array;
    }

海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序
前提:内存只有1G,需要排序的数据有100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1.先把文件切分成200份,每个512M
2.分别对512M排序,因为内存已经可以放的下,所以任意排序方式都可以
3.进行2路归并,同时对200份有序文件做归并过程,最后就有序了(一轮2路合并结束之后,再次开始新的一伦2路合并,直至结束为止)

计数排序(了解)

【1】时间复杂度:O(N+范围)
【2】空间复杂度:O(范围)
【3】稳定性:稳定
计数排序的时间复杂度和空间复杂度与你给定的范围有关

//计数排序
    public static void coutSort(int[] array){
    int minVal=array[0];//是元素值
    int maxVal=array[0];
    //1.我们先找出这组数中的最大值和最小值
        for (int i = 0; i < array.length-1 ; i++) {
            if(array[i]<minVal){
                minVal=array[i];
            }
            if(array[i]>maxVal){
                array[i]=maxVal;
            }
        }
        //2.创建一个新的计数数组,来存放数据
    int[] count=new int[maxVal-minVal+1];
        //3.遍历原来的数组,利用这个数组来计数,看每个元素出现了几次
        for (int i = 0; i < array.length-1 ; i++) {
            count[array[i]-minVal]++;
        }
        //4.遍历count数组,将元素写回array数组中
        int index=0;
        for (int i = 0; i < count.length-1 ; i++) {
            while(count[i]>0){
                array[index]=minVal+i;
                index++;
                count[i]--;
            }
        }
    }

几种排序算法的知识点汇总

1.占用辅助空间:(即空间复杂度)
a>归并排序:N;
b>快排 最好情况:logN 最坏情况:N
c>希尔为1;
d>堆排为1;
e>直接插入排序:1;
f>选择排序:1
g>冒泡排序:1
2.时间复杂度:
a>归并排序:nlogn;
b>快排:最好情况:O(N*logN) ;最坏情况:O(N^2)
c>希尔为n^1.3 ^- n^1.5
d>堆排为nlogn;
e>直接插入排序:最好情况:O(N);最坏情况:O(N^2)
f>选择排序:不管是最好还是最坏都是O(N)
g>冒泡排序:O(n^2),如果加了优化,最好情况为O(N)
3.最坏时间复杂度不为O(N^2)
a>堆排序最坏时间复杂度为nlogn;
b>快排如果每次划分只有一半区间,则时间复杂度为n^2;
c>选择排序:时间复杂度始终为n^2;
d>插入排序:如果序列逆序,每次都需要移动元素,时间复杂度n^2;
4.使用场景
a>快排:初始排序影响较大,有序是性能最差;
b>插入:接近有序,性能最好;
c>希尔:希尔是对插入排序的优化,这种优化在无序序列中才有明显的效果,如果序列接近有序,反而是插入最优
d>堆排序,归并排序,选择排序这些对队列初始顺序不敏感,所以对其算法的性能无影响
5.各个算法的稳定程度:
a>直接插入一般可以从前向后进行元素的插入,相同元素的相对位置可以不发生变化
b>归并也可以保证相对位置不变
c>冒泡排序在元素相同的情况下也可以不进行交互,也可以保证稳定
d>选择排序的思想是每次选出最值,放在已排序序列的末尾,如果最值有多个,而选出的为最后一个最值,会导致相对位置发生变化。当然选择排序也可以变成稳定的,只要 保证相同的值选择第一个就可以
**6.目前所学的稳定的排序:**插入排序,冒泡排序,归并排序
7.排序方法中,每一趟排序结束时都至少能够确定一个元素最终位置的方法是① 选择排序② 快速排序③堆排序

  • 21
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值