题目描述

本题来自于Leetcode的算法题库第315题,具体题目描述如下:

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

最容易想到的解题思路

这个题目看上去很容易读懂也很容易下手,但是在难度设置上是hard,其实最大的问题就是复杂度的问题,最基本的思路就是遍历输入的数组,比较每一个元素和其之后的元素使用计数器记录并添加到输出数组中去,这样的算法复杂度很显然是$O(n^2)$, 代码如下:

1
2
3
4
5
6
7
8
9
10
class :
def countSmaller(self, nums):
result = []
for i in range(len(nums)):
temp = 0
for j in range(i,len(nums)):
if nums[i] > nums[j]:
temp += 1
result.append(temp)
return result

但是这样做出来提交直接就TLE了,因此需要寻找其他的新的思路。

逆序对问题

观察这个问题会发现其实本质上可以将这个问题考虑为一个逆序对问题,逆序对问题问题描述如下:

给定一列数$a_1,a_2,cdots,a_n$, 求有多少个有序对$(i,j)$使得$i<j$但$a_i >a_j$, 这里的$n$的数值可以达到$10^6$

问题是类似的,暴力求解直接可以使用循环遍历的方法,复杂度还是$O(n^2)$,显然是会超时的,因此需要考虑别的解法,这里借鉴到的是归并排序的方法。

归并排序

归并排序思想如下图所示:

核心思想就是分而治之的思想,在拿到一个无序数组时,我们对其进行折半划分,递归划分到每一个内部只有一个数时,反向进行“并”的操作,对所有的数据进行从下往上的有序合并,最终得到有序的数组。上图只是给了拆分那一步的操作。关于归并排序的算法复杂度其实也很容易计算,首先对数据进行折半拆分,算法复杂度应该为$ O(log N)$, 而排序过程的复杂度是$O(N)$,因此这个算法的复杂度为$O(Nlog N)$。下面给出Python实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class :
def merge_sort(self,nums):
if len(nums) == 1:
return nums
mid = len(nums) // 2
left = nums[:mid]
right = nums[mid:] left_list = self.merge_sort(self,left)
right_list = self.merge_sort(self,right)
result = [] #用于存放排序完成的数组
#定义两个指针分别用于左右两边的数组
left_pointer = right_pointer = 0
while left_pointer < len(left_list) and right_pointer < len(right_list):
if left_list[left_pointer] < right_list[right_pointer]:
result.append(left_list[left_pointer])
left_pointer += 1
else:
result.append(right_list[right_pointer])
right_pointer += 1
#最后左右数组会存在剩余的元素,直接加到最后去
result += left_list[left_pointer:]
result += right_list[right_pointer:]
return result

回到逆序对问题

借鉴归并排序的方法,逆序对问题的求解可以分为三步走:

  • 划分问题:将原序列分解为尽可能长度相等的两个子序列
  • 递归过程:统计左右子序列的逆序对
  • 合并问题:统计左右子序列合并之后的逆序对

一张图可以更加清楚的理解归并排序和逆序对之间的关系:

当在result中放入一个元素时,它所在的数组的另一个数组中的数据实际上都会与它构成逆序对。可以使用前面的数组元素出列时计算逆序对个数也可以用后面的数组出列时计算逆序对个数。


下面给出计算逆序对的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Recursively implementation of Merge Sort and Inversion Pairs counts
number = 0
def merge(left, right):
result = []
global number
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
#count += len(right)
else:
result.append(right.pop(0))
number += len(left)
if left:
result += left
if right:
result += right
return result def merge_sort(L):
if len(L) <= 1:
# When D&C to 1 element, just return it
return L
mid = len(L) // 2
left = L[:mid]
right = L[mid:]
left = merge_sort(left)
right = merge_sort(right)
return merge(left, right)
# return the answer of sub-problem
if __name__ == "__main__":
test = [5,2,6,1,3]
print("original:", test)
print("Sorted:", merge_sort(test))
print("Inversion Pairs numbers:",number)

回到出发点Leetcode第315题

刚刚我们已经通过归并排序成功求解了给定一个数组其中的逆序对的个数,现在回到第315题,这个问题虽然不是直接求逆序对,但是很大程度上思想是类似的,因此我们只需要做一些简单的修改就可以实现CountSmaller的要求。

这道题目与逆序对求解最大的不同在于逆序对只要求计数,而这道题目要求的是输出每个元素的右边小于它的元素数量,是更加具体的求解。思想上可以考虑为:left数组中的元素出列时,计算right数组中已经有了多少个元素出列,这么考虑的原因在于那些right数组中已经出列的元素都是小于当前考虑的left数组中的元素。

这里我们需要使用到一个索引数组的概念,原因是基于归并排序的过程元素的位置会发生变动,而我们希望的是得到原始位置下的结果,因此需要使用一个索引来记录元素的初识位置。

下面给出实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class :
def countSmaller(self, nums):
if len(nums) == 0: return []
if len(nums) == 1: return [0]
#创建一个索引数组用于进行排序
#原数组nums不进行排序,仅仅用作比较大小的作用
index = [i for i in range(len(nums))]
#用于存放最终每一个位置上的smaller的个数
result = [0 for _ in range(len(nums))]
self.merge_sort(self,nums,index,result)
return result def merge_sort(self,nums,index,result):
if len(index) <= 1:
return index
mid = len(index) // 2
left = index[:mid]
right = index[mid:]
#递归拆分数组直至全部变为一个元素的list为止
left = self.merge_sort(self,nums,left,result)
right = self.merge_sort(self,nums,right,result)
#print(left)
#print(right)
#如果出现了已经构成了有序的数组直接返回
left_pointer = 0
right_pointer = 0
if nums[left[-1]] < nums[right[0]]:
return index
else:
for i in range(len(index)):
if left_pointer >= len(left):
index[i] = right[right_pointer]
#result[index[i]] += len(right)
right_pointer += 1
elif right_pointer >= len(right):
index[i] = left[left_pointer]
left_pointer += 1
result[index[i]] += len(right)
elif nums[left[left_pointer]] <= nums[right[right_pointer]]:
index[i] = left[left_pointer]
left_pointer += 1大专栏  leetcode_315_逆序对问题an>
result[index[i]] += len(right[:right_pointer])
else:
index[i] = right[right_pointer]
#print('process is here')
right_pointer += 1
#result[index[i]] += len(left)
#print('index_list',index)
return index

其他的一些方法

实际上个人认为上面的这种基于归并排序求逆序对的算法理解起来还是很难得,我花了很多很多的时间才理解算法的整个过程,可能过不几天就把这个算法的具体流程给忘了哈哈哈。另外的话在这一道题的Leetcode的交流区还发现了一些大神给出的一些更加简单且易懂的算法。

基于二分查找的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class :
def bisearch(self,tmp,a):
left = 0
right = len(tmp)
while left < right:
mid = (left + right) //2
if tmp[mid] <a:
left = mid +1
else:
right = mid
return left def countSmaller(self, nums: List[int]) -> List[int]:
ans=[]
tmp = []
for i in range(len(nums)-1,-1,-1):
pos = self.bisearch(tmp, nums[i])
ans.append(pos)
tmp.insert(pos,nums[i])
ans.reverse()
return ans

这个名为“望舒”的大神提供了一种很好的方法,思路基本如下:从给定数组的最后一个开始遍历,使用二分查找法将其插入一个新的数组,这个新的数组会是有序的,那么此时遍历到的那个数字在新数组中的坐标就是原来数组中其右边所有比它小的数字的个数。通过查找插入的位置来得到该数后面比其小的数字的数量。这个算法的复杂度应该是$O(N^2)$。

二叉搜索树(BST)

这里简要给出一下Binary Search Tree的说明,搜索树也被称为排序树,该树构建的核心在于所有的左子树的值都小于根结点的值,所有的右子树的值都大于根节点的值。

给出二叉搜索树的简单实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
class BTNode:
def __init__(self,data):
self.data = data
self.left = None
self.right = None class BTree:
def __init__(self,root):
self.root = root def is_empty(self):
if self.root == None:
return None
'''
查找指定的值
'''
def find_value(self,value):
if self.root == None:
return None
else:
return self.search(value,self.root)
#使用递归寻找数值
def search(self,value,node):
if node == None:
return None
elif node.data == value:
return node
elif value < node.data:
return self.search(value,node.left)
else:
return self.search(value,node.right)
'''
插入指定的值
'''
def insert(self,value):
#创建值为value的结点
node = BTNode(value)
#如果没有根节点则将这个node赋给root
if self.root == None:
self.root = node
else:
#从根节点开始搜索
current_node = self.root
while True:
#如果当前想要插入的值小于根节点则继续往左子树寻找
if value <= current_node.data:
if current_node.left != None:
current_node = current_node.left
else:
current_node.left = node
break
elif value > current_node.data:
if current_node.right != None:
current_node = current_node.right
else:
current_node.right = node
break
else:
break
'''
删除指定的结点
'''
def delete(self,value):
delete_node = self.find_value(value)
if delete_node == None:
raise ValueError("Sorry, Not value in Tree")
else:
#需要删除的是叶子结点则直接删除即可
if delete_node.left == None and delete_node.right == None:
delete_node.data = None
#单分支结点,只需要考虑修改左右子树的位置,这种情况只有右子树
elif delete_node.left == None:
delete_node = delete_node.right
#单分支结点,只需要考虑修改左右子树的位置,这种情况只有左子树
elif delete_node.right == None:
delete_node = delete_node.left
#考虑此时的情况删除节点的左右子树都不为空
elif delete_node.left != None and delete_node.right != None:
pre = delete_node.right
if pre.left == None:
#如果待删除节点的右孩子没有左子树,则待删除节点的整个右子树最小值为其右孩子
delete_node.data = pre.data
delete_node.right = pre.right
del pre
else:
next_node = pre.left
while next_node.left != None:
pre = next_node
next_node = next_node.left
delete_node.data = next_node.data
pre.left = next_node.right
del next_node #二叉树中序遍历
def print_btree(self,node,result_list):
if node == None:
return node
result_list.append(node.data)
self.print_btree(node.left,result_list)
self.print_btree(node.right,result_list) def print_function(self):
result_list = []
self.print_btree(self.root,result_list)
print(result_list)

回到原来的题目,使用BST如何计算右侧更小的数据的个数呢?

主要基于这样的思想:对于计算右侧小于当前数的规则,首先需要从维护一棵二叉树的角度考虑,我们逆序遍历给定的nums数组,利用此数组构造一个二叉排序树,举个例子(以给出的测试样例为例[5,2,6,1])更加形象一些:

  1. 首先说明为什么要逆序插入?

    逆序插入后我们可以发现对于任意节点nums[i]来说其右侧的数据都已经进入了二叉树中,当它插入BST时,插入的过程结束,计算右侧数据小于其的数量也就得到结果了。

  2. countSmaller = root.count + 1的含义?

    当下一个节点nums[i]加入BST时,如果加入了root的右子树,则说明root的左子树上的所有的值都是小于该数的,同时还有root自身也是小于nums[i]的,计算过程就显得很容易了。

leetcode_315_逆序对问题的更多相关文章

  1. 【CQOI2011】动态逆序对 BZOJ3295

    Description 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计 ...

  2. CH Round #72 奇数码问题[逆序对 观察]

    描述 你一定玩过八数码游戏,它实际上是在一个3*3的网格中进行的,1个空格和1~8这8个数字恰好不重不漏地分布在这3*3的网格中. 例如:5 2 81 3 _4 6 7 在游戏过程中,可以把空格与其上 ...

  3. POJ3928Ping pong[树状数组 仿逆序对]

    Ping pong Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 3109   Accepted: 1148 Descrip ...

  4. NOIP2013火柴排队[逆序对]

    题目描述 涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度. 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为: ∑(ai-bi)^2 其中 ai 表示 ...

  5. bzoj 3295 动态逆序对 CDQ分支

    容易看出ans[i]=ans[i-1]-q[i],q[i]为删去第i个数减少的逆序对. 先用树状数组算出最开始的逆序对,预处理出每个数前边比它大的和后边比它小的,就求出了q[i]的初始值. 设b[i] ...

  6. 诸城模拟赛 dvd的逆序对

    [题目描述] dvd是一个爱序列的孩子. 他对序列的热爱以至于他每天都在和序列度过 但是有一个问题他却一直没能解决 给你n,k求1~n有多少排列有恰好k个逆序对 [输入格式] 一行两个整数n,k [输 ...

  7. 归并求逆序数(逆序对数) && 线段树求逆序数

    Brainman Time Limit: 1000 MS Memory Limit: 30000 KB 64-bit integer IO format: %I64d , %I64u   Java c ...

  8. BZOJ 3295: [Cqoi2011]动态逆序对

    3295: [Cqoi2011]动态逆序对 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 3865  Solved: 1298[Submit][Sta ...

  9. BZOJ 3295 【Cqoi2011】 动态逆序对

    Description 对于序列\(A\),它的逆序对数定义为满足\(i<j\),且\(A_i>A_j\)的数对\((i,j)\)的个数.给\(1\)到\(n\)的一个排列,按照某种顺序依 ...

随机推荐

  1. SQL:找到特定日期每个顾客最高购买量:Find the highest purchase amount ordered by the each customer on a particular date, with their ID, order date and highest purchase amount.

    A: SELECT customer_id,ord_date,MAX(purch_amt) FROM orders GROUP BY customer_id,ord_date; find the hi ...

  2. 量化投资_关于Multicharts砖型图(传统砖型图和非传统砖型图)最详细的解释_调用篇

    1. 砖图形成数组后,我们再对他们复制到数组中然后再调用出来看一下. 2. 如下是累计只取20个数组大小为例,如果开始阶段数组长度组成初始的Array[20]的长度,然后这个数组的最后一个值填满后,进 ...

  3. elasticsearch minhash 测试应用

    上一章看了代码实现,算是搞明白了各参数的意义,现在开始测试,为方便以ik分词示例(对elasticsearch支持较好,测试操作简单) 首先建index,自定义 analysis ik分词用 ik_s ...

  4. 脚手架搭建vue项目

    1.安装安装node.js: 2.cnpm install vue-cli -g (全局安装,需要注意的是我这里是用淘宝镜像安装的,没有安装cnpm的需要先去安装一下) 3.vue --version ...

  5. mysql之DTS的那些事

    最近才考虑数据库迁移,想起了之前做DTS踩过的那些坑. 基于数据库迁移,比如从源A库迁移到源B库,包括但不限于数据库上云. 数据库迁移方案有两种场景: (1).停机迁移方案 这种方案是允许停服的场景, ...

  6. Codeforces Round #559(Div.1)

    A 签到贪心题,特判了n=1或m=1的情况才发现2<=n,m<=1e5 #include<bits/stdc++.h> using namespace std; typedef ...

  7. J. Stone game(背包问题)

    题:https://nanti.jisuanke.com/t/41420 定义 dp[x][y] 表示第 x 个数到最后一个数能组成和为 y 的方案数 #include<bits/stdc++. ...

  8. php7 安装mongodb扩展

    下载 mongodb-1.6.0.tgz   wget https://pecl.php.net/get/mongodb-1.6.0.tgz  版本太低的话有些语法不一样,起码1.5以上吧 进入 mo ...

  9. java 的CAS

    CAS:什么是 CAS 机制?cas目的是实现原子操作解释一下:"原子操作(atomic operation)是不需要synchronized",这是多线程编程的老生常谈了.所谓原 ...

  10. servlet之间传递数据的方式

    Servlet传递数据方式 基本概述 Servlet传递数据的方式有很多,这里提供五种方式: 1.静态变量 2.HttpServletResponse的sendRedirect()方法 3.HttpS ...