Live2D

拓展必学1.1 常见排序方法

今天遇到了需要排序的题目,我们已知的就是用已有的算法进行排序,也就是使用Arrays.sort(nums[]),这个算法本质是快排,在算法比赛的时候可以使用,但是如果需要手撕代码就不可以了,所以我们需要积累常见的数组排序方法。

一、冒泡排序(改进版)

基本思想:
      冒泡排序(Bubble Sort)是一种简单的排序。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有需要交换,也就是说该数列已经排序完成。


算法描述
1.比较相邻的元素,如果第一个比第二个大,就交换他们两个
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

 

具体代码如下:

public static void BubbleSort(int[] numbers){
        //外层循环控制比较次数
        for (int i=0;i<numbers.length-1;i++){

            int flag=0;//默认标记为0
            //内层循环控制到达位置
            for (int j=0;j<numbers.length-1-i;j++){
                //比较前面的元素比后面元素大交换,同理也可以比较小
                if (numbers[j]>numbers[j+1]){
                    int temp=numbers[j];
                    numbers[j]=numbers[j+1];
                    numbers[j+1]=temp;
                    flag=1;//如果还有交换,标记为1
                }
            }

            if (flag==0){//如果没有交换过的元素,则已经有序了
                return;
            }
        }
    }

//调用
        int[] n={60,38,5,14,7,23,89,77,88,4,35,45,67,99,87};
        System.out.println(Arrays.toString(n));
        BubbleSort(n);
        System.out.println(Arrays.toString(n));
        
      //结果
System.out: [60, 38, 5, 14, 7, 23, 89, 77, 88, 4, 35, 45, 67, 99, 87]
System.out: [4, 5, 7, 14, 23, 35, 38, 45, 60, 67, 77, 87, 88, 89, 99]

 

需要注意的是:这个是冒泡排序改进版,也就是在内部循环的时候,可能会出现内部已经排好了,但是外部还在循环的情况(因为设置需要循环length-1次),所以这个时候我们设置一个flag,如果内部没有改动,则跳出循环,减少循环次数。

【易错】同时有个易错点!!内部循环的时候,是使用 j 变量,也就是内层循环的变量,不要用外层 i

 

二、快速排序

这个排序方法相对复杂一些,所以需要充分理解,这里详细说明一下:

分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从找一个小于6的数,再从找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边,让哨兵j指向序列的最右边。同时永远设置序列最左边为基准数。

这里写图片描述

 

首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要。哨兵j一步一步地向左挪动(即j--),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。

现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:

6 1 2 5 9 3 4 7 10 8

这里写图片描述


这里写图片描述

到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:

6 1 2 5 4 3 9 7 10 8

这里写图片描述
这里写图片描述

第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:

3 1 2 5 4 6 9 7 10 8

 

 

 

 

这里写图片描述

到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。


现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。

左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧

如果你模拟的没有错,调整完毕之后的序列的顺序应该是:

2 1 3 5 4

OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下:

1 2 3 4 5 6 9 7 10 8

对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下

1 2 3 4 5 6 7 8 9 10

到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面用图片来描述下整个算法的处理过程。

这里写图片描述

快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。其实快速排序是基于一种叫做“二分”的思想。

 

实现代码如下:

public static void quickSort(int[] nums, int left, int right) {

        if(left>right) return;
        int i = left;
        int j = right;
        
        int basic = nums[i];
        while(i < j) {
            //寻找比basic小的数,相等的不要管
            while(i < j && nums[j] >= basic) {
                j--;
            }
            
            //寻找比basic大的数,相等的不要管
            while(i < j && nums[i] <= basic) {
                i++;
            }
            
            if(i < j) {
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
        }
        //现在i=j,不管什么情况都是
        nums[left] = nums[i];
        nums[i] = basic;
        
        quickSort(nums, left, i - 1);
        quickSort(nums, i + 1, right);
    }

需要注意的是,在循环中交换完比basic大的值和小的值之后不用移动 i 和 j ,因为交换之后不会出现死循环了。

在一轮交换完之后,会出现左边的序列比basic都要小,右边的序列比basic大,但是等于basic的不一定(可能在左,可能在右)

 

 


三、直接插入排序

实现的主要原理如下图所示:

算法描述
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1.从第一个元素开始,该元素可以认为已经被排序
2.取出下一个元素,在已经排序的元素序列中从后向前扫描(从第2个元素开始和前面已经排序好的比较)
3.如果该元素(已排序)大于新元素,将该元素移到下一位置
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
5.将新元素插入到该位置后
6.重复步骤2~5

 实现代码如下:
public void insertSort(int[] nums){
        for(int i = 0;i<nums.length - 1;i++){
            for(int j = i+1;j>0;j--){
                if(nums[j]<nums[j-1]){
                    int temp = nums[j];
                    nums[j] = nums[j-1];
                    nums[j-1] = temp;
                }
                else{
                    break;
                }
            }
        }
    }

需要注意的是,外层循环要从0循环到倒数第二个数,因为内层循环是外层的后一个元素。内层循环是从后往前进行比较,所以是注意是减减(--)。

排序算法中In-place和Out-place是什么意思?

排序算法中,我们可能会遇到In-place和Out-place

in-place 占用常数内存,不占用额外内存

out-place 占用额外内存

In-place

假如问题规模是n,在解决问题过程中,只开辟了常数量的空间,与n无关,这是原址操作,就是In-place。

冒泡排序中,为了将arr排序,借用了一个temp的临时变量,开辟了一个临时空间,这个空间是常数量,这就是in-place。

 

Out-place

如果开辟的辅助空间与问题规模有关,则是out-place。
拿上面的例子来说,假设你把排序时把数组中的数按顺序放入了一个新的数组,我就开了一个n规模大小的数组,这个就与数据规模有关。


 

四、希尔排序(插入排序升级版)

整体思路:

1、首先确定要将一组序列分为几组,默认一般都设置为长度的一半(length/2个组)

2、然后对各组中的数字采取直接插入排序的操作(比较num[j]和num[j-gap]),使每个组内的顺序是排好的。

3、然后将小组数量继续减半(每个组内的小组数量加倍了)

4、继续执行步骤 2 ,注意是对整个小组内的数据进行排序,不是只排一次。

5、直到gap=1(也就是只剩下一组)的时候,对整体进行了直接插入排序,至此结束。

 代码如下:

public void shellSort(int[] nums){
        for(int gap = nums.length / 2;gap>0;gap /= 2){
            for(int i = gap;i<nums.length;i++){
                for(int j = i;j - gap >= 0 && nums[j] < nums[j - gap];j-=gap){
                    int temp = nums[j];
                    nums[j] = nums[j - gap];
                    nums[j - gap] = temp;
                }
            }
        }
    }

这里注意几点:首先是gap减少的方式是本身除2。j是比较的对象,j-gap是为了继续比较组内前面的元素(就和直接插入排序一样),j-gap>=0是为了不要超出数组的范围,=0是因为会比较第一个元素。

 

 

 

 五、简单选择排序

这个排序比较简单,基本思路是:

1、从第一个元素开始,假定它是最小的,然后进行循环,依次查看后面的元素。

2、如果后面的元素比这个元素小,则进行标记,如果比这个元素还小,则标记这个元素,直到找到最小的元素。

3、将最小的元素和第一个元素进行交换,然后再从第二个元素开始,重复执行1、2、3操作。

 

 

代码实现如下:

public void selectSort(int[] nums){
        for(int i = 0;i<nums.length;i++){
            int min = i;
            for(int j = i + 1;j<nums.length;j++){
                if(nums[j] < nums[min]){
                    min = j;
                }
            }
            if(min!=i){
                int temp = nums[min];
                nums[min] = nums[i];
                nums[i] = temp;
            }
        }
    }

 

posted @ 2023-05-02 22:33  江洋国际  阅读(153)  评论(0)    收藏  举报