数据结构与算法8-排序算法:插入排序、希尔排序、归并排序

发布于:2022-12-16 ⋅ 阅读:(294) ⋅ 点赞:(0)

目录

排序算法分析方面

插入排序

希尔排序

归并排序

算法稳定性


排序,是一个大的内容,也是很重要的一个东西,我们在平常的开发中也很可能会遇到

排序算法分析方面

目前出现的排序算法有很多,那,我们应该从哪些方面来分析一个排序算法

1、时间效率,决定算法运行多久

2、空间复杂度

3、比较次数或者交换次数

4、稳定性,同样的数据,相对位置是不变的

扩展解释一下,第三点,比较次数或者交换次数,比较或者交换,也都是需要资源的,所以也在分析范围内

而第四点,我们举个例子,比如 1,21,2,2,7,11,排序出来是1,2,2,7,11,21

有两个2,如果我们给前面的2标记为2.1,后面标记为2.2,注意,是标记,并非改变他们的值

那么原数字序列就变成了1,21,2.1,2.2,7,11,排序结果会是1,2.1,2.2,7,11,21,也会是1,2.2,2.1,7,11,21

所谓相对位置不变,就只有结果是1,2.1,2.2,7,11,21的符合这个稳定性

那这个稳定排序有什么意义呢?有什么应用呢?

我们做这样一个场景,假设外部系统给我们一批信息,这批信息先按照用户名的长度来排序,用户名长度相同的,按照注册时间再次排序,假设外部系统给我们的时候,已经按照注册时间排序好了,我们只需要按照用户名再排序就好了

如果这个时候选择了不稳定的排序算法,那就得比较两个字段,要比两次,如果选择稳定的排序算法,那就只需要比较一个字段值就好

插入排序

什么是插入排序,我们类比一个很形象的场景,打扑克,打牌

打牌的时候有两个地方的牌,一个是手中的排序好的,一个是要拿的等待排的牌

所以,插入排序,就是把无序的数列一个个插入到有序的数列中

一个有序的数组,我们往其中插入一个新的数据后,只要遍历这个数组,找到数据应该插入的地方插入,就可以保持数据的有序

插入排序的思路就是,往一个有序的集合里插入元素,插入后仍然有序

步骤可以参考如下

1、将数组分为已排序和未排序,初始化的时候,已排序只有一个元素

2、从未排序取元素插入到已排序,并且保证插入后已然是有序的

3、重复执行操作,直到所有元素都排序完成

我们举个例子写一下

/**
 * @author create by YanXi on 2022/9/13 16:03
 * @email best.you@icloud.com
 * @Description: 插入排序
 */
public class InsertSort {

    public static void main(String[] args) {
        int a[] = {1, 4, 12, 6, 0, 23, 1, 6, 8, 23, 1, 6};
        int n = a.length;
        // 这儿循环从1开始,因为已经排序的刚开始就没有,第一个进来没有和谁比较的,所以直接就从1开始
        for (int i = 1; i < n; i++) {
            int data = a[i];
            int j = i - 1;
            // 开始比较,从后往前比较
            for (; j >= 0; j--) {
                if (a[j] > data) {
                    a[j + 1] = a[j];
                } else {
                    break;
                }
            }
            a[j + 1] = data;
        }
        for (int i = 0; i < n; i++) {
            System.out.print(a[i] + ",");
        }
    }

}

关于这个从后往前,我们这儿来仔细分辨一下,所谓从后往前,其实就是说,后一位往前比较,比如我们画个图

 结合这个代码,就是说,第0位的45不动,从第1位的15开始比较

第一个循环,a[i] 拿到第1位的数值15

第二个循环,因为设置了 j = i -1,所以 a[j] 拿到的是第0位的45, a[j + 1] 其实就是 15,也就是15

所以,第二个循环的判断就是

if (45 > 15) {

        15的位置赋值45

}


然后 j--,一直往前对比,如果前一位不比它大,那就直接结束,因为前面是有序的,这个都不比它大,那前面的更不可能比它大了

第二层循环结束

注意,这个时候15这个值是放在内存里的,就是data

a[j + 1]就是15位置的前一位,也就是索引0,将data给0,那就是0索引位给数值15

换个数组也是一样的

上来第一次第一个循环,拿到了 data是111

第一次第二个循环,a[j]拿到了33,33不大于111,所以走else,索引1位置赋值111,没变化

-----

第二次第一个循环,拿到了data是5

第二次第二个循环,a[j]拿到了111,111大于5,走第一个if,5的位置赋值111,也就是索引2位置赋值111

j--,a[j]拿到了33,33大于5,a[j+1]对应的位置是索引1的位置,也就是说,111赋值33

继续,j--,小于0了,退出第二个循环

a[j+1]代表的位置,就是0的位置,将data的数据放到0的位置,我们data拿到的一直是5

大循环结束,排序完成

 所以总结下来其实就是那样,拿着一个位置的值,和这个位置的前面挨个对比,前面比它大,那就把前面的值放在它的位置,这个数据的位置其实变相的已经从原来位置向前挪动了一下,虽然没赋值,然后再继续和前面对比,如果还是前面的大,再把它最初位置前面的前面放到它前面的位置,直到它对比到比它小了,那就把值赋给当前位置

希尔排序

通过上面的插入排序,我们可以看到,如果走了第二次的break多了,那么时间复杂度就能降低

如果有一个很长的数组,最后一位是0,前面都比0大,那么这个0需要对比整个数组,跨越整个数组才能去到正确的位置

而走break的时候,是数组前面已经排完序,也就是说,我们尽可能多的排序,所以就有了希尔排序

希尔排序是插入排序的改进,选取一段跨度进行分组,组内排序,不断组内排序,直到跨度为1

我们举个例子来看,假设有这样一个数组需要排序

11 55 0 18 98 121 185 64 30 21

我们直接除以2取结果,以这个结果作为跨度

对应的分组就是这样的,我们进行组内分别排序

得到这样结果,我们再继续除以2取跨度

我们再组内排序,下来是这样的

再最后一次跨度为1排序

而跨度已经为1,我们直接再来一次就好了,等同于插入排序

我们尝试用代码实现一下

/**
 * @author create by YanXi on 2022/9/15 7:56
 * @email best.you@icloud.com
 * @Description: 希尔排序
 */
public class ShellSort {

    public static void main(String[] args) {
        int a[] = {11, 4, 12, 6, 0, 23, 1, 6, 8, 23, 1, 6};
        int n = a.length;
        for (int add = n / 2; add >= 1; add /= 2) {
            for (int i = add; i < n; i++) {
                int data = a[i];
                int j = i - add;
                for (; j >= 0; j-= add) {
                    if (a[j] > data) {
                        a[j + add] = a[j];
                    } else {
                        break;
                    }
                }
                a[j + add] = data;
            }
        }

        for (int i = 0; i < n; i++) {
            System.out.print(a[i] + ", ");
        }
    }

}

归并排序

先把所有数据进行拆分,分到只有一个的时候,进行合并

/**
 * @author create by Gao YanXi on 2022/9/15 7:57
 * @email best.you@icloud.com
 * @Description: 归并排序
 */
public class MergeSort {

    public static void main(String[] args) {
        int a[] = {11, 4, 12, 6, 0, 23, 1, 6, 8, 23, 1, 6};
        mergetSort(a, 0, a.length - 1);
        System.out.println(a.toString());
    }

    public static void mergetSort(int[] data, int left, int right) {
        // 左边等于右边,说明只有一个数了,就不用拆了
        if (left < right) {
            int mid = (left + right) / 2;
            // 左半部分
            mergetSort(data, left, mid);
            // 右半部分
            mergetSort(data, mid + 1, right);
            merge(data, left, mid, right);
        }
    }

    public static void merge(int[] data, int left, int mid, int right) {
        int tmp[] = new int[data.length];
        // 左边第一个数的位置
        int point1 = left;
        // 右边第一个数的位置
        int point2 = mid + 1;
        // 表示当前我们在临时数组的哪个位置
        int loc = left;
        while (point1 <= mid && point2 <= right) {
            if (data[point1] < data[point2]) {
                tmp[loc] = data[point1];
                point1++;
                loc++;
            } else {
                tmp[loc] = data[point2];
                point2++;
                loc++;
            }
        }
        // 左边不一定走完了
        while(point1 <= mid) {
            tmp[loc++] = data[point1++];
            loc++;
        }
        // 右边不一定走完了
        while(point2 <= right) {
            tmp[loc++] = data[point2++];
            loc++;
        }
        // 只取这个临时数组对应原数组的左右位置就好
        for (int i = left; i <= right; i++) {
            data[i] = tmp[i];
        }
    }

}

能看明白上面代码不,其实就是拆分,然后用递归的方式,拆到不能拆,然后各个不能拆的分段开始排序,最后回并的时候,一个大的排序

这儿主要是两个地方比较绕,第一处是递归各自拆分各自的,第二处是各个分段排序的时候对于分开后左右两端的补充处理

一定要注意,左右两端从中间分开,不一定左边或者右边处理完了,很可能左边或者右边没处理完,所以后边加上左边右边的单独处理,毕竟我们第一个while循环判断的是左边与上右边

用PPT画一幅图,绿色代表排序合并

我们来看一下归并排序的时间复杂度

看上面的图,拆分是个logn,而合并就执行了一遍,是个n,时间复杂度就是nlogn

算法稳定性

我们来看一下之前说的算法的稳定性

对比一下三个排序算法,插入排序是稳定的,归并排序是稳定的,希尔排序是不稳定的

但这不是绝对的

插入排序的这儿,加个等于号,就不是稳定的了

至于希尔排序,那是因为上来要分组,天知道不同组会不会换位置,所以并不稳定

这次就先整理总结这三个排序算法,后续整理其他的排序算法

嘛,拜~

本文含有隐藏内容,请 开通VIP 后查看