题目描述
本题来自于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\)的一个排列,按照某种顺序依 ...
随机推荐
dlib安装踩过的坑
使用到dlib时遇到了下载错误,不能正常安装 以下是成功安装的记录: 首先打开Anaconda prompt选定一个python环境,安装cmake和boost pip install cmake - ...
servletHomeWork
2. http全称是什么? 超文本传输协议(HTTP, HyperText Transfer Protocol)是互联网上应用为最广泛的一种网络协议. 3.http协议是无状态的协议是什么意思?请说明 ...
吴裕雄--天生自然python机器学习:Logistic回归
假设现在有一些数据点,我们用 一条直线对这些点进行拟合(该线称为最佳拟合直线),这个拟合过程就称作回归.利用Logistic回归进行分类的主要思想是:根据现有数据对分类边界线建立回归公式,以此进行分类 ...
linux 上zookeeper安装
一.zookeeper 的安装及配置 .Zookeeper下载 wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.3.6/zooke ...
Java集合源码剖析——ArrayList源码剖析
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
Office 365管理员添加自定义域名
添加自定义域,以便Office 365允许更短.更熟悉的的电子邮件或用户ID用于服务 一.Office 365小型企业版添加自定义域名 1.使用Office 365管理员账户登陆到由世纪互联运营的Of ...
简单Arp欺骗
title date layout tags 简单arp欺骗 2018-05-08 post Python #-*- coding:utf-8 -*- from scapy.all import Et ...
关于使用MyEclipse自动生成Hibernate和Struts出现的jar不兼容的问题(antlr.collections.AST.getLine()I)
今天做Hibernate和Struts2结合的练习,使用MyEclipse自动创建Hibernate和Struts的相关配置文件和jar包,写完一个查询方法后,准备测试一下结果,简单的查询那种,很诡异 ...
EMCCD
EMCCD 即电子倍增CCD,是探测领域内灵敏度极高的一种高端光电探测产品. 在光子探测领域的应用发展对探测器灵敏度的要求不断提高,EMCCD (Electron-Multiplying CCD)技术 ...
maxquant|
使用maxquant 设定修饰: 设定打分值: 设定有标定量方法:iBAQ,虽然是无标定量方法,但是设定该值后也会有相应有标定量方法的iBAQ值. 根据计算机性能设定线程数: 设定酶切位点 最后生成文 ...