算法-分治法-计算右侧小于当前元素的个数(逆序数)

算法-分治法-计算右侧小于当前元素的个数(逆序数)

1 题目概述

1.1 题目出处

https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/

1.2 题目描述

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

2 分治法+归并+索引

2.1 解题思路

这个题首先想到的肯定是暴力扫描,即每个数都和右边所有数比,时间复杂度为O(N^2)。

有没有更好的方式呢?试试归并,复杂度O(N*logN)。

拆分成单独元素,合并排序时,记录下每个元素的原始位置以及计算出右侧小于当前元素的个数。

在第二次归并时,因为左侧元素肯定在右侧元素左边(对,这是句废话,但是很重要),且我们的元素已经排序。那么俩边比较的时候,只要左边元素L1比右边元素R1更大,也就意味着左边元素的下一个元素L2也比右边元素R1更大

通过这个方式,我们就不用比较每个元素了,节约了大量时间。

此外,前面提到过要记录下每个元素的原始位置,这里因为整型元素可能重复,所以我们建立了一个Node来保存他的原始下标,顺带还存放了元素的值和右侧小于当前元素的个数,使用更方便。

2.2 代码

class Solution {
    // 保存结果数组
    private List<Integer> resultList = new ArrayList<>();
    // Node中存放了元素的值、在原始数组中的下标、右侧小于当前元素的个数
    private List<Node> nodeList = new ArrayList<>();
    public List<Integer> countSmaller(int[] nums) {
        if(nums.length == 0){
            return resultList;
        }
        // 初始化resultList和nodeList
        for(int i = 0; i < nums.length; i++){
            resultList.add(0);
            Node node = new Node(nums[i], i);
            nodeList.add(node);
        }
        // 开始归并排序及计算右侧小于当前元素的个数
        count(0, nums.length-1);
        return resultList;
    }

    //  Node中存放了元素的值、在原始数组中的下标、右侧小于当前元素的个数
    class Node{
        public int val;
        public int index;
        public int cnt = 0;
        public Node(int val, int index){
            this.val = val;
            this.index = index;
        }
    }

    // 拆分计算和归并
    private void count(int low, int high){
        if(low < high){
            int middle = (low+high)/2;
            count(low, middle);
            count(middle + 1, high);
            merge(low, middle, high);
        }
    }

    // 归并过程和计算右侧小于当前元素的个数
    private void merge(int low, int middle, int high){
        // 暂存已按大小排序Node,用来复制回原List时使用
        ArrayList<Node> tmpList = new ArrayList<>();
        int i = low;
        int j = middle + 1;
        // 两次遍历之间左边元素计算得到的右侧小于当前元素的个数的增量
        int increase = 0;
        // 标识该次循环是否和上次循环使用同一个左边元素
        boolean repeat = false;
        while(i <= middle && j <= high){
            Node left = nodeList.get(i);
            Node right = nodeList.get(j);
            if(left.val <= right.val){
                // 左边小于等于右边元素
                if(!repeat){
                    // 首次遍历该左边元素,此时他更小,就只是加上此前的增量
                    // 因为增量是前面遍历的更小的左边元素和右边元素比较得到的统计数,
                    // 意味着当前元素也比那些已遍历过的右边元素更大
                    left.cnt = left.cnt + increase;
                    // 更新该下标元素的`右侧小于当前元素的个数`
                    resultList.set(left.index, left.cnt);
                }else{
                    // 重复遍历该元素,说明上一轮是改元素比右边元素大,已经更新过,这里不再更新
                    repeat = false;
                }
                // 将该元素放入tmpList,tmpList是按从小到大排序的list
                tmpList.add(left);
                // 准备遍历左边元素的下一个元素
                i++;
            }else{
                // 左边大于右边元素
                if(!repeat){
                    // 首次遍历该左边元素,此时他更大,就需要加上此前增量,
                    // 同时还需要加1,因为此时比右边元素大
                    left.cnt = left.cnt  + 1 + increase;
                }else{
                    // 重复遍历时,只需要将当前元素的`右侧小于当前元素的个数`加1即可
                    left.cnt += 1;
                }
                // 左边元素比右边元素更大,则每次将增量加一
                increase++;
                // 标记下次还要用自己
                repeat = true;
                // 更新该下标元素的`右侧小于当前元素的个数`
                resultList.set(left.index, left.cnt);
                tmpList.add(right);
                // 准备遍历右边元素的下一个元素
                j++;
            }
        }

        if(i <= middle){
            // 说明至少左边有一个较大元素比右边最大的元素还大
            // 将该元素放入临时列表
            tmpList.add(nodeList.get(i++));

            // 从该元素的下一个元素开始处理,因为该元素已经在前面循环处理过
            while(i <= middle){
                Node left = nodeList.get(i++);
                left.cnt += increase;
                resultList.set(left.index, left.cnt);
                tmpList.add(left);
            }
        }

        if(j <= high){
            // 说明至少右边有一个元素比左边最大元素还大
            // 此时只需要更新他们的resultList
            // 而不需要复制到tmpList,因为他们是靠右的已排序元素,
            // 所以排序后依然位置不变
            while(j <= high){
                Node right = nodeList.get(j);
                resultList.set(right.index, right.cnt);
                j++;
            }
        }
        // 将临时list复制到原始nodeList,此时nodeList的low->high位置的元素有序
        for(int k = 0; k < tmpList.size(); k++, low++){
            nodeList.set(low, tmpList.get(k));
        }
    }
}

2.3 时间复杂度

在这里插入图片描述

2.4 空间复杂度

O(NlogN)

3 分治法+归并+索引 优化一

3.1 解题思路

前面代码中merge方法太过复杂,很容易写错。

思考了下,其实根本不用每次都判断是否重复,流程如下:

  1. 我们只需要在右边元素出列的时候将统计数字(rightCnt)加1,然后放入临时list
  2. 而左边元素出列的时候,将对应的右侧小于当前元素的个数更新为 原值 + rightCnt,然后放入临时list
  3. 没处理完的左边元素,依次取出按步骤2处理
  4. 没处理完的右边元素,不需要处理,因为他们是靠右的已排序元素,所以排序后依然位置不变

这样处理完后,自然就得到已排序列表和他们对应的右侧小于当前元素的个数了。

3.2 代码

class Solution {
    // 保存结果数组
    private List<Integer> resultList = new ArrayList<>();
    // Node中存放了元素的值、在原始数组中的下标、右侧小于当前元素的个数
    private List<Node> nodeList = new ArrayList<>();
    public List<Integer> countSmaller(int[] nums) {
        if(nums.length == 0){
            return resultList;
        }
        // 初始化resultList和nodeList
        for(int i = 0; i < nums.length; i++){
            resultList.add(0);
            Node node = new Node(nums[i], i);
            nodeList.add(node);
        }
        // 开始归并排序及计算右侧小于当前元素的个数
        count(0, nums.length-1);
        return resultList;
    }

    //  Node中存放了元素的值、在原始数组中的下标、右侧小于当前元素的个数
    class Node{
        public int val;
        public int index;
        public int cnt = 0;
        public Node(int val, int index){
            this.val = val;
            this.index = index;
        }
    }

    // 拆分计算和归并
    private void count(int low, int high){
        if(low < high){
            int middle = (low+high)/2;
            count(low, middle);
            count(middle + 1, high);
            merge(low, middle, high);
        }
    }

    // 归并过程和计算右侧小于当前元素的个数
    // 1. 我们只需要在右边元素出列的时候将统计数字(rightCnt)加1,然后放入临时list
    // 2. 而左边元素出列的时候,将对应的`右侧小于当前元素的个数`更新为 原值 + rightCnt,然后放入临时list
    // 3. 没处理完的左边元素,依次取出按步骤2处理
    // 4. 没处理完的右边元素,不需要处理,因为他们是靠右的已排序元素,所以排序后依然位置不变
    private void merge(int low, int middle, int high){
        // 暂存已按大小排序Node,用来复制回原List时使用
        ArrayList<Node> tmpList = new ArrayList<>();
        int i = low;
        int j = middle + 1;
        // 右侧出列进入临时list的元素数量
        int rightCnt = 0;
        while(i <= middle && j <= high){
            Node left = nodeList.get(i);
            Node right = nodeList.get(j);
            if(left.val <= right.val){
                // 左边小于等于右边元素
                left.cnt += rightCnt;
                // 更新该下标元素的`右侧小于当前元素的个数`
                resultList.set(left.index, left.cnt);
                // 将该元素放入tmpList,tmpList是按从小到大排序的list
                tmpList.add(left);
                // 准备遍历左边元素的下一个元素
                i++;
            }else{
                // 左边大于右边元素
                rightCnt++;
                tmpList.add(right);
                // 准备遍历右边元素的下一个元素
                j++;
            }
        }

        // 处理比右边元素都大的左边元素
        while(i <= middle){
            Node left = nodeList.get(i++);
            left.cnt += rightCnt;
            resultList.set(left.index, left.cnt);
            tmpList.add(left);
        }


        // 不处理比左边元素都大的右边元素
        // 不需要复制到tmpList,因为他们是靠右的已排序元素,
        // 所以排序后依然位置不变

        // 将临时list复制到原始nodeList,此时nodeList的low->high位置的元素有序
        for(int k = 0; k < tmpList.size(); k++, low++){
            nodeList.set(low, tmpList.get(k));
        }
    }
}

3.3 时间复杂度

在这里插入图片描述

3.4 空间复杂度

O(NlogN)

4 分治法+归并+索引 优化二 索引数组

4.1 解题思路

优化一中仅仅去掉了repeat判断,但是从执行时间来看其实优化不大。

仔细看看原来代码,需要多次使用Node对象,jvm对对象处理是很花费时间的。

仔细观察Node中的元素:

class Node{
    public int val;
    public int index;
    public int cnt = 0;
    public Node(int val, int index){
        this.val = val;
        this.index = index;
    }
}
  • cnt代表右侧小于当前元素的个数,这个其实可以通过resultList直接维护不需要存

  • val和index
    放置在这里的原因就是index会变,而val是不会变的。那么我们只要能维护一个索引数组,在里面存放元素在原始数组的下标不就行了吗?还能通过这个来找到元素的值,一举两得。

    也就是说,要排序时,我们只需要交换这个索引数组就行。

4.2 代码

class Solution {
    // 保存结果数组
    private List<Integer> resultList = new ArrayList<>();
    // 索引数组,存储每个元素在原始数组中的下标
    // 后续我们交换元素时,只改变他们的索引数组
    private int[] indexes;
    // 暂存已按大小排序数字的索引
    private int[] tmpIndexes;
    public List<Integer> countSmaller(int[] nums) {
        if(nums.length == 0){
            return resultList;
        }

        indexes = new int[nums.length];
        tmpIndexes = new int[nums.length];
        // 初始化resultList和indexes
        for(int i = 0; i < nums.length; i++){
            resultList.add(0);
            indexes[i] = i;
        }
        // 开始归并排序及计算右侧小于当前元素的个数
        count(nums, 0, nums.length-1);
        return resultList;
    }

    // 拆分计算和归并
    private void count(int[] nums, int low, int high){
        if(low < high){
            int middle = (low+high)/2;
            count(nums, low, middle);
            count(nums, middle + 1, high);
            merge(nums, low, middle, high);
        }
    }

    // 归并过程和计算右侧小于当前元素的个数
    // 1. 我们只需要在右边元素出列的时候将统计数字(rightCnt)加1,然后将元素下标放入tmpIndexes
    // 2. 而左边元素出列的时候,将对应的`右侧小于当前元素的个数`更新为 原值 + rightCnt,然后将元素下标放入tmpIndexes
    // 3. 没处理完的左边元素,依次取出按步骤2处理
    // 4. 没处理完的右边元素,不需要处理,因为他们是靠右的已排序元素,所以排序后依然位置不变
    private void merge(int[] nums, int low, int middle, int high){
        int i = low;
        int j = middle + 1;
        // 右侧出列进入临时list的元素数量
        int rightCnt = 0;
        // 记录当前放入tmpIndexes的下标
        int k = 0;
        while(i <= middle && j <= high){
            int left = nums[indexes[i]];
            int right = nums[indexes[j]];
            if(left <= right){
                // 左边小于等于右边元素
                // 更新该下标元素的`右侧小于当前元素的个数`
                resultList.set(indexes[i], resultList.get(indexes[i]) + rightCnt);
                // 将该元素下标放入tmpIndexes,tmpIndexes是按下标对应元素值从小到大排序的
                tmpIndexes[k++] = indexes[i];
                // 准备遍历左边元素的下一个元素
                i++;
            }else{
                // 左边大于右边元素
                rightCnt++;
                tmpIndexes[k++] = indexes[j];
                // 准备遍历右边元素的下一个元素
                j++;
            }
        }

        // 处理比右边元素都大的左边元素
        while(i <= middle){
            resultList.set(indexes[i], resultList.get(indexes[i]) + rightCnt);
            tmpIndexes[k++] = indexes[i++];
        }

        // 不处理比左边元素都大的右边元素
        // 不需要复制到tmpIndexes,因为他们是靠右的已排序元素,
        // 所以排序后依然位置不变

        // 将临时tmpIndexes复制到原始indexes,此时indexes索引对应的元素大小按low->high有序
        // 注意这里必须用k,而不能用tmpIndexes.length,因为前面未把右边元素放入tmpIndexes
        for(int l = 0; l < k; l++, low++){
            indexes[low] = tmpIndexes[l];
        }
    }
}

4.3 时间复杂度

O(NlogN)

  • 划分logN,N个元素
    在这里插入图片描述

4.4 空间复杂度

O(N)

  • O(N)的indexes和O(N)的tmpIndexes

5 插入排序法

5.1 解题思路

倒序遍历原数组,每次放入一个已按从小到大排序的数组中,只要找到合适位置后再统计前面有几个数就得到了右侧小于当前元素的个数

这个方法最好理解,但是时间复杂度超过归并排序法。

5.2 代码

public List<Integer> countSmaller2(int[] nums) {
    // 保存结果数组
    List<Integer> resultList = new ArrayList<>();
    if(null == nums || nums.length == 0){
        return resultList;
    }
    for(int i = 0; i < nums.length; i++){
        resultList.add(0);
    }
    List<Integer> sortedList = new ArrayList<>();
    for(int i = nums.length -1; i >= 0; i--){
        sortedList.add(nums[i]);
        int j = sortedList.size() - 2;
        for(; j >= 0; j--){
            if(nums[i] <= sortedList.get(j)){
                sortedList.set(j+1, sortedList.get(j));
                sortedList.set(j, nums[i]);
            }else{
                break;
            }
        }
        resultList.set(i, j + 1);
    }
    return resultList;
}

5.3 时间复杂度

O(N^2)
在这里插入图片描述
太惨了

5.4 空间复杂度

O(N)

6 二叉搜索树(BST) - 循环版本

6.1 解题思路

前面插入排序因为时间复杂度O(N^2)导致超时无法通过,关于排序还可以想到BST、堆等数据结构。

但是这里堆不适用,应该使用BST。

6.2 代码

class Solution {
    // 保存结果数组
    private List<Integer> resultList = new ArrayList<>();
    
    class Node{
        int val;
        // 表示左子树有多少节点
        int cnt;
        Node left;
        Node right;
        public Node(int val, int cnt){
            this.val = val;
            this.cnt = cnt;
        }
    }

    public List<Integer> countSmaller(int[] nums) {
        if(nums.length == 0){
            return resultList;
        }

        for(int i = 0; i < nums.length; i++){
            resultList.add(0);
        }

        Node head = new Node(nums[nums.length - 1], 0);
        resultList.set(nums.length - 1, 0);
        Node currentNode = head;

        for(int i = nums.length - 2; i >= 0; i--){
            Node newNode = new Node(nums[i], 0);
            int count = 0;
            while(currentNode != null){
                if(newNode.val > currentNode.val){
                // 新节点大于当前节点,需要更新count,加上当前节点的左子树节点总数以及当前节点的数量1
                    count = count + currentNode.cnt + 1;
                    if(currentNode.right != null){
                        // 右子树不为空就继续判断右子树
                        currentNode = currentNode.right;
                    }else{
                        // 否则就将新节点节点作为左子树   
                        currentNode.right = newNode;
                        // 插入结束,将新节点的`右侧小于当前元素的个数`设为累积到的count
                        resultList.set(i, count);
                        break;
                    }
                }else{
                // 新节点小于等于当前节点,则当前节点左子树节点数量加1
                    currentNode.cnt++;
                    if(currentNode.left != null){
                        // 左子树不为空就继续判断左子树               
                        currentNode = currentNode.left;
                    }else{
                        // 否则就将新节点节点作为左子树     
                        currentNode.left = newNode;
                        // 插入结束,将新节点的`右侧小于当前元素的个数`设为累积到的count
                        resultList.set(i, count);
                        break;
                    }
                }
            }
            currentNode = head;
        }
        return resultList;
    }
}

6.3 时间复杂度

平均O(NlogN)

  • 插入logN,一共N个元素
    在这里插入图片描述

6.4 空间复杂度

O(N)

  • 构建N个Node

7 二叉搜索树(BST) - 递归版本

7.1 解题思路

前面的循环版本有点复杂,我们用递归版本试试。

7.2 代码

class Solution {
    // 保存结果数组
    private List<Integer> resultList = new ArrayList<>();
    class Node{
        // 在原数组下标
        int index;
        int val;
        // 表示左子树有多少节点
        int cnt;
        Node left;
        Node right;
        public Node(int index, int val, int cnt){
            this.index = index;
            this.val = val;
            this.cnt = cnt;
        }
    }
    
    private Node insertBst(Node head, Node newNode, int count){
        if(head == null){
            // head为空,代表之前插入时的节点的子树为空
            // 此时结束插入,将当前新节点的统计结果写入resultList
            resultList.set(newNode.index, count);
            // 返回当前节点作为之前节点的子树
            return newNode;
        }

        if(newNode.val > head.val){
            // 新节点大于当前节点,需要更新count,加上当前节点的左子树节点总数以及当前节点的数量1
            // 然后继续往右子树插入
            head.right = insertBst(head.right, newNode, count + head.cnt + 1);
        }else{
            // 新节点小于等于当前节点,则当前节点左子树节点数量加1
            head.cnt++;
            // 继续往左子树插入
            head.left = insertBst(head.left, newNode, count);
        }
        // 走到这里,说明将新节点插入到本节点的某个子树后边去了,
        // 所以这里返回当前节点作为父节点的子树,保持不变
        return head;
    }

    public List<Integer> countSmaller(int[] nums) {
        if(nums.length == 0){
            return resultList;
        }

        // 初始化resultList,以便后面使用
        for(int i = 0; i < nums.length; i++){
            resultList.add(0);
        }

        // 初始化树根节点,用的是最后一个、没有比自己更小的右边数字的那个元素
        Node head = new Node(nums.length - 1, nums[nums.length - 1], 0);

        // 从倒数第二个数开始往BST中插入
        for(int i = nums.length - 2; i >= 0; i--){
            insertBst(head, new Node(i, nums[i], 0), 0);
        }

        return resultList;
    }
}

7.3 时间复杂度

平均O(NlogN)

  • 插入logN,一共N个元素
    在这里插入图片描述
    虽然看着简单明了,但还慢了1ms?

7.4 空间复杂度

O(N)

  • 构建N个Node
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
K-D Tree(K-Dimensional Tree)算法是一种基于分治法据结构,用于高维空间的搜索和排序。它的基本思想是将多维空间中的点以某种方式分割成更小的子空间,然后在每个子空间中递归地进行搜索。这样可以大大降低搜索的复杂度。 具体来说,K-D Tree算法可以分为以下几步: 1. 选择一个维度,将据点按照该维度的值进行排序。 2. 找到该维度的中位,将其作为当前节点,并将据点分为左右两个子集。 3. 递归地构建左子树和右子树,每次选择一个新的维度进行划分。 4. 最终得到一个K-D Tree。 在搜索时,我们可以从根节点开始,按照一定的规则向下遍历,直到找到目标点或者无法继续向下搜索。具体的规则是: 1. 如果目标点在当前节点的左子树中,则继续向左子树搜索。 2. 如果目标点在当前节点的右子树中,则继续向右子树搜索。 3. 如果目标点和当前节点在选定的维度上的值相等,则说明已经找到目标点。 分治法是一种常见的算法思想,它将一个大规模的问题分解成若干个小规模的子问题,每个子问题独立地求解,然后将这些子问题的解合并起来得到原问题的解。分治法通常包含三个步骤:分解、求解、合并。 具体来说,分治法可以分为以下几步: 1. 分解:将原问题分成若干个子问题,每个子问题规模较小且结构与原问题相同。 2. 求解:递归地求解每个子问题,直到问题规模足够小可以直接求解。 3. 合并:将所有子问题的解合并成原问题的解。 分治法的优点是可以有效地降低算法的时间复杂度。但是它的缺点是需要额外的空间来存储子问题的解,而且分解和合并的过程也需要耗费一定的时间。因此,需要根据实际情况选择合适的算法

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值