【算法】排序_归并排序

归并排序

定义

归并排序是采用分治法的一个非常典型的应用,将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序
若将两个有序表合并成一个有序表,称为二路归并

稳定性

稳定

时间复杂度

O(N*logN)

代码

测试用例

int arr[] = {2,4,3,1,6,5};

原代码

    /**
     * 归并排序(重载)
     * 为了创建临时数组 完成数组的复用
     *
     * @param arr 无序数组
     */
    public static void mergeSort(int[] arr){
        System.out.println("初始数据" + Arrays.toString(arr));
        int[] temp = new int[arr.length];
        mergeSort(arr,temp,0,arr.length-1);
    }

    /**
     * 归并排序
     * @param arr
     * @param temp
     * @param start
     * @param end
     */
    public static void mergeSort(int[] arr,int[] temp,int start,int end){
       if (start >= end){
           return;
       }
       int mid = (start + end) / 2;
       // 排序第一个数组
       mergeSort(arr,temp,start,mid);
       // 排序第二个数组
       mergeSort(arr,temp,mid+1,end);
       // 合并两个有序数组
       merge(arr,temp,start,mid,end);
    }

运行结果
(输出语句见 二路归并 算法)
在这里插入图片描述

扩展1 二路归并

    /**
     * 二路归并 合并两个有序序列
     * 现将两个有序序列合并成一个序列 然后开始排序
     * @param arr   存放两个有序列表的数组
     * @param temp  临时数组
     * @param start 左侧有序列表开始的下标
     * @param mid   左侧结束位置下标  右侧开始位置下标:mid+1
     * @param end   右侧有序列表结束下标
     */
    public static void merge(int[] arr,int[] temp,int start,int mid,int end){
        System.out.println("前一组开始下标 "+start);
        System.out.println("前一组结束下标 "+mid);
        System.out.println("后一组开始下标 "+(mid+1));
        System.out.println("后一组结束下标 "+end);
        int i = start; // i 用来记录 第一个数组的遍历位置
        int j = mid + 1; // 用来记录第二个数组的遍历位置
        int k = start; // 用来表示临时数组的下标
        // i=mid+1:第一个数组先遍历完成
        // j=end+1:第二个数组先遍历完成
        while (i != mid +1 && j != end +1){
            if (arr[i]>arr[j]){
                temp[k++] = arr[j++];
            }else {
                temp[k++] = arr[i++];
            }
        }
        // 第一个数组没有遍历完毕
        while (i != mid + 1){
            temp[k++] = arr[i++];
        }

        // 第二个元素没有遍历完毕
        while (j != end + 1){
            temp[k++] = arr[j++];
        }

        for (int n = start;n<=end;n++){
            arr[n] = temp[n];
        }
        System.out.println("本次合并结果:"+Arrays.toString(arr));
    }

扩展二 递归的时间复杂度

master公式

T(N) = a * T( N b {N\over b} bN ) + O(Nd)

  • N:数据样本量
  • a:子过程调用次数
  • N b {N\over b} bN:子过程数据样本量
  • O(Nd):除去递归之外的时间复杂度

递归时间复杂度
在这里插入图片描述

扩展三 应用

数组小和问题

题面
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和
示例
[2,3,4,1,5] 小和为17
- 2左边比其小的数:
- 3左边比其小的数:2
- 4左边比其小的数:2 3
- 1左边比其小的数:
- 5左边比其小的数:2 3 4 1
思路
求数组中每个数左侧比其小的所有数的和,等同于求每个数右侧比其大的数的数量乘以这个数的和进行累加
代码实现



    /**
     * 求数组小和
     * @param arr
     */
    public static void minSum(int[] arr){
        if (arr == null || arr.length<2){
            System.out.println("所求小和为 0");
            return;
        }

        int[] temp = new int[arr.length];
        int minsum = mergeSort4MinSum(arr,temp,0,arr.length-1);

        System.out.println("所求小和值为 "+minsum);
    }


    /**
     * 求数组小和
     * @param arr
     * @param temp
     * @param start
     * @param end
     * @return
     */
    public static int mergeSort4MinSum(int[] arr,int[] temp,int start,int end){
        if (start >= end){
            return 0;
        }
        int mid = (start + end) / 2;

        // 排序第一个数组 计算第一个数组小和
        int leftMinSum = mergeSort4MinSum(arr,temp,start,mid);
        // 排序第二个数组 计算第二个数组小和
        int rughtMinSum = mergeSort4MinSum(arr,temp,mid+1,end);
        // 合并两个有序数组 计算总的数组小和
        int allMinSum = merge4MinSum(arr,temp,start,mid,end);

        return leftMinSum + rughtMinSum +allMinSum;
    }

    /**
     * 求数组小和
     * @param arr   存放两个有序列表的数组
     * @param temp  临时数组
     * @param start 左侧有序列表开始的下标
     * @param mid   左侧结束位置下标  右侧开始位置下标:mid+1
     * @param end   右侧有序列表结束下标
     * @return 本组小和
     */
    public static int merge4MinSum(int[] arr,int[] temp,int start,int mid,int end){
        System.out.println("前一组开始下标 "+start);
        System.out.println("前一组结束下标 "+mid);
        System.out.println("后一组开始下标 "+(mid+1));
        System.out.println("后一组结束下标 "+end);
        int i = start; // i 用来记录 第一个数组的遍历位置
        int j = mid + 1; // 用来记录第二个数组的遍历位置
        int k = start; // 用来表示临时数组的下标
        int minSumResult = 0;
        // i=mid+1:第一个数组先遍历完成
        // j=end+1:第二个数组先遍历完成
        while (i != mid +1 && j != end +1){
            if (arr[i]>arr[j]){
                temp[k++] = arr[j++];
            }else {
                minSumResult += arr[i]<arr[j]?(end-j+1) * arr[i]:0; // 计算小和语句
                temp[k++] = arr[i++];
            }
        }
        // 第一个数组没有遍历完毕
        while (i != mid + 1){
            temp[k++] = arr[i++];
        }

        // 第二个元素没有遍历完毕
        while (j != end + 1){
            temp[k++] = arr[j++];
        }

        for (int n = start;n<=end;n++){
            arr[n] = temp[n];
        }
        System.out.println("本次合并结果:"+Arrays.toString(arr));
        return minSumResult;
    }

逆序对和问题

题面
在一个数组中,左边的数如果比右边的数大,则两个数构成一个逆序对,请输出逆序对的数量
示例
[2,3,4,1,5,6]中逆序对的数量为3,分别为(2,1) (3,1) (4,1)
思路
即求每个数右边比其小的个数的和
代码实现


/**
 * @Classname AntiOrder
 * @Description 将归并的从小到大排序 换成 从大到小排序
 *
 */
public class AntiOrder {

    // 记录逆序对信息
    private static Map<String,Object> antiOrderMap = new HashMap<>();
    
    public static void main(String[] args) {
        // 测试数据
        int[] arr = {2,4,3,1,6,5};
        antiOrder(arr);
    }


    /**
     * 求逆序对
     * @param arr
     */
    public static void antiOrder(int[] arr){
        if (arr == null || arr.length<2){
            System.out.println("逆序对的数量为 0");
            return;
        }

        int[] temp = new int[arr.length];
        int minsum = mergeSort4AntiOrder(arr,temp,0,arr.length-1);

        System.out.println("逆序对的数量为  "+minsum);
        System.out.println("逆序对如下:");
        System.out.println(antiOrderMap.toString());
        antiOrderMap = new HashMap<>();
    }


    /**
     * 求逆序对
     * @param arr
     * @param temp
     * @param start
     * @param end
     * @return
     */
    public static int mergeSort4AntiOrder(int[] arr,int[] temp,int start,int end){
        if (start >= end){
            return 0;
        }
        int mid = (start + end) / 2;

        // 排序第一个数组 计算第一个数组中逆序对
        int leftAnti = mergeSort4AntiOrder(arr,temp,start,mid);
        // 排序第二个数组 计算第二个数组中逆序对
        int rughtAnti = mergeSort4AntiOrder(arr,temp,mid+1,end);
        // 合并两个有序数组 计算总的数组中逆序对
        int allAnti = merge4AntiOrder(arr,temp,start,mid,end);

        return leftAnti + rughtAnti +allAnti;
    }

    /**
     * 求逆序对
     * @param arr   存放两个有序列表的数组
     * @param temp  临时数组
     * @param start 左侧有序列表开始的下标
     * @param mid   左侧结束位置下标  右侧开始位置下标:mid+1
     * @param end   右侧有序列表结束下标
     * @return 本组小和
     */
    public static int merge4AntiOrder(int[] arr,int[] temp,int start,int mid,int end){
        System.out.println("前一组开始下标 "+start);
        System.out.println("前一组结束下标 "+mid);
        System.out.println("后一组开始下标 "+(mid+1));
        System.out.println("后一组结束下标 "+end);
        int i = start; // i 用来记录 第一个数组的遍历位置
        int j = mid + 1; // 用来记录第二个数组的遍历位置
        int k = start; // 用来表示临时数组的下标
        int antiResult = 0;
        // i=mid+1:第一个数组先遍历完成
        // j=end+1:第二个数组先遍历完成
        while (i != mid +1 && j != end +1){
            if (arr[i]<arr[j]){
                temp[k++] = arr[j++];
            }else {

                // 统计逆序对
                if (arr[i]>arr[j]){
                    antiResult += end-j+1; // 计算逆序对数量
                    // 逆序对内容
                    for (int index = j;index<=end;index++){
                        antiOrderMap.put(antiOrderMap.size()==0?"1":(antiOrderMap.size()+1)+"","("+arr[i]+","+arr[index]+")");

                    }
                }


                temp[k++] = arr[i++];
            }
        }
        // 第一个数组没有遍历完毕
        while (i != mid + 1){
            temp[k++] = arr[i++];
        }

        // 第二个元素没有遍历完毕
        while (j != end + 1){
            temp[k++] = arr[j++];
        }

        for (int n = start;n<=end;n++){
            arr[n] = temp[n];
        }
        System.out.println("本次合并结果:"+ Arrays.toString(arr));
        return antiResult;
    }
}

运行结果
在这里插入图片描述

瑞士轮问题

题面
在双⼈对决的竞技性⽐赛,如乒乓球、⽻⽑球、国际象棋中,最常⻅的赛制是淘汰赛和循环赛。前者
的特点是⽐赛场数少,每场都紧张刺激,但偶然性较⾼。后者的特点是较为公平,偶然性较低,但⽐
赛过程往往⼗分冗⻓。

本题中介绍的瑞⼠轮赛制,因最早使⽤于1895年在瑞⼠举办的国际象棋⽐赛⽽得名。它可以看作是淘
汰赛与循环赛的折衷,既保证了⽐赛的稳定性,⼜能使赛程不⾄于过⻓。

2*N 名编号为 1~2N 的选⼿共进⾏ R 轮⽐赛。每轮⽐赛开始前,以及所有⽐赛结束后,都会对选⼿
进⾏⼀次排名。排名的依据是选⼿的总分。选⼿的总分为第⼀轮开始前的初始分数加上已参加过的所
有⽐赛的得分和。总分相同的,约定编号较⼩的选⼿排名靠前。
每轮⽐赛的对阵安排与该轮⽐赛开始前的排名有关:第1名和第2名、第3名和第4名、……、第 2K-1
名和第 2K 名、…… 、第 2N-1 名和第 2N 名,各进⾏⼀场⽐赛。每场⽐赛胜者得1分,负者得0
分。也就是说除了⾸轮以外,其它轮⽐赛的安排均不能事先确定,⽽是要取决于选⼿在之前⽐赛中的
表现。
现给定每个选⼿的初始分数及其实⼒值,试计算在 R 轮⽐赛过后,排名第 Q 的选⼿编号是多少。我们
假设选⼿的实⼒值两两不同,且每场⽐赛中实⼒值较⾼的总能获胜。

示例
根据下表3名选⼿实⼒值及初始分数,求取4轮对阵后排名第2的选⼿为:编号1选⼿
在这里插入图片描述
思路
⾸先根据初始分数排序,然后在每次对阵结束后分为胜者组和败者组,分组时保证先进⾏对阵的选⼿
在前,后对阵的选⼿在后,这样既保证了胜者组和败者组两组先天有序,再进⾏归并排序的merge操
作,准备下次对阵。时间复杂度:O(N * logN + R * N)
代码实现

to be continued …

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值