算法00:选择排序、插入排序、冒泡排序

418 阅读5分钟

选择排序、插入排序、冒泡排序,基本上每本面向入门的算法书籍都会讲到,如今它们存在的意义似乎就是为了抛砖引玉,要是有面试官问:实际开发的时候将一些数字排序你会怎么做?如果你来一句用冒泡排序,完了,这基本就等于在说:我没啥实际开发经验,算法也只是听说过一些名词,脑子里就记得有个冒泡排序……

选择排序

维基百科的定义:

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

它是一种原地排序算法,意思就是不需要再定义额外的数组。它比较符合人的直觉:假设你手里已经有了10张牌,要按从小到大的顺序排序,我们应该怎么排呢?首先,我们用肉眼扫一下未排序的10张牌,把1到9号这9张牌中最小的拿出来和0号比较,如果它小于0号则交换(也就是最左边,假设我们将最左边的位置序号记为0,次左边的位置序号记为1,依次类推),然后再用肉眼扫一下未排序的8张牌,把2到9号这8张牌中最小的拿出来和1号位置的牌交换……不断重复该过程,直到完成排序,用Java代码表述就是这样:

public class TestSelectSort {
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            for (var j = i + 1; j < a.length; ++j) {
                if (a[j] < a[i]) {
                    var temp = a[i];
                    a[i] = a[j];
                    a[j] = temp;
                }
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

上述代码还是有优化余地的:内层for循环每次比较都在交换元素,其实可以等找到了最小元素后再交换。改进一下:

public class TestSelectSort {
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            var min = i;
            for (var j = i + 1; j < a.length; ++j) {
                if (a[j] < a[i]) {
                    min = j;
                }
            }
            if (min != i) {
                var temp = a[i];
                a[i] = a[min];
                a[min] = temp;
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

插入排序

和选择排序的场景不同,我们想象一下从桌上起牌的场景:假设我们是左手握牌,右手起牌,开始时左手是空的,然后一张一张的从桌上将牌拿到手里,从左到右从小到大排好:

起第1张牌时,它自然是目前最小的,放在最左边;

起第2张牌时,它和手里的第1张相比,如果它比第1张牌小,则插入到第一张牌前边,第一张牌自然要后移(右移)一个位置;

起第3张牌时,它和手里的已经有的2张牌相比,右手拿着这个新起来的牌和左手里已有的2张牌从右到左逐一对比,假设它比第2张牌小,则继续和第1张牌对比,假设它比第1张牌小,则它插在第1张牌前边,后面2张牌要后移(右移)一个位置;

起第n张牌时,依次类推……

Java代码:

public class TestInsertSort{
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            var pos = -1;
            for (var j = i - 1; j >= 0; --j) {
                if (a[i] < a[j]) {
                    pos = j;
                } else {
                    break;
                }
            }

            if (pos >= 0) {
                var v = a[i];
                for (var k = i - 1; k >= pos; --k) {
                    a[k + 1] = a[k];
                }
                a[pos] = v;
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

代码完全是按照插入排序的场景来写的,外层for循环表示起牌,a[i]表示当前起到的牌,内层第1个for循环就是在左手里排好序的这些牌里找到合适的位置,好让a[i]这张牌插入,用pos记录了下了这个位置,内层第2个for循环将左手里pos到i-1这些牌向右移动一个位置,然后将a[i]插入。 不可否认上面的代码有点长,有优化的余地,毕竟第1个内层for循环在比较的时候就可以移动了:

public class TestInsertSort{
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            for (var j = i - 1; j >= 0; --j) {
                if (a[j + 1] < a[j]) {
                    var tmp = a[j + 1];
                    a[j + 1] = a[j];
                    a[j] = tmp;
                } else {
                    break;
                }
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

上面的代码还有优化的余地,内层for循环每次交换都要执行三次赋值语句,其实我们完全可以将目标元素a[i]先暂存起来,内层for循环结束后再将它插入到合适位置,代码如下 :

public class TestInsertSort {
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            var tmp = a[i];
            var j = i - 1;
            for (; j >= 0 && a[j] > tmp; --j) {
                a[j + 1] = a[j];
            }
            a[j + 1] = tmp;
        }
        System.out.println(Arrays.toString(a));
    }
}

冒泡排序

本科计算机相关专业的学生估计在学习数据结构或算法相关课程的时候,老师一般都会讲过这个算法。基本思路是这样:

第1轮:从第1个元素开始,相邻的元素做比较,如果前边的元素比后边的元素大,则二者交换,这样比到最后一组元素,结束好,最大的元素一定会处于最后的位置,这样看来,冒泡排序其实也是沉底排序:大的元素在慢慢沉下去。

第2轮:依然从第1个元素开始两两比较,只不过比到倒数第2个元素就行了,毕竟上一轮已经把最大的元素沉底了。

第3轮:依次类推……

Java代码:

public class TestBubbleSort{
    public static void main(String[] args) {
        var a = new int[]{2, 1, 23, 11, 9, 5, 7};
        for (var i = 0; i < a.length; ++i) {
            for (var j = 0; j < a.length - i - 1; ++j) {
                if (a[j] > a[j + 1]) {
                    var tmp = a[j + 1];
                    a[j + 1] = a[j];
                    a[j] = tmp;
                }
            }
        }
        System.out.println(Arrays.toString(a));
    }
}

下一篇文章我们介绍希尔排序。