常见的八大排序算法解析

116 阅读13分钟

一、基础概念

1、冒泡排序:不断迭代,每次把最大值往右靠

2、选择排序:不断迭代,每次把最小值往左靠

3、插入排序:不断迭代,每次维护一个有序数组,并且将当前元素插入到有序数组合适位置

4、希尔排序:枚举增量,对不同下标增量进行分组,分别执行插入排序,再减小增量重复执行

5、归并排序:每次将数组分成长度接近的两部分,分别执行归并排序后。进行合并,再回溯

6、快速排序:选择一个基点,比基点小的元素到它左边,比基点大的元素到它右边,分别对两堆递归执行快速排序

7、计数排序:一次遍历,将所有数组映射到哈希表,再遍历哈希表进行输出

8、基数排序:从最低的位开始映射到0-9的痛,利用计数排序思想,让最低位先保证有序,在处理次低位,直到最高位处理完毕后,数组有序

二、动图演示

1、冒泡排序:

640.gif

2、选择排序:

640.gif

3、插入排序:

642.gif

4、希尔排序:

643.gif

5、归并排序:

644.gif

6、快速排序:

645.gif

7、计数排序:

645.gif

8、基数排序:

647.gif

三、八大排序复杂度

image.png

排序前后,相同元素的相对位置不变,则称排序算法是稳定的;否则 排序算法是 不稳定的

四、原理说明

1、归并排序

说明:

分治思想,先拆,按照对半的长度拆,一直拆到都只剩一个元素的子数组,然后,重新组合。

递归(深度优先遍历),按照索引长度处理

步骤:

  1. 按照长度半,分左右递归拆分
  2. 聚合,遵从递归的逻辑,先从最底层聚合
    • 定义临时数组,存放当前截取索引长度的数组数据

    • 将截取的数组内容,排序存放到临时数组

    • 将临时数组内容拷贝至原数组索引位置

步骤拆解:

如数组:8,4,5,7,1,6,3,2

第一步:递归拆解之后,得:

1.png

第二步:相邻两组进行合并:

2.png

3.png

最终得:

4.png 第三步,继续相邻合并

定义2个变量i和j,分别代表p1的第一个值和p2的第一个值,i和j比较,4<5,将4放入p中,i向后挪一位

5.png

i和j继续比较,8>5,将5放入p,j向后挪一位

6.png

继续比较,得到下面2个数组

7.png

采用同样的方式比较

8.png

9.png

10.png

11.png

12.png

代码:

public static void main(String[] args) {
    int[] nums = {1,2,2,5,4,3,765,34,65}; 
    sortAsc(nums, 0, nums.length-1);
    System.out.println(Arrays.toString(nums)); 
}

private static void sortAsc(int[] nums, int low, int high){ 
    int middle = (low + high) / 2;
    if (low < high){ 
        //处理左边数组 
        sortAsc(nums, low, middle); 
        //处理右边数组 
        sortAsc(nums, middle+1, high); 
        //归并 
        merge(nums, low, middle, high); 
    }
}

private static void merge(int[] nums, int low, int middle, int high) { 
    int[] temp = new int[high-low+1];
    int i = low;
    int j = middle+1; 
    int index = 0; 
    while (i<=middle && j<=high){
        if (nums[i] < nums[j]){
            temp[index++] = nums[i++]; 
        } else {
            temp[index++] = nums[j++]; 
        } 
    } 
    while (i<=middle){
        temp[index++] = nums[i++]; 
    } 
    while (j<=high){
        temp[index++] = nums[j++];
    } 

    for (int k = 0; k<temp.length; k++){
        nums[k+low] = temp[k];
    }
 }

2、快速排序

说明:

  • 分支思想,找一个基准数,先从后往前找,(以升序为例),找比基准数小的数,将其赋予基准那个位置;再从前往后找,找比基准数大的数,放到刚刚那个位置,对比结束,将基准数给到最后找到的那个数的位置。一句话:找基准,分别找小数和大数,每次能找到2个数,所以叫做快速排序

步骤:

    1. 取数组最左侧数,为基准数,记录下来作为一个临时变量t
    2. 从数组最右侧开始找,找比基准数小的数,找到了,就给到基准数位置处,那么此时,数组是一个错误的数组的,因为基准数位置被赋予了找到的那个较小的数
    3. 从数组左侧开始找,找比基准数大的数,找到了,就给到上一步找大数的那个位置,此时,数组中,当前位置的数是不对的,需要将基准数放到这
    4. 将基准数放到最后一个位置
    5. 至此,找出了一个比基准数大的数,一个比基准数小的数,并且他们3个的位置顺序已确定
    6. 依此类推,递归找基准数左右两边的

步骤拆解

如:数组:5,3,7,6,4,1,0,2,9,10,8

①、base=5, start=0,end=11,从后往前找,第一个比5小的数是 2(i=8),因此,数组变为:

2,3,7,6,4,1,0,5,9,10,8。此时,start=0, end=8;

②、base=5, start=1,end=8,从前往后找,第一个比5大的数是7(i=2),因此,数组变为:

2,3,5,6,4,1,0,7,9,10,8。此时,strat=2, end=8;

③、base=5,satrt=2,end=8,从i=8的位置开始前找,到i=2的位置截止,找到比5小的数,为0(i=6),此时,数组变为:2,3,0,6,4,1,5,7,9,10,8。此时,start=2,end=6;

④、base=5,start=2,end=6,从i=2的位置开始往后找,到i=6截止,找到比5大的数,为6(i=3),此时数组变为:2,3,0,5,4,1,6,7,9,10,8。此时,strat=3,end=6;

⑤、base=5, start=3,end=6,从i=6的位置开始往前找,到i=3截止,找到比5小的数,为1(i=5),此时,数组变为:2,3,0,1,4,5,6,7,9,10,8 。此时,start=3,end=5;

⑥、base=5, start=3, end=5,从i=3的位置往后找,到i=5截止,找到比5大的数,没有,数组不变,此时 i=j=5,循环条件不满足,退出循环,第一次排序结束,得到:i=j=5,数组为:

2,3,0,1,4,5,6,7,9,10,8

⑦、将数组以i=5分为2部分,第一部分:2,3,0,1,4,5 ;第二部分:6,7,9,10,8

⑧、将2个子数组分别执行1-6的操作,即可得到每个子数组的排序,整体数组即完成排序

0,1,2,3,4,5,6,7,8,9

代码:

public static void main(String[] args) {
    int[] nums = {2,1,3,5,1,6,5,9}; 
    sortAsc(nums, 0, nums.length-1); 
    System.out.println(Arrays.toString(nums));
} 

private static void sortAsc(int[] nums, int start, int end){ 
    if (start < end) { 
        //选中基准数 
        int base = nums[start]; 
        int i = start;
        int j = end;
        while (i < j) {
            //从后往前找,找到比基准数小的数,往前移动 
            while (i < j && nums[j] >= base) { 
                j--; 
            } 
            //此时的j就是找到的数的目标位置,两者进行对调 
            nums[i] = nums[j]; 
            //然后从前往后找,找到比基准数大的数,往后移 
            while (i < j && nums[i] <= base) { 
                i++;
            } 
            //此时的i就是找到的目标位置,进行对调 
            nums[j] = nums[i];
        } 
        //把基准数给到i的位置 
        nums[i] = base; 
        //左区间递归处理 
        sortAsc(nums, start, i); 
        //右区间递归处理 
        sortAsc(nums, i + 1, end); 
    }
}

3、选择排序

说明:

  • 每次找到剩余数组的最小值,按照索引顺序依次排列

步骤:

  • 遍历全部数据,获取最小数,与index=0的数互换位置
  • 遍历index>=1的所有数据,获取最小数,与index=1的数互换位置
  • 遍历index>=2的所有数据,获取最小是,与index=2的数互换位置
  • 依次类推

步骤拆解:

如:10,14,27,33,35,19,42,44

a.png

代码:

public static void main(String[] args) { 
    int[] nums = {2,1,3,3,4,9,54,32,21}; 
    sortAsc(nums); 
    System.out.println(Arrays.toString(nums)); 
    sortDescForHeap(nums); 
    System.out.println(Arrays.toString(nums)); 
} 

/** 
* 简单选择排序 
* */ 
private static void sortAsc(int[] nums) { 
    for (int i = 0; i < nums.length; i++) {
        int minIndex = i; 
        
        for (int j = i+1; j < nums.length; j++) {
            if (nums[j] < nums[minIndex]) { 
                minIndex = j; 
            } 
        } 
        
        //compare and swap 
        if ( i != minIndex) { 
            int temp = nums[i];
            nums[i] = nums[minIndex]; 
            nums[minIndex] = temp; 
        } 
    } 
}

4、堆排序

说明:

  • 大顶堆:完全二叉树,根节点大于左右节点,公式:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]

b.png

  • 小顶堆:完全二叉树,根节点小于左右节点,公式:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

c.png

步骤:

  1. 将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
  2. 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

步骤拆解

如:给出数组:4, 5, 8, 2, 3, 9, 7, 1

1、构建大顶堆:

d.png 2、找到最后一个非叶子节点,即元素 2,比较它的左右节点,是否比他大,如果大就交换位置,1<2,不用交换位置

3、比较下一个,即元素 8,比他的左子节点 9 小,故需要交换位置:

1.png

4、比较下一个非叶子节点 5,比他的左右节点大,故不需要调整

5、比较下一个非叶子节点 4,比他的右节点 9 小,估需要交换位置:

2.png

6、此时,右节点 4,8,7,根节点比左子节点 8 小,故需要交换位置:

4.png

7、至此,大顶堆构建完成

8、排序,将堆顶元素,放到最后面,如此往复,即可得到升序的数列;如果要做降序,则用小顶堆即可(升序,先找最大值;降序,先找最小值——与选择排序的的区别)

代码:

public static void main(String[] args) { 
    int[] nums = {2,1,3,3,4,9,54,32,21}; 
    sortHeap(nums); 
    System.out.println(Arrays.toString(nums)); 
} 
private static void sortHeap(int[] nums){
    //1、构建大顶堆 //i初始值来自大顶堆定义: 
    //大顶堆:nums[i]>=nums[2i+1] && nums[i] >= nums[2i+2] 
    //note:小顶堆定义:nums[i] <= nums[2i+1] && nums[i] <= nums[2i+2] 
    //要从第一个非叶子节点,则其至少要有一个叶子节点, 
    //即2i+1 为叶子节点 或 2i+2为叶子节点 
    // 若2i+2为叶子,则2i+1肯定存在,范围过大, 
    //因此,只需要满足2i+1为叶子节点即可, 
    // 即满足 2i+1<=nums.length,推导得出:i <= (nums.length-1)/2 
    for (int i = (nums.length-1)/2; i>=0; i--){ 
        //从第一个非叶子节点从下至上,从右至左调整结构 
        adjustHeap(nums, i, nums.length); 
    } 
    //2、调整结构 
    for (int j = nums.length-1; j>=0; j--){ 
        //堆顶元素与末尾元素交换 
        int temp = nums[0];
        nums[0] = nums[j];
        nums[j] = temp;
        //元素交换之后,数组可能不是大顶堆,需要重新构造大顶堆 
        adjustHeap(nums, 0, j);
    } 
} 

private static void adjustHeap(int[] nums, int i, int length) { 
    //先取出当前元素 
    int temp = nums[i]; 
    //从i的左子节点开始,也就是2i+1 
    for (int k = i*2+1; k<length; k=2*k+1){
       //如果左子节点小于右子节点,k指向右子节点 
       if (k+1<length && nums[k]<nums[k+1]){
           k++;
       }
    
       //如果子节点大于父节点,将子节点的值给父节点(不用交换): 
       // 不用替换的原因,子节点的子节点可能还比他大,需要再次赋值,
       // 这种依次赋值,需要用到之前那个最大值, 
       // 另外,只需要最后将temp那个值给到最后那个位置即可 
       if (nums[k] > temp){
          nums[i] = nums[k]; i = k;
       } else {
          break; 
       }
    } 
    //将temp放到最终的位置 
    nums[i] = temp; 
}

5、插入排序

说明:

  • 直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止

步骤

2.png 步骤拆解

我们以一个未排序的数组为例。

3.jpeg

插入排序比较前两个元素。

4.jpeg

它发现14和33都已按升序排列。 目前,14位于已排序的子列表中。

5.jpeg

插入排序向前移动并将33与27进行比较。

6.jpeg

并发现33不在正确的位置。

7.jpeg

它将33与27交换。它还检查已排序子列表的所有元素。 在这里,我们看到排序的子列表只有一个元素14,而27大于14.因此,排序的子列表在交换后仍然排序。

8.jpeg

到目前为止,我们在已排序的子列表中有14和27。 接下来,它将33与10进行比较。

9.jpeg

这些值不是按排序顺序排列的。

10.jpeg

所以我们交换它们。

11.jpeg

但是,交换使27和10未分类。

12.jpeg

因此,我们也交换它们。

13.jpeg

我们再次以未排序的顺序找到14和10。

14.jpeg

我们再次交换它们。 到第三次迭代结束时,我们有一个包含4个项目的已排序子列表。

15.jpeg

代码:

public static void main(String[] args) { 
    int[] nums = {1,2,5,2,3,7,5,4,9}; 
    sortAsc(nums); 
    System.out.println(Arrays.toString(nums));
} 

private static void sortAsc(int[] nums) { 
    for (int i=1; i < nums.length-1; i++) { 
        //如果取出的元素比他前一个元素小, 
        //则需要继续往前比较,知道找到比他大的元素位置, 
        //然后插入到比他大的元素前面 
        //compare and swap 
        //当前值比前面的值小 
        if (nums[i] < nums[i-1]) {
            int j; int temp = nums[i]; 
            //不断往前找,直到比他小为止,
            //比他小的那个数nums[j]后面位置即可放置当前值 
            for (j = i-1; j >= 0 && nums[j] > temp; j--) {
                nums[j+1] = nums[j]; 
            } 
            //此时,由于吧i位置的数一直挪,挪到j+1的位置了, 
            //但是j位置的数是和j+1保持一致的, 
            // 需要重新给j位置赋值 
            nums[j+1] = temp; 
        } 
        //否则,他两顺序是对的,不需要处理 
    }
}

说明:

  • 冒泡排序就是从序列中的第一个元素开始,依次对相邻的两个元素进行比较,如果前一个元素大于后一个元素则交换它们的位置。如果前一个元素小于或等于后一个元素,则不交换它们;这一比较和交换的操作一直持续到最后一个还未排好序的元素为止。

步骤:

  • 比较待排序序列中相邻的两个元素,如果发现左边的元素比右边的元素大,则交换这两个元素

步骤拆解:

1.png

2.png

3.png

4.png

代码:

public static void main(String[] args) { 
    int[] nums = {1,5,3,2,9,3,5,6};
    sortAsc(nums); System.out.println(Arrays.toString(nums)); 
} 

private static void sortAsc(int[] nums){
    int temp; 
    for (int i = 0; i < nums.length; i++) {
        //相邻2个元素比较,将最大值传递到尾部 
        for (int j = 0; j < nums.length-1-i; j++) { 
            //compare and swap 
            if (nums[j] > nums[j+1]) {
                temp = nums[j]; 
                nums[j] = nums[j+1]; 
                nums[j+1] = temp; 
            } 
        } 
    } 
}

7、希尔排序

说明:

插入排序经过改进后的一个高效版本

步骤:

1.gif

步骤拆解

  • 1、step = 4

11.png

  • 2、step = 2 12.png
  • 3、step=1 13.png
  • 至此,排序完成

代码:

public static void main(String[] args) { 
    int[] nums = {1,2,2,5,4,3,765,34,65};
    sortAsc(nums); 
    System.out.println(Arrays.toString(nums)); 
}

private static void sortAsc(int[] nums) { 
    //gap为步长, 每次为原来的一半 
    for (int gap = nums.length/2; gap > 0; gap/=2) {
        //对每一组都执行插入排序 
        for (int i = 0; i<gap; i++){
            for (int j = i+gap; j<nums.length;j+=gap) {
                //compare and swap
                if (nums[j] < nums[j-gap]) {
                    int k; 
                    int temp = nums[j];
                    for (k = j-gap; k>=0 && nums[k] > temp; k-= gap) { 
                        nums[k+gap] = nums[k]; 
                    }
                    nums[k+gap] = temp;
                }
       
            } 
        } 
    }  
}

参考

@夜深人静写算法

@学到牛牛

@感觉来了

@嵌入式linux