在讨论合并排序(和分治法)的时候我们还会遇到寻找逆序对的问题。一个逆序对是这样子的:在一个数组a当中,当下标i<j,而a[i]>a[j],那么(a[i], a[j])就称为一个逆序对(inversion)。在一个数组中寻找逆序对的数量常常会作为合并排序算法问题的一种变形。
合并算法的主要操作在于merge函数,并且它具有n*log(2)n的时间复杂度。当要合并左、右两个有序的数组,merge函数会逐一从每个数组里面取出一个元素,进行比较,将小的值放到目标数组里面,然后再从被使用了的数组里面继续取值...直到达到左、右数组中一个的结尾。利用merge函数来找出逆序对,因为在左右数组比较中,如果右边数组所取的值比左数组取的值要小,那么则说明左数组剩下的数字和右数组当前数字的关系都是逆序的:
如图,当在合并a1(左数组)和a2(右数组)到数组a时,当发现a2取的当前值“2”比a1当前值“3”要小的时候,则a1中剩下的未用的数字都会比2大,而这个数字的大小就刚好是“a1的数组长度减去当前取值的下标(如果下标从0开始的话)”或者是“a1的数组长度减去当前取值的下标(如果下标从1开始的话)加1”。
因此我们将merge函数修改如下:
代码中“cnt”代表的就是逆序对的数量,并注意函数组后一行中的“(+ cnt (- sz1 i))”就是表示将a1中所剩下的元素个数作为新发现的逆序对数目。merge-cnt函数现在返回包含一个已排序向量和该向量里原来包含的逆序对数目。
由于函数返回值增加了一个数值,因此对应的sort函数也要修改:
这个函数也是返回一个包含排好序的数组和原数组中逆序对个数的向量。
现在我们就可以直接写一个只取逆序对个数的函数了:
(defn cnt-inv [a]
(second (sort-cnt a)))
到此,这个求逆序对的函数全部完成。下面我们做一些简单的测试。
为了测试对比,我们使用一个暴力查找逆序对的函数:
(defn brute-force-cnt [a]
(loop [i 0 cnt 0]
(if (= i (count a))
cnt
(recur (+ i 1) (+ cnt (count (filter #(< % (a i)) (subvec a (+ i 1)))))))))
然后使用随机生成数组,并做比较:
(defn test-inv-cnt []
(let [a (rnm (rand-int 10) 100)]
(println a)
(println (str "cnt-inv: " (cnt-inv a)))
(println (str "brute-force-cnt: " (brute-force-cnt a)))))
稍微运行了一下,发现两者算出来的结果没有区别:
user=> (test-inv-cnt)
[35 92 20 26 74 75 71 98 21]
cnt-inv: 16
brute-force-cnt: 16
nil
user=> (test-inv-cnt)
[33 54 45 34 66 52 62 19 23]
cnt-inv: 20
brute-force-cnt: 20
nil
user=> (test-inv-cnt)
[83 65 43 29 19 72]
cnt-inv: 11
brute-force-cnt: 11
nil
user=>
有兴趣的读者不妨自己试试看。