leetcode-315-计算右侧小于当前元素的个数-java

这篇博客介绍了如何使用Java解决LeetCode上的第315题,即计算数组中每个元素右侧有多少个更小的元素。博主首先介绍了朴素的O(N^2)解法,然后详细讲解了一个基于归并排序的高效解决方案,阐述了归并排序的思想,并在排序过程中统计元素右侧更小元素的个数。文章还特别强调了归并排序的稳定性对于正确结果的重要性。

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

题目及测试

package pid315;
/*计算右侧小于当前元素的个数

给定一个整数数组 nums,按要求返回一个新数组 counts。数组 counts 有该性质: counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。

示例:

输入: [5,2,6,1]
输出: [2,1,1,0] 
解释:
5 的右侧有 2 个更小的元素 (2 和 1).
2 的右侧仅有 1 个更小的元素 (1).
6 的右侧有 1 个更小的元素 (1).
1 的右侧有 0 个更小的元素.


*/

import java.util.List;

public class main {
	
	public static void main(String[] args) {
		int [][] testTable = {{5,2,6,1},{0,1,2,-3,-1}};
		for (int[] ito : testTable) {
			test(ito);
		}
	}
		 
	private static void test(int[] ito) {
		Solution solution = new Solution();
		List<Integer> rtn;
		long begin = System.currentTimeMillis();
		for(int i=0;i<ito.length;i++){
			System.out.print(ito[i]+"  ");
		}
		System.out.println();
		//开始时打印数组
		
		rtn= solution.countSmaller(ito);//执行程序
		long end = System.currentTimeMillis();	
		
		System.out.println("rtn=" );
		for(int i=0;i<rtn.size();i++){
			System.out.print(rtn.get(i)+" ");
		}
		System.out.println();
		System.out.println("耗时:" + (end - begin) + "ms");
		System.out.println("-------------------");
	}

}

解法1(成功,822ms,极慢)

朴素的方法,循环去找右边比自己小的数,速度O(N^2)

package pid315;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

public class Solution {
    public List<Integer> countSmaller(int[] nums) {
    	int length=nums.length;
    	List<Integer> result=new ArrayList<>(length);
    	for(int i=0;i<length;i++){
    		int now=nums[i];
    		int num=0;
    		for(int j=i+1;j<length;j++){
    			if(nums[j]<now){
    				num++;
    			}
    		}
    		result.add(num);
    	}
        
    	return result;
    }

}

解法2(别人的)

归并排序法

归并排序将序列先分成两个相同长度的子序列,分别排序后,再归并两个有序的子序列。

在归并排序的过程中,顺便完成个数统计。

关键在于归并操作。

例如:[-1, 5, -5, -5, -4, -2, -3, 5, 2, -5] N=10

分成: [-1, 5, -5, -5, -4 ] [ -2, -3, 5, 2, -5]

假设已经将子序列排好序后,并且每个元素的右侧比其小的个数都求出来了。

左边 的结果

0    1    2    3    4
---------------------------------
-5    -5    -4    -1    5
0    0    0    3    3

右边的结果

5    6    7    8    9
---------------------------------
-5    -3    -2    2    5
0    1    2    1    2

我们看最后一步归并。

A[0] = -5 <= A[5] = -5 取结果T[0] = A[0]

A[1] =-5 <= A[5] = -5 取结果T[1] = A[1]

A[2] =-4 > A[5] = -5 取结果T[2] = A[5]

A[2] = -4 < A[6] = -3 取结果T[3] = A[2],但是A[2]右边比A[2]小元素的个数要加上 第二个子序列中 A[6]前 的元素个数1. COUNTS[3] = 0 + 1 =1。

为什么?

因为取A[2]表示在第二个子序列中A[6]之前的元素都小于A[2],因此一定要加上。

A[3] = -1 > A[6] = -3 取结果T[4] = A[6]

A[3] = -1 > A[7] = -2 取结果T[5] = A[7]

A[3] = -1 < A[8] = 2 取结果T[6] = A[3] COUNTS[6] = 3 + 3 =6。第二个3表示 在第二个子序列中A[8]之前的小于A[3] 的个数。

A[4] = 5 > A[8] = 2 取结果T[7] = A[8]

A[4] = 5 <= A[9] = 5 取结果T[8] = A[4] COUNTS[4] = 3 + 4 =7。第二个4表示 在第二个子序列中A[9]之前的小于A[4] 的个数。

取结果T[9] = A[9] 。

从上面的可看出,在归并操作中,当取第一个子序列中的元素a时,为其增加第二个子序列中比a小的元素个数。

但是要强调一个case。

例如:[2,1,2]

我们知道其结果是[1,0,0].

如果在排序的过程中第一个2和第二个2交换了位置,那么最终结果是[0,0,1]。这显然是错误的。

这要求排序算法必须是稳定的。而归并排序是稳定的。

因此,本算法不会出现上述的问题。

整体思路:

A[x,...,y)序列归并排序;
merge_sort(A,COUNTS,T,x,y){
    m = x + (y-x)/2;
    merge_sort(A,C,x,m);
    merge_sort(A,C,m,y);
    a = x, b = m;
    p = x;
    while(a < m || b < y){
        if(b == y || a < m && A[a] <= A[b]){
            COUNTS[a] += b - m;
            T[p++] = A[a++];
        }else T[p++] = A[b++];
    }
    A[x,...,y) = T[x,...,y);
}

但是,实际上为了将下标与元素和个数绑定起来,方便计数。

在数字数组A上又加了层索引数组W。归并排序过程中,元素顺序的调整,只会修改索引数组W。

增加了索引数组W后的完整代码:

public class Solution {

    private int[] temp;
    private int[] counter;
    private int[] indexes;

    public List<Integer> countSmaller(int[] nums) {
        List<Integer> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        temp = new int[len];
        counter = new int[len];
        indexes = new int[len];
        for (int i = 0; i < len; i++) {
            indexes[i] = i;
        }
        mergeAndCountSmaller(nums, 0, len - 1);
        for (int i = 0; i < len; i++) {
            res.add(counter[i]);
        }
        return res;
    }

    /**
     * 针对数组 nums 指定的区间 [l, r] 进行归并排序,在排序的过程中完成统计任务
     *
     * @param nums
     * @param l
     * @param r
     */
    private void mergeAndCountSmaller(int[] nums, int l, int r) {
        if (l == r) {
            // 数组只有一个元素的时候,没有比较,不统计
            return;
        }
        int mid = l + (r - l) / 2;
        mergeAndCountSmaller(nums, l, mid);
        mergeAndCountSmaller(nums, mid + 1, r);
        // 归并排序的优化,同样适用于该问题
        // 如果索引数组有序,就没有必要再继续计算了
        if (nums[indexes[mid]] > nums[indexes[mid + 1]]) {
            mergeOfTwoSortedArrAndCountSmaller(nums, l, mid, r);
        }
    }

    /**
     * [l, mid] 是排好序的
     * [mid + 1, r] 是排好序的
     *
     * @param nums
     * @param l
     * @param mid
     * @param r
     */
    private void mergeOfTwoSortedArrAndCountSmaller(int[] nums, int l, int mid, int r) {
        // 3,4  1,2
        for (int i = l; i <= r; i++) {
            temp[i] = indexes[i];
        }
        int i = l;
        int j = mid + 1;
        // 左边出列的时候,计数
        for (int k = l; k <= r; k++) {
            if (i > mid) {
                indexes[k] = temp[j];
                j++;
            } else if (j > r) {
                indexes[k] = temp[i];
                i++;
                // 此时 j 用完了,[7,8,9 | 1,2,3]
                // 之前的数就和后面的区间长度构成逆序
                counter[indexes[k]] += (r - mid);
            } else if (nums[temp[i]] <= nums[temp[j]]) {
                indexes[k] = temp[i];
                i++;
                // 此时 [4,5, 6   | 1,2,3 10 12 13]
                //           mid          j
                counter[indexes[k]] += (j - mid - 1);
            } else {
                // nums[indexes[i]] > nums[indexes[j]] 构成逆序
                indexes[k] = temp[j];
                j++;
            }
        }
    }

    public static void main(String[] args) {
        int[] nums = new int[]{5, 2, 6, 1};
        Solution solution = new Solution();
        List<Integer> countSmaller = solution.countSmaller(nums);
        System.out.println(countSmaller);
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值