老生常谈的直接插入排序、冒泡和选择排序

排序原理

有些排序算法,可以归纳为“有序区扩张,无序区收缩”。所谓有序区,就是在此区间的元素都已经是有顺序的了,无序区则是在此区间的元素都是杂乱无章的。接下来我们先看几个典型的“有序区扩张,无序区收缩”算法。

直接插入排序

首先,选定一端为有序区,然后将其余所有元素归入无序区,比如说有个待排序列是这样的:

[0, 1, 2, 3,  4,  5,  6, 7]  // 下标
[1, 5, 8, 10, 36, 24, 5, 9]  // 元素列表

首先,我们可以选定以下标 0 为有序区,其余所有元素 1 - 7 则都属于无序区,按照有序区扩张,无序区收缩的原则,每次从无序区中选定一个元素加入有序区中,对于直接插入排序来说,则顺序地从无序区中选择一个元素,然后放入有序区中,我们用 | 来分隔有序区和无序区,则刚开始时:

[1, | 5, 8, 10, 36, 24, 5, 9]

然后这样:

[1, 5, | 8, 10, 36, 24, 5, 9]

就这样直到无序区不再有元素:

[1, 5, 8, | 10, 36, 24, 5, 9]
[1, 5, 8, 10, | 36, 24, 5, 9]
[1, 5, 8, 10, 36, | 24, 5, 9]
[1, 5, 8, 10, 24, 36, | 5, 9]
[1, 5, 5, 8, 10, 24, 36, | 9]
[1, 5, 5, 8, 9, 10, 24, 36 |]    // 无序区不再有任何元素,结束

然后转化为核心代码就像这样:

// 参数表示从下标为 begin - end 的元素集合将被排序,下标从 0 开始,包括 begin 和 end
public static void insertSort(int[] data, int begin, int end) {

    // i 表示无序区第一个元素的下标,即等于在无序区中选中了一个元素,相当于无序区在收缩
    for (int i = begin + 1; i <= end; i++) {

        // 那么接下来就是要把这个从无序区中选中的元素放入有序区中,等同于有序区在扩张
        // 如果从无序区中选中的元素已经比有序区最后一个元素大了,也就是不用再找位置了,直接坐下就行!
        if(data[i] < data[i-1]) {

            // 用一个临时值代表这个选中的元素,从而避免需要频繁交换元素
            int tmp = data[i];

            // j 初始化为有序区最后一个元素下标
            int j = i - 1;

            // 怎么放呢?这里提供一种方式:
            // 从有序区逆序遍历,只要有序区中选中的元素不如无序区中选中的元素小,那么自动向前一步
            for (; j >= begin && data[j] > tmp; j--) {
                data[j + 1] = data[j];
            }

            // 经过有序区的大元素让步后,无序区中选中的元素就找到了自己的位置
            // 为什么是 j + 1 呢?因为下标为 j 的元素不满足向前一步的条件,所以是在它前面空了一个位置
            data[j + 1] = tmp;
        }
    }
}

冒泡排序

所谓冒泡排序,就是说像鱼吐泡泡一样,从无序区的第一个元素开始遍历直到无序区再无元素,每个元素都与前一个元素对比,这叫一趟冒泡,每趟都会产生一个元素进入有序区,当无序区再无元素时,待排数列已是有序。可能我说地太抽象了,来看下这张图:

初始时,大家都在无序区,一趟冒泡后,变成了下面这样,有一个元素脱离了无序区,进入了有序区:

就是像这样,一个两个喵,你和我,心连心,手牵手... 咳咳,打个比方,有某桃园三结义某天排队去买奶茶,他们刚开始是这样排的(他们三个都属于无序区):

| 小刘、小张、小关 |

突然大哥觉得自己应该在前面,也就是看上了二弟的位置,于是对三弟说,三弟,站大哥后面来!小张一听,大哥这是想插队啊,但秉着尊老爱幼的美德还是站到了大哥后面,然后就变成了这样:

| 小张、小刘、小关 |

大哥再看,二弟还在前面,不行,再来,于是就变成了这样,大哥成功占领二弟的位置:

| 小张、小关、| 小刘

这里,小刘的这种做法就是典型的一趟冒泡,最终小刘进入了有序区,只留下二弟和三弟在无序区中凌乱

转化为核心代码就像这样:

// 参数表示从下标为 begin - end 的元素集合将被排序,下标从 0 开始,包括 begin 和 end
public static void bubbleSort(int[] data, int begin, int end) {

    // i 表示从无序区中的选出来的元素将要放到有序区中的位置
    for (int i = end; i > begin; i--) {

        // 如果一轮冒泡中元素之间未发生过任何位置交换,那么这序列已是有序的了
        boolean changed = false;

        // 有序区的位置已经确定了,那么接下来就是从无序区中确定谁将坐上那个位置!
        // 怎么确定呢?打一架就行,谁赢谁上,赢者继续打,直到产生一个最终赢家,无疑它就是全场最厉害的
        for (int j = begin; j < i; j++) {

            // 如果打赢了就能够往前一步,继续下一场比赛
            if (data[j] > data[j + 1]) {
                swap(data, j, j + 1);

                // 元素位置发生了交换
                changed = true;
            }
        }

        // 如果没有元素交换过位置,已是有序,结束
        if (!changed) return;
    }
}

选择排序

有了冒泡排序的基础,难道还要慌这个吗?前面说过,冒泡排序是先确定了从无序区中选中的某个元素在有序区的位置,然后开始从无序区中选择一个元素放入这个位置。在冒泡排序中使用的是一个两个喵,啊呸,是擂台赛打架的方式确定最强者,打赢的就往前走,赢者的赢者的赢者...就是走在时代前列的人!但这样打一下走一下实在太费时间了,大家原地打就好了,如果我是最强者,我直接坐上有序区的宝座,就不用走那么多弯路了。

那么,既然这样,代码就变成这个风格了:

public static void selectSort(int[] data, int begin, int end){

    // i 表示本轮有序区的唯一位置,依次扩张
    for (int i = end; i > begin; i--) {
        
        // 假设现在坐在有序区唯一宝座上的是最强者
        int max = i;

        // 底层人物个个不服这最强者,纷纷欲与之一较高下
        for (int j = begin; j < i; j++) {
            
            // 打赢的
            if (data[j] > data[max]){
                // 最强者称号就是你的,但还不能坐上宝座,还得接受别人的挑战,直到大家都打不过你
                max = j;
            }
        }

        // 都打服之后,如果发现最强者不是原来坐在宝座上的人,那么不好意思,你来我的位置,我带着我的荣耀登上这宝座
        if (max != i){
            swap(data, i, max);
        }
    }
}
posted @ 2022-03-27 21:44  lizhpn  阅读(82)  评论(0编辑  收藏  举报