一篇文章带你搞懂七大排序

七大排序

这七大排序是基于比较的排序!
数据结构中的重中之重啊,来自可爱老师的句句忠告
别记忆代码了好吗? 算法重要的是思想啊
有了思想,有可能你每次写出来的代码都不一样
看不懂代码,你就上手debug 啊,画图啊,傻傻的盯着代码有啥用!

插入排序

插入排序

朋友们,扑克牌都玩过吧,手上拿着一堆扑克牌,是不是得把牌排整齐,对子对子放一起,顺子顺子放一起。每次盲摸一张牌,往自己已经整理好的手牌中放的过程就是一次插入排序哈。

思想简述

在这里插入图片描述

算法思路整理

在这里插入图片描述

代码实现

public static  void insertSort(int[] arr){
        //数据一共有arr.length 个
        //所以子操作需要执行的次数 arr.length-1
        for(int i = 0;i < arr.length -1;i++){
            //有序区间[0 - i]
            //无序区间[i+1 - arr.length)
            //每次拿出来的数字是无序区间的第一个 [i+1]
            int key = arr[i+1];
            //依次在有序区间进行比较--采用从后往前的比较方式
            //之所以这么做是有原因的
            int j = 0;
            for(j = i;j >= 0;j--){
                //[j] 就是需要和key比较的数字
                //key  arr[j]
                //比较可能出现三种情况 大于 等于 小于
                if(key < arr[j]) {
                    arr[j+1] = arr[j];
                }else {
                    break;
                }
            }
            arr[j+1] = key;
        }
    }

时间和空间复杂的分析

在这里插入图片描述
最好情况的时候,数据完全有序,,每次只用拿出无序区间的数字即可,不用进行不断的向前搜索。
最坏情况,每一个无序的数据都需要向前搜索
初始数据越有接近有序,时间效率越高

希尔排序(选择排序的升级版)

简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,
比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。
而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,
然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序
小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动

思想简述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

算法思路整理

显然希尔排序比起插入排序的优越性在于,它加上了带分组的排序。(一般这个分组 用gap = arr.length / 2 或者还有 gap = ( gap / 3 ) - 1 本博客采用前者哈 )使得他有了跳跃性!
具体代码的大致思路就是
要有一个实现带有 gap 功能方法的插入排序,而由于gap 是不断减小的 该方法需要不断的被调用,直到 gap = = 1 ( 即为所有的数据为一组)。

画图详解一下带gap 的插入排序

在这里插入图片描述

代码实现

public class ShellSort {
 	public static void shellSort(long[] arr) {
        int gap = arr.length / 2;
        while (gap >= 1) {
            insertSortWithGap(arr,gap);
            gap = gap/2;
        }
    }

    public static void insertSortWithGap(long[] arr,int gap) {
        //i 表示从gap 位置开始
        //通过外层循环可以把数据分成gap 组
        for(int i = gap;i < arr.length;i++) {
            //和插排的道理一样先把要插排的拿出来
            //一个个的和组里面的元素比较
            long key = arr[i];
            //内层循环j
            int j = 0;
            //选出应该和 下标i 一组的数据进插入排序
            //for(j = i - gap ;j >= 0;j--) {---错的啊  //注意这里是跳到下一个gap 的地方
            for(j = i - gap ;j >= 0;j = j -gap) {
                if(key < arr[j]) {
                    arr[j +gap] = arr[j];
                }else {
                    break;
                }
            }
            //退出的时候 一种是因为j 越界了
            //还有一种是当前j的位置 小于等于 key ,那么就把key相当于再次填回去
            arr[j+gap] = key;
        }
    }

   
}

时间和空间复杂的分析

在这里插入图片描述
关于稳定性:分组的时候很难保证相同的数被分到同一个组里面,那么自然很难保证有序

选择排序

直接选择排序

就是主要通过选择来完成的排序,每一趟从待排数据中选择一个最大或者最小的数据,将其放到序列的起始,直到全部数据排完。

思想简述

在这里插入图片描述

算法思路整理

在这里插入图片描述

代码实现

public static void selectSort(int[] arr){
        //无序区间[0 - arr.length-i)
        //有序区间[arr.length -i,arr.length)

        //多少次选择的过程
        for(int i = 0;i < arr.length-1 ;i++) {
            int maxIndex = 0;
            int j = 0;
            //每一次都要在序区区间里面找到最大的,然后和无序区间的最后一个交换
            for (j =0; j < arr.length - i; j++) {
                if (arr[j] > arr[maxIndex]) {
                    maxIndex = j;
                }
            }
            //找到了无序区间里面最大执行交换的逻辑
            int temp = arr[maxIndex];
            arr[maxIndex] = arr[j-1];
            arr[j-1] = temp;
        }
    }

时间和空间复杂的分析

在这里插入图片描述
直接选择排序的最好和最坏都是一样的,不管数据有序无序,都要对数据进行一次遍历,搜索最大值的过程。只是说数据如果无序就不再需要执行交换的逻辑。
在这里插入图片描述

双向选择排序

上面的直接选择排序是每遍历一次数据,只选出最大或者最小的元素,而双向则是一次遍历,同时把无序区间里面最大和最小都确定

思想简述

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

算法思路整理

和直接选择排序相比,我们的无序区间的上下界的范围都发生了变化,而直接选择排序是只有上界或者下届发生变化的。

  • 无序区间的范围无序区间 [i , arr.lenght -i)
  • 注意下标冲突!!
    发现数据根本没有变化,为什么会产生这样的原因,本质上就是因为max 和 min 所指向的位置刚好是无序区间的头和尾。导致数据发生紊乱,下标冲突。

所以我们在写代码的时候一定要加上一个判断,看看当前max 或者min 所指向的位置是不是头 或者 尾,如果是,那么对应的max 和 min 的下标是需要变化的。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码实现

public static void selectSort2(int[] arr){
		//外层循环表示需要选择的次数
        for(int i = 0;i < (arr.length)/2 ;i++) {
            int maxIndex = i;
            int minIndex = i;
            int j = 0;
            //每一次都要在序区区间里面找到最大的,然后和无序区间的最后一个交换
            //最小的和无序区间里面的第一个执行交换的逻辑
            //所以无序区间的左右两边一定要是变化动态的。
            for (j =i; j < arr.length - i; j++) {
                if (arr[j] > arr[maxIndex]) {
                    maxIndex = j;
                }
                if(arr[j] < arr[minIndex]){
                    minIndex = j;
                }
            }
           swap(arr,maxIndex,j-1);
            //出现下标冲突的问题
            if(minIndex == j-1){
                minIndex = maxIndex;
            }
            swap(arr,minIndex,i);
        }
    }

时间和空间复杂度分析

双向选择排序
时间复杂度:
平均情况:O(n^2)
最好情况O(n^2)
最坏情况O(n^2)
空间复杂度:O(1)
虽然时间复杂度还是O(n^2),但是实际上算法的效率优化了将近40-50%

堆排序

思想简述

举例:对数据进行建大堆操作

  • 和直接排序相比较,优化的地方就是利用堆的特性可以使得找出最值的过程简化所以每一次就可以确定无序数组的第一个是最大值,这样查找最大值的时间复杂度降低

为什么建立的是大堆?
可以建小堆排升序吗? – 不可以

可以利用小堆排倒序,但是做不到排升序
堆顶就是最小的,之后要在剩余的数字中找最小的,把谁当作堆顶?堆的结构都被破坏掉了

算法思路整理

1.对数组进行建大堆的操作
2.由于建立的是一个大堆,那么堆的第一个值是最大的,所以将第一个值和无序数组的最后一个进行交换,然后对这个数字向下调整
3.之后继续和无序数组的最后一个交换,然后继续向下调整堆!(注意这里堆的个数要变了!也就是为什么要把size 作为一个参数的原因,)
4.就是一个不断的进行交换和向下调整的过程

每一次操作包括一次交换和向下调整 使得无序区间一个最大的归位

一个二刷排序的补充:
不是说只有建立一个小根堆才可以用向下调整。
向下调整只是一种思想,并不是说 向下调整只可以 用来建立小堆
它的使用前提是 除了 当前要调整结点的和它的左右孩子,其他位置均满足 堆的性质即可!所以自然可以用来找最大值!

其次如何建立堆,那就是从第一个非叶子结点开始进行向下调整! 直到调整到根节点 是一个循环
其次建立堆的时候,为什么要找第一个非叶子结点,是因为要使用向下调整!
反过来,如果要向使用下调整,就要先找到第一个非叶子结点!

代码实现

public static void heapSort(int[] arr) {
        //1.对数组进行建立大堆的操作
        createHeap(arr, arr.length);

        //2.进行选择的过程,一共需要arr.length-1 组
        for (int i = 0; i < arr.length - 1; i++) {

            //无序区间[ 0 - arr.length-i-1]
            //先交换
            swap(arr, 0, arr.length - 1 - i);
            //再向下调整
            //无序区间[ 0 - arr.length-i-2]
            //传入的是无序区间的个数以及需要向下调整的位置
            //为什么需要知道数组的长度,我一路向下调整,需要到什么时候结束呢
            //就是知道需要调整的位置是叶子结点就说明结束了
            //所以需要知道数组的长度,来判断当前是不是叶子结点
            adjustDown(arr, arr.length - i - 1, 0);
        }
    }

    /**
     * 建堆操作
     * 找到第一个非叶子结点,然后对其进行向下调整
     * 一次从后往前遍历
     * @param arr
     * @param length
     */
    private static void createHeap(int[] arr, int length) {
        for(int i = (length-2)/2;i >= 0;i--){
            adjustDown(arr,length,i);
        }
        /*//如何确定第一个非叶子结点 -- 那就是最后一个结点的父亲
        int theLastIndex = length-1;
        int theLastParentIndex = (theLastIndex-1)/2;
        for(int i = theLastParentIndex;i >= 0;i--){
            adjustDown(arr,length,i);
        }*/

    }

    /**
     * 递归版本
     * 循环版本
     * @param arr
     * @param length
     * @param index
     */
    private static void adjustDown(int[] arr, int length, int index) {
        /**
         * 循环版本,同时进行简化
         */
        //这里判断是不是叶子结点
        while (2 *index+1 < length) { //进来代表一定有叶子结点
            int maxIndex = 2*index+1;//那么就假定最大的是左孩子

            //找到左右子树的最大的 判断是不是需要和index交换
            int rightIndex = maxIndex+1;

            if(rightIndex < length && arr[rightIndex] > arr[maxIndex]){
                maxIndex++;
            }
            //孩子中最大的没有index位置的大
            if(arr[index] > arr[maxIndex]){
                break;
            }
            //执行交换的逻辑
            swap(arr,index,maxIndex);
            index = maxIndex;
        }

        /**
         * 递归版本
         */
        /*//找到左右子树的最大的和当前的index位置交换
        int rightIndex = leftIndex+1;
        int maxIndex = leftIndex;

        if(rightIndex < length && arr[rightIndex] > arr[index]){
            maxIndex = rightIndex;
        }

        if(arr[index] > arr[maxIndex]){
            return;
        }
        //执行交换的逻辑
        swap(arr,index,maxIndex);
        index = maxIndex;
        //继续进行向下调整
        adjustDown(arr,arr.length,index);*/
    }

时间和空间复杂的分析

建堆的时间复杂度+向下调整的时间复杂度
n*log(n) + n *log(n) = 2n *log(n)
最终的时间复杂的 n *log(n),最好最坏平均都一样
空间复杂度o(1)
不具备稳定性,向下调整的过程中是无法保证的。

交换排序

冒泡排序

思想简述

它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序”。
图形描述
在这里插入图片描述

算法思路整理

根据上图我们不难发现,如果要对七个数字进行冒泡排序,实则冒泡的过程是6 次,在每一次冒泡中会产生两两比较交换,那么比较的次数又该如何计算呢?

按照升序排序的算法,数组刚开始的有序区间是 [arr.length-i ~arr.length)
可以这样理解 即为当 i=0 我准备第一次冒泡的时候,有序区间里面一个元素也没有
那么无序区间自然是 [0 ~arr,length - i )
对于升序排序的冒泡就是每一次在无序区间中找到最大的元素 使其沉到最底
每进行一次冒泡,一个最大的元素归位置

一定要在这里很认真的强调,找到无序区间和有序区间的位置很重要
比如冒泡的无序区间不好一下子想出来的时候,从有序区间先开始想,然后反推有序区间会很容易

现在我们知道了冒泡过程执行的次数,以及每一次冒泡需要比较的次数。
在这里插入图片描述

代码实现

  public static void bubbleSort(long[] arr) {
        //--冒泡过程自始至终不涉及对有序区间的操作
       //外层循环表示需要冒泡的过程
        for(int i = 0 ;i < arr.length-1;i++) {
 		    //内层循环表示需要比较的次数
            //无序区间
            // [0 - arr.length-i) 
            // 为什么是arr.length-i -1
            // 是因为两两比较数组下标的界限的前一个
            //有序区间  【arr.length-i - arr.length)
            for(int j = 0 ; j < arr.length-i -1;j++) {
                if(arr[j] > arr[j+1]) {
                    long temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                } 
                   
             }//内层for 结束
             
        }//外层for结束
        
    }
   

无序区间的下标前面慢慢说过是 [0 ~arr,length - i )转成闭区间应该是 [0 ~arr,length - i -1]
为什么这里内层for 循环变成了[0 ~ arr.length-i -1) 是开区间呢?
在这里要注意
前一个和后一个比较要保证数组下标不越界

优化算法

假设我们现在排序ar[]={1,2,3,4,5,6,7,8,10,9}这组数据,
按照上面的排序方式,第一趟排序后将10和9交换已经有序,接下来的8趟排序就是多余的,什么也没做。
所以我们可以在交换的地方加一个标记,如果某一趟排序没有交换元素,说明这组数据已经有序,不用再继续下一次排序

public static void bubbleSort(long[] arr) {
        //--冒泡过程自始至终不涉及对有序区间的操作
        //外层循环表示需要冒泡的过程
        /**
         * 用来表示当前第 i 趟是否有交换,
         * 如果有则要进行 i+1 趟,如果没有,
         * 则说明当前数组已经完成排序。实现代码如下:
         */
        for(int i = 0 ;i < arr.length-1;i++) {
            boolean isSorted = true;//优化,一开始假设是有序的
            for(int j = 0 ; j < arr.length-i -1;j++) {
                if(arr[j] > arr[j+1]) {
                    long temp = arr[j];
                    arr[j] = arr[j+1];
                    arr[j+1] = temp;
                    isSorted = false; //交换过就说明无序
                }
            //完整的一次冒泡走完了  却发现没有交换一次那就可以说明有序了
            //那么下一次就不用再走外层的冒泡过程了
            if(isSorted) {
                break;
            }
        }
    }

时间复杂度空间复杂度分析

在这里插入图片描述
采用优化版,优化版最好情况就是无序区间进行一次冒泡,冒泡的过程完整走一次发现没有比较,那么就退出。
最坏情况就是每一次冒泡都有交换

快速排序

思想简述

快速排序
1.选择数据中的一个数字作为基准
2.然后将无序数据进行partition,分为比基准大 和比基准小的 左右两个区间
3.分别对左右两个区间进行处理
4.直到小区间已经有序的时候就不需要处理 ( 小区间有序 就是区间内的元素个数等于0 或者等于1)

算法思路整理

本质就是不断做partition 然后分区间,然后再对区间partition, 再划分。
递归的结束条件就是区间没有或者只有一个元素。
区间划分:采用全闭的区间[lowIndex ,highIndex-1] [highIndex,arr.length-1]
当选择最左边的数字作为基准的时候,应该先从右边走,否则会出现一些特殊情况。

代码实现

public static void quickSort(int[] arr){
        //第一次代表需要对所有区间进行排序
        quickSortInternal(arr,0,arr.length-1);
 }
    private static void quickSortInternal(int[] arr, int lowIndex, int highIndex) {

        int size = highIndex- lowIndex +1;
        //递归结束的条件
        if(size <= 1) return;

        //遍历进行partition  返回keyIndex 经过partition之后最终所在的下标
        int keyIndex = partition挖坑(arr,lowIndex,highIndex);
        //分别对左右区间进行相同的处理过程-- 递归方法
        quickSortInternal(arr,lowIndex,keyIndex-1);
        quickSortInternal(arr,keyIndex+1,highIndex);
    }

partition 的三种方式:最常用的最挖坑法,也是必须掌握的!

/**
     * 第一种方法 hover方法
     * 特殊情况分析就是当你的数字选择在最左边的时候,你需要让最右边的先走
     * 最后跳出循环的位置 和key交换就可以了
     */
    private static int partitionHover(int [] arr, int lowIndex, int highIndex) {
        //选择最左边的一个数字
        int key = arr[lowIndex];
        int leftIndex = lowIndex;
        int rightIndex = highIndex;
        //相等的时候就不用继续比较了
        while (leftIndex < rightIndex){
            //选择最左边要从右边先走
            while (leftIndex < rightIndex && arr[rightIndex] >= key){
                rightIndex--;
            }
            //说明遇到了比key小的
            while (leftIndex < rightIndex && arr[leftIndex] <= key){ //一定要写成小于等于 刚开始的指向是key本身,如果不写左边就没有办法走都无法往后面走
                leftIndex++;
            }
            //说明遇到了比key大的
            //执行交换逻辑
            //为什么这里不用写等号的原因就是 我上面的条件是<
            //所以就算是因为不满足 leftIndex < rightIndex 而结束循环
            //交换也只不过是原地交换而已
            swap(arr,leftIndex,rightIndex);
            /*if(leftIndex <= rightIndex){
                //执行交换的流程
             swap(arr,leftIndex,rightIndex);
            }*/
        }
        swap(arr,lowIndex,leftIndex);
        return leftIndex;
    }

    /**
     * 第二种方法:挖坑方法
     * 不在是分别找到两个进行交换,而是把一个下标所指向的位置当作一个坑
     * 不会产生重复交换的--效率高!
     */
    private static int partition挖坑(int [] arr, int lowIndex, int highIndex) {
        int key = arr[lowIndex];
        int leftIndex = lowIndex;
        int rightIndex = highIndex;
        while (leftIndex < rightIndex){
            while (leftIndex < rightIndex && arr[rightIndex] >= key){
                rightIndex--;
            }
            //找到了之后,填入坑里面 -- 然后rightIndex就可以当作是一个坑
            arr[leftIndex] = arr[rightIndex];
            while (leftIndex < rightIndex && arr[leftIndex] <= key){
                leftIndex++;
            }
            arr[rightIndex] = arr[leftIndex];
        }
        //最后填入key leftIndex == rightIndex
        arr[leftIndex] = key;
        return leftIndex;
    }
    /**
     * 第三种方法
     * 快慢指针
     * 快指针:负责遍历整个数据
     * 慢指针:保证慢指针左侧的全部数据都是小于等于key
     * 当快指针遇到了比key小的,就和慢指针交换
     */
    private static int partition快慢(int [] arr, int lowIndex, int highIndex) {
        int key = arr[lowIndex];
        int seperate = lowIndex+1;//保证low左侧的都是小于等于key
        for(int fast = lowIndex+1;fast <= highIndex;fast++){
            //当前位置小于key 就执行交换的逻辑
            if(arr[fast] <= key){ // 加上等号可以确保和key一样的放在前半段区间
                swap(arr,seperate,fast);
                seperate++;
            } //如果比key大什么也不做,继续往后遍历就好了
        }
        swap(arr,lowIndex,seperate-1); //最后让key和分割指针的前一个交换即可
        return seperate-1;
    }

时间复杂度空间复杂度分析

时间复杂度 :每一层partition 的时间复杂度是o(n),一共又多少层,看树的高度。
一般高度是o(log n) 最坏是 o(n)
时间复杂度:
最好/平均: o(n *log n)
最坏:o(n ^2)
在这里插入图片描述空间复杂度
最好/平均: o(log n)
最坏:o(n )
在这里插入图片描述
稳定性:不具备稳定性
比如数据: 5933

关于快排一些说明

1.数量比较小的情况,快排并不是最快,数据规模低于某一个值使用插排速度最快。
在java Arrays.sort 中,数据规模小使用的是插入排序,数据规模大,使用的是快速排序。

但是很明显看到一个问题就是数据有序的时候,反而时间复杂度最低,出现问题的原因在于我们选择的特殊数字的问题。
2.优化选择特殊数的方式 – 目前的方式选择的是最左边

  • 随机选
  • 挑几个数字,选大小为中间值(三数取中)

归并排序

归并排序

采用二路归并排序

思想简述

分治法的思想:把原问题拆解成若干个与原问题结构相同但规模更小的子问题,待子问题解决以后,原问题就得以解决。
“归并排序”和“快速排序”都是分治法思想的应用
其中“归并排序”先无脑地“分”,在“合”的时候就麻烦一些;“快速排序”开始在 partition 上花了很多时间,即在“分”上使了很多劲,然后就递归处理下去就好了,没有在“合”上再花时间。

算法思路整理

分而治之的思想
向下分治 然后向上整合

  1. 把数组平均分成两份,分别对左右两个区间进行相同处理(归并排序),直到区间的个 数(size =0 /1)
  2. .合并左右两个有序数组–(通过额外的数组辅助)
    还是涉及到把数组划分成区间,使用下标的方式,采用半开半闭的区间

代码实现

public static void mergeSort(int[] arr){
        mergeSortInternal(arr,0,arr.length);
    }

    private static void mergeSortInternal(int[] arr, int lowIndex, int highIndex) {
        int size = highIndex-lowIndex;
        if(size <= 1)return;
        int middleIndex = (lowIndex+highIndex)/2;
        //左区间 [0 - middleIndex)
        //右区间 [middleIndex - highIndex)
        mergeSortInternal(arr,lowIndex,middleIndex);;
        mergeSortInternal(arr,middleIndex,highIndex);

        //左右两个区间都有序了,开始进行合并
        merge(arr,lowIndex,middleIndex,highIndex);
    }

    //合并两个有序的数组
    //需要一个额外的数组作为辅助的空间
    private static void merge(int[] arr, int lowIndex, int middleIndex, int highIndex) {
        int size = highIndex-lowIndex;
        int[] extra = new int[size];
        int leftIndex = lowIndex;
        int rightIndex = middleIndex;
        int extraIndex = 0;
        while (leftIndex < middleIndex && rightIndex < highIndex){
            if(arr[leftIndex] <= arr[rightIndex]){ //保证稳定性
                extra[extraIndex++] = arr[leftIndex];
                leftIndex++;
            }
            if(arr[leftIndex] > arr[rightIndex]){
                extra[extraIndex++] = arr[rightIndex];
                rightIndex++;
            }
        }
        //谁没有走完就全部搬进来
        while (leftIndex < middleIndex){
            extra[extraIndex++] = arr[leftIndex];
            leftIndex++;
        }
        while (rightIndex < highIndex){
            extra[extraIndex++] = arr[rightIndex];
            rightIndex++;
        }
        //最后把数据再搬回去
        for(int i =0 ; i< size;i++){
            arr[i+lowIndex] = extra[i];
        }
        //源数组 - 目的数组
        //System.arraycopy(extra,0, arr,lowIndex,size);
    }

时间复杂度空间复杂度分析

  • 时间复杂度: o(n log n)
    划分的时间复杂度 o(log n) + 合并的时间复杂度 o(n)
    没有最好最坏 都是 o(n log n)
  • 空间复杂度:o(n)
    每一层都需要额外的数组 o(n) + 额外的调用栈 o (log n)
    但实际上是 取系数大的 o(n)
  • 稳定性: 具有稳定性 左边一个数,右边一个数字,当两个数字相等的时候,确定是对左边的数字进行选择

表格总结时间复杂度空间复杂度和稳定性

在这里插入图片描述

一些小总结

  • 保证稳定性的排序:插入排序、冒泡排序和归并排序(插排和冒泡两个排序,上冒泡可以做到的,插排都可以,而且当数据比较接近有序的时候,插排做的更好。)
  • 空间复杂度不是o(1) 的排序 只有 快排和归并 ,快排是因为不断的递归保存栈帧,归并是不断的递归+额外的数组空间(最后递归的栈帧的空间忽略不计啊)
  • 堆排序和归并排序属于波澜不惊的选手,不管怎么样都是o(log n)。 选择排序也是一样差的波澜不惊,永远是o(n^2)。
  • 最好情况可以达到o(n) 的是插排和冒泡,希尔是需要特意的构建,这里不重点提他。
  • 如果数据完全逆序:希尔或者堆排
  • 如果数据大概率有序:插入排序和冒泡排序
  • 对数据是否有序不清楚:希尔或者堆排 插排(希尔)本质上是在遍历有序区间,直接选择,堆排和冒泡本质上都是在遍历无序区间。
  • 区间比较小的排序:使用插入排序
  • 这七大排序里面 必须掌握的 快排 归并 插排和冒泡必须十分清楚。

  • 前五个其实本质上是一种减治算法,相当于一次处理一个数据,大循环套小循环,代码结构很像。而后两种其实是分治算法,都是大问题化解小问题不断递归的过程。

JDK中提供的排序

1.如果对数组进行排序,可以使用Arrays类下提供的sort方法
Arrays.sort 中提供了很多的重载的排序方法。
在这里插入图片描述

基本类型排序用的是快排,对象排序(Object)用的是归并,如果对小数据排序用的是插入排序。
2. 如果对list 进行排序,可以使用list本身的sort方法(基本数据类型也需要传入比较器),或者Collections.sort 方法(只有自定义对象需要传入比较器)。
3. 如果对对象进行排序,而不是基本类型,需要考虑使用Comparable或者Comparator

海量数据的排序

使用的算法也是归并排序,只不过是多路归并

上面的排序是内部排序,即为所有的数据都是在内存中存储的。 而海量数据内存存储不下,所以需要对算法进行改进。

海量数据的特点-内存中存储不下,必须借助硬盘。
具体步骤

  1. 先将数据平分为n份(每份的大小比较小)
  2. 由于每一份数据比较少,就可以用内存分别对每份数据进行排序(每份的大小比较小),可以将任务分配到多个计算机共同参与排序
  3. 至此得到了n个分别有序的数据文件–放在硬盘里面
  4. 借助内存,进行n个有序数据文件的合并
    • 将每份数据文件中最小的数字放入内存(实际可以不止放入1个)
    • 将最小的数字选出来,尾插到最后的有序文件中
      参考回答

扩展十大排序

在这里插入图片描述

上面的七大排序都是基于比较的排序,也就是说凡具有比较能力的对象都可以使用这七大排序。

计数排序

计数排序的时间复杂度是o(n)
只需要遍历一遍数据
在这里插入图片描述

动图演示

排序的动图演示可以看看这里

一些其他

碎碎念:以下部分和排序毫无关系,可以跳过不看。

其实网上整理排序算法的博客文章非常多,那自己为什么还要整理呢?整理不是为了一定要让自己的博客写的是最棒的,是帮助自己梳理知识逻辑。也许,今天写完这个博客,再过一段时间问我某些排序的问题,我还是会忘记,但这些博客至少证明这些思路,这些原理我曾经想通,想懂过,那么只是我遗忘的问题。 前段时间提起最近在学数据结构,倒也不是数据结构学的让人痛苦,只是好像学习走到了一个爬坡的地方了,每一天都觉得有很多事情要做,有很多思路要整理,有很多oj 要写,可是真的写不完,还有很多的peer presure。 真的感觉自己不太会把握时间。
前段时间,路过一个考研人的桌子,书上有句标语,至今记忆犹新,“请享受现在无法回避的痛苦,然后继续升级打怪兽”。乍看这句话,也是略有矫情,仔细想来,何尝不是这样。朋友也提起自己起初走上这条路也不过是看中了互联网的红利,可是到后来,他开始感受到了思维逻辑的趣味,代码也没有那么无聊,尤其是当你真的看到一点点产出,真的会很兴奋。虽然说的这种乐趣,我暂时体会的还没有那么深刻,但是能自己跑过一道oj题,也确实够我满足一天。虽然紧接着还是会焦头烂额,倒也无所畏惧。
去年10 24 程序员日看到的朋友圈的句子“每一行代码,都是改变世界的力量”。当初那个信誓旦旦说自己以后绝不学计算机,搞代码的小孩也不知去了哪里… 还是很久之前告诫自己的话,提高自己的核心竞争力,没有永远的红利行业,唯有不断学习,提升自己才是正道,更何况是在每年交替换新的互联网行业,没有人永远年轻,但永远有人正年轻。跟不上节奏,被淘汰也是自然而然。只是可能这个行业更加的明显而已。每天看一遍自己的置顶博客,想想当初立下的flag以及想要看齐的人,就更多了些学习的动力!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值