排序算法列举以及优化

排序算法目前有很多算法,很多文章也做了一些讲解,我也会为了自己能有一份记录和回溯,才记录,直接开胃菜吧!

一、冒泡排序

针对这个算法大家已经耳熟能详了,就是挨个元素和后面的元素进行对比,大的就和后面的元素进行位置替换,这样每一轮都能保证当次循环的最大的元素会在右边,知道循环结束,上算法代码:

static void popPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 39, 30};

    for (int i = nums.length - 1; i > 0; i--) {
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
            }
        }
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + 、");
    }

}

打印如下:1、4、8、8、9、10、12、20、30、39、

首先我们现在的这个算法假如有10个元素,那么就会for循环执行9次,虽然每次for循环的元素个数在减1,不过依旧可以看到有很多可以提升的空间。

场景1:比如说当我们在第三次就已经把序列排好了,那么就不用继续再排了,我们做一下改进:

static void popPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 39, 30};
    for (int i = nums.length - 1; i > 0; i--) {
        System.out.println("lcs排序一次");
        //1
        boolean isNext = false; // 声明一个变量来控制后续是否还需要进行遍历冒泡
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                //2
                isNext = true;
            }
        }
        //3
        if (!isNext) {
            break;
        }
    }
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

代码中我们加了一个变量isNext,每一次循环都初始化为false,然后如果当前for循环没有进行元素置换那说明现在的顺序已经排好了,我们在2的地方把变量设置为true,在3处直接break就好了,打印如下:

排序一次
排序一次
排序一次
1、4、8、8、9、10、12、20、30、39、

可以看到我的“排序一次”打印了三次,正常要打印9次,大大的提高了效率。

场景2:如果序列中最后的几个元素的顺序已经是排好的了,不一定是一开始就这样,也许是在其中的某一次出现这种情况,那么我们的优化点是忽略最后几个连续的元素,我们只关注前面不连续的元素,也在一定程度上节约了性能,增加了排序的速度,代码如下:

static void popPaiXu2() {
    int[] nums = {1, 12, 8, 4, 10, 8, 9, 20, 21, 22};

    for (int i = nums.length - 1; i > 0; i--) {
        System.out.println("lcs排序一次");
        //1
        int lastIndex = 0;  // 声明一个index,记录最后一次进行冒泡替换的索引值,下一次直接从这里进行for循环,20\21\22本身就是有序的,所以无需进行for循环从而直接过
        for (int j = 0; j < i; j++) {
            if (nums[j] > nums[j + 1]) {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
                //2
                lastIndex = j + 1; // 把下一次要外部for循环的索引值存储下来
            }
        }
        //3
        i = lastIndex;
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }

}

核心点是我们在1处声明了一个索引值,初始化为0,然后进入一样的冒泡for循环,只要发生元素交换动作我们就把下一个元素的索引保存起来,这样保证在最后一次交换元素操作完成之后我们保存的是交换元素下一个元素的索引值,重要的是3我们直接把索引值赋值给i,也就是外面的for循环,这样for循环就从上一次的位置直接到了上一次最后交换元素的下一个元素的位置,因为中间我们过滤到了一系列连续的元素,我们是不需要进行冒泡运算的了。

结果如下:

排序一次
排序一次
排序一次
1、4、8、8、9、10、12、20、21、22、

我们发现我们的for循环走了三次,同样得到了优化,节约了性能。

二、选择排序

选择排序的算法也是非常简单,和冒泡不一样的是我们需要设定一个maxIndex索引,来保存我们最大值的索引,怎么获取最大值的索引呢,也是利用两次for循环,依次拿第一个元素的值和后面的元素挨个作对比,发现有比它大的了,就把索引值当前比较大的元素的索引值赋值给maxIndex变量,这样一轮一下我们就找到了当前for循环的最大的数值,然后进行元素的替换,把最大值的元素放到数组的最后,代码如下:

static void selectPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};
    for (int i = nums.length - 1; i > 0; i--) {
        int maxIndex = 0;
        for (int j = 0; j < i; j++) {
            if (nums[maxIndex] <= nums[j + 1]) {
                maxIndex = j + 1;

            }
        }
        int temp = 0;
        temp = nums[maxIndex];
        nums[maxIndex] = nums[i];
        nums[i] = temp;
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

结果如下:1、4、8、9、10、12、20、21、22、88、

三、插入排序

插入排序是一种非常有意思且比较高效的排序方法,同时插入排序是希尔排序的基础,现在我们来看看插入排序

核心的思想就是在原数组的基础之上创建了子数组的思想,刚开始的时候数组是无序的,其实还有一个有序的数组在前面,只是我们看不到,因为它目前是空的而已。于是我们从无序的数组之中选中一个元素插入到这个有序的数组之中即可,起初在无序之中的第一个数组也可以称之为有序数组之中的起始数组的第一个元素,OK,因为只有一个元素,所以也是有序的。于是我们依次从无序的数组之中拿取元素插入到有序的数组之中适当的位置即可,代码如下:

static void chaRuPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};

    int cur;
    for (int i = 0; i < nums.length - 1; i++) {
        //1
        cur = nums[i + 1];
        //2
        int pre = i;
        while (pre >= 0 && cur < nums[pre]) {
            // 3
            nums[pre + 1] = nums[pre];
            pre--;
        }
        //4
        nums[pre + 1] = cur;
        
        for (int k = 0; k < nums.length; k++) {
            System.out.print(nums[k] + "、");
        }
        System.out.println(" ");
    }

    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

首先我们获取最靠近有序数组的元素也就是nums[i + 1]这个元素,保存起来,然后保存当前的索引值到pre之中,3然后拿当前的元素依次和有序数组之中的元素进行遍历比较,当发现小于有序数组之中的元素的时候,就把大的元素赋值给后面的元素也就是我们当初选定的要插入到有序数组之中的那个值的位置,然后pre--,直到发现大于有序数组之中的值或者数组织中第一个值的时候,我们while也就结束了,然后最最重要的一步是我们要把有序数组之中当前小于我们起初的那个值也就是cur赋值成cur,因为每一次命中while循环的时候都会出现两个一样的值,也就是nums[pre + 1] 和nums[pre]是相同的,当最后一次命中之后while循环就结束了,我们需要再一次把nums[pre + 1]进行最终的元素交换,也就是赋值我们起初的那个cur的值,这个cur也就是真正的找到了自己的位置了,我们也实现了一次插入数组的操作,结果如下:

1、12、8、4、10、88、9、20、21、22、 
1、8、12、4、10、88、9、20、21、22、 
1、4、8、12、10、88、9、20、21、22、 
1、4、8、10、12、88、9、20、21、22、 
1、4、8、10、12、88、9、20、21、22、 
1、4、8、9、10、12、88、20、21、22、 
1、4、8、9、10、12、20、88、21、22、 
1、4、8、9、10、12、20、21、88、22、 
1、4、8、9、10、12、20、21、22、88、 

上面这个打印是每一次的变化过程,while循环中的数据变化我列了一下前面几个元素的变化规律,如下:

////        1、8、12、4;
////        1、8、12、12;
////        1、8、8、12;
////        1、4、8、12;
////        1、4、8、12、10;
////        1、4、8、12、12;
////        1、4、8、10、12;

每一次while命中之后都会出现两个一样的元素,这也是为什么要在最后我们要把cur值还原,不然就会丢失了,也就失去了插入排序的意义了。

最终结果如下:

1、4、8、9、10、12、20、21、22、88、

四、希尔排序

希尔排序的算法的基础是插入排序,希尔排序又称缩小增量排序,是希尔本人提出的,所以自己就命名为希尔排序了。它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。

希尔排序就是按照一定的gap值,不断地对数组进行插入排序。不一样的希尔排序算法可能采用不一样的gap值。经典希尔算法的gap值为N/2, N/4, ...... 直到gap值为1,这里的N为数组的长度。

代码如下:

static void xiErPaiXu() {
    int[] nums = {1, 12, 8, 4, 10, 88, 9, 20, 21, 22};
    int gap = nums.length;
    while (gap > 1) {
        //1
        gap = (int) Math.floor(gap / 2);
        System.out.println("gap - " + gap);
        int cur;
        for (int i = gap; i < nums.length; i++) {
            //2
            cur = nums[i];
            //3
            int pre = i - gap;
            while (pre > 0 && cur < nums[pre]) {
                //4
                nums[i] = nums[pre];
                pre -= gap;
            }
            //5
            nums[pre + gap] = cur;
        }
        for (int k = 0; k < nums.length; k++) {
            System.out.print(nums[k] + "、");
        }
        System.out.println(" ");
    }
    for (int i = 0; i < nums.length; i++) {
        System.out.print(nums[i] + "、");
    }
}

我们在1处得到要分成的列数,总的元素是10个,所以第一次的列数是5,每个组是两个元素进行比较,大的放到右边,小的放到左边,这一次下来基本上大的小的就一左一右分开了。在2处获取当前元素的数值,这个跟插入排序是一样的算法,在3处获取上一个元素的索引,我们以gap为长度依次递减的,然后进行while循环,cur当前元素值依次和有序序列进行比较(插入排序有提到),从而在5的地方cur会插入到有序序列的适当的位置,然后gap为1,也就是只有一列了,此时的数组已经比较接近有序序列了,只要再进行一次插入排序,稍微的做一下调整即可达到有序序列了。

结果如下:

1, 12, 8, 4, 10, 88, 9, 20, 21, 22 //原数组

gap - 5
1、9、8、4、10、88、12、20、21、22、 第一次
gap - 2
1、4、8、9、10、20、12、22、21、88、 第二次
gap - 1
1、4、8、9、10、12、20、21、22、88、 第三次

为什么希尔排序要比插入排序高效呢?因为当数组长度很大时,使用插入排序就会有个弊端,就是如果最小值排在很末端的时候,插入排序需要从末端开始,逐个往前比较到第一个位置,很低效。而希尔排序通过分组的方式,直接让前端跟末端的元素进行比较,解决了插入排序的这个弊端。

当一开始 gap 很大的时候,每一个子数组的元素是很少的,所以对每个子数组用插入排序进行内部排序是很高效的;而后随着gap不断减小,这个数组会变的越来越有序了,此时使用插入排序也是很有利的了。

谢谢观看!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章