题目描述

本题来自于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. Laravel常见问题总结

    1.Whoops, looks like something went wrong. 一般报这个问题是由于复制框架文件时没有把相应的env (隐藏文件) 复制 导致新复制的框架没有配置选项 解决方法: ...

  2. jest 测试入门(一)

    说实话,作为前端来说,单元测试,并不是一种必须的技能,但是确实一种可以让你加法的技能 之前我一个库添加了单元测试,加完之后感悟颇深,所以写下这篇文章来记录 环境搭建 一般来说,普通的库,如果没有添加 ...

  3. 注解ImportBeanDefinitionRegistrar ImportSelector

    ImportBeanDefinitionRegistrar动态注册bean https://blog.csdn.net/jiachunchun/article/details/94569246 Spr ...

  4. spring02-组件注册-@ComponentScan-自动扫描组件&指定扫描规则

    上一篇我们讲到,讲@Bean注解标在某个方法上,那么ioc容器启动的时候就会将方法返回值放到ioc容器中 在开发中,实际上包扫描用的比较多,接下来我们会介绍两种方式一种是基于xml,一种是基于注解. ...

  5. java中集合,数组,字符串相互转换

    数组转List String[] staffs = new String[]{"Tom", "Bob", "Jane"}; List sta ...

  6. Android 5.0 5.1 webview 闪退问题

    自定义webview /** * 处理Android 5.0 5.1 webview 闪退 */ class MyWebView : WebView { companion object{ priva ...

  7. 再来看看Java的新特性——其他新特性

    关于剩余Java8新特性知识点总结,包含:默认方法.Optional.CompletableFuture.时间相关. 默认方法 默认方法皆在帮助Java新功能可以兼容低版本JDK已开发的程序. 比如说 ...

  8. [GX/GZOI2019]宝牌一大堆(DP)

    出这种麻将题有意思吗? 乍看很难实则很水,就是麻将式DP,想必大家很熟悉了吧.首先把“国士无双”和“七对子”两种牌型判掉,然后观察牌胡的形式,发现每多一张牌实际上就是把1个面子变成1个杠子,然后可以直 ...

  9. Flink(三) —— 运行架构

    Flink运行时组件 JobManager 作业管理器 TaskManager 任务管理器 ResourceManager 资源管理器 Dispatcher 分发器 任务提交流程 任务调度原理 Job ...

  10. iOS 部分API理解

    - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typica ...