06-归并排序

6. 归并排序

6.1 基础归并排序

分层两半,而后合并。

重点:MargeSort把比较变成了有序的东西,这个有序的东西可以帮我们做很多事情

6.1.1 递归的归并排序

两个函数:

分:process(arr,L,R) --> 保证[L,R]范围上有序。

public static void mSort(int[] arr , int l, int r){
	if(l == r){
        return;
	}
	int mid = (l+r)/2;
	mSort(arr,l,mid);
	mSort(arr,mid+1,r);
	marge(arr,l,mid,r);
}

合:每个process中都要利用marge合一下,利用一个辅助数组长度就是R-L。把这两部分按顺序复制到辅助数组中,再把辅助数组替换到L-R之间。

public static void marge(int[] arr , int l,int mid, int r){
    int[] help = new int[r-l+1];
    int i = l, j = mid+1;
    int k = 0;
    while(i <= mid && j <= r){
        if(arr[i] <= arr[j]){
            help[k] = arr[i];
            i++;
        }else {
            help[k] = arr[j];
            j++;
        }
        k++;
    }

    while(i <= mid){
        help[k] = arr[i];
        i++;
        k++;
    }
    while(j <= r){
        help[k] = arr[j];
        j++;
        k++;
    }
    for (int m = 0; m < help.length; m++) {
        arr[l+m] = help[m];
    }
}

6.1.2 非递归的归并排序

非递归模拟步长来进行计算,marge不变。

step从1开始不断翻倍直到超过N,停止。

按照步长为step从0到n遍历,可以计算出right和mid,然后marge即可。

public static void mSortWithoutRecursion(int[] arr ){
    int  N =  arr.length;
    int step = 1;
    // 5 4 8 2 3

    while(step < N){
        int left = 0, mid, right;
        while(left < N){
            right = left+step*2-1 >= N-1?N-1:(left+step*2-1);
            mid = (left+right)/2;
            marge(arr,left,mid,right);
            left = right+1;

        }
        //// 假设integer.max = 2e9,N = 2e9; step = 1e9+1 ,翻倍就炸了
        if(step > N/2){
            break;
        }
        step <<=1;
    }
}

6.2 数组小和

1. 题目

在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。

例子: [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 

2. 思路

数组最小和也可以看成:假设第i个数后面有x个比他大,那么在数组最小和中,第i个数出现的次数就是i*x次。

第i个数出现的次数似乎就是x次,而累加的值为arr[i]*x。(2023年9月24日)

ps:通过降序考虑也可以。

因此通过归并排序,在归并的时候,一个数组有分成左右部分(L/R),此时两部分应该都有序,规则如下:

  1. L中的数如果入小于R中的数(也就是进入辅助数组的时候),记录R中剩下的元素的个数(也就是比L[i]大的数的个数),这个数乘上L[i]就是当前这个阶段,比L[i]大的数的累加值。

  2. R中的数如果进入辅助数组,不进行相加,因为R在右侧,右侧的数不在乎有什么数在左面。

  3. 递归只需要返回smallNum(左)+smallNum(右)+marge(左右合并的数组)即可。

3.代码

public static int smallSum(int[] arr , int l, int r){
        if(l == r || arr.length == 0){
            return 0;
        }
        int mid = (l+r)/2;
        return smallSum(arr,l,mid)+
        smallSum(arr,mid+1,r)+
        marge(arr,l,mid,r);

    }
    public static int marge(int[] arr , int l,int mid, int r){
        int[] help = new int[r-l+1];
        int i = l, j = mid+1;
        int k = 0;
        int ans = 0;
        while(i <= mid && j <= r){
            if(arr[i] < arr[j]){
				// 这里记录了左侧数组进入后右侧数组剩余的元素个数
                ans += arr[i]*(r-j+1);
                help[k++] = arr[i++];
            }else {
                help[k++] = arr[j++];
            }
        }
        while(i <= mid) help[k++] = arr[i++];
        while(j <= r) help[k++] = arr[j++];
        for (int m = 0; m < help.length; m++) arr[l+m] = help[m];
        return ans;
    }

6.3 翻转对

1. 题目

https://leetcode.cn/problems/reverse-pairs/

在一个数组中,对于每个数num,求有多少个后面的数 * 2 依然比num小,求总个数。

比如:[3,1,7,0,2]
3的后面有:1,0
1的后面有:0
7的后面有:0,2
0的后面没有
2的后面没有
所以总共有5个

2. 思路

如果已经部分排好序了,如[1,3] 和[0,2,7],可以O(n)的方式知道左数组中的数在右数组中的btt的个数。

初始arr[i] = 1,arr[j] = 0;
如果arr[j]*2 < arr[i] :j++并且记录答案为j;  --- 越界可以开long
否则 i++; (因为arr[i] < arr[i+1],所以j前面的数一定也可以满足前提,直接从j判断即可)
右侧的数不需要操作,因为需要去重复

3.代码

public static int marge(int[] arr , int l,int mid, int r){
    // 假设L[l...mid],R[mid+1...r]都已经排好序了,求此时L中的数对于R中的数的btt
    int ans = 0;
    int j = 0;
    for(int i = l; i <= mid; i++){
        while(j+mid+1 <= r &&(long)arr[i] > (long)arr[j+mid+1]*2){
            j++;
        }
        ans += j;
    }
    
// 正常的归并
    int[] help = new int[r-l+1];
    int i = l;
    j = mid+1;
    int k = 0;
    while(i <= mid && j <= r){
        if(arr[i] <= arr[j]){
            help[k] = arr[i];
            i++;
        }else {
            help[k] = arr[j];
            j++;
        }
        k++;
    }

    while(i <= mid){
        help[k] = arr[i];
        i++;
        k++;
    }
    while(j <= r){
        help[k] = arr[j];
        j++;
        k++;
    }
    for (int m = 0; m < help.length; m++) {
        arr[l+m] = help[m];
    }
    return ans;
}

6.4 逆序对

1. 题目

https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/

在一个数组中,任何一个前面的数a,和任何一个后面的数b,如果(a,b)是降序的,就称为逆序对。返回数组中逆序对的个数。

2. 思路

归并的本质就是无序的代码可以让我们有序比较。

对于有序数组L[l...mid] 和 R[mid+1...r]来说,

1. L数组加入help的时候无序更改(因为左侧代表已经算过逆序对的数)
2. R数组中加入help的时候,逆序对个数增加为:mid-i+1个。即,此时L数组中还未加入help中的数的个数。

3.代码

    public int merge(int[] nums,int l,int mid,int r){
        if(l == r){
            return 0;
        }
// 核心代码开始
        // 假设L[l...mid] 和 R[mid+1...r]都有序的情况下
        int j = 0;
        int ans = 0;
        // i为遍历R数组的index
        for(int i = l; i <= mid; i++){
            while(j+mid+1 <= r && nums[i] > nums[j+mid+1]){
                ans += mid-i+1;
                j++;
            }
        }
// 核心代码结束
        int[] help = new int[r-l+1];
        int i = l;
        j = mid+1;
        int k = 0;
        while(i <= mid && j <= r){
            if(nums[i] < nums[j]) help[k++] = nums[i++];
            else help[k++] = nums[j++];
        }
        while(i <= mid) help[k++] = nums[i++];
        while(j <= r) help[k++] = nums[j++];

        for(int m = 0; m < help.length; m++){
            nums[m+l] = help[m];
        }
        return ans;
    }

6.7 区间和的个数(未完成)

1. 题目

https://leetcode.cn/problems/count-of-range-sum/

给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。

区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。

输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。

输入:nums = [0], lower = 0, upper = 0
输出:1
 

2. 思路

首先,求一个范围内的值在lower,upper之间,就要先确定一个范围内的值 => 优化:前缀和来求。

然后用归并排序,得到

3.代码

posted @ 2023-11-09 09:00  犹豫且败北  阅读(48)  评论(0)    收藏  举报