力扣算法之八大排序算法总结

本文详细介绍了冒泡排序、选择排序、插入排序、折半插入排序、希尔排序、快速排序、归并排序和堆排序这八种常见的排序算法,包括它们的实现原理和在LeetCode上的执行效率。其中,快速排序和归并排序在时间复杂度上表现出色,而堆排序则由于其复杂性成为最难理解的排序方法之一。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

分析:题目那是相当简单,但是本题你也可以自己给自己增加难度,比如,让你用8大常用排序算法来解决该问题呢?下面我们就来一个一个做出解释和分析。

一、冒泡算法

该算法应该是我们学习第一门编程语言时最先接触到的算法了。也是最简单的算法,其核心思想就是每次遍历整个数组,都把最大值向后置换到最后一位。循环数次后便可以得到有序数组:

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

该算法简单,但负责度高,在面试中一般复杂度这一关就过不去,在力扣上也是超时。

二、选择排序

选择排序应该是和冒泡排序一起接触学习的。因为选择排序和冒泡排序是两个对立的情况,冒泡排序是每一次把最大的放最后,而选择排序是每一次选择最小的放最前。

public int[] sortArray(int[] nums){
        for(int i = 0; i < nums.length; i++){
            for(int j = i+1; j < nums.length; j++){
                if(nums[i] > nums[j]){
                    int a = nums[j];
                    nums[j] = nums[i];
                    nums[i] = a;
                }
            }
        }
        return nums;
    }

该函数复杂度几乎等价于冒泡,所以理解即可,面试中应用不大。力扣上执行结果还是超时。

三、插入排序

插入排序也是一种非常经典的排序方法,它的核心思想是:
指针从第二个元素出发,然后向前找该元素的合适位置放入即完成一轮操作。也就是说当在执行第i轮时,其第i个元素之前的元素一定是有序的。

public int[] sortArray(int[] nums){

        for(int i = 0; i < nums.length; i++){
            
            int a = nums[i];

            int j = i-1;

            while(j >= 0 && a < nums[j]){
                nums[j+1] = nums[j];
                j = j - 1;
            }

            nums[j+1] = a;
        }

        return nums;
    }

该算法复杂度就要小一些了,其在力扣上执行用时1812ms,已经是有效解决方法了。这主要是因为前面的元素都是有序的,所以在执行的时候减少了时间损耗。

4、折半插入排序

你如果明白了插入排序是在前面的有序序列中寻找一个合适的位置,那么你肯定能想到前面的有序序列为什么不用二分查找,而要使用逐层遍历呢?

public int[] sortArray(int[] nums){
        int i,j,low,high,mid;
        for( i=0;i<nums.length;i++ ){
            int tmp = nums[i];
            low = 0;high = i-1;
            while(low<=high) {
                mid = low+(high-low)/2;
                if(nums[mid] > tmp){
                    high = mid - 1;
                }else{
                    low = mid + 1;
                }
            }
            for(j=i-1;j>=high+1;j--){
                nums[j+1] = nums[j];
            }
            nums[high+1] = tmp;
        }
        return nums;
    }

该方法效果有了巨大的提升,力扣执行用时11ms,几乎提升了上百倍的执行速度。在折半方法的实现中,记住我们要找的tmp能插入的左边界,也就是最小的大于tmp的值。

5、希尔排序

希尔排序是思想其实还是来自插入排序,只不过它不在是进行简单粗暴的直接插入排序了,而是将其按照一定大小分组(一般默认是总长度的一半作为第一次分组大小,随后每次缩小一半,直到为1),对每一个分组内的第一个元素执行插入排序,第二个元素执行插入排序,,,分组内最后一个元素执行插入排序。

public int[] sortArray(int[] nums){
        for(int dk = nums.length/2;dk>=1;dk=dk/2){
            // for(int i=dk; i<nums.length; i=i+dk) { 
            for(int i=dk; i<nums.length; i=i+1) {
                if(nums[i]<nums[i-dk]){
                    int tmp = nums[i],j;
                    for(j = i-dk;j>=0&&tmp<nums[j];j-=dk){
                        nums[j+dk] = nums[j];
                    }
                    nums[j+dk]=tmp;
                }
            }
        }
        return nums;
    }

在力扣上运行时间也是12ms,速度可以说很快了。其实前面的你都可以做个简单了解即可,真正有难度和面试常问的主要集中在下面三种排序上。

6、快速排序

快速排序是排序算法中非常经典和常用的方法,其在源码中也有应用。其核心思想就是:
从第一个元素开始,从右边往左找一个比它小的,放到该元素位置,在从该元素下一个位置开始往右找一个比它大的,放到刚刚移过来的右侧元素的位置。就这样重复一直到左右指针相遇结束,此时把该第一个元素放入到相遇节点处。我们的数组以该元素为分界点,左侧都小于它,右侧都大于它,那我们就可以分而治之了。

public int[] sortArray(int[] nums){
        
        Test(nums, 0, nums.length-1);
        return nums;
    }

    void Test(int[] nums, int left, int right){
        if(left > right){
            return;
        }
        int begin = left;
         int end = right;
        //  int index = left;
         int total = nums[begin];

         while(begin < end){
            
            while(begin < end && nums[end] > total){
                end -= 1;
            }

            if(begin < end){
                nums[begin] = nums[end];
                begin++;
            }

            while(begin < end && nums[begin] < total){
                begin++;
            }

            if(begin < end){
                nums[end] = nums[begin];
                end--;
            }
         }

         nums[begin] = total;

         Test(nums, left, begin-1);
         Test(nums, begin+1, right);
    }

可以发现,快速排序,其运行时间花费了883ms,但也算是很快的。比较插入排序划分1800多ms,,

7、归并排序

其实归并排序也含有分而治之的思想,所谓归并排序就是先让左右两侧都有序,在将两个有序数组合并即可。所以其核心就是:
找数组的中点,然后分成两段递归调用,一直分到不能再分位置,然后开始左右两侧有序数组合并,一层层回溯即可。

int[] tmp;

    public int[] sortArray(int[] nums) {
        tmp = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1);
        return nums;
    }

    public void mergeSort(int[] nums, int l, int r) {
        if (l >= r) {
            return;
        }
        int mid = (l + r) >> 1;
        mergeSort(nums, l, mid);
        mergeSort(nums, mid + 1, r);
        int i = l, j = mid + 1;
        int cnt = 0;
        while (i <= mid && j <= r) {
            if (nums[i] <= nums[j]) {
                tmp[cnt++] = nums[i++];
            } else {
                tmp[cnt++] = nums[j++];
            }
        }
        while (i <= mid) {
            tmp[cnt++] = nums[i++];
        }
        while (j <= r) {
            tmp[cnt++] = nums[j++];
        }
        for (int k = 0; k < r - l + 1; ++k) {
            nums[k + l] = tmp[k];
        }
    }

在最后位置对原数组进行修改替换即可。其力扣运行时间花费11ms,这也是最块的一种算法了。

8、堆排序

堆排序无疑是最难的一种排序算法,你需要的做的有:
1)先对整个数组建立堆,大根堆或者小根堆
2)将堆顶元素和最后一个元素交互位置,然后重新根据除最后一个元素外前面的元素再次整理建堆,依次循环便可以得到结果。

难点在于建堆以及如何整理堆。当我们传入一组数组进行建堆时,我们需要从下向上建堆,叶子节点不需要建堆,因为就一个节点,自己就是堆。所以我们就从最底下第一个有孩子的节点上开始建堆,该节点就是len / 2处的节点。逐层向上,一直到最顶上,堆创建完成。
在每一次循环建堆时,我们都要做一个判断,看看当前根节点和左右孩子节点是不是符合堆规定,如果符合,我们就可以直接终止本次循环,进入到前一个节点的建堆过程中,这是因为我们采用自下而上建堆,所以下面的堆自然都是有序的,只要当前节点和左右孩子符合堆设定,那么以当前节点为跟的子树肯定符合堆,因此就不需要调整,可以进入前一个节点的调整了。但是,如果当前节点和左右孩子不符合堆的设定呢?那我们就需要将(比如堆设定为大根堆)最大值调整到父亲节点,然后对调整的子节点进行循环分析是否符合大根堆设定,因此,我们需要一个循环,从当前节点一直到最下面最后的一个有孩子节点。来进行逐次调整堆。在循环内,如果父节点比左右孩子节点中都大,那么就说明不需要调整了,break即可。如果不是,那就将子节点中最大的调整到父节点,然后将该子节点送入到遍历中,向下调整即可。

public int[] sortArray(int[] nums) {
        heapSort(nums);
        return nums;
    }

    public void heapSort(int[] nums) {
        int len = nums.length - 1;
        buildMaxHeap(nums, len);
        for (int i = len; i >= 1; --i) {
            swap(nums, i, 0);
            len -= 1;
            maxHeapify(nums, 0, len);
        }
    }

    public void buildMaxHeap(int[] nums, int len) {
        // 从倒数第二层开始的往下建立大根堆,最后一层不能建成堆,因此就直接从倒数第二层最后一个有孩子的节点开始建堆
        for (int i = len / 2; i >= 0; --i) {
            maxHeapify(nums, i, len);
        }
    }

    public void maxHeapify(int[] nums, int i, int len) {
        for (; (i << 1) + 1 <= len;) {
            int lson = (i << 1) + 1;
            int rson = (i << 1) + 2;
            int large;
            if (lson <= len && nums[lson] > nums[i]) {
                large = lson;
            } else {
                large = i;
            }
            if (rson <= len && nums[rson] > nums[large]) {
                large = rson;
            }
            if (large != i) {
                swap(nums, i, large);
                i = large;
            } else {
                break;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值