转载出处:http://blog.youkuaiyun.com/beiyeqingteng
前言:
这道题是一道非常常见的面试题,也是一道能够考察一个人的编程能力和算法的一道题。如果要求复杂度为 O(k), 是比较容易做出来的,但是,一般来讲,面试官要求给出更低复杂度的算法。网上有很多不同的解法,但是,总的来讲,那些程序考虑的因素太多,比较难懂,而且结构很乱。在这里写出自己的方法。本文的算法复杂度为 O(lg K)。
PS. 如果你没有见过这个题目,并且能够在30分钟内写出没有bug的,复杂度为 O(lg K) 的程序,我愿意拜你为师,呵呵。
问题:
给定两个已经排序的数组(假设按照升序排列),然后找出第K小的数。比如数组A = {1, 8, 10, 20}, B = {5, 9, 22, 110}, 第 3 小的数是 8.
分析:
### 最好自己先动手写一写,然后再看后面的分析,因为自己写过一遍以后,才能发现这个程序的难处在哪里。###
因为两个数组已经排序了,所以,要找出第 k 小的值,我们可以给每个数组设置一个“指针”, 比如pa, pb。在初始状态,两个指针分别指向第一个起始值,即pa = 0, pb = 0。然后,我们开始进行比较,如果A数组的值比B数组的值小, 把A数组的指针pa向前移动,然后再进行比较。每次移动,我们都能够得到当前第 i 小的值(随着pa,pb 的移动,i 会逐渐变大)。
比如对于数组A = {1, 8, 10, 20}, B = {5, 9, 22, 110},初始时,pa = 0; pb = 0,因为(A[pa] = 1) < (B[pb] = 5), 所以,第 1 小的值为 1,这个时候,我们会把 pa 向右移动,即 pa = 1, 并且 pb 保持不变: pb = 0. 然后再次比较A[pa] 和 B[pb] 的值,因为 (A[pa] = 8) > (B[pb] = 5), 所以,第 2 小的值是 5, 然后,我们增加 pb 的值。请注意,如果我们用一个变量来保存当前第 i 小的值 (随着pa, pb的移动,i 的值也会增加,那个变量保存的值也会改变)。那么,当pa + pb = k 的时候,那么那个变量保存的一定是第 k 小的值。
这里要注意的是,当一个数组的指针已经“到头”了,这个时候怎么进行越界处理呢?如果我们用if 来处理,在程序里会有一大堆的 if 判断语句,太难看了。我们这里可以用一个简单的判断语句就可以处理了。
- int Ai = (pa == A.length) ? Integer.MAX_VALUE : A[pa];
- int Bj = (pb == B.length) ? Integer.MAX_VALUE : B[pb];
好了,有了上面的分析,下面的代码就不难理解了。
- public static int kthTwoSortedArray(int[] A, int[] B, int k) throws Exception {
- if (k > A.length + B.length || k < 1) throw new Exception("out of range!");
- // pointer of array A
- int pa = 0;
- // pointer of array B
- int pb = 0;
- //store the kth value
- int kthValue = 0;
- while (pa + pb != k) {
- int Ai = (pa == A.length) ? Integer.MAX_VALUE : A[pa];
- int Bj = (pb == B.length) ? Integer.MAX_VALUE : B[pb];
- if (Ai < Bj) {
- pa++;
- kthValue = Ai;
- } else {
- pb++;
- kthValue = Bj;
- }
- }
- return kthValue;
- }
上面这个程序的复杂度是O(k),分析很简单,就不再讲了。
现在再来看如何得到 O(lg K) 的算法。
在上面的程序里,我们是逐一比较数组A 和数组B的值,但是,这样做还是太慢了,因为两个数组是已经排过序的,我们完全可以利用二分查找的方法来解决这样的问题。但是具体介绍怎么做之前,先讲一些预备知识。
对于数组A 、 B , 如果 B[pb] < A[pa] && B[pb] > A[pa - 1], 那么 B[pb] 一定是第 pa + pb + 1 小的数。 比如数组A = {1, 8, 10, 20}, B = {5, 9, 22, 110}, pa = 2, pb = 1, 这时,(B[pb] = 9) < (A[pa] =10) && (B[pb] = 9) > (A[pa - 1] = 8) ,那么,B[pb] = 9 一定是第 pa+pb+1 = 4 小的数。 换一句话说,如果我们要找第 k 小的数,那么, pa + pb = k - 1。而且,B[pb] < A[pa] && B[pb] > A[pa - 1] 或者 A[pa] < B[pb] && A[pa] > B[pb - 1] 中的其中一个必须成立。 如果不成立, 我们需要移动pa 和 pb 的位置来使得其中的一个式子成立 (参看代码)。这是本题算法的本质。
但是,这里也会出现”边界“问题,比如,k = 1, 那么 pa + pb = 1 - 1, 即 pa + pb = 0. 因为 pa >= 0, pb >=0, 所以,我们得到 pa = 0, pb = 0. 那么这样的话, 求A [pa-1]值的时候 就会有exception. 同理,对于数组A = {1, 8, 10, 20}, B = {5, 9, 22, 110},当k = 8 的时候, pa + pb = 8 - 1, 即 pa + pb = 7, 但是,pa <= 3, pb <= 3。边界的处理是这道题最最麻烦的地方。对于这个问题,我们用下面这段代码来解决。
- int Ai_1 = (pa == 0) ? Integer.MIN_VALUE : A[pa-1];
- int Bj_1 = (pb == 0) ? Integer.MIN_VALUE : B[pb-1];
- int Ai = (pa == A.length) ? Integer.MAX_VALUE : A[pa];
- int Bj = (pb == B.length) ? Integer.MAX_VALUE : B[pb];
有了上面的分析,代码就容易写出来了。
在程序里,我们设置 pa 的初始值为Math.min(A.length, k - 1),然后,通过增加或者减少pa 的值,来使得 B[pb] < A[pa] && B[pb] > A[pa - 1] 或者 A[pa] < B[pb] && A[pa] > B[pb - 1] 成立,代码中, delta 指的是 pa 的变化量,每次递归以后,delta的值变成一半。
- public class FindKthSmallestValue {
- public static int findKthSmallest(int[] A, int[] B, int pa, int delta, int k) {
- int pb = (k - 1) - pa;
- int Ai_1 = ((pa == 0) ? Integer.MIN_VALUE : A[pa-1]);
- int Bj_1 = ((pb == 0) ? Integer.MIN_VALUE : B[pb-1]);
- int Ai = ((pa == A.length) ? Integer.MAX_VALUE : A[pa]);
- int Bj = ((pb == B.length) ? Integer.MAX_VALUE : B[pb]);
- //满足其中之一条件,就返回
- if (Bj_1 <= Ai && Ai <= Bj) return Ai;
- if (Ai_1 <= Bj && Bj <= Ai) return Bj;
- //delta表示pa的变化量(增加或者减少)
- //如果 Ai > Bj, 我们要缩小pa的值,即 pa = pa - delta
- //因为 pb = (k - 1) - pa, 所以,如果delta的值太大,
- //pa会变得很小,因而 可能会导致 pb > B.length. 所以需要处理一下。
- // 对于pa = pa + delta 的处理也是一样
- if (Ai > Bj) {
- pa = ((k - 1) - (pa - delta) > B.length) ? k - 1 - B.length : pa - delta;
- return findKthSmallest(A, B, pa, (delta + 1) / 2, k);
- } else {
- pa = (pa + delta > A.length) ? A.length : pa + delta;
- return findKthSmallest(A, B, pa, (delta + 1) / 2, k);
- }
- }
- public static void main(String[] args) {
- int[] A = {1, 8, 8, 10, 20};
- int[] B = {5, 8, 8, 9, 22, 110};
- int k = 7;
- int pa = Math.min(A.length, k - 1);
- System.out.println(findKthSmallest(A, B, pa, (pa + 1) / 2, k));
- }
- }