题目描述
本题来自于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])更加形象一些:
首先说明为什么要逆序插入?
逆序插入后我们可以发现对于任意节点nums[i]来说其右侧的数据都已经进入了二叉树中,当它插入BST时,插入的过程结束,计算右侧数据小于其的数量也就得到结果了。
countSmaller = root.count + 1的含义?
当下一个节点nums[i]加入BST时,如果加入了root的右子树,则说明root的左子树上的所有的值都是小于该数的,同时还有root自身也是小于nums[i]的,计算过程就显得很容易了。
【CQOI2011】动态逆序对 BZOJ3295
Description 对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数.给1到n的一个排列,按照某种顺序依次删除m个元素,你的任务是在每次删除一个元素之前统计 ...
CH Round #72 奇数码问题[逆序对 观察]
描述 你一定玩过八数码游戏,它实际上是在一个3*3的网格中进行的,1个空格和1~8这8个数字恰好不重不漏地分布在这3*3的网格中. 例如:5 2 81 3 _4 6 7 在游戏过程中,可以把空格与其上 ...
POJ3928Ping pong[树状数组 仿逆序对]
Ping pong Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 3109 Accepted: 1148 Descrip ...
NOIP2013火柴排队[逆序对]
题目描述 涵涵有两盒火柴,每盒装有 n 根火柴,每根火柴都有一个高度. 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为: ∑(ai-bi)^2 其中 ai 表示 ...
bzoj 3295 动态逆序对 CDQ分支
容易看出ans[i]=ans[i-1]-q[i],q[i]为删去第i个数减少的逆序对. 先用树状数组算出最开始的逆序对,预处理出每个数前边比它大的和后边比它小的,就求出了q[i]的初始值. 设b[i] ...
诸城模拟赛 dvd的逆序对
[题目描述] dvd是一个爱序列的孩子. 他对序列的热爱以至于他每天都在和序列度过 但是有一个问题他却一直没能解决 给你n,k求1~n有多少排列有恰好k个逆序对 [输入格式] 一行两个整数n,k [输 ...
归并求逆序数(逆序对数) && 线段树求逆序数
Brainman Time Limit: 1000 MS Memory Limit: 30000 KB 64-bit integer IO format: %I64d , %I64u Java c ...
BZOJ 3295: [Cqoi2011]动态逆序对
3295: [Cqoi2011]动态逆序对 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 3865 Solved: 1298[Submit][Sta ...
BZOJ 3295 【Cqoi2011】 动态逆序对
Description 对于序列\(A\),它的逆序对数定义为满足\(i<j\),且\(A_i>A_j\)的数对\((i,j)\)的个数.给\(1\)到\(n\)的一个排列,按照某种顺序依 ...
随机推荐
Sublime Text 3 快捷键的汇总
Sublime Text 3非常实用,但是想要用好,一些快捷键不可或缺,所以转了这个快捷键汇总. 选择类 Ctrl+D 选中光标所占的文本,继续操作则会选中下一个相同的文本. Alt+F3 选中文本按 ...
js获取前n天或后n天的天数
1获取n天后的某天的日期 <!DOCTYPE html> js获取前n天或者后n天的天数 </style> <body > <h1 id="date ...
[USACO09OCT]谷仓里的回声Barn Echoes(hush、STL)
https://www.luogu.org/problem/P2957 题目描述 The cows enjoy mooing at the barn because their moos echo b ...
sql server 重命名表名,字段名
重命名表名: exec sp_rename 'oldName','newName'; 重命名字段名: exec sp_rename 'tableName.[oldName]','newName','c ...
3)利用Build.php自动创建目录和文件
(1)首先做法参照: thinkphp5的手册的 命令行--->自动生成目录结构 或者看云的资料:https://www.kancloud.cn/manual/thinkphp5/118021 ...
day13-面向对象
#解决同一类问题,使用面向对象的思想.类是制造对象的模具,类是抽象的,我们能知道它有哪些属性(name,age,saraly),但不知道具体的属性值. #看下面代码:类Penson制造了实例化对象re ...
hibernate多表查询sql,以及所得对象的处理
String sql ="SELECT id FROM tea WHERE tea.name=? "; SQLQuery query = this.getSession().cre ...
ACG记录整理
资料来源 日文维基百科 bangumi番组计划 中文维基百科 百度百科 豆瓣电影 资料类型 テレビアニメ OVA アニメ映画 Webアニメ 内容说明 番名,带超链接介绍,尽量选用国内网站介绍, ...
Block to|wreck|Range|Reach|span|chase around|amuse|exploit |instructed
English note: Block to 纷涌而至 destroy多指彻底地.毁灭性地破坏,含导致无用,不能或很难再修复的意味. wreck侧重指船只.车辆.房屋等受到严重破坏或完全毁坏,也可指计 ...
解决Navicat无法连接到MySQL的问题
解决Navicat无法连接到MySQL的问题 问题一:本地IP(xxx.xxx.xxx.xxx)没有访问远程数据库的权限.于是下面开启本地IP(xxx.xxx.xxx.xxx)对远程mysql数据库的 ...