力扣刷题笔记 315. 计算右侧小于当前元素的个数 C#

今日签到题,题目如下:

给定一个整数数组 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 个更小的元素.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

最简单的思路就是暴力解法,查询每个元素的时候遍历它右侧的所有元素以获得结果,时间复杂度O(N*N),明显会超时。

看官方题解,果然又得跪。第一次接触树状数组,看了半天不是很明白,先看看其他解法。看完二分解法就看不下别的了。以下仅讨论二分解法和树状数组,其他我都不会。

二分查找

二分查找的解法相对其他应该是最简单的。因为查找的是 nums[i] 右侧小于 nums[i] 的元素数量,所以我们可以把 nums 逆序存入一个列表 sortList 中,每个元素存入的同时进行升序排序,元素插入对应排序位置。由于 nums[i] 存入之前 sortList  内只有 nums[i] 右侧的元素,并且插入位置为排序位置,所以 nums[i] 在 sortList 位置左边的所有元素都是满足条件的元素,故 nums[i] 在 sortList  中的下标即使 counts[i] 的值。

以上还没有提到二分,其实二分只是在这里做一次优化,如果不使用二分,顺序遍历查找 nums[i] 在列表中的位置,时间复杂度还会是O(N*N)。所以在查找位置的过程中使用二分查找。

这里提醒自己注意一点,二分查找对应元素更新位置时应该是 left = mid + 1 或者 right = mid -1,之前解题因为这个导致陷入死循环。当然,具体场景具体分析,这里只是提醒自己。

复杂度分析:

首先遍历数组所有元素 O(N),内层使用二分查找 O(logN),时间复杂度 O(N * logN)。也有其他题解表示因为列表插入的实现是遍历列表,所以插入的时间复杂度也是 O(N),总时间复杂度是 O(N * (logN + N))。关于这个我还没验证,但是如果是这样的话,时间复杂度反而大于暴力解法。

使用一个额外的列表存入数组元素,空间复杂度 O(N)。

以下为自己提交的代码:

public class Solution {
    public IList CountSmaller(int[] nums) {
        if (nums.Length == 0)
        {
            return new int[]{};
        }
        List sortList = new List();
            sortList.Add(nums[nums.Length - 1]);
            int[] ans = new int[nums.Length];
            ans[nums.Length - 1] = 0;
            for (int i = nums.Length - 2; i >= 0; i--)
            {
                int left = 0;
                int right = sortList.Count - 1;
                while (left < right)
                {
                    int mid = (left + right) >> 1;
                    if (sortList[mid] < nums[i])
                    {
                        left = mid + 1;
                    }
                    else
                    {
                        right = mid;
                    }
                }
                ans[i] = sortList[left] < nums[i] ? left + 1:left;
                sortList.Insert(ans[i], nums[i]);
            }
        return ans;
    }
}

树状数组

这个概念我之前没接触过,结构原理网上也是大把的资料,大致了解了原理也不想写出来了,具体回顾一下实现思路。

先将数组中所有元素不重复且升序存入一个新的数组 a,然后新建一个相同长度的数组 c。

数组 a 的目的,是为了将离散的数组元素映射到下标中,方便使用树状数组。

数组 c 的目的,是为了以树状数组的形式记录数字出现的次数。由于树状数组的使用,我们可以通过 c 很快的得到某个区间中所有元素出现的次数总和。由于题目需要所有小于 nums[i] 的元素数目,所以区间就是第一个元素到 num[i] 的上一个元素。注意,这里的一个元素指的是 nums 中最小的数字在 a 中的下标即 0,到 nums[i] 上一个元素同样也是指 nums[i] 在 a 中的下标 - 1。

如果将 nums 整个数组记录到 c 中,显然得不到我们需要的,右边小于 nums[i] 的个数这个答案。我们采用逆序存入,元素 nums[i] 存入的时候,更新 c 的值。这样,更新后的数组 c 就是 nums[i - 1]  右边的所有元素出现次数(注意:由于树状数组, c 的元素并非直观地表示次数)。然后,只要找出 c 第一个元素到 num[i] 上一个元素区间的总和,即为 counts[i]。

复杂度分析:

我本来以为时间成本应该会小于二分查找,提交之后还是 300+ ms 的时间消耗,仅击败 12.5% 的用户。本来觉得挺不可思议的,回顾一遍思路下来,好像也确实和二分查找的时间复杂度一样。遍历 nums 所有元素时间为 O(N),更新树状数组和获取区间和的时间复杂度应该也是 O(logN),所以时间复杂度也是 O(N * logN)。

使用了 2 个最大长度为 N 的数组 a 和 c,空间复杂度为 O(N)。

以下为自己提交的代码:

public class Solution {
    public IList CountSmaller(int[] nums)
    {
        if (nums.Length == 0)
        {
            return new int[0];
        }
        int[] a = (int[])nums.Clone();
        a = a.Distinct().ToArray();
        Array.Sort(a);

        int[] c = new int[a.Length];
        List ans = new List();
        for (int i = nums.Length - 1; i >= 0; i--)
        {
            int index = Array.BinarySearch(a, nums[i]);
            Console.WriteLine(index);
            int j = index;
            int ret = 0;
            while (j > 0)
            {
                ret += c[j];
                j -= j & (-j);
            }
            ans.Add(ret);
            j = index + 1;
            while (j < a.Length)
            {
                c[j] += 1;
                j += j & (-j);
            }
        }
        ans.Reverse();
        return ans;
    }
}

自我提醒一下,排序都作为这两种解法的基本算法,不能单纯使用 list.sort 或者 array.sort,还是要多练习练习排序算法。另外二分查找和树状数组也要多做点练习。

你可能感兴趣的:(基础算法)