基于逆序对的“排序距离”度量与Kendall Tau相关性的计算优化
题目描述
我们通常用逆序对的数量来衡量一个排列相对于完全有序状态的“混乱程度”。在统计学和信息检索中,Kendall Tau 等级相关系数(Kendall Tau Rank Correlation Coefficient)正是基于这个思想,用于度量两个排列(或排名序列)之间的相似性。
具体来说,给定两个长度均为 n 的排列 arr1 和 arr2(它们都是数字 1 到 n 的某种排列),我们可以定义一个“顺序一致对”的概念:对于一对索引 (i, j) 且 i < j,如果在两个排列中,元素 arr1[i] 和 arr1[j] 的相对顺序(即谁前谁后)与 arr2[i] 和 arr2[j] 的相对顺序一致,则称这对索引是顺序一致的;否则,就是顺序不一致的(即构成一个“逆序对”)。
Kendall Tau 距离(或相关性)就是通过计算顺序不一致对的数量来定义的。本题要求:
- 理解如何将一个排列“对齐”到另一个排列,从而将问题转化为标准逆序对计数问题。
- 设计一个基于归并排序的高效算法,计算两个给定排列之间的顺序不一致对数量(即逆序对数量)。
- 分析算法的时间复杂度和空间复杂度。
解题过程
步骤1:问题转化——映射建立
我们无法直接比较 arr1 和 arr2 在不同索引下的值。关键技巧是,我们以其中一个排列为“参考基准”,将另一个排列“映射”到一个新的序列上,使得在新序列中计算逆序对,就等价于计算两个原始排列之间的顺序不一致对。
具体操作:
- 假设我们以
arr1作为基准,认为它是标准顺序[1, 2, ..., n](注意,arr1本身是1到n的一个排列,我们可以将其看作一个“自定义”的标准顺序)。 - 构建一个映射
position:对于arr1中的每个数字x,记录它出现的位置索引(即arr1[position[x]] = x)。由于arr1是排列,每个数字出现且仅出现一次。 - 根据这个映射,将
arr2“翻译”成一个新序列mappedArr:对于arr2中的每个数字y,mappedArr中对应位置的值是position[y]。也就是说,mappedArr[i] = position[arr2[i]]。
为什么这样做?
- 在原始问题中,我们比较的是数字在两个排列中的相对顺序。
- 经过映射后,
mappedArr[i]表示的是数字arr2[i]在arr1(基准排列)中的位置。 - 现在,在
mappedArr中,如果存在一对索引(i, j)且i < j,但mappedArr[i] > mappedArr[j],这意味着什么?- 在
arr2中,数字arr2[i]排在arr2[j]前面。 - 但是,在基准
arr1中,arr2[i]的位置(position[arr2[i]])却比arr2[j]的位置(position[arr2[j]])靠后。 - 因此,这对
(i, j)在两个排列中的相对顺序是不一致的!它正好构成了一个我们想找的“顺序不一致对”。
- 在
- 结论:计算
mappedArr中的逆序对数量,就等于计算原始排列arr1和arr2之间的顺序不一致对数量。
举例:
- 设
n = 4。 arr1 = [1, 3, 4, 2](我们的基准排列)。arr2 = [4, 2, 1, 3]。- 构建
position:position[1] = 0(数字1在arr1的索引0)position[3] = 1position[4] = 2position[2] = 3
- 构造
mappedArr:arr2[0]=4->position[4]=2->mappedArr[0]=2arr2[1]=2->position[2]=3->mappedArr[1]=3arr2[2]=1->position[1]=0->mappedArr[2]=0arr2[3]=3->position[3]=1->mappedArr[3]=1- 所以
mappedArr = [2, 3, 0, 1]。
- 计算
mappedArr的逆序对:- 对 (0,2): 2>0 ✓
- (0,3): 2>1 ✓
- (1,2): 3>0 ✓
- (1,3): 3>1 ✓
- (2,3): 0<1 ✗
- 共有4个逆序对。
- 验证:原始两个排列
[1,3,4,2]和[4,2,1,3]的顺序不一致对确实也是4个(可以手动枚举验证)。
步骤2:基于归并排序的逆序对计数
现在问题转化为:给定序列 mappedArr,如何高效计算它的逆序对数量?一个经典且高效的方法是在归并排序的过程中进行统计。
算法思想(递归分治):
- 分解:将数组
mappedArr平均分成左右两半。 - 递归解决:递归地对左半部分和右半部分分别进行排序,并分别得到左半部分内部的逆序对数量
leftInv和右半部分内部的逆序对数量rightInv。 - 合并与统计:在将两个已排序的子数组合并成一个有序数组的过程中,统计“跨越”左右两部分的逆序对数量
crossInv。- 假设左半部分为
L,右半部分为R,它们各自已经有序。 - 我们使用双指针
i(指向L)和j(指向R)进行归并。 - 关键观察:当
L[i] > R[j]时,由于L有序,L[i]及其之后的所有元素都大于R[j]。因此,对于当前的R[j],它与左半部分中从i到末尾的所有元素都构成逆序对。 - 所以,
crossInv可以增量计算:当我们将R[j]放入合并后的数组时,给计数器加上(mid - i),其中mid是左半部分的结束位置索引。
- 假设左半部分为
- 汇总:总逆序对 =
leftInv + rightInv + crossInv。
步骤3:详细算法实现
我们实现一个函数 countInversions(arr, temp, left, right),它返回子数组 arr[left..right] 中的逆序对数量,并在过程中将这部分排序到临时数组 temp 中,最后拷贝回原数组。
伪代码/关键步骤:
函数 countAndSort(arr, temp, left, right):
如果 left >= right:
返回 0
mid = (left + right) // 2
invCount = 0
invCount += countAndSort(arr, temp, left, mid) // 左半逆序对
invCount += countAndSort(arr, temp, mid+1, right) // 右半逆序对
invCount += mergeAndCount(arr, temp, left, mid, right) // 跨左右逆序对
返回 invCount
函数 mergeAndCount(arr, temp, left, mid, right):
i = left // 左半部分起始
j = mid+1 // 右半部分起始
k = left // 临时数组起始
invCount = 0
当 i <= mid 且 j <= right:
如果 arr[i] <= arr[j]:
temp[k] = arr[i]
i++
否则: // arr[i] > arr[j],发现逆序对
temp[k] = arr[j]
invCount += (mid - i + 1) // 核心计数!
j++
k++
// 拷贝剩余部分
当 i <= mid:
temp[k] = arr[i]; i++; k++
当 j <= right:
temp[k] = arr[j]; j++; k++
// 将排序后的片段拷回原数组
将 temp[left..right] 复制到 arr[left..right]
返回 invCount
主函数计算Kendall Tau距离:
函数 kendallTauDistance(arr1, arr2):
n = arr1 的长度
创建数组 position[1..n] // 假设数字从1开始,否则需要偏移
for i from 0 to n-1:
position[arr1[i]] = i
创建数组 mappedArr[0..n-1]
for i from 0 to n-1:
mappedArr[i] = position[arr2[i]]
创建临时数组 temp[0..n-1]
返回 countAndSort(mappedArr, temp, 0, n-1)
最终返回的逆序对数量,就是两个排列之间的 Kendall Tau 距离。如果需要Kendall Tau相关系数,公式为:τ = 1 - (4 * distance) / (n*(n-1)),其值范围在[-1, 1]之间,1表示完全一致,-1表示完全相反。
步骤4:复杂度分析
- 时间复杂度:
O(n log n)。构建映射position和mappedArr需要O(n)时间。基于归并排序的逆序对计数也是O(n log n)时间。因此总时间复杂度为O(n log n)。 - 空间复杂度:
O(n)。主要开销来自归并排序时使用的临时数组temp,以及映射数组position和mappedArr,它们的大小均为O(n)。
总结
通过巧妙的映射,我们将两个排列的相似性比较问题,转化为一个经典序列的逆序对计数问题。再利用分治思想(归并排序)高效地解决逆序对计数。这种方法不仅高效,而且清晰地揭示了“顺序不一致”的直观含义与逆序对之间的本质联系。该算法是计算 Kendall Tau 等级相关系数的标准高效方法,在数据分析和推荐系统等领域有广泛应用。