思考题 2-4 逆序对问题
设A[1,…n]是一个n元素数组。若在i< j 的情况下,有A[i]> A[j],则(i, j)就称为A中的一个逆序对(inversion)。例如:数组[2,3, 8, 6, 1]中有5个逆序对(1, 5)、(2,5)、(3,4)、(3,5)和(4,5)。一个极端的情况是数组A中元素按逆序大小排列,此时数组含有最多的逆序对,为
\[\left( \begin{gathered}
n \\
2 \\
\end{gathered} \right) = \frac{{n(n - 1)}}{2}\]个逆序对。
试基于合并排序算法,给出一个算法,使之能用O(n(lgn))的最坏情况运行时间,来确定由n个任意排列的元素构成的数组的逆序对数目。
思路: 既然是采用合并排序算法的思想,首先我们假定一个数组可以分为左右两部分L和R且两部分都已经排好序。取L中的第i个元素与R中的第j个元素相比较大小,如果L(i)> R(j),则L中的i以后的所有元素都大于R中的第j个元素,此时能确定有数目为len(L)– i + 1大小的逆序对。然后比较L(i)与R(j+1),如果L(i)<= R(j+1),则表示L里面第i个元素不存在与之相关的逆序对,此时i+1,用L(i+1)与R(j+1)比较,以此类推,最终得到所有逆序数。接着,我们对任意排列的n元素数组(n最好为2的幂数)采用分治法的思想进行拆分再整合,分为n/2个包含两个元素的左右子数组。
解决如上图所示的合并排序及寻找逆序对问题:原始数组为[5,2, 4, 7, 1, 3, 2, 6]被拆分为L1,R1,L2,R2四个子数组,首先对四个子数组进行寻找逆序对,发现存在[5,2]的1个逆序对。然后分别进行排序合并为[2,5, 4, 7]和[1, 3, 2, 6]左右两个子数组。从中寻找逆序对,对于左边我们比较[2,5]与[4,7]发现有[5,4]的1个逆序对;同理右边有[3,2]的1个逆序对。然后再对左右两个子数组进行合并排序得到[2,4, 5, 7, 1, 2, 3, 6],从中寻找逆序对。同样的我们比较左右四个排好序的数得到4+3+3+1=11个逆序对,总的逆序对为1+1+1+11=14个,从原始数组中统计得逆序对也为14个。注意从底层分裂的多个子数组到顶层排序好的一个数组,从始至终左右两部分的逆序数统计都是分开的互不影响的, 统计过逆序数就通过合并排序消除了这个逆序,从而使得前后统计互不重复。由此可见,合并排序的过程就是消除逆序数的过程。
合并排序与统计逆序数的整合伪代码如下:
COUNT-INVERSIONS(A, p, r) inversions ← 0 # 初始化逆序数 if p < r then q ← (p + r)/2 # 取中点拆分原数组 inversions ← inversions +COUNT-INVERSIONS(A, p, q) # 统计左边的逆序数 inversions ← inversions +COUNT-INVERSIONS(A, q + 1, r) # 统计右边的逆序数 inversions ← inversions +MERGE-INVERSIONS(A, p, q, r) # 统计总的逆序数 return inversions MERGE-INVERSIONS(A, p, q, r) # 合并排列统计逆序数子函数 n1 ← q - p + 1 # 左边L的长度 n2 ← r – q # 右边R的长度 create arrays L[1 . . n1 + 1] and R[1 . . n2 + 1] for i ← 1 to n1 do L[i] ← A[p + i - 1] for j ← 1 to n2 do R[ j] ← A[q + j] # 创建L和R左右子数组 L[n1 + 1] ← ∞ R[n2 + 1] ← ∞ # 添加哨兵牌,作用是当一部分取完值,另一部分直接添加到合并的数组中 i ← 1 j ← 1 # 初始化i,j遍历两个子数组 inversions ← 0 counted ← FALSE # 设置开关变量,使得先统计逆序数再进行合并排序 for k ← p to r # 遍历要合并的母数组 do if counted = FALSE and R[ j] < L[i] # 判断开关变量为FALSE以及L(i)比较R(j) then inversions ← inversions + n1 - i + 1 # L(i) > R(j),存在逆序数,因为L中i之后的所有数都大于R(j),则此时逆序数增加n1-i+1 counted ← TRUE # 开关变量置为TRUE表示开始执行合并排序 if L[i] ≤ R[ j] # 判断L(i)是否小于R(j) then A[k] ← L[i] # 条件成立则赋给母数组L(i),此时不存在逆序数 i ← i + 1 # 继续访问L数组的下一个值 else A[k] ← R[ j] # 条件不成立则赋给母数组R(j),以此消除逆序数 j ← j + 1 # 继续访问R的下一个值 counted ← FALSE # 由于前面只统计了L关于R(j)的逆序数,此时访问R(j+1)需要再次统计L关于R(j+1)的逆序数,开关变量置为FALSE return inversions
2. 用Python实现任意排列的数列的逆序数统计
# 用Python统计任意排列数组的逆序数 def MERGE_INVERSIONS(A, p, q, r): """结合合并排序的过程,对左右两部分L和R都排好序的数列A统计逆序数""" n1 = q - p n2 = r - q L = [] R = [] # 定义左右两个空数组 for i in range(0, n1): L.append(A[p + i]) for j in range(0, n2): R.append(A[q + j]) L.append(float('inf')) R.append(float('inf')) # 添加哨兵牌 i = 0 j = 0 inversions = 0 # 逆序数的初始值置为0 key = 0 # 定义开关变量 for n in range(p, r): if key == 0 and R[j] < L[i]: # 开关变量置为0表示进行统计L中关于R[j]的逆序数的操作 inversions = inversions + n1 - i key = 1 # 改变开关变量 if L[i] <= R[j]: A[n] = L[i] # 进行合并操作 i = i + 1 else: A[n] = R[j] j = j + 1 # 进行合并操作,这里消除了逆序数 key = 0 # 重置开关变量,继续统计L中关于R[j+1]的逆序数 return inversions def COUNT_INVERSIONS(A, p, r): """创建统计逆序数函数,统计合并排序过程中消除的逆序数""" inversions = 0 # 将逆序数置为0 if p + 1 < r: q = int((p + r)/2) inversions += COUNT_INVERSIONS(A, p, q) # 统计左一部分的逆序数 inversions += COUNT_INVERSIONS(A, q, r) # 统计右一部分的逆序数 这里可以看出左右统计逆序数互不影响 inversions += MERGE_INVERSIONS(A, p, q, r) # 统计全部逆序数 这里可以看出前后统计逆序数同样互不影响 return inversions B = [5, 2, 4, 7, 1, 3, 2, 6] p = 0 r = len(B) inversions_num = COUNT_INVERSIONS(B, p, r) print(inversions_num)