左程云算法基础第二章:认识O(NlogN)的排序

本文是左程云算法基础第二章,主要讲解了O(NlogN)级别的排序算法——归并排序和快速排序。内容包括归并排序的master公式及其在小和问题中的应用,以及不同版本的快速排序实现,如荷兰国旗问题。重点解析了快速排序在最坏情况下的时间复杂度,并提出了通过随机选择划分值来改善效率的方法。

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

左程云算法基础第二章:认识O(NlogN)的排序

  • 在二分查找等需要把数组进行二等分的操作中,取中点可以采用以下写法

    mid = left + ((right - left) >> 1)
    

    在正常情况下的写法mid = (left + right) / 2,但如果范围太大可能会导致溢出,那么换成right - left就不会溢出。

  • 递归函数的递归过程类似一个二叉树的后序遍历


归并排序:

/**
 * @Description:
 * 1.把长度为n的输入序列分成两个长度为n/2的子序列;
 * 2. 对这两个子序列分别采用归并排序;
 * 3. 将两个排序好的子序列合并成一个最终的排序序列。
 *
 * 平均时间复杂度:O(NlgN)
 * 最优时间复杂度:O(NlgN)
 * 最差时间复杂度:O(NlgN)
 * 空间复杂度:O(n)
 * 稳定性:稳定
 * @Author: chong
 * @Data: 2021/5/28 10:31 上午
 */
public class MergeSortTemplate {
//    创建一个辅助数组
    private int[] aux;
    public void mergeSort(int[] nums){
//        为辅助数组分配空间
        aux = new int[nums.length];
        int low = 0;
        int high = nums.length - 1;
        mergeSort(nums, low, high);
    }

    public void mergeSort(int[] nums, int low, int high){
//        如果子数组没有元素或只有一个元素了就返回
        if (low >= high)
            return;
//        找到子数组的中点
        int middle = low + ((high - low) >> 1);
//        对于左右子数组进行递归
        mergeSort(nums, low, middle);
        mergeSort(nums, middle + 1, high);
//        递归结束之后从最小的数组开始聚合(最小有两个元素)
        merge(nums, low, middle, high);
    }

    /**
     * 对子数组进行聚合,实际的排序在此发生,对于每个小数组进行小数组范围内的排序,每次保证输入的两个子数组合并且变为有序的
     * @param nums
     * @param low:左子数组的开始下标
     * @param middle:左右子数组的中点,middle下标处的元素属于左子数组
     * @param high:右子数组的结束下标
     */
    public void merge(int[] nums, int low, int middle, int high){
//        这一步是记录下左子数组和右子数组的开始下标,记为left,right
        int left = low;
        int right = middle + 1;
//        要把左右子数组所涉及的元素保存在辅助数组中
        for (int k = low; k <= high; k++){
            aux[k] = nums[k];
        }
//        由于辅助数组已经保存下来了原来的值,故可以直接在原数组nums上做改动
//        high是子数组最大元素下标,也要遍历到
        for (int h = low; h <= high; h++){
//            left>middle时说明左子数组已经添加完了,所以添加右子数组的元素right++
            if (left > middle)
                nums[h] = aux[right++];
//            right>high说明右子数组添加完了,添加左子数组元素left++
            else if (right > high)
                nums[h] = aux[left++];
//            比较左右子数组指针所指位置元素大小,左子数组(已排序)要比右子数组(已排序)小,
//            所以如果aux[left] > aux[right] 就把小的元素aux[right]添加进去(从小到大的顺序),反之相反。
//            使用辅助数组进行比较,原数组在排序过程中会改变
            else if (aux[left] > aux[right])
                nums[h] = aux[right++];
            else
                nums[h] = aux[left++];
        }
    }
}
  • 归并排序的master公式:T(N) = 2T(N/2) + O(N)
    在这里插入图片描述

  • 归并排序相关题目应用

小和问题在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。例子:[1,3,4,2,5]1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;5左边比5小的数,1、3、4、2;所以小和为1+1+3+1+1+3+4+2=16

换个思路,从第一个数开始,看右面有多少个数比他大,就有多少个这个数的小和,如上面的数组中1的右边有四个数比它大,则产生4个1的小和,3右边有两个数比他大,产生2个3的小和,依次求解。

这个思路下可以用归并排序的思想解决,先将数组不断分到最小,然后开始merge,在merge的过程中,如果左子数组中的元素比右子数组的元素小,那么就产生小和,产生了n个左子数组当前元素的小和,这个n可以通过右子数组当前元素的下标知道右子数组中还有几个比左子数组当前元素大的元素。

代码:

/**
 * @Description:
 * 归并排序思想的应用:
 * 小和问题在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
 * 例子:[1,3,4,2,5]1左边比1小的数,没有;3左边比3小的数,1;4左边比4小的数,1、3;2左边比2小的数,1;
 * 5左边比5小的数,1、3、4、2;所以小和为1+1+3+1+1+3+4+2=16
 * @Author: chong
 * @Data: 2021/6/19 8:04 下午
 */
public class LittleSum {
    int[] aux;
    int result;
    public int littleSum(int[] nums){
        aux = new int[nums.length];
        result = 0;
        littleSum(nums, 0, nums.length - 1);
        return result;
    }

    private void littleSum(int[] nums, int low, int high){
        if (low == high)
            return;
        int mid = low + ((high - low) >> 1);

        littleSum(nums, low, mid);
        littleSum(nums, mid + 1, high);

        merge(nums, low, mid, high);
    }

    private void merge(int[] nums, int low, int mid, int high){
        int left = low;
        int right = mid + 1;
        for (int i = low; i <= high; i++)
            aux[i] = nums[i];

        for (int i = low; i <= high; i++){
            if (left > mid)
                nums[i] = aux[right++];
            else if (right > high)
                nums[i] = aux[left++];
            else if (aux[left] < aux[right]){
                result += aux[left] * (high - right + 1);
                nums[i] = aux[left++];
//                这里注意当两个指针位置上的元素相等时,一定先拷贝右子数组,才能知道左子数组多少元素比右子数组小
            }else
                nums[i] = aux[right++];
        }
    }


    @Test
    public void test(){
        int[] nums = {1, 1, 5, 1, 1, 3};
        int res = littleSum(nums);
        System.out.println(res);
    }
}


逆序对问题,在一个数组中,如果左边的数字比右边的数字大,则这两个数字构成一个逆序对,请打印所有逆序对

leetcode hard题:[剑指 Offer 51. 数组中的逆序对]

/**
 * @Description:
 * 归并应用:逆序对问题,在一个数组中,如果左边的数字比右边的数字大,则这两个数字构成一个逆序对,请打印所有逆序对,求出总数
 * @Author: chong
 * @Data: 2021/6/20 2:27 下午
 */
public class reversePairs {
    int[] aux;
    int res;
    public int reversePairs(int[] nums) {
        aux = new int[nums.length];
        res = 0;
        reversePairs(nums, 0, nums.length - 1);
        return res;
    }

    public void reversePairs(int[] nums, int low, int high){
        if (low >= high)
            return;
        int mid = low + ((high - low) >> 1);
        reversePairs(nums, low, mid);
        reversePairs(nums, mid + 1, high);
        merge(nums, low, mid, high);
    }

    public void merge(int[] nums, int low, int mid, int high){
        int left = low;
        int right = mid + 1;
        for (int i = low; i <= high; i++){
            aux[i] = nums[i];
        }

        for (int i = low; i <= high; i++){
            if (left > mid)
                nums[i] = aux[right++];
            else if (right > high)
                nums[i] = aux[left++];
            else if (aux[left] > aux[right]){
                res += mid - left + 1;
                for (int k = left; k <= mid; k++)
                    System.out.println("逆序对:[" + aux[k] + ", " + aux[right] + "]");
                nums[i] = aux[right++];
            }else
                nums[i] = aux[left++];
        }
    }

    @Test
    public void test(){
        int[] nums = {7, 5, 6, 4};
        int res = reversePairs(nums);
        System.out.println(res);
    }
}

快速排序

给定一个数组arr,和一个数num,请把小于等于num的数放在数 组的左边,大于num的数放在数组的右边。

要求额外空间复杂度O(1),时间复杂度O(N)

设定好num,用i遍历数组,当i比num小于等于的时候进入情况一:将i与小于等于区域的下一位交换,小于等于区域向右扩一位,i++。当i比num大的时候进入情况二:i++

  • 荷兰国旗问题

给定一个数组arr,和一个数num,请把小于num的数放在数组的 左边,等于num的数放在数组的中间,大于num的数放在数组的 右边。

要求额外空间复杂度O(1),时间复杂度O(N)

这道题与leetcode上75. 颜色分类相同,实际上利用了双指针指明大于和小于区域,需要注意怎么变化。

本题题解可见【LeetCode刷题笔记】75.颜色分类

同样分为三种情况:

  1. i<num:i和小于区域下一个做交换,小于区域右扩,i++
  2. i=num:i++
  3. i>num:i和大于区域前一个交换,大于区域左扩,i原地不变

在这里插入图片描述

快速排序1.0

用数组最后一个数字做划分(或者第一个),把整个数组划分为小于等于最后一个数字的和大于最后一个数字的

快速排序2.0

用数组最后一个数字做划分,把整个数组划分为小于最后一个数字的和等于最后一个数字的和大于最后一个数字的,比1.0版本稍快

最差时间复杂度:O(N^2)
最差的时候是数组已经是排好序的时候
产生的原因是划分值打得很偏

快速排序3.0

在数组中随机选一个数,放在最后一个位置上,再拿它做划分值(随机选保证好坏情况等概率发生)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值