数据结构与算法——十大排序算法

基于LeetCode912题进行各种排序算法的学习。

题目描述:

给你一个整数数组 nums,请你将该数组升序排列。

 示例 :

输入:nums = [5,2,3,1]
输出:[1,2,3,5]

不常用的算法我就不写代码了,了解原理即可!!

时间复杂度:运行算法所需要执行的指令数(时间)。

 十种排序算法时间复杂度,空间复杂度比较

 

 时间复杂度为O(n^2),空间复杂度为1的4种排序:冒泡,选择,插入,希尔

1.冒泡排序

冒泡排序算法的原理如下: 
1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2.对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
3.针对所有的元素重复以上的步骤,除了最后一个。
4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
class Solution {
    public int[] sortArray(int[] nums) {
        //大循环表示一共要进行n-1轮,n表示需要排序的数字个数
        for(int i=0;i<nums.length-1;i++){
            //小循环表示每轮要进行的交换,第一轮交换n-1次,第二轮交换n-2次...
            for(int j=0;j<nums.length-1-i;j++){
                //每次小循环中进行的交换操作
                if(nums[j]>nums[j+1]){
                    int temp = nums[j];
                    nums[j] = nums[j+1];
                    nums[j+1] = temp;
                }
            }
        }
        return nums;       
    }
}

注释已经解释的很清楚了,要注意写代码的时候因为数组下标从0开始,所以一定要注意length是否要减一,取不取等号。如果不确定,可以代入一个实际的例子去检查一下。

例如:我们要比较5个数,那么我们一共进行4轮比较。第一轮需要比较4次,第二轮3次,第三轮2次,第四轮1次。

代码中体现为:大循环为 i = 0~3,共4次;小循环为第一轮 j = 0~3,第二轮 j = 0~2, 第三轮 j = 0~1, 第四轮 j = 0。

 

2.选择排序

选择排序算法的原理如下:

1.首先在未排序序列中找到最小元素,存放到排序序列的起始位置

2.再从剩余未排序元素中继续寻找最小元素,然后放到已排序序列的末尾。

3.重复第二步,直到所有元素均排序完毕。

 

3.插入排序

插入排序算法的原理如下:

1.首先默认第一个元素有序。从第2个元素开始,若它比第1个元素小,就放到第一个元素左边,反之放第一个元素右边。

2.从第2个元素到最后一个元素,依次扫描,将扫描到的每个元素插入有序序列的适当位置。

注意:如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。

 

4.希尔排序

希尔排序是插入排序的升级版本。

我看的这个视频,讲的很通俗易懂:希尔排序——新原家龙之介

希尔排序算法的原理如下:

以一定的间隔把所有数分组,第一次一般间隔为n/2,索引余数相同的分为一组,一共分为n/2组。然后用插入排序,对每组中的数据进行组内排序(间隔为n/2就是每组中只有2个数进行排序)。注意:组内排序时,每个小组会产生位置交换,但数组整体的位置不会变动。(这个咋说呢,反正视频里讲的很清楚!)

然后取一个更小的间隔(一般来说每次减半,但不强制),同样用插入排序进行组内排序。直到间隔为1,也就是所有人都是一个小组,进行最后一次插入排序。这样就完成了这组数的希尔排序。

比如:我们有8个数。那么第一次分成4组,每组2人;第二次分成2组,每组4人;第三次分成1组,每组8人。

也许你会有疑惑,反正最后一次还是对所有数进行插入排序,那么为什么还要前面的步骤呢?那是因为希尔排序这样做能够节约交换的次数!!,总交换次数是远小于直接插入排序的!

 

接着讲时间复杂度为O(nlogn)的3种排序:归并,堆,快速

这三种排序都非常重要,要重点掌握,还要会代码实现,并关注其拓展应用!

5.归并排序

归并排序——秒懂算法

归并排序算法的原理如下:

先将待排序数组对半分成left,right两部分。然后再分别将left,right对半分。直到不可再分。对半分体现在代码中就是sortArray方法的递归体

然后将每部分两两合并成一个有序数组。最后合并成原数组的有序排列。合并体现在代码中就是merge方法

public int[] sortArray(int[] nums) {
        //将待排序的数组复制一份为arr
        int[] arr = Arrays.copyOf(nums, nums.length);
        if (arr.length < 2) {
            return arr;
        }
        //定义一个middle指针,指向中间元素
        int middle = (int) Math.floor(arr.length / 2);
        //把左边部分的数复制到left数组中
        int[] left = Arrays.copyOfRange(arr, 0, middle);
        //把右边部分的数复制到right数组中
        int[] right = Arrays.copyOfRange(arr, middle, arr.length);
        return merge(sortArray(left), sortArray(right));      
    }
    //合并两个有序数组
    protected int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        int i = 0;
        //两部分数组都还有数,比较两个数组的头部元素,把较小的数放到result中,
        //每放一个数,result指针右移准备放下一个数,left/right截掉这个已经放入result的数,更新头部元素
        while (left.length > 0 && right.length > 0) {
            if (left[0] <= right[0]) {
                result[i++] = left[0];
                left = Arrays.copyOfRange(left, 1, left.length);
            } else {
                result[i++] = right[0];
                right = Arrays.copyOfRange(right, 1, right.length);
            }
        }
        //right数组中的数已经放完了,那么直接把left数组中的数按顺序放到result尾部即可
        while (left.length > 0) {
            result[i++] = left[0];
            left = Arrays.copyOfRange(left, 1, left.length);
        }
        //left数组中的数已经放完了,那么直接把right数组中的数按顺序放到result尾部即可
        while (right.length > 0) {
            result[i++] = right[0];
            right = Arrays.copyOfRange(right, 1, right.length);
        }  
        return result;
    }

 

6.堆排序

推荐一下正月点灯笼小哥哥的视频,声音真好听,讲的也清楚:堆排序

堆的概念和堆排序的思想之前都写过了,这里粘过来复习一下,并完成代码实现

概念:大顶堆:根节点永远比左右子节点大。小顶堆:根节点永远比左右子节点小

总体思想:(以大顶堆为例)将需要比较的所有数构造一个大顶堆,输出并删除堆顶数字(最大值)。将剩下的数重新构造一个大顶堆,输出并删除堆顶数字(倒数第2大值).重复以上操作,直到取完堆中的数字。

具体排序方法:

1.对一个无序的树,从倒数第2层最右边的节点开始排序。

2.查看该节点是否比其左右子节点都要大,若是,保持不变。若左子节点比它大,则该节点与左子节点交换,右边亦然。若左右子节点都比它大,则该节点与左右子节点中较大的那个交换。这样完成了一组交换。

3.从右到左,从下到上一层一层地进行步骤2中的交换。直到最大元素出现在根节点处。

4.从左到右,从上到下一层一层检查根节点以下的部分是否满足大顶堆的条件,对不满足的部分重新进行交换。

5.输出并删除根节点(即为最大的数)。将最右下角的节点放置到根节点的位置,重复步骤2~4,直到当前树为null。

 代码实现:

1.写一个swap方法,用于交换arr数组中索引i与索引j位置的数。

private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
}

2.写一个heapify方法,用于形成一个数组形式的大顶堆

    //这个方法的目的是形成一个大顶堆
    //这里输入的是数组arr,操作完成后arr仍然是数组,但是数组可以表示一个堆(因为堆是完全二叉树)
    private void heapify(int[] arr, int i, int len) {
        //i表示当前节点的下标
        //i节点的左孩子节点的下标为2i+1
        int left = 2 * i + 1;
        //i节点的右孩子节点的下标为2i+1
        int right = 2 * i + 2;
        //我们先假设i,i的左孩子,i的右孩子三个节点中数值最大的是i,最大值的下标用变量largest表示
        int largest = i;
        //len表示数组的长度,也表示堆的节点数,在更新largest值时首先要保证left与right不出界
        //如果左孩子节点比较大,那么我们更新largest为左孩子节点的下标
        if (left < len && arr[left] > arr[largest]) {
            largest = left;
        }
        //如果右孩子节点比较大,那么我们更新largest为右孩子节点的下标
        if (right < len && arr[right] > arr[largest]) {
            largest = right;
        }
        //到现在我们已经将这三个节点中最大的值的下标存入了largest
        //如果最大的值不是arr[i],那么我们需要将arr[i]与arr[largest]交换
        //然后我们进行递归,往下遍历
        if (largest != i) {
            swap(arr, i, largest);
            heapify(arr, largest, len);
        }

3.对有孩子节点的下标最大的节点开始,到根节点依次做heapify

private void buildMaxHeap(int[] arr, int len) {
            for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
                heapify(arr, i, len);
            }
}

4.排序,把最大的数往右边放,然后左边部分不断构造大顶堆,直到所有元素排序完毕

public int[] sort(int[] nums){
            //将原数组复制一份到arr中    
            int[] arr = Arrays.copyOf(nums, nums.length);
            //arr长度为len
            int len = arr.length;
            //构造大顶堆
            buildMaxHeap(arr, len);
            for (int i = len - 1; i > 0; i--) {
                //把最大的数放到数组未排序部分的最右边,第一次为最右边,第二次为右数第二个...
                swap(arr, 0, i);
                //未排序的部分继续构造堆
                len--;
                heapify(arr, 0, len);
            }
            return arr;
}

 

7.快速排序

总体思想:选择一个数作为中心轴,然后将大于该中心轴的数放在它右边,将小于该中心轴的数放在它左边。分别对其左右子序列重复以上操作,直到子序列只剩一个数。

具体排序方法:

1.将作为中心轴的数取出。

2.设置left指针,指向最左边的数的左边一位。设置right指针,指向最右边的数。跳转到步骤3.

3.比较right指向的数与中心轴的大小。若right指向的数较小,则将right指向的数放到left指针指向的位置,然后left指针向右边移动一位,跳转到步骤4。

   如果right指向的数较大,则将right指针向左边移动一位,然后重新执行步骤3.

4.比较left指向的数与中心轴的大小。若left指向的数较大,则将left指向的数放到right指针指向的位置,然后right指针向左边移动一位,跳转到步骤3。

   如果left指向的数较小,则将left指针向右边移动一位,然后重新执行步骤4.

5.若left与right指针重合,则将中心轴放到该重合位置,结束本次分割。

6.分别对分割后的左右子序列进行1~5的操作,直到子序列只剩下一个数。

代码实现:

1.仍然需要先写一个swap方法,这里就省略了。

2.这个方法的作用是,把l(最左边)指向的数当作中轴,将比中轴小的数放中轴左边,比中轴大的数放中轴右边。

中轴是最后才放到中间去的。这段代码看的我一口老血吐出来了。。。大概理解一下那两个没有循环体的while,作用是i是从左边开始走的指针,找到比中轴(l)大的数退出循环;j是从右边开始走,找到比中轴小的数时退出循环,然后把i,j指向的数换一下,这样就能把比中轴小的数放左边,比中轴大的数放右边。其他的细节我也很糊,#¥%*%……这面试可咋整啊...

private int partition(int[] a, int l, int h) {
        int i = l, j = h + 1;
        while (true) {
            while (a[++i] < a[l] && i < h) ;
            while (a[--j] > a[l] && j > l) ;
            if (i >= j) {
                break;
            }
        swap(a, i, j);
    }
    swap(a, l, j);
    return j;
}

3.快速排序的核心部分——对中轴左右两部分数组递归地进行排序

private int[] quickSort(int[] arr, int l, int h) {
        if (l < h) {
            //记录当前中轴的下标
            int partitionIndex = partition(arr, l, h);
            //对中轴左半部分进行递归
            quickSort(arr, l, partitionIndex - 1);
            //对中轴右半部分进行递归
            quickSort(arr, partitionIndex + 1, h);
        }
        return arr;
}

4.主方法中,我们复制一份原数组,用它地拷贝进行快速排序!注意这里的下标不是抽象的变量了,而是实际的范围:0~arr.length-1

public int[] sortArray(int[] nums) {
        int[] arr = Arrays.copyOf(nums, nums.length);
        return quickSort(arr, 0, arr.length - 1);
}

 

最后我们讲一下桶排序思想相关的3种排序:

8.计数排序

计数排序我看了一些文字的描述都没看懂,然后看了马士兵的这个视频看懂了:计数排序——马士兵

我用自己的话描述一下吧:

计数排序适用于数量大,但是数字范围比较小的数字排序,比如员工的年龄,高考成绩。

计数排序其实也用到了桶排序的思想。

首先我们根据待排序数组,开辟一个新的计数数组,容量为待排序数组的max-min+1。

比如,待排序数组中最大的数为8,最小的数为0,那么我们就开辟一个容量为9的计数数组。

然后我们对待排序数组中出现的每个数进行计数,将计数结果按顺序写入计数数组。

举个例子:待排序数组[5,7,3,4,8,0,2,4,7,6,1,3,3]

那么我们的计数数组就应该为:[1,1,1,3,2,1,1,2,1]

表示的含义为,0出现了1次,1出现了1次,2出现了1次,3出现了3次,4出现了2次,5出现了1次,6出现了1次,7出现了2次,出现了1次。

当然,如果这边最小的数不是0,那么计数数组的下标就不能直接表示对应的数出现的次数哦。

然后我们遍历这个计数数组,就可以得到原数组的有序结果:[0,1,2,3,3,3,4,4,5,6,7,7,8]

这是怎么写的呢?就是,0出现1次,我们就写一个0;1出现1次,我们就写个1;2出现1次,我们就写个2;3出现3次,我们就写3个3......

这样就完成了计数排序。

 

9.基数排序

马士兵——基数排序

基数排序是一种多关键字排序。

比如,如下图第一行数字,最大的数的位数为3,所以我们有3个关键字:个位,十位,百位

第二行:按个位排序的结果

第三行:在按个位排完的基础上,按十位排序

第四行:在按十位排完的基础上,根据百位排序,完成排序~

如果有的数字不足位数,那么不足的位就按0计。

 

10.桶排序

马士兵——桶排序

桶排序的思想是:

把找到待排序数组的最大值和最小值,根据这个范围分成若干组(若干个桶),遍历原数组,把每个数字都放进相应范围的桶中。然后我们对每个桶中的数进行内部排序(归并排序/快速排序),最后把几个桶按顺序组合起来,就排序完成了。

这个主要是掌握思想,不常用。

但是,桶排序的思想应用很多,之前排序专题就写过两道桶排序的应用了,注意复习!!

 

posted @ 2020-06-09 23:08  菅兮徽音  阅读(397)  评论(0编辑  收藏  举报