算法之逆序对儿查找

算法之逆序对儿查找

话说昨晚一夜没休息好,满脑子都是想的这到算法题目。早上决定把它记录下来。对于数组中逆序对儿的查找是非常普遍的一到算法题,各种面试中都会出现它和它的变种。我们首先来看看查找逆序对儿的问题定义:

输入:给定元素各不相同数组; 输出:数组中逆序对儿的个数; 其中,逆序对儿的定义是,给定数组A下标i, j,其中i < j,但是A[i] > A[j],则我们称A[i]和A[j]是一个逆序对儿。

举个栗子

逆序对儿是啥,干巴巴的看定义有点生硬。这里我们举个栗子,给定数组:

[1, 3, 5, 2, 4, 6] 那么,我们很明显的能看到3在2的前面,所以3和2是一对儿逆序对儿;同样,5和2, 5和4又是另外两对儿逆序对儿。

意义

这里你又要问了,费这老大劲儿查找数组的逆序对儿有个鸡毛用呀。这就涉及到具体应用场景了。这里举个简单的应用场景。我们都喜欢把自己看的书和电影都在豆瓣上进行评分、评论等标记,那么豆瓣是如何知道你有什么兴趣、跟其他人兴趣是否相似的呢?假设这里有10部大部头的书,你对它们进行喜好程度的排名得到数组A,而A中的值对应着每一部书在其它第三人哪里的排名,则就根据逆序对儿的查找可以判断出兴趣的相似性。比方说《白鹿原》,你排在第一,故把它放在了A1这个位置,但是另一个做相似性对比的人觉得它也就能拍个第8,则A1=8。类似这样就能得到一个长度为10的数组,根据其中的逆序对儿即可计算出兴趣爱好的相似性。 知道意义之后,我们来看看怎么查找数组中的逆序对儿。

遍历数组解决

估计你我都一样,跟绝大数其它同学一样能想到最直接的方法就是遍历数组,然后做对比。这里因为很简单,我们只给出简单的伪代码,具体实现留给读者自己。

// 问题:给定数组长度为N的数组,元素各不相同,返回该数组中逆序对儿的个数。
inversionsNum = 0;
for i = 0; i 小于n; i++
   for j = i+1; j < n; j++
       if A[i] > A[j]
          inversioinsNum++; 
复制代码

这里,根据伪代码我们很容易知道这里原始遍历数组的方法的复杂度为O(n的平方)。那么,至此我们仍然要问一下自己,我们是否能再做的好一点?答案是显然的,否则也就不用继续写下去了。

归并查找逆序对儿

既然我们之前了解过归并排序,同时也了解了分治策略,那么何不把它在这里应用一番。在继续之前,我们需要明白几个概念,并且为了利用归并排序重新定义一下问题:

概念: 左逆序对儿:假设数组从中间一分为二,则出现在左半边数组A1的逆序对儿被称为左逆序对儿; 右逆序对儿:同左逆序对儿类似,出现在右半边数组A2的逆序对儿被称为右逆序对儿; 交叉逆序对儿:即逆序对儿A[i]和A[j],其中A[i]出现在左半边数组A1中,A[j]出现在右半边数组A2中。 问题:我们将过去定义的返回数组中逆序对儿的个数改为返回数组逆序对儿的个数和排序后的数组。

知道这些后,我们来看看为什么又把归并排序给扯上关系了。 分治策略的根本就是分而治之,这里我们把数组对半分下去就是这个原因,这个没啥说的。这里需要重点说明一下归并排序中归并对于交叉逆序对儿的意义。我们知道,给定一个数组,假设用我们上面给的数组A=[1, 3, 5, 2, 4, 6],通过归并排序后最终得到的是A1=[1, 3, 5]和A2=[2, 4, 6]。在最终归并这两个数组的时候,即一个个从A1或者A2中取值放到B中的时候,很明显,如果没有逆序对儿,那么A1的元素肯定先放完。如果A1的元素还没放完,A2的元素就要往B中放,那么肯定出现了逆序对儿,并且逆序对儿的个数还与A1中剩下的元素有关。 例如:第一个放A1[1]即1到B中,第二个放A2[1]即2到B中,则这时候A1中还有3和5这两个元素,则出现了两个逆序对儿;继续放A1[2]即3到B中,然后放A2[2]即4到B中,这时候A1中还有5这个元素没放,则出现了一个逆序对儿;然后继续直到放置完毕。 从这里我们可以看到,交叉逆序对儿的个数跟A2的元素放置的时候A1中没被放置的个数有关。至此,我们设计算法可以将数组无限划分直到元素个数为1,然后再逐步归并查找逆序对儿。

归并查找逆序对儿伪代码
定义函数inversionsCount
if n =0 or n = 1
  return (arr, 0)
else 
  (C, leftInversions) = inversionsCount(A左半边数组)
  (D, rightInversions) = inversionsCount(A右半边数组)
  (B, splitInversions) = mergeSplitCount(C, D)
  return (B, leftInversions + rightInversions + splitInversions)

其中mergeSplitCount算法为:
定义函数mergeSplitCount
i = j = 0; k = 0; cts = 0;
for k=0; k < n; k++
   if C[i] > D[j]  
       B[k] = D[j]
       k++;
       j++;
       cts = n - i;
   else 
       B[k] = C[i]
       k++;
       i++
return (B, cts)
复制代码

至此,希望你已经完全看懂了所有的步骤。下面我们看看具体的JavaScript中的实现吧。

归并查找逆序对儿JavaScript的实现
function inversionsCount (arr) {

  let l = arr.length, i = Math.floor(l/2);

  if ( [0, 1].indexOf(l) !== -1) {
    return {
      arr: arr,
      cts: 0
    };
  } else {
    let lfObj = inversionsCount(arr.slice(0, i));
    let rtObj = inversionsCount(arr.slice(i, l));
    let mdObj = mergeSplitCount(lfObj.arr, rtObj.arr);
    return {
      arr: mdObj.arr,
      cts: lfObj.cts + rtObj.cts + mdObj.cts
    }
  }

}


function mergeSplitCount (arrLeft, arrRight) {
  var ll = arrLeft.length, lr = arrRight.length, cts = 0, i = j = k = 0, resArr = [];

  while (i < ll && j < lr) {
    if (arrLeft[i] < arrRight[j]) {
       resArr[k] = arrLeft[i];
       k++;
       i++;
    } else {
       resArr[k] = arrRight[j];
       k++;
       j++;
       cts += ll - i;
    }
  }

  while (k < ll + lr) {
    if (i >= ll) {
      resArr[k] = arrRight[j];
      k++;
      j++;
    } else {
      resArr[k] = arrLeft[i];
      i++;
      k++;
    }
  }

  return {
    arr: resArr,
    cts: cts
  }

}
复制代码
归并查找逆序对儿的时间复杂度

这里很容易看到归并查找逆序对儿跟归并排序类似,都是二分法将原数组分治,然后逐级归并解决问题。因此很容易得出复杂度为O(nlogn)

原文同样发表于Github Issues,欢迎关注。

转载于:https://juejin.im/post/5c8f0faee51d45614372ae15

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值