常见排序算法(一)(冒泡排序、插入排序)

相关文章:

常见排序算法(零)(各类排序算法总结与比较)

常见排序算法(一)(冒泡排序、插入排序)

常见排序算法(二)(选择排序)

常见排序算法(三)(快速排序、归并排序、计数排序)

常见排序算法(四)(基数排序、桶排序)


冒泡排序(Bubble Sort)

       冒泡排序是最简单的一种排序算法,基本的冒泡排序用两重循环遍历数组,数组中相邻的两个数进行比较,如果前一个数比后一个数要大,则两个数交换。通过这种方法,最大的数会慢慢地沉到数组的底部。该算法的基本思路如下:

1、遍历整个数组[0, n – 1](n为数组长度),相邻的数两两进行比较,如果前一个数比后一个数大,那么两个数互换,直到最后一个数。

2、再次遍历数组,范围为[0, n – 2],最后一个数不需要比较,因为它已经是整个数组里面最大的一个了,相邻的数两两进行比较,如果前一个数比后一个数大,那么两个数互换,直到最后一个数。

3、重复第二步,直到n – i和0相等,这时候只剩下一个数,没有必要再进行比较,终止循环,排序结束。

该算法的java实现:

//  data为待排序的数组引用
for (int i = data.length - 1; i > 0; i--) {
    for (int j = 0; j < i; j++) {
        if (data[j + 1] < data[j]) {
            swap(j, j + 1);  //  交换两个数
        }
    }
}


改进版的冒泡排序

       冒泡排序可以做一点改进让它的速度稍微快一点,设想,当某一次循环中,两两比较之后,没有一次交换发生,意味着此时数组已经排好序了,这个时候在进行之后的循环是没有意义的,纯粹是浪费时间,所以在这个时候应该结束循环,排序结束。

改进版冒泡排序的java实现:

boolean sign = true;  //  标志位,初始化为true
for (int i = data.length - 1; i > 0; i--) {
    for (int j = 0; j < i; j++) {
        if (data[j + 1] < data[j]) {
            swap(j, j + 1);
            sign = false;  //  如果发生交换,说明数组仍为无序,置标志位为false
        }
    }
    //  如果标志位为true,说明上一次循环中没有交换发生,排序可以结束了
    if (sign) {
        return;
    }
}

       基本版的冒泡排序的时间复杂度最好和最差情况下都是O(n²),改进版的冒泡排序最好情况下时间复杂度可以达到O(n),最差情况下时间复杂度仍为O(n²)。两个版本的冒泡排序平均时间复杂度都为O(n²),空间复杂度为O(1),因为冒泡排序不占用多余的空间。

       冒泡排序是一种原地排序(In-place sort)并且稳定(stable sort)的排序算法,优点是实现简单,占用空间小,缺点是效率低,时间复杂度高,对于大规模的数据耗时长。


插入排序(Insertion Sort)

       插入排序分为直接插入排序,二分插入排序,希尔排序。其中直接插入排序和二分插入排序是稳定的算法,希尔排序是不稳定的算法。


直接插入排序

       通过设置哨位,每次将哨位中的数插入哨位前已经排序好的序列,得到新的排序好的序列,然后哨位递增,直到哨位挪动至待排序序列的最后一位为止。

基本思路:

1、设置哨位i = 1

2、将a[i]与a[0]~a[i – 1]的序列从后往前进行比较,如果哨位位置的数比哨位前一位要小,那么两个位置的数交换,标志待排序数的标志位递减。注意,这里需要一个标志位来标志待排序的数挪动到哪个位置,而不是挪动哨位。

3、如果标志位的数比标志位前一位的数要小,则一直交换,直到标志位的数不比标志位前一位的数小或者标志位已经挪动到数组最前端为止(即标志位为0)。

4、递增哨位,重复第二步,直到哨位挪动至数组最末端,排序结束

直接选择排序的java实现:

for (int key = 1; key < data.length; key++) {
    for (int i = key; i > 0; i--) {
        if (data[i] < data[i - 1]) {
            swap(i - 1, i);  //  交换两个位置的数
        } else {
            break;
        }
    }
}

       直接选择排序的最差时间复杂度为O(n
2),最优时间复杂度为O(n),平均时间复杂度为O(n
2),空间复杂度为O(1),优点是实现简单,不占用多余空间,但效率较低,并且经常需要交换数据。


二分插入排序

       二分插入排序与直接插入排序的不同在于:直接插入排序插入数据的方式是将数据与前面排好序的数据从后往前逐个比较,如果比它小就交换,从而将数据挪动到正确地位置;二分插入排序插入数据的方式是先使用二分查找的方式找到数据应该插入的位置,然后将该位置的数及其之后的数向后移,再将数据插入该位置。

基本思路:

1、设置哨位i = 1;

2、使用二分查找查找序列[0, i – 1]中应该插入哨位位置的数的位置,假设位置为pos,那么将[pos, i – 1]位置的数都向后挪动一位,然后让pos位置的数等于哨位位置的数。注意,此时应提前将哨位位置的数取出,否则哨位位置的数会被前一个数覆盖掉。

3、递增哨位,重复第二步,直至哨位到达数组末端。

二分插入排序的java实现:

//  从第一位开始二分插入排序
for (int key = 1; key < data.length; key++) {
    binarySort(key);
}
/**
 * 二分查找key下标的值应该插入在哪个位置并插入
 * @param key
 */
private void binarySort(int key) {
    int temp = data[key];
    int beginPos = 0;  //  查找开始的位置
    int endPos = key - 1;  //  查找结束的位置
    while (endPos - beginPos > 1) {  //  如果数组分割到两端相差小于等于1的时候,跳出循环
    int middle = (beginPos + endPos) / 2;  //  找到中间位置
        //  如果中间位置的数比哨位位置的数要小,那么说明哨位位置的数应该在中间位置的右边,改变beginPos的值
        if (data[middle] < temp) {
            beginPos = middle;
        } else {
            //  如果中间位置的数比哨位位置的数要大,
            //  那么说明哨位位置的数应该在中间位置的左边,改变endPos的值
            endPos = middle;
        }
    }
    if (temp < data[beginPos]) {  //  如果temp值比beginPos小,那么应该插在beginPos前面
        moveData(beginPos, key);
    } else if (temp < data[endPos]) {  //  如果temp值比endPos小,那么应该插在endPos前面
        moveData(endPos, key);
    } else {    //  如果temp值比endPos大,那么应该插在endPos后面一位
        moveData(endPos + 1, key);
    }
}
/**
 * 将数组中从begin到end位置的数循环右移一位
 * @param begin
 * @param end
 */
private void moveData(int begin, int end) {
    int temp = data[end];  //  先取出最后一位,防止被覆盖掉
    for (int i = end; i > begin; i--) {  //  从后开始,使后一位的数等于前一位的数
        data[i] = data[i - 1];
    }
    data[begin] = temp;  //  让第一位等于之前取出的最后一位
}

       二分插入排序的平均时间复杂度仍然为O(n2),空间复杂度为O(1),挪动的次数不变,只是减少了比较的次数,时间上会比直接插入排序要快一些,但时间复杂度不变。

希尔排序(Shell Sort)

       希尔排序是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因DL.Shell于1959年提出而得名。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

基本思路:

1、设置每段中数据间隔的大小,一般初始化设置为待排序数组长度的一半

2、将每段中的数据进行直接插入排序,得到有序数组

3、将增量减半,得到新的分段序列,重复第二步,直到增量小于1。

下面举个例子说明一下希尔排序的工作过程:

初始序列:42, 75,65, 2, 23, 12, 12*

第一次分段,以3为增量(7 / 2 = 3),分为三段,[42, 2, 12*]、[75, 23]、[65, 12]

第一次分段后进行直接插入排序,然后合并:2、12*、42、23、75、12、65

然后以1(3 / 2 = 1)为增量,进行直接插入排序,得到2、12*、12、23、42、65、75,排序完成。可以看到12和12*的相对位置发生了变化,因此,希尔排序是不稳定的排序算法。

希尔排序的java实现:

//  增量设置为待排序数组长度的一半,之后不断折半,直到等于1
for (int cap = data.length / 2; cap > 0; cap = cap / 2) {
    //  从数组下标为0的位置开始,到增量长度大小的位置停止,这其中每个数都是希尔排序分段中每一段的开头
    for (int i = 0; i < cap; i++) {
        //  从这里开始,是对每一段进行直接插入排序
        //  由于希尔排序是不占用多余空间的,而且将这些数虽然被分为一段,但是在待排序的数组中是分散的,
        //  并且还不知道分段的最后一位是在哪一位,所以综合考虑,只能是使用直接插入排序来使它们有序
        //  key是每一段开头的下一个数,这里的下一个是指分段中的下一个,分段中每一个数都是不连续的
        for (int key = i + cap; key < data.length; key = key + cap) {
            for (int j = key - cap; j >= 0; j = j - cap) {
                if (data[j + cap] < data[j]) {  //  如果后一位比前一位小,那么交换这两个数
                    swap(j + cap, j);
                } else {  //  否则,无需继续比较,跳出循环,记得加上这一句,不然很浪费时间
                    break;
                }
            }
        }
    }
}

       希尔排序的最差时间复杂度是O(n
2),最优时间复杂度是O(n),平均时间复杂度为O(n
1.5),需要强调的是,希尔排序的平均时间复杂度与增量的选择有关,在我的实现中使用的是折半的方法,不一定是最好的增量序列,查阅资料表明,当增量之间成倍数关系时效率其实是不高的。希尔排序的时间复杂度比快速排序要高,它的优势在于平均情况下和最差情况下的表现接近,在这点上比快排有优势,并且易于实现。因此有资料表明,任何排序工作在一开始都可以用希尔排序来做,在表现不佳的时候,再换成快速排序来做。本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。 原因是,当n值很大时数据项每一趟排序需要的个数很少,但数据项的距离很长。当n值减小时每一趟需要和动的数据增多,此时已经接近于它们排序后的最终位置。正是这两种情况的结合才使希尔排序效率比插入排序高很多。(以上说明部分来自于百度百科)。

本文所使用的java代码已上传至github,为java project:https://github.com/sysukehan/SortAlgorithm.git

    原文作者:排序算法
    原文地址: https://blog.csdn.net/sysukehan/article/details/52661369
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞