题目描述
本题来自于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\)的一个排列,按照某种顺序依 ...
随机推荐
Windows10配置Jmeter环境
注:在安装Jmeter之前,请先检查下电脑有没有装JDK:[Win+R]然后输入cmd->进入命令行界面,输入java -version 出现以下信息就是此电脑已安装了JDK.由于jmeter要 ...
dubbo的启动时检查
修改的消费者(xml) 修改的消费者(注解)
java去掉数字后面的0
有些财务业务场景是需要把数字多余的0去掉的. 可以这么写 private String getRealData(BigDecimal num) { if (num == null) { return ...
selenium 2.x 为什么我录制的脚本回放时几乎必然失败呢?
本人菜鸟一枚,最近自己在自学selenium,录制的脚本回放从未直接成功过! 我打开百度,搜索selenium,然后点击第一个结果——selenium的百度百科,但是提示打开错误! 录制的任何脚本都不 ...
oracle 查询表空间
测试用户连接C:\Users\ZP>sqlplus /nologconn hbcxuser/hbcxpass --查看所有表空间 select * from user_tablespaces-- ...
Opencv笔记(十)——卷积基础
卷积 什么是二维卷积呢?看下面一张图就一目了然: 卷积就是循环对图像跟一个核逐个元素相乘再求和得到另外一副图像的操作,比如结果图中第一个元素5是怎么算的呢?原图中3×3的区域与3×3的 ...
Apsara Clouder云计算专项技能认证:云服务器基础运维与管理
一.三个理由拥抱云服务器 1.课程目标 如何拥有一台属于自己的ECS 出现一些问题的时候,对这台云服务器进行很好的管理 如何保证一台云服务出现问题的时候提前进行防范 2.云服务的定义 云服务器(Ela ...
Idea mac
Idea 的破解 http://idea.lanyus.com/ Idea 的常用配置 模版及模版的使用 创建 JavaWeb 或 Module 关联数据库 版本控制 断点调试 配置 maven 其他 ...
STM32学习中出现的错误
1.添加了多个文件后编译发现出现了无效的重复声明: 原因:文件(头文件)调用的时候重复调用, 解决办法:每个头文件写的时候包含以下代码: #ifndef __STM32F10X_H //头文件 ...
SSH(struts+spring+hibernate)常用配置整理
SSH(struts+spring+hibernate)常用配置整理 web.xml配置 <?xml version="1.0" encoding="UTF-8&q ...