leetcode_315_逆序对问题

题目描述

本题来自于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)$,显然是会超时的,因此需要考虑别的解法,这里借鉴到的是归并排序的方法。

归并排序

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

Merge Sort

核心思想就是分而治之的思想,在拿到一个无序数组时,我们对其进行折半划分,递归划分到每一个内部只有一个数时,反向进行“并”的操作,对所有的数据进行从下往上的有序合并,最终得到有序的数组。上图只是给了拆分那一步的操作。关于归并排序的算法复杂度其实也很容易计算,首先对数据进行折半拆分,算法复杂度应该为$ 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数组中的元素。

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

index list for record the position of all elements

下面给出实现的代码:

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])更加形象一些:

An Toy Example

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

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

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

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

原文地址:https://www.cnblogs.com/lijianming180/p/12302554.html