算法导论读书笔记:合并排序算法统计逆序数及其Python实现

思考题 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)

扫描二维码关注公众号,回复: 143098 查看本文章

猜你喜欢

转载自blog.csdn.net/weixin_40170902/article/details/79047297