【左神算法2】O(Nlog(N))排序 归并排序 和快速排序

本文详细介绍了归并排序和快速排序这两种经典的O(NlogN)排序算法。归并排序通过分治策略,先将数组分成两半分别排序,然后合并。快速排序的核心是分区操作,选取基准值,将数组分为小于、等于和大于基准的三部分,递归排序两部分。文章还涉及了荷兰国旗问题和快速排序的平均时间复杂度分析。

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

目录

01 master公式求递归时间复杂度

02 归并排序

2.1 算法步骤

2.2 归并排序代码

2.3 归并排序拓展: 小和问题

03 快速排序

3.1 算法步骤

3.2 荷兰国旗问题

3.3 经典快排

01 master公式求递归时间复杂度

master公式(也称主方法)是用来利用分治策略来解决问题经常使用的时间复杂度的分析方法,(补充:分治策略的递归解法还有两个常用的方法叫做代入法和递归树法,以后有机会和亲们再唠),众所周知,分治策略中使用递归来求解问题分为三步走,分别为分解、解决和合并,所以主方法的表现形式:

T [n] = aT[n/b] + f (n)(直接记为T [n] = aT[n/b] + O (N^d))

其中 a >= 1 and b > 1 是常量,其表示的意义是n表示问题的规模,a表示递归的次数也就是生成的子问题数,子问题规模应该一致,b表示每次递归是原来的1/b之一个规模,f(n)表示分解和合并所要花费的时间之和。

解法:
①当d < logb a时,时间复杂度为O(n^(logb a))
②当d == logb a时,时间复杂度为O((n^d)*logn)
③当d > logb a时,时间复杂度为O(n^d)

三个系数一旦确定,时间复杂度就确定了

public class RecursionTest {
    // 递归原理分析

    public static int getMax(int[] arr){
        return process(arr,0, arr.length - 1);
    }

    // 求arr数组中i 到 j 中的最大值
    public static int process(int arr[], int L, int R){
        if(L == R){
            return arr[L];
        }
        int mid = L + ((R - L) >> 1); // 求中点:如果直接用(l + r)/2 可能会超出界限
        int leftMax = process(arr, L, mid);  // 分而治之的思想
        int rightMax = process(arr, mid + 1, R);
        return Math.max(leftMax,rightMax);
    }
}

 02 归并排序

归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

2.1 算法步骤

        先让左侧部分有序, 再让右侧部分有序生成两个有序序列

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;

  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;

  4. 重复步骤 3 直到某一指针达到序列尾;

  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

2.2 归并排序代码

import java.util.Arrays;

public class MergeSort {

    public static void mSort(int[] arr, int L, int R){ // 归并排序:范围 L 到 R
        if(L == R){
            return;
        }
        int mid = L + ((R - L) >> 1); // 求中点
        mSort(arr, L, mid);  // 递归调用左侧子序列
        mSort(arr,mid + 1,R); // 递归调用右侧子序列
        // 最终合并两个子序列的方法:
        merge(arr,L,mid,R); //不是递归行为

    }
    //在merge中完成排序

    public static void merge(int[] arr, int L , int mid, int R){
        int[] help = new int[R - L + 1]; // 新建一个存放有序数组的新空间

        //左侧序列: L 到 mid  右侧序列: mid + 1 到 R
        int i = 0;
        int p1 = L;
        int p2 = mid + 1;

        while(p1 <= mid && p2 <= R){ // 当两个下标都不越界,一直循环
            if(arr[p1] <= arr[p2]){
                help[i] = arr[p1];
                p1++;
                i++;
            }else if(arr[p1] > arr[p2]){
                help[i++] = arr[p2++];
            }
        }
        // 当有任一下标越界
        while(p1 <= mid){  // 直接复制左边剩下的
            help[i] = arr[p1];
            i++;
            p1++;
        }
        while(p2 <= R){  // 直接复制右边剩下的数
            help[i++] = arr[p2++];

        }
        for(i = 0; i < help.length; i++){  // 将help中的数拷贝回arr
            arr[L + i] = help[i];
        }

    }

    // 统一函数接口
    public static void MergeSort(int[] arr){
        if(arr == null || arr.length < 1){
            System.out.println("无需排序");
            return;
        }
        mSort(arr, 0, arr.length - 1);
    }

    public static void main(String[] args){
        int[] arr = new int[]{3,5,2,-2,7,9,-10};
        MergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

2.3 归并排序拓展: 小和问题

import java.util.Arrays;

public class SmallSum {
    public static int smallSum(int[] arr){
        if(arr == null || arr.length <= 1){
            return 0;
        }
        return process(arr, 0, arr.length - 1);
    }

    // 排序过程,将arr数组中L 到 R 序列进行排序,同时求小和
    public static int process(int[] arr, int L, int R){
        if(L == R){
            return 0;
        }
        int mid = L + ((R - L ) >> 1);
        // 求左侧序列小和,求右侧序列小和

        return process(arr, L, mid) +  // 左侧小和的数量
        process(arr, mid + 1, R)+ // 右侧小和的数量
        merge(arr, L,mid, R);  // merge之后小和的数量
    }

    // 在merge中进行排序,并求每个序列的小和
    public static int merge(int[] arr, int L, int mid, int R){
        int[] help = new int[R - L + 1];
        int i = 0;
        int p1 = L;
        int p2 = mid + 1;
        int res = 0; // 保存小和结果

        while(p1 <= mid && p2 <= R){
            // 只有左数比右数小,才产生小和增加的行为
            if(arr[p1] < arr[p2]){
                res += arr[p1] * (R - p2 + 1);
                help[i++] = arr[p1++];
            }else{
                res += 0;
                help[i++] = arr[p2++];
            }
        }

        while(p1 <= mid){
            help[i++] = arr[p1++];
        }

        while(p2 <= R){
            help[i++] = arr[p2++];
        }

        for(i = 0; i < help.length; i++){
            arr[L + i] = help[i];
        }
        return res;
    }

    public static void main(String[] args){
        int[] arr = new int[]{1,2,3,6,4,5};
        System.out.println(smallSum(arr));
        System.out.println(Arrays.toString(arr));
    }
}

03 快速排序

时间复杂度O(Nlog N), 额外空间复杂度 O(Nlog N) 不稳定的排序

问题1
给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。

方法:

用x标记小于区域的最后一个数的位置,然后用for循环依次遍历数组中的数,如果大于指定数字则跳过,不进行交换;如果小于或等于num,则与x的下一个位置进行交换。

如果第一个位置的数小于等于num,其实就是和自己交换,i一定永远大于等于x。

   public static void main(String[] args) {
           int[] arr = new int[]{3,5,4,2,6,7};
           int num = 4;
           int x = -1;
           for (int i = 0; i < arr.length; i++){
               if (arr[i] <= num){
                   swap(arr, i, ++x);
               }
           }
       }
       
       public static void swap(int[] arr, int i, int j){
           if (i == j)
               return ;
           arr[i] = arr[i] ^ arr[j];
           arr[j] = arr[i] ^ arr[j];
           arr[i] = arr[i] ^ arr[j];
       }

3.1 算法步骤

  1. 从数列中挑出一个元素,称为 "基准"(pivot);

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

3.2 荷兰国旗问题

import java.util.Arrays;

public class HolandFlag {

    /*
    荷兰国旗问题是把数组分成三个部分,小于 等于 和 大于
     */
    public static void HolandFlag(int[] arr,int L, int R){
        int less = L - 1;  // 限制小于区域右侧指针
        int more = R + 1; // 限制大于区域左侧指针

        int pivot = arr[L]; // 指定一个pivot
        for(int i = 0; i < more; ){  // 这里遍历到大于区域即可停止遍历
            // 遍历时有三种情况
            // 1:小于基准值
            if(arr[i] < pivot){
                swap(arr, i, ++less);  // 与小于区域后一个数交换,小于区域右扩
                i++;
            }else if (arr[i] == pivot){
                i++; // 直接后移
            }else{  // 大于pivot
                swap(arr, i, --more); // i保持不动 ,因为换过来的数没有被查看过
            }
        }
    }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }

    // 测试
    public static void main(String[] args) {
        int[] arr = {3,4,5,6,1,2,3,4,5,4,3};
        System.out.println(Arrays.toString(arr));
        HolandFlag(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

}

3.3 经典快排

	public class quickSort {

    // 规范化排序方法接口
    public static void quickSort(int[] arr){
        if (arr == null || arr.length < 2) {
            return;
        }
        QuickSort(arr, 0, arr.length - 1);
    }

    // 排序
    public static void QuickSort(int[] arr, int L, int R){
        if(L < R){   // 要确定L < R 时才执行, 不然会造成空指针异常
            // 先在数组中随机取一个数作为pivot, 这样可以将时间复杂度概率性将为 NlogN
            int N = L + (int)Math.random() * (R - L + 1); // 随机取数组范围L - R 之间一个整数
            // 将这个数与数组最后一个数进行交换
            swap(arr,N,R);

            // 调用分配函数 得到 小于区域最右侧索引, 和 大于区域最左侧索引
            int[] p = partition(arr,L,R);

            // 第一轮分配完成后 递归调用左侧和右侧子序列,此时less 和 more 指向相等区域的左右两个端点
            QuickSort(arr, L, p[0] - 1);
            QuickSort(arr, p[1] + 1, R);
        }

    }
    public static int[] partition(int[] arr, int L, int R){   // 这里要返回 less 和 more
            int less = L - 1;
            int more = R;
            // 进行分类
            //for(int i = L; i < more; ){  // 这里遍历到大于区域即可停止遍历
            while(L < more){    // 额外空间复杂度O(1),这里不能重新定义变量i进行遍历
                // 遍历时有三种情况
                // 1:小于基准值   // 基准值pivot被换到了数组最后一个位置
                if(arr[L] < arr[R]){
                    swap(arr, L++, ++less);  // 与小于区域后一个数交换,小于区域右扩
                }else if (arr[L] == arr[R]){
                    L++; // 直接后移
                }else{  // 大于pivot
                    swap(arr, L, --more); // i保持不动 ,因为换过来的数没有被查看过
                }
            }
            // 循环完毕后,more指向大于区域最左侧位置,此时将arr[more] 和 arr[R] 交换
            swap(arr, more, R);

             return new int[]{less + 1, more};
        }

    public static void swap(int[] arr, int i, int j) {
        int tmp = arr[i];
        arr[i] = arr[j];
        arr[j] = tmp;
    }


    public static void main(String[] args) {
        int[] arr = {3,4,5,6,1,2,3,4,5,4,3};
        System.out.println(Arrays.toString(arr));
        quickSort(arr);
        System.out.println(Arrays.toString(arr));
    }

}

依据概率问题,最差情况O(N^2), 平均时间复杂度为O(NLogN)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值