算法面试与实战-01排序算法- 时间复杂度为O(n2)的排序算法(冒泡排序深度优化)

hi~数据结构我们基本完结了,当然后面有新的知识会再添加。现在开始学习算法。本来想先写算法思想的,我找了各种对比,发现比较抽象,感觉还是先易后难比较容易入门。

本期我们学习常见的时间复杂度为O(n2)的排序算法

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序

希尔排序比较特殊,他的性能介于O(n^{2}),但是又比不上O(nlog n),暂且归于本类。本篇以比较经典的冒泡排序为例。讲解,后面若是有时间在将其他部分进行补充。

1、冒泡排序

冒泡排序的英文是bubble sort,它是一种基础的交换排序。
大家一定都喝过汽水,汽水中常常有许多小小的气泡哗啦哗啦飘到上面来。 这是因为组成小气泡的二氧化碳比水轻,所以小气泡可以一点一点地向上浮动。

而冒泡排序之所以叫冒泡排序,正是因为这种排序算法的每一个元素都可以像小气泡一样,根据自身大小,一点一点地向着数组的一侧移动。
 

有8个数字组成一个无序数列{5,8,6,3,9,2,1,7},希望按照从小到大的顺序对其进行排序,按照冒泡排序的思想,我们要把相邻的元素两两比较, 当一个元素大于右侧相邻元素时, 交换它们的位置; 当一个元素小于或等于右侧相邻元素时, 位置不变。冒泡排序是一种稳定排序,值相等的元素并不会打乱原本的顺序。 由于该排序算法的每一轮都要遍历所有元素,总共遍历(元素数量-1)轮,所以平均时间复杂度是O(n2);这里在看看他的原理,虽然有点简单但是要过一下,温故知新。

  • 首先让5和8比较,发现5比8要小,因此元素位置不变。
  • 接下来让8和6比较,发现8比6要大,所以8和6交换位置。

  • 然后8和3比较,发现8比3大,所以8和3交换位置

.....以此类推,最后得到:

这是我们看到两两比较后会把最大值“冒泡”到最右边,其实这个时候我们可以将9以后所在数据是有序的,可以称之为有序区。比如当我们进行第二次循环比较后,可以将第二大的值移到右边。

如此循环往复,最终可得:

这个时候就是有序的了。冒泡排序是一种稳定排序,值相等的元素并不会打乱原本的顺序。 由于该排序算法的每一轮都要遍历所有元素,总共遍历(元素数量-1)轮,所以平均时间复杂度是O(n^{2})。思路了解了,我们看看代码如何实现。

   public void testBubbleTest() {
        int[] array = {5, 8, 6, 3, 9, 2, 1, 7};
        for (int i = 0; i < array.length - 1; i++) {
            int temp = 0;
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                }
            }
        }
        Arrays.stream(array).forEach(e -> System.out.print(e));
    }

代码非常简单,使用双循环进行排序。 外部循环控制所有的回合,内部循环实现每一轮的冒泡处理,先进行元素比较,再进行元素交换。

冒泡排序还有优化空间,比如上图中,我们在进行第六次排序的时候是有序的了但是在进行一次遍历,这个可以省略。在这种情况下,如果能判断出数列已经有序,并做出标记,那么剩下的几轮排序就不必执行了,可以提前结束工作。
代码优化第二版如下:

 public void testBubbleTest() {
        int[] array = {5, 8, 6, 3, 9, 2, 1, 7};
        for (int i = 0; i < array.length - 1; i++) {
            int temp = 0;
            //标记是否已经有序
            boolean isSorted=true;
            for (int j = 0; j < array.length - 1 - i; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    //进入这里交换位置,说明不是有序 所以isSorted=false
                    isSorted=false;
                }
            }
            //如果是有序就跳出大循环
            if (isSorted){
                break;
            }
        }
        Arrays.stream(array).forEach(e -> System.out.print(e));
    }

这里做了小小的改动,利用布尔变量isSorted作为标记。 如果在本轮排序中,元素有交换,则说明数列无序;如果没有元素交换,则说明数列已然有序,然后直接跳出大循环。但是这样就结束了么,不是这只是优化的第一步,下面我们在仔细研究下:

假设给我们的是这样的一个数组:

我们debug看看代码的执行情况:

当j>=3以后,后面的数组已经是有序的了,但是我们看到代码执行,当j=4后,arrary[4]还是会和array[5]去比较。这个问题的关键点在哪里呢?关键在于对数列有序区的界定。按照现有的逻辑,有序区的长度和排序的轮数是相等的。实际上,数列真正的有序区可能会大于这个长度,因此后面的许多次元素比较是没有意义的。那么如何去避免这个问题呢?我们可以在每一轮排序的最后,记录下最后一次元素交换的位置,那个位置也就是无序数列的边界,再往后就是有序区了。代码如下:

public void testBubbleTest() {
        int[] array = {3, 2, 1, 5, 6, 7, 8, 9};
        //可以将temp提取出去,不需要每次循环重新新建局部变量。
        int temp = 0;
        //记录最后一次交换位置的下标
        int lastExchangeIndex = 0;
        //假设最极端情况下无序下标边界在最后一个元素
        int noSortedIndex = array.length - 1;
        for (int i = 0; i < array.length - 1; i++) {
            //标记是否已经有序
            boolean isSorted = true;
            for (int j = 0; j < noSortedIndex; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    //进入这里交换位置,说明不是有序 所以isSorted=false
                    isSorted = false;
                    //把无序数列的边界更新为最后一次交换元素的位置
                    lastExchangeIndex = j;
                }
            }
            //最后一次交换元素位置标示在j之前是无序的
            noSortedIndex = lastExchangeIndex;
            //如果是有序就跳出大循环
            if (isSorted) {
                break;
            }
        }
        Arrays.stream(array).forEach(e -> System.out.print(e));
    }

看代码可知,其实我们就是对无序的部分进行循环排序。减少无用的循环。没想到冒泡排序还有这么多花样~,算法是无止境的,其实还有优化的空间,各位读者听说过鸡尾酒排序么,这个苗某刚开始也不知道,就知道个鸡尾酒哈哈~

下面我们在看看冒泡排序的再度优化,鸡尾酒排序。

2、鸡尾酒排序

冒泡排序的每一个元素都可以像小气泡一样,根据自身大小,一点一点地向着数组的一侧移动。 算法的每一轮都是从左到右来比较元素, 进行单向的位置交换的。而鸡尾酒排序的元素比较和交换过程是双向的。比如下面一个例子

由8个数字组成一个无序数列{2,3,4,5,6,7,8,1},希望对其进行从小到大的排序。如果是冒泡排序的话:

元素2、 3、 4、 5、 6、 7、 8已经是有序的了, 只有元素1的位置不对, 却还要进行7轮排序,这个就太浪费时间了吧。

鸡尾酒排序是什么样子呢?让我们来看一看详细过程:

  • 第一轮(和冒泡排序一样,8和1交换)

  • 第二轮:此时开始不一样了,我们反过来从右往左比较和交换:8已经处于有序区,我们忽略掉8,让1和7比较。元素1小于7,所以1和7交换位置:

以此类推.....最终成为了下面的结果:

 

  • 第三轮(虽然已经有序,但是流程并没有结束)

鸡尾酒排序的第三轮,需要重新从左向右比较和交换:1和2比较,位置不变;2和3比较,位置不变;3和4比较,位置不变......6和7比较,位置不变。没有元素位置交换,证明已经有序,排序结束。

这就是鸡尾酒排序的思路。排序过程就像钟摆一样,第一轮从左到右,第二轮从右到左,第三轮再从左到右......本来要用7轮排序的场景, 用3轮就解决了。还是挺巧妙的,让我们来看看代码的具体实现吧。

@Test
    public void TestSort() {
        int[] array = new int[]{2, 3, 4, 5, 6, 7, 8, 1};
        cocktailSort(array);
        System.out.println(Arrays.toString(array));
    }

    private void cocktailSort(int[] array) {
        int temp = 0;
        //鸡尾酒排序是左右循环比对。
        for (int i = 0; i < array.length / 2; i++) {
            //有序标记,每一轮的初始是true
            boolean isSorted = true;
            //奇数轮,像冒泡排序一样从左向右
            for (int j = i; j < array.length - i - 1; j++) {
                if (array[j] > array[j + 1]) {
                    temp = array[j];
                    array[j] = array[j + 1];
                    array[j + 1] = temp;
                    //有元素交换
                    isSorted = false;
                }
            }
            if (isSorted) {
                break;
            }

            //偶数轮,在开始之前将isSorted重新设置为true
            isSorted = true;
            //偶数轮,从右向左比较和交换
            for (int j = array.length - 1; j > i; j--) {
                if (array[j] < array[j - 1]) {
                    temp = array[j];
                    array[j] = array[j - 1];
                    array[j - 1] = temp;
                    //因为有元素交换,所以isSortedfalse
                    isSorted = false;
                }
            }
            if (isSorted){
                break;
            }
        }
    }

这段代码是鸡尾酒排序的原始实现。代码外层的大循环控制着所有排序回合,大循环内包含两个小循环,第一个循环从左向右比较并交换元素,第二个循环从右向左比较并交换元素。

之前讲冒泡排序时, 有一种针对有序区的优化, 鸡尾酒排序是不是也能用到呢?答案是肯定的,但是写起来有些麻烦,因为对于单向的冒泡排序,我们需要设置一个边界值,对于双向的鸡尾酒排序,我们需要设置两个边界值。这里称之为正冒泡的边界和反冒泡的边界。具体代码实现如下:

 private void cocktailSortPlus(int[] array) {
        int temp = 0;
        //记录最右侧最后一次交换的位置。
        int lastRightExchangeIndex=0;//默认起始位置
        //记录偶数轮从左向右最后一次交换的位置
        int lastLeftExchangeIndex=0;
        //无序数列的右边界,每次比较只需要比到这里为止。
        int rightSortBorder=array.length-1;
        //设置无序数列的左边界,每次比较只需要比到这里为止。
        int leftSortBorder=0;
        for (int i = 0; i <array.length/2 ; i++) {
            //有序标记,每一轮的初始是true
            boolean isSorted = true;
            //奇数轮,像冒泡排序一样从左向右
            for (int j = leftSortBorder; j <rightSortBorder ; j++) {
                if (array[j]>array[j+1]){
                    temp= array[j];
                    array[j]=array[j+1];
                    array[j+1]=temp;
                    isSorted=false;
                    lastRightExchangeIndex=j;
                }
            }
            rightSortBorder=lastRightExchangeIndex;
            if (isSorted){
                break;
            }
            isSorted=true;
            for (int j = rightSortBorder; j >leftSortBorder ; j--) {
                if (array[j]<array[j-1]){
                    temp=array[j];
                    array[j]=array[j-1];
                    array[j-1]=temp;
                    isSorted=false;
                    lastLeftExchangeIndex=j;
                }
            }
            leftSortBorder=lastLeftExchangeIndex;
            if (isSorted){
                break;
            }
        }
    }

代码中使用了左右两个边界值,rightSortBorder 代表右边界,leftSortBorder代表左边界。在比较和交换元素时,奇数轮从 leftSortBorder 遍历到 rightSortBorder 位置,偶数轮从 rightSortBorder 遍历到 leftSortBorder 位置。

所以鸡尾酒排序比较适合在大部分有序的情况下的排序。大大减少循环的次数。

好了关于冒泡排序的算法就讲到这里了....

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值