leetcode 315. Count of Smaller Numbers After Self(Hard)

题面:

You are given an integer array nums and you have to return a new counts array. The counts array has the property where counts[i] is the number of smaller elements to the right of nums[i].

Example:

Given nums = [5, 2, 6, 1]

To the right of 5 there are 2 smaller elements (2 and 1).
To the right of 2 there is only 1 smaller element (1).
To the right of 6 there is 1 smaller element (1).
To the right of 1 there is 0 smaller element.
Return the array [2, 1, 1, 0].

题目分析:

题目在leetcode的分类是分治算法,但是乍看觉得跟分治算法关系并不是很大,鄙人也并非通过分治算法求解,应该是看问题的角度不同导致不同的结论。
题目要求找出一个数组中每一个数所在的位置之后有多少个比该数小的数,并将结果返回。乍看,这不就是一个两重循环的题目吗?居然也算是hard。于是我也按照两重循环的方法写了一份代码提交。

class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        vector<int> result;
        result.assign(nums.size(), -1);
        for (int i = 0; i < nums.size(); i++) {
          int count = 0;
          for (int j = i + 1; j < nums.size(); j++) {
            if (nums[i] > nums[j])
              count++;
          }
          result[i] = count;
        }
        return result;
    }
};

结果并没有TLE……

这里写图片描述

但还是要继续做下去。
但是怎样用一种比较高级的方法来解这个题目呢?结果,我想了半天没想出来。于是在discuss看到了大佬的想法,并加以实现了一下,下面解析一下做法。

解题思路:

通过建立二叉搜索树,在建立二叉搜索树的过程中生成答案数组。将可以将O(n^2)降到O(nlog n)。

首先,是二叉搜索树的节点:

struct node {
  int val, dup, count;
  node *left, *right;
  node(int t_val, int t_count, node* t_left = NULL, node* t_right = NULL, int t_dup = 1) {
    val = t_val;
    dup = t_dup;
    count = t_count;
    left = t_left;
    right = t_right;
  }
};

可以看到每个节点除了维护值和左右子树节点变量之外,还额外维护dup和count变量。dup是duplication的缩写,表示若数组中有重复的数值,只生成一个节点,并通过dup变量记录;而count则是表示在二叉搜索树中该节点的左子树总共有多少个”节点“,即左子树中所有节点的dup值的和

而对于题目要求需要完成的函数,代码如下:

vector<int> countSmaller(vector<int>& nums) {
  vector<int> result;
  result.assign(nums.size(), 0);
  if (nums.size() == 0) return result;
  node* root = new node(nums[nums.size() - 1], 0);
  for (int i = nums.size() - 2; i >= 0; i--) {
    insert_node(root, nums[i], i, result, 0);
  }
  return result;
}

可以看到,与一般的二叉搜索树的生成算法很类似,主要是insert_node函数的参数不同。

insert_node函数的代码如下:

void insert_node(node* root, int val, int i, vector<int>& result, int pre_sum) {
  if (val == root->val) {
    result[i] = pre_sum + root->count;
    (root->dup)++;
  } else if (val < root->val) {
    (root->count)++;
    if (root->left == NULL) {
      root->left = new node(val, 0);
      result[i] = pre_sum;
    } else { 
      insert_node(root->left, val, i, result, pre_sum);
    }
  } else if (val > root->val) {
    if (root->right == NULL) {
      root->right = new node(val, 0);
      result[i] = pre_sum + root->dup + root->count;
    } else {
      insert_node(root->right, val, i, result, pre_sum + root->dup + root->count);
    }
  }
}

可以看到该函数总共有5个参数:

  • root表示的是树的根节点
  • val表示要插入的节点的值
  • i用来在生成树的同时记录result中的值
  • result即时用来记录答案的数组
  • pre_sum后文说明

根据函数中的条件语句来解释具体的算法:
当需要插入的值与树中的某个节点的值相同时,根据上文的约定,是通过将该节点的dup值自增。
而关于:

result[i] = pre_sum + root->count;

上文中说到的root->count是用来记录该节点的左子树中所有节点的dup值的和。但是仅仅只有root->count并没有完全表示当前树中所有比root的值小的节点总数。因为root->count只是所有刚好插入到root左子树的节点的数量,当然,这些节点的值都比root小。但是,从二叉搜索树的结构可以直到,root的父节点的左子树的节点的值也全部比root的值小。如此递归到根节点,则可以得到所有比root小的节点的个数。而这个个数即是result[i]的值,由此,可以推断出pre_sum即是该除了root的左子树外该搜索树中所有比root的值小的节点的个数。
但是,难道我们每次插入都需要重新回溯到根节点吗?答案是不必的,因为我们每一次插入都是从根节点开始的,因此,我们可以通过一个变量来记录这个值,而这个变量就是pre_sum。
通过对如下代码的解析,你应该能够更加清楚:

else if (val < root->val) {
  (root->count)++;
  if (root->left == NULL) {
    root->left = new node(val, 0);
    result[i] = pre_sum;
  } else { 
    insert_node(root->left, val, i, result, pre_sum);
  }
} else if (val > root->val) {
  if (root->right == NULL) {
    root->right = new node(val, 0);
    result[i] = pre_sum + root->dup + root->count;
  } else {
    insert_node(root->right, val, i, result, pre_sum + root->dup + root->count);
  }
}

这段代码是从上面insert_node函数截取的部分条件分支语句。与二叉搜索树节点插入很相似。
可以看到,当插入的节点的值小于root的值时,由二叉搜索树的生成规则可知,应该插入到左子树,于是继续判断,若此时左子树为空,则将该节点作为root的左子树,否则,递归插入该节点。
此时,应该注意的是插入一个节点到root的左子树,那么很自然,我们需要将root->count的值自增1。

(root->count)++;

而如果root的左子树为空,此时,节点应该成功插入,同时,可以得出result[i]的值。

result[i] = pre_sum;

因为此时的root的的左子树为空,所以result[i]的值只有pre_sum一部分。

当插入的节点的值大于root的值,应该对右子树操作,如果右子树为空,那么直接插入,此时可以得到result[i]的值

result[i] = pre_sum + root->dup + root->count;

仔细观察,可以看到,result[i]的值是由三部分组成,pre_sum表示新插入的节点除了root外的所有祖先节点的左子树节点dup的和;而root->dup则是root节点的“数量”(因为上文提到,同值的数只生成一个节点,通过dup标记);而root->count则是root节点的所有左子树节点的dup的和。可以看到这就是此种插入方式所得到的result[i]的表示方式。
而若不为空,递归插入,此时注意看代码:

insert_node(root->right, val, i, result, pre_sum + root->dup + root->count);

最后一个参数的值,该值是下一层节点的pre_sum值,可以,因此,也就不难理解了。

代码:

class Solution {
public:
    struct node {
      int val;
      int dup;  //represent the number of the node with this value
      int count; //represent the result
      node *left, *right;
      node(int t_val, int t_count, node* t_left = NULL, node* t_right = NULL, int t_dup = 1) {
        val = t_val;
        dup = t_dup;
        count = t_count;
        left = t_left;
        right = t_right;
      }
    };

    void insert_node(node* root, int val, int i, vector<int>& result, int pre_sum) {
      if (val == root->val) {
        result[i] = pre_sum + root->count;
        (root->dup)++;
      } else if (val < root->val) {
        (root->count)++;
        if (root->left == NULL) {
          root->left = new node(val, 0);
          result[i] = pre_sum;
        } else { 
          insert_node(root->left, val, i, result, pre_sum);
        }
      } else if (val > root->val) {
        if (root->right == NULL) {
          root->right = new node(val, 0);
          result[i] = pre_sum + root->dup + root->count;
        } else {
          insert_node(root->right, val, i, result, pre_sum + root->dup + root->count);
        }
      }
    }

    vector<int> countSmaller(vector<int>& nums) {
      vector<int> result;
      result.assign(nums.size(), 0);
      if (nums.size() == 0) return result;
      node* root = new node(nums[nums.size() - 1], 0);
      for (int i = nums.size() - 2; i >= 0; i--) {
        insert_node(root, nums[i], i, result, 0);
      }
      return result;
    }
};

结果:

可以看到,时间大幅减少。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值