十大排序算法

一、冒泡法排序

算法思想:

从头开始相邻的两个数字互相比较(第0个和第1个比较,然后第1个和第2个比较以此类推),把较大的放到后面(如果前面的比后面的数大就交换位置),最后最大的数就会被放到最后面,重复n次就能排好序了

优化理解:

我们可以理解成每一次循环比较都是在给最大的数找到合适的位置,找到之后就从剩下的数里面(每次循环比较剩下的数都会减一)重复这个过程,如果有n个数的话我们只用做n-1次这样的循环比较,因为其它的数已经排好了,最后剩下的那个一定是最小的数不用在比较了。

代码实现:

  public static int[] sortArr = {3, 5, 7, 9, 4, 6, 8, 2, 1};
    public static void bubbleSort() {
        for (int i = 1; i < sortArr.length; i++) { // n - 1 次循环
            for (int j = 0; j < sortArr.length - i; j++) { // 每次都减一
                if (sortArr[j] > sortArr[j + 1]) {
                    int temp = sortArr[j];
                    sortArr[j] = sortArr[j + 1];
                    sortArr[j + 1] = temp;
                }
            }
        }
    }

二、插入法排序

算法思想:

假设第一个数就是最小得数直接放到第一位,拿第二个数和第一个比较如果比第一个数大就位置不动,如果比第一个数小就交换位置,第三个数和第二个比较如果比第二个数大就位置不动,如果比第二个数小就交换位置,然后继续和第一个数比较。以此类推就能排好序

优化理解:

我们把一个集合分成两个集合,这两个集合一个是有序的(从小到大),一个是无序的,开始有序集合有一个元素(这就代表着第一个元素不用参与循环插入,所以只用循环插入n-1次),剩下的n-1个数都在无序集合里面,我们拿无序集合里的第一个数通过循环比较把它插入到有序集合里面(有序集合有几个元素,最多比较几次,只要它在循环插入中比一个元素大,那它当前插入的位置就是它在有序集合中该插入的位置,如果他比有序集合中的数都小,才会出现和有序集合每个数都比较一次的情况,那假如有序集合有j个,那它这次最多比较j次)

代码实现:

    public static int[] sortArr = {3, 5, 7, 9, 4, 6, 8, 2, 1};
    public static void insertSort() {
        for (int i = 1; i < sortArr.length; i++) { //只用把无序集合n-1个数循环插入
            for (int j = i; j > 0; j--) { //有序集合有几个元素,每次循环插入最多比较几次
                int temp;
                if (sortArr[j] < sortArr[j - 1]) {
//因为是有序的从小到大,倒着比较,如果它比前一个元素小需要调换他们俩的位置保证有序性
                    temp = sortArr[j - 1];
                    sortArr[j - 1] = sortArr[j];
                    sortArr[j] = temp;
                }else { 
//因为是有序的从小到大,倒着比较,只要他比前一个元素大那当前的位置就是它的位置
                    break;
                }
            }
        }
    }

三、选择排序

排序思想:

把第一个元素拿出来,和后面n-1个数循环比较,每次选择那个更小的留下,比过一圈后就能选择出来最小的元素,把它放到第一个,以此类推

优化理解:

第一个最小元素筛选出来后,下次就不用管这个元素了,所以筛选出i个就下次直接从i开始,最后一个不用拿出来选择一遍,因为前面都确定了,最后一个一定是最大的,所以只用不断循环选择n-1次。

代码实现:

    public static void selectSort() {
        for (int i = 0; i < sortArr.length - 1; i++) { // 最后一个不用出来选则,所以n-1次
            for (int j = i + 1; j < sortArr.length; j++){
                //选择出i个就,从第i开始,自己不用和自己比较所以j = i + 1
                int temp;
                if (sortArr[i] > sortArr[j]){
                    temp = sortArr[j];
                    sortArr[j] = sortArr[i];
                    sortArr[i] = temp;
                }
            }
        }
    }

 四、快速排序

1.单路快排

排序思想:

首先一组数据,我们以最右边的数作为基准数(用来比较的一个数)
基准数左边采用快慢指针思想,快指针负责向前遍历基准数全部左边的数,并且快指针会和基准数比较大小,如果比基准数大,快指针++,慢指针不动,如果快指针指向的数比基准数小,那就把快指针指向的数和慢指针指向的数交换,然后慢指针++,快指针也++;注意:慢指针是和快指针交换一次,向前移动一个,快指针一直向前移动
当快指针走到基准数时,则慢指针和基准数交换,这样完成之后,比基准数小的都放在了左边,然后慢指针左边和右边再执行上述过程

优化理解:

慢指针只要不是和快指针同时指向一个数,慢指针一定是指向比基准数大的数(大家画一下这个流程就知道了快指针遇到比基准数大的会继续++,而慢支针只有在快指针遇到比基准数小的和快指针的数交换之后才++),快指针在遍历的时候寻找到比基准数小的和慢指针交换,这样就能把小的搞到慢指针前面去,大的放到慢指针的后面来,当快指针遇到基准数的时候,慢指针这时候前面都是比基准数小的,慢指针当前指向的数后面都大于等于基准数,

代码实现:

    public static void singleQuickSort(int[] sortArr, int i, int p) {
        int quickIndex = i;
        int slowIndex = i;
        int standardNumIndex = p;
        if (i >= p) { //只有一个元素的时候或者没有元素的时候不需要排序,直接退出
            return;
        }

        while (quickIndex < standardNumIndex) { //快指针和基准数没有碰头就得一直遍历
            while (sortArr[quickIndex] > sortArr[standardNumIndex] && quickIndex < standardNumIndex) {
                //如果快指针指向的数比基准数大,并且还没和基准数碰头,就自增1
                quickIndex++;
            }
            //到这里要么遇到比基准数小的,要么和基准数碰头了,这时候需要把快慢指针的数交换
            int temp = sortArr[quickIndex];
            sortArr[quickIndex] = sortArr[slowIndex];
            sortArr[slowIndex] = temp;

            if (quickIndex < standardNumIndex) {
                // 如果快指针和慢指针还没有碰头说明还不能结束,快指针和慢指针各自增1
                quickIndex++;
                slowIndex++;
                if (quickIndex == standardNumIndex){
                    //注意有可能出现快指针自增1之后正好和基准数碰头了,这时候需要交换基准数的慢指针
                    // 因为大循环的判断导致会退出循环,所以这里需要快指针和基准数碰头的时候补充一次交换基准数和慢指针
                    temp = sortArr[quickIndex];
                    sortArr[quickIndex] = sortArr[slowIndex];
                    sortArr[slowIndex] = temp;
                }
            }
        }
        singleQuickSort(sortArr, i, slowIndex - 1);
        singleQuickSort(sortArr, slowIndex + 1, p);
    }

当初始序列是有序的时候,时间复杂度会退化到O(n^2),所以我们每次在left和right之间随机取一个数当作基准数,可以有效的减少这种情况,就出现了单路随机快排

2.随机快排

就是随机再left和right中取一个数,然后把它和最左边的数交换,然后它作为基准值,这样如果原来是有序的,通过这样第一个数和随机取得数交换位置,现在就变成无序的了。

当重复数据太多的时候,发现时间复杂度又会退回到O(n^2),双路快排如何优化这个问题?

3.双路快排

排序思想:

开始选定一个基准k,一般是第一个数,然后设置两个指针一个i 从头开始指向第一个数,一个j从尾开始指向最后一个数,开始我们必须从尾部开始从右往左(下面会说为什么)找第一个比这个基准数k小的数时j指向这个数,如果这时候j和i没有碰头,就从头开始i++查找比第一个基准数k大的数,找到后i指向这个数,然后把i指向的数和j指向的数做交换,然后继续j--找比k大的数,继续i++找比k小的数,直到i和j碰头,碰头后i和j指向同一个数,把这个数和基准数k做交换,经过这样的流程后就会把所有小于k的都换到左边,大于k的都换到右边,所以这时k的左边都比k小,k的右边都比k大。然后再分别递归k的左右集合这样最后就能排序成功。

问题:为啥选定左边第一个为基准数时,一定要从右边开始。

我们先举例然后再概括:

1.先搞一个特殊的例子:0 5 2 8 9 3,如果先从左边开始的话,按照上面的流程直接gg了,开始i++

5比0大,i指向5停止,j从右边j--一直碰头了要交换,可是5比零大交换不了。

2.再搞一个普遍的例子:6 1 2 7 9 11 4 5 10 8  这个例子推荐动手画一下

如果从左边先开始 i指向7,j指向5 然后两者交换数值变成 6 1 2 5 9 11 4 7 10 8

继续: i指向9,j指向4 交换数值: 6 1 2 5 4 11 9 7 10 8

然后到关键时刻了,i指向4,j指向9,i先动,i++后i指向11比基准数大停止,然后j--到11后碰头了,这时不能交换,因为11比6大。

总结:如果选左边第一个数为基准,最后i和j碰头的时候,会出现碰头的指向的这个数比基准数大导致不能和基准数交换,先从右往左就不会有这个问题,最后碰头的时候一定比基准数小,为什么呢?

碰头分为两种情况,一种是i++的时候碰到j,一种是j--的时候碰到i

如果是左边先动的话:

1).如果是i++碰到j,只要交换过一次这时j指向的值一定比基准数大(因为交换的目的就是把比基准数大和比基准数小的交换位置),然后又是i要先++,这时候碰头的数是要比基准数k大的不能交换

2).如果是j--碰到i,j--碰到i一定是i先++找到比基准数大的值,然后再j--,这时碰到还是比基准数大不能交换。

如果是右边先动的话:

1).如果i++碰到j,只要交换过一次这时j指向的值一定比基准数大,但是这次是j先--,j会找到比基准数小的停在那,如果这时候i++碰到了j是可以交换的。

2)如果j--碰到了i,只要交换过一次,因为是j先动,如果j--碰到i,这种一定是交换完后j--,这时候i指向的值是小于基准数的(因为刚交还完,刚交换完,i和j都没动的话,i的值比基准值小,j的值比基准值大),j--遇到i比基准值小能交换。

综上只要基准数选在左边,j先动,i和j碰头的时候一定是比基准数小可以交换,如果i先动,i和j碰头一定是比基准数大导致不能交换。

优化理解:

我们的目的是选一个基准数,然后通过循环比较,把比基准数小的放到基准数左边,把比基准数大的放到基准数右边。

代码实现:

    public static void quickSort(int[] sortArr, int i, int j) {
        int k = i;
        int right = j;
        int left = i;
        if (left >= right) {//递归退出的条件,分割后只剩一个数或者一个数没有时就不需要再进行排序
            return;
        }
        while (i != j) {
            //从右往左寻找比基准数小的
            while (sortArr[j] >= sortArr[k] && i < j) {
                j--;
            }

            //从左往右寻找比基准数大的
            while (sortArr[i] <= sortArr[k] && i < j) {
                i++;
            }

            int temp;
            if (i == j) {
                //i == j 这时i和j左边的都是小于基准值的,i和j右边都是大于基准值的
                //而且i和j现在指向的数一定是小于基准值的
                //把基准值和ij相遇的值交换
                temp = sortArr[k];
                sortArr[k] = sortArr[i];
                sortArr[i] = temp;

                quickSort(sortArr, left, i - 1);
                quickSort(sortArr, i + 1, right);
            } else {
                // i != j 互换 i 和 j 的值
                temp = sortArr[j];
                sortArr[j] = sortArr[i];
                sortArr[i] = temp;
            }

        }
    }

4.三路快排 

排序思想:

之前的快速排序算法都是将序列分成<=v和>v或者是<v和>=v的两个部分,而三路快速排序是

将序列分成三个部分:<v、=v、>v。

  • 定义索引lt记录等于基准数第一个元素的位置;因为第一个元素是基准值不能被替换所以lt初始值为 left + 1
  • 定义索引gt为=基准数的最后一个元素的位置;
  • 遍历nums[l+1:r]中的元素,比较当前位置(i)的元素与基准数的大小;
  • 当前遍历元素==基准数,i++即可;
  • 当前遍历元素<基准数,i和lt的元素互换位置,lt++;i++
  • 当前遍历元素>基准数,i和gt的元素互换位置,gt–-;注意这里不要i++
  • 当i>gt时,结束循环;
  • 最后l和lt - 1的元素互换位置;
  • 然后再递归 left 和 lt -1 ,gt + 1 和 right

优化理解:

目的就是把小于基准数的放到基准数的左边,大于基准数的放到基准数右边,等于基准数的在中间,如何体现中间呢?

就是加了lt和gt两个索引,在一趟比较结束后分别指向第一个等于基准数的和最后一个等于基准数的。

如何具体操作使lt左边都是小于基准数,lt和gt中间都是等于基准数,gt右边都是大于基准数呢?

在遍历的时候如果i指向的数比基准数小,那就把这个数和lt指向的数交换然后lt++,这样lt左边的就会都是小于基准数了,然后i++ 继续比较下一个数。 当遇到等于基准数的直接遍历的索引i++,如果遇到比基准数大的就和gt的数交换然后gt--,这样gt后面的数就都是大于基准值的了 注意这时候不能i++,因为之前gt指向的数和当前索引i指向的数刚交换了,gt那个数还没有和基准数比较呢,所以不能i++,应该继续比较。

代码实现:

  /**
     * 定义索引lt为=v的一个元素的位置,因为第一个元素是基准值不能被替换所以lt初始值为 left + 1
     * 定义索引gt为=v的最后一个元素的位置;
     * 遍历nums[l+1:r]中的元素,比较当前位置(i)的元素e与v的大小;
     * e==v,i++即可;
     * e<v,i和lt的元素互换位置,lt++;i++
     * e>v,i和gt的元素互换位置,gt–-;
     * 当 i>gt 时,结束循环;
     * 最后l和lt - 1的元素互换位置;
     * 然后再递归 left 和 lt -1 ,gt + 1 和 right
     *
     目的就是把小于基准数的放到基准数的左边,大于基准数的放到基准数右边,等于基准数的在中间,如何体现中间呢?
     就是加了lt和gt两个索引,在一趟比较结束后分别指向第一个等于基准数的和最后一个等于基准数的。
     如何具体操作使lt左边都是小于基准数,lt和gt中间都是等于基准数,gt右边都是大于基准数呢?
     在遍历的时候如果i指向的数比基准数小,那就把这个数和lt指向的数交换然后lt++,这样lt左边的就会都是小于基准数了,然后i++
     继续比较下一个数。
     当遇到等于基准数的直接遍历的索引i++,如果遇到比基准数大的就和gt的数交换然后gt--,这样gt后面的数就都是大于基准值的了
     注意这时候不能i++,因为之前gt指向的数和当前索引i指向的数刚交换了,gt那个数还没有和基准数比较呢,所以不能i++,应该继续比较。
     *
     */
    public static void threeQuickSort(int l, int r) {
        if (l >= r) {
            return;
        }
        int v = sortArr[l];        //l 指向的数是基准数
        int lt = l + 1; // 第一个是基准数不能在遍历时被换,所以从l + 1开始
        int gt = r;
        int i = l + 1; //基准数不用和自己比较,所以遍历从 l + 1开始
        while (i <= gt) {
            // 我们一趟循环的目的是为了确定lt和gt的位置,gt右边的都比基准数大,所以i遍历到gt就可以了,
            // 注意gt指向的数没有被比较过,也需要比较一次所以是i <= gt
            if (sortArr[i] < v) {
                int temp = sortArr[i]; // 如果当前位置元素<v,则将lt和i元素互换,lt++这样lt左边的都是小于基准数的
                sortArr[i] = sortArr[lt];
                sortArr[lt] = temp;
                i++;                             // i++  考虑下一个元素
                lt++;
            } else if (sortArr[i] > v) { // 如果当前位置元素>v,则将当前位置元素与gt交换然后gt--这样gt右边的都是大于基准数的
                int temp = sortArr[i];  // 此时i不用动,因为交换过来的元素还没有考虑他的大小
                sortArr[i] = sortArr[gt];
                sortArr[gt] = temp;
                gt--;
            } else {    //  如果当前位置元素=v   则只需要将i++即可,表示=v部分多了一个元素
                i++;
            }
        }

        int temp = sortArr[l]; //最后把基准数和lt左边的一个元素交换,lt左边的元素肯定小于基准数
        sortArr[l] = sortArr[lt - 1];
        sortArr[lt -1 ] = temp;

        threeQuickSort(l, lt - 1); 
        threeQuickSort(gt + 1, r);
    }

五、希尔排序

希尔排序时间复杂度是 O(n^(1.3-2)),空间复杂度为常数阶 O(1)。希尔排序没有时间复杂度为 O(n(logn)) 的快速排序算法快 ,因此对中等大小规模表现良好,但对规模非常大的数据排序不是最优选择,总之比一般 O(n^2 ) 复杂度的算法快得多。

排序思想:

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

增量和按照增量分组:增量一般就是一个小于总长度n的整数,我们一般取n/2,按照增量分组的意思是,把第一个数和第一个数+增量和第一个数+增量+增量,依次类推直到溢出停止,然后把这些数分为一组,然后继续第二个数+增量和第二个数+增量+增量,依次类推直到溢出停止,增量是几就会分几组,可能会出现增量过大导致加第一个增量就溢出这种情况,它自己算一组。思考一下为啥增量是几就会分几组?假如增量是k,第一个数和k+1是一组,第一个数和k+1之间的数每个都会是一组。

画图辅助优化理解:

以4为增量进行分组 ,9和4和6、5和1、8和3、2和7一共分了四组,对每个组进行直接插入法排序

然后再缩小增量,一般缩小为原来的二分之一,4/2 = 2。

 以2为增量进行分组,4和3和6和8和9、1和2和5和7一共分了两组,对每个组进行直接插入法排序 

然后再缩小增量,2/2 = 1,这样所有数都在一组中,再进行直接插入法排序

在排序过程中,发现实际还是在靠直接插入法在确定数字的顺序,只不过经过这种增量分组,然后再缩小增量的方法,减少了逆序的个数或者说想对来说比之前更有序。

为什么减少逆序的个数然后再用直接插入法排序就会更快呢?

插入排序有一个缺点,那就是对于有序性高的序数列,排序效果好,也就是速度快,但对于无序性高的数列来说,插入排序的速度就会很慢。为什么呢?因为,每次从无序数列中向有序数列插入时,都要依次从后往前和有序数列中的每一个数比较,这样的话效率就比较低,经过增量分组后会使得无序数列中的逆序减少或者说比之前更加有序,这样再进行从无序插入有序序列时就会减少比较的次数。

代码实现:

    public static void shellSort() {
        int increment = sortArr.length; //增量初始值为数组长度的1/2
        while (increment != 1) { //增量为1,所有的数都分为一组,
            // 插入法排序保证所有所有的数都有序
            increment = increment / 2; //增量每次都为原来的1/2
            for (int n = 0; n < increment; n++) { //增量是多少就有多少组,每一组都需要用插入法排序
                //下面就是直接插入法排序,只不过需要特殊处理一下,只排各自的分组
                for (int i = n + increment; i < sortArr.length; i = i + increment) {
                    for (int j = i; j >= increment; j = j - increment) {
                        int temp;
                        if (sortArr[j] < sortArr[j - increment]) {
                            temp = sortArr[j - increment];
                            sortArr[j - increment] = sortArr[j];
                            sortArr[j] = temp;
                        } else {
                            break;
                        }
                    }
                }
            }
        }
    }

六、归并排序

排序思想:

先从整体到局部的划分,先把集合从中间分成两份,然后继续把左右两个分出来的集合再从中间分开,直到每个子集合只有一个元素,每次划分我们都知道是从哪个父集合分出来的哪两个子集合,当每个子集合只有一个元素的时候,再从局部到整体的归并,把两个分出来的子集合(这些子集合都是有序的)进行排序然后重新放入父集合,不断的往上归并,最后会把开始划分的那两个大集合合成一个总集合,只不过这两个开始划分的大集合都变成了有序的。

优化理解:先二分法,把集合分成一个个独立的元素,然后原路归并(从哪个分出来的就合并成哪个,只不过合并后的是有序的)

思考问题
1.如果集合个数是奇数的,二分法怎么分:

举例    {1,2,3,4,5}    5 / 2 = 2 划分为: {1,2} , {3,4,5}

{1,2} 划分为:  {1},{2} 

{3,4,5}  3 / 2 = 1 划分为: {3},{4,5}

{4,5} 划分为: {4},{5}

2.两个有序的子集合如何归并成一个有序的子集合

假设是从n[]集合分出的两个子集合 a[] 和 b[],开始i = 0,j = 0, k = 0

两个子集合从头开始a[i]和b[j]比较,把小的赋值给n[k],假如a[i] < b[j] ,则把a[i]赋值给n[k],然后

i++继续让a集合的下一个元素和b[j]比较,这样知道a或者b先遍历完,这时就可以把另一个没有遍历完的元素直接放到n的后面,因为两个集合是有序的,我们上面的操作一直在从小到大的排元素,如果把a或者b其中一个元素拍完了,那另一个集合剩下没有排的元素都比之前的大而且是有序的。

代码实现:

    public static void mergeSort(int[] sortArr) {
        if (sortArr.length == 1) { // 划分的时候递归出口,每个子集合只有一个元素
            return;
        }
        int split = sortArr.length / 2; //二分
        int[] preArr = Arrays.copyOfRange(sortArr, 0, split); // 从中间分出两个集合来
        int[] proArr = Arrays.copyOfRange(sortArr, split, sortArr.length);
        mergeSort(preArr); //递归直到每个子集合只有一个元素
        mergeSort(proArr);
        int i = 0, j = 0;
        int p = 0;
        //运行到这里,因为递归,preArr和proArr是由sortArr划分出得两个子集合,
        // 我们把把两个子集合排序后放到父集合sortArr中
        while (i < preArr.length && j < proArr.length) {
            //从头遍历比较两个子集合,从小到大排序,直到其中一个集合遍历完
            if (preArr[i] < proArr[j]) {
                sortArr[p] = preArr[i];
                i++;
            } else {
                sortArr[p] = proArr[j];
                j++;
            }
            p++;
        }
        //把另一个没有遍历完的集合剩下的元素直接放到父集合的后面
        if (i < preArr.length) {
            for (; i < preArr.length; i++) {
                sortArr[p] = preArr[i];
                p++;
            }
        }

        if (j < proArr.length) {
            for (; j < proArr.length; j++) {
                sortArr[p] = proArr[j];
                p++;
            }
        }
    }

七、堆排序

用堆排序先要知道数据结构二叉堆:

数据结构堆_AllenC6的博客-CSDN博客一、堆1.定义堆就是用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。2.堆的属性堆是一颗完全二叉树,这样实现的堆也被称为二叉堆堆中节点的值都大于等于(或小于等于)其子节点的值,堆中如果节点的值都大于等于其子节点的值,我们把它称为大顶堆,如果都小于等于其子节点的值,我们将其称为小顶堆。...https://blog.csdn.net/m0_37707561/article/details/123629085

排序思想:用堆排序就是先建堆,取根节点,然后再不断的删除根节点,再经过操作符合堆的性质后,再取根节点。

优化理解:

说一点,取根之后删除根,拿最后那个元素补上来这个操作,可以转化成,取根之后把根和最后一个元素交换,最后那个元素就是已排好序的元素,下次进行shifDown的时候就不需要在比较最后一个元素,然后在剩下元素的集合中再做这个操作,直到只剩一个元素

代码实现:

    public static void heapSort() {
        buildHeap();
        int n = sortArr.length - 1;
        while (n > 0) {
            int temp = sortArr[0]; //把根节点和最后一个元素交换
            sortArr[0] = sortArr[n];
            sortArr[n] = temp;
            n--; // 交换之后,最后一个元素是最大的了,以后没必要在比较这个元素,所以n--
            shiftDown(0, n);
        }
    }

    //自顶向下建堆,时间复杂度是O(nlog2n)
    public static void buildHeap() {
        for (int i = 1; i < sortArr.length; i++) {
            shiftUp(i);
        }
    }

    //在插入的时候,一般都是插入到末尾,所以需要向上调 index 是插入数的索引
    public static void shiftUp(int index) {
        int parentIndex = (index - 1) / 2;
        if (index > 0) {
            if (sortArr[index] > sortArr[parentIndex]) {
                int temp = sortArr[index];
                sortArr[index] = sortArr[parentIndex];
                sortArr[parentIndex] = temp;
                shiftUp(parentIndex);
            }
        }
    }

    //在删除根结点的时候,一般都是拿末尾的数来替换,所以需要向下调整 index 删除的节点的索引
    public static void shiftDown(int index, int end) {
        int sunLeft = 2 * index + 1; // 左孩子的索引
        int sunRight = 2 * index + 2; // 右孩子的索引

        //如果两个孩子都不是null
        //堆的兄弟节点是无序的,所以需要比较出哪个节点更大,和更大的那个交换
        if (sunLeft <= end && sunRight <= end) {
            if (sortArr[sunLeft] >= sortArr[sunRight]) {
                if (sortArr[index] < sortArr[sunLeft]) {
                    int temp = sortArr[index];
                    sortArr[index] = sortArr[sunLeft];
                    sortArr[sunLeft] = temp;
                    shiftDown(sunLeft, end);
                }
            } else {
                if (sortArr[index] < sortArr[sunRight]) {
                    int temp = sortArr[index];
                    sortArr[index] = sortArr[sunRight];
                    sortArr[sunRight] = temp;
                    shiftDown(sunRight, end);
                }
            }
        } else if (sunLeft <= end) {
            if (sortArr[index] < sortArr[sunLeft]) {
                int temp = sortArr[index];
                sortArr[index] = sortArr[sunLeft];
                sortArr[sunLeft] = temp;
                shiftDown(sunLeft, end);
            }
        }
    }

以上七种排序都是比较排序,下面三个不是比较排序

八、计数排序

计数排序不是一个比较排序算法,该算法于1954年由 Harold H. Seward提出,通过计数将时间复杂度降到了O(N)

排序思想:

1.基础版思路:

第一步:找出原数组中元素值最大的,记为max

第二步:创建一个新数组count,其长度是max加1,其元素默认值都为0。

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

第四步:创建结果数组result,起始索引index

第五步:遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。

第六步:返回结果数组result

基础版代码实现:

    public static void countSort(){
        int max = Integer.MIN_VALUE;
        for (int i = 0; i < sortArr.length; i++){
            if (max < sortArr[i]){
                max = sortArr[i];
            }
        }

        int [] countArr = new int[max+1];

        for (int i = 0; i < sortArr.length; i++){
            countArr[sortArr[i]]++;
        }

        int j = 0;
        for (int i = 0; i < countArr.length; i++){
            while (countArr[i] > 0){
                sortArr[j] = i;
                j++;
                countArr[i]--;
            }
        }
    }

基础版能够解决一般的情况,但是它有一个缺陷,那就是存在空间浪费的问题。

比如一组数据{101,109,108,102,110,107,103},其中最大值为110,按照基础版的思路,我们需要创建一个长度为111的计数数组,但是我们可以发现,它前面的[0,100]的空间完全浪费了,那怎样优化呢?

将数组长度定为max-min+1,即不仅要找出最大值,还要找出最小值,根据两者的差来确定计数数组的长度

2.优化版思路:

第一步:找出数组中的最大值max、最小值min

第二步:创建一个新数组count,其长度是max-min加1,其元素默认值都为0。

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

优化版代码实现:

    public static void optimizeCountSort(){
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < sortArr.length; i++){
            max = Math.max(max,sortArr[i]);
            min = Math.min(min,sortArr[i]);
        }

        int [] countArr = new int[max - min + 1];

        for (int i = 0; i < sortArr.length; i++){
            countArr[sortArr[i] - min]++;
        }

        int j = 0;
        for (int i = 0; i < countArr.length; i++){
            while (countArr[i] > 0){
                sortArr[j] = i + min;
                j++;
                countArr[i]--;
            }
        }
    }

如果我们想要原始数组中的相同元素按照本来的顺序的排列,那该怎么处理呢?

即如何保证稳定性呢?看进阶版。

3.进阶版思路:

第一步:找出数组中的最大值max、最小值min

第二步:创建一个新数组count,其长度是max-min加1,其元素默认值都为0。

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

第四步:对count数组变形新元素的值是前面元素累加之和的值,即count[i+1] = count[i+1] + count[i];

第五步:创建结果数组result,长度和原始数组一样。

第六步:遍历原始数组中的元素,当前元素A[j]减去最小值min,作为索引,在计数数组中找到对应的元素值count[A[j]-min],再将count[A[j]-min]的值减去1,就是A[j]在结果数组result中的位置,做完上述这些操作,count[A[j]-min]自减1。

是不是对第四步和第六步有疑问?为什么要这样操作?

首先我们明确一点,计数排序的思想就是把原数组的值作为count数组的下标并且通过计数的方式记录重复的值,因为下标是有序的,所以通过遍历count数组的下标就能获取有序的序列,但是通过遍历count数组的方式来排序,是不能保证稳定性的,因为它只通过count数组索引的大小确定了原数组值的排序关系,并没有确立原数组和count数组元素之间的一一对应关系,如果能够确立原数组和count数组元素之间的一一对应关系,就能想办法维持相同的数值排序前和排序后的位置不变,即保证稳定性。

那如何确立原数组和count数组的一一对应关系呢?这就是第四步和第六步要达到的目的。

让计数数组count存储的元素值,等于在原始数组中最终排第几个,下标等于这个值 - 1,计数数组count的索引 + min就是对应原数组的值

那为什么第四步这么做,就能让计数数组count存储的元素值,等于原始数组中最终排第几个即下标等于这个值- 1?

计数数组count的索引 + min如果等于原数组的值,那计数数组count的当前索引对应存储的值就+1,初始值为0,已知索引是有序排列的,数组对应索引存储的值代表原数组有几个和 count数组索引 + min 一样重复的值,如果计数数组count当前索引对应存储的值(注意理解这个值是数组的索引对应存储的值例:a[i]),等于数组当前索引对应的值+前面所有数组索引对应存储的值的和,前面所有数组索引对应存储的值的和是几就说明前面有几个元素,而且前面的元素还是有序排列的,这不正好是有序的定义吗,那这个对应存储的值就是在排好序的原数组排第几个。这样原数组和计数数组count中的元素产生了一一对应关系。

我们画图理解一下这个对应关系:

通过第四步使原数组和计数数组count元素之间产生了一一对应关系,即计数数组中的元素的值为当前索引对应原数组的值在有序的原数组中排第几个,因为索引从0开始所以需要 - 1。产生一一对应关系后,需要想办法保证稳定性,如何保证稳定性呢?

首先我们看看这种进阶后如何得到有序的原数组,通过从左到右的遍历原数组,然后通过原数组元素的值得到计数数组count的下标,然后获取这个下标对应的数组的值,这个值就是元素在有序的原数组中排第几个,又因为索引从0开始所以在原数组的索引需要这个值 - 1,把这个数排好后需要对计数数组对应下标存储的值-1。

但是这种方式会破坏稳定性,例如上图的例子,第一次遍历到3对应的计数数组的下标是0,那下标0对应的值是2,那第一个三就会放到原数组下标为2的位置,放好后计数数组对应下标存储的值要-1即下标0对应的值2要减一 2 - 1 = 1,当第二次遍历到3的时候,还是对应计数数组的下标是0,现在下标0对应的值是1,那要放到原数组下标为1的位置,这时两个相同的3排序前和排序后的相对顺序变化了,破坏了稳定性。

为什么会破坏稳定性呢,因为从原数组放入计数数组的顺序是正序的,而且原数组和计数数组已经建立了一一对应的关系,如果原数组一个数重复了两次那计为2,重复的数前面的那个应该对应的是第一个,后面的那个才是对应的第二个,如果我们还以正序的遍历原数组去排序,遍历到原数组前面重复的那个数的时候,通过值找到对应的计数数组的下标,然后再获取这个下标对应的值,这个值是2,这时第一个重复的数却和第二个记录对应起来,那前面的就变成对应的是后面的那个记录,后面的对应的是前面的记录,反过来了。那如何解决呢?我们发现正序遍历原数组,会使重复的数对应关系倒转,那我们逆序遍历原数组不正好使重复的数对应关系正过来吗。

代码实现:

    public static void advancedCountSort(){
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < sortArr.length; i++){
            max = Math.max(max,sortArr[i]);
            min = Math.min(min,sortArr[i]);
        }

        int [] countArr = new int[max - min + 1];

        for (int i = 0; i < sortArr.length; i++){
            countArr[sortArr[i] - min]++;
        }

        // 给计数数组变形,使原数组和计数数组产生一一对应关系
        //即计数数组count存储的元素值,等于原始数组中最终排序的下标
        for (int i = 1; i < countArr.length; i++){
            countArr[i] = countArr[i - 1] + countArr[i];
        }

        //原数组的元素值,对应计数数组的下标,通过计数数组的下标获取计数数组存储的元素值,这个值就是
        //原数组排好序后排在第几个,下标是这个位置 - 1,我们为了保证稳定性倒序的去遍历原数组
        int[] result = new int[sortArr.length];
        for (int i = sortArr.length - 1; i >= 0; i--){
            result[countArr[sortArr[i] - min] - 1] = sortArr[i];
            countArr[sortArr[i] - min]--; // 排完一个元素需要给计数数组对应的值 - 1
        }
        sortArr = result;
    }

总结:以上就是计数排序算法的全部内容了,虽然它可以将排序算法的时间复杂度降低到O(N),但是有两个前提需要满足:一是需要排序的元素必须是整数,二是排序元素的取值要在一定范围内,并且比较集中。只有这两个条件都满足,才能最大程度发挥计数排序的优势。

九、桶排序

排序思想:

一句话总结:划分多个范围相同的区间,每个子区间自排序,最后合并。

桶排序不仅可以排整数,也可以排带小数的。

桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。

优化理解:

代码实现:

    public static double[] sortArr1 = {7, 1,2, 7,9, 3,3, 1, 4,5, 8, 4, 9,2,6};
    public static void bucketSort(){
        List<LinkedList<Double>> bucketList = new ArrayList<>(sortArr1.length);
        for (int i = 0; i < sortArr1.length; i++){
            bucketList.add(new LinkedList<>());
        }

        double max = Integer.MIN_VALUE;
        double min = Integer.MAX_VALUE;
        for (int i = 0; i < sortArr1.length; i++){
            max = Math.max(max,sortArr1[i]);
            min = Math.min(min,sortArr1[i]);
        }

        double intervalSpan = (max - min) / (sortArr1.length - 1); //获取区间跨度

        for (int i = 0; i < sortArr1.length; i++){
            int bucketIndex = (int) ((sortArr1[i] - min) / intervalSpan); // 获取当前值在哪个桶中
            LinkedList<Double> bucket = bucketList.get(bucketIndex); //做一个直接插入排序
            if (bucket.isEmpty()){
                bucket.add(sortArr1[i]);
            }else {
                for (int j = bucket.size() - 1; j >= 0; j--){
                    if (bucket.get(j) >= sortArr1[i]){
                        bucket.add(j,sortArr1[i]);
                        break;
                    }
                }
            }
        }

        int x = 0;
        for (int i = 0; i < bucketList.size(); i++){
            for (int j = 0; j < bucketList.get(i).size(); j++){
                sortArr1[x] = bucketList.get(i).get(j);
                x++;
            }
        }

    }

十、基数排序

1.LSD的排序方式由数值的最右边(低位)开始,从低位到高位

LSD的基数排序适用于位数少的数列

排序思想:

基数排序(Radix Sort)是桶排序的扩展,它的基本思想是:将整数按位数切割成不同的数字,然后按每个位数分别比较。数字0到9被分为10个桶
具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

代码实现:

    public static void radixSort() {
        List<LinkedList<Integer>> bucketList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            bucketList.add(new LinkedList<>());
        }

        int maxDigitNum = 1;

        for (int i = 0; i < sortArr.length; i++) {
           maxDigitNum = Math.max(maxDigitNum,getDigits(sortArr[i]));
        }

        for (int i = 0; i < maxDigitNum; i++){
            for (int j = 0; j < sortArr.length; j++){
                int digit = getDigit(sortArr[j],i);
                bucketList.get(digit).add(sortArr[j]);
            }
            int n = 0;
            for (int j = 0; j < bucketList.size(); j++){
                for (int x = 0; x < bucketList.get(j).size(); x++){
                    sortArr[n] = bucketList.get(j).get(x); 
            //从0开始取从低到高,而不是从高到低,保证了稳定性
                    n++;
                }
                bucketList.get(j).clear();
            }
        }
    }

    //获取有多少位
    public static int getDigits(int num) {
        int n = 1;
        while ((int) (num / (Math.pow(10, n))) > 0) {
            n++;
        }
        return n;
    }

    //获取第几位 个位是0 十位是1 依次类推
    public static int getDigit(int num, int index) {
        int digits = getDigits(num);
        if (digits < index + 1) {
            return 0;
        }

        return (num - ((num / ((int) Math.pow(10, index + 1))) * ((int) Math.pow(10, index + 1)))) / ((int) Math.pow(10, index));
    }

2.MSD由数值的最左边(高位)开始,从高到低

如果位数多的话,使用MSD的效率会比较好。

排序思想:

MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。

在进行完最低位数的分配后再合并回单一的数组中。

优化理解:

类似按照数量级排序,先从高到低找到同一数量级的,分成几个不同数量级的集合,这样由数量级排出几个不同数量级集合的顺序,然后再按照同样的方法给同一数量级的集合内部排序。

代码实现:

//获取有多少位
    public static int getDigits(int num) {
        int n = 1;
        while ((int) (num / (Math.pow(10, n))) > 0) {
            n++;
        }
        return n;
    }

    //获取第几位 个位是0 十位是1 依次类推
    public static int getDigit(int num, int index) {
        int digits = getDigits(num);
        if (digits < index + 1) {
            return 0;
        }

        return (num - ((num / ((int) Math.pow(10, index + 1))) * ((int) Math.pow(10, index + 1)))) / ((int) Math.pow(10, index));
    }


    private static int index = 0;
    public static void msdRadixSort() {
        int maxDigitNum = 1;
        index = 0;
        for (int i = 0; i < sortArr.length; i++) {
            maxDigitNum = Math.max(maxDigitNum, getDigits(sortArr[i]));
        }
        msdRadixSort(sortArr, --maxDigitNum);
    }

    // digit 0是个位,1是十位依次类推
    private static void msdRadixSort(int[] sortArr, int digit) {
        if (digit < 0) {
            return;
        }
        List<LinkedList<Integer>> bucketList = new ArrayList<>();
        for (int i = 0; i < 10; i++) { // 创建十个桶 0 - 9
            bucketList.add(new LinkedList<>());
        }

        for (int i = 0; i < sortArr.length; i++) { // 按digit位数的数字分别放入桶中
            bucketList.get(getDigit(sortArr[i], digit)).add(sortArr[i]);
        }

        digit--;
        for (int i = 0; i < bucketList.size(); i++) {
            if (bucketList.get(i).size() == 1) { // 这个很重要这个是排序的核心,
                SortMain.sortArr[index++] = bucketList.get(i).get(0);
            }
            if (bucketList.get(i).size() > 1) { // 一个桶中大于1个元素
                if (digit < 0) { // 个位比较完后,如果一个桶里还有超过一个元素,说明这些元素相等,直接放到最终数组进行排序
                    for (int j = 0; j < bucketList.get(i).size(); j++) {
                        SortMain.sortArr[index++] = bucketList.get(i).get(j);
                    }
                } else { // 如果digit >= 0 说明还需要递归分组处理
                    int[] newArr = new int[bucketList.get(i).size()];
                    for (int j = 0; j < bucketList.get(i).size(); j++) {
                        newArr[j] = bucketList.get(i).get(j);
                    }
                    msdRadixSort(newArr, digit);
                }
            }
        }
    }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值