寻找两个有序数组的中值

1. 算法描述

有两个数组 A 和 B,均为有序排列,A的长度为m,B的长度为n,求 A 和 B 合在一起后的中值.

2. 问题分析

  • 这里要注意一下:要充分利用 A和B均为有序的特性
  • 该问题进一步可转化为求A和B的任意K值,如三分位、四分位.

思路一:将A和B合并成新的数组

[java]  view plain  copy
  1. /** 
  2.      * 合并有序数组,然后寻找K值 
  3.      *  
  4.      * @param a 
  5.      *            有序数组a 
  6.      * @param b 
  7.      *            有序数组b 
  8.      * @param k 
  9.      *            k值位置,0<=k<=a.length+b.length-1 
  10.      * @return k值 
  11.      */  
  12.     public static int findKthByMerge(int[] a, int[] b, int k) {  
  13.         System.out.println("Find kth by merge array first");  
  14.         int[] ab = new int[a.length + b.length];  
  15.         int ai = 0, bi = 0, abi = 0;  
  16.         while (ai < a.length && bi < b.length) {  
  17.             ab[abi++] = (a[ai] < b[bi]) ? a[ai++] : b[bi++];  
  18.         }  
  19.         while (ai < a.length) {  
  20.             ab[abi++] = a[ai++];  
  21.         }  
  22.         while (bi < b.length) {  
  23.             ab[abi++] = b[bi++];  
  24.         }  
  25.         System.out.println(Arrays.toString(ab));  
  26.   
  27.         return ab[k];  
  28.     }  

这种方法最容易想到,合并成有序数组后即可求任意k值, 其时间复杂度为 O(m+n), 空间复杂图为O(m+n)

这里反思一下:真的需要合并数组吗?


思路二:采用扫描计数方法

[java]  view plain  copy
  1. /** 
  2.      * 无需合并数组,利用计数机寻找K值 
  3.      *  
  4.      * @param a 
  5.      *            有序数组a 
  6.      * @param b 
  7.      *            有序数组b 
  8.      * @param k 
  9.      *            k值位置,0<=k<=a.length+b.length-1,k同时充当计数器 
  10.      * @return k值 
  11.      */  
  12.     public static int findKthByCounter(int[] a, int[] b, int k) {  
  13.         System.out.println("Find kth by counter");  
  14.         int ai = 0, bi = 0;  
  15.         int kth = 0// 保存K值  
  16.         while (ai < a.length && bi < b.length && k >= 0) {  
  17.             kth = (a[ai] < b[bi]) ? a[ai++] : b[bi++];  
  18.             k--;  
  19.         }  
  20.         while (ai < a.length && k >= 0) {  
  21.             kth = a[ai++];  
  22.             k--;  
  23.         }  
  24.         while (bi < b.length && k >= 0) {  
  25.             kth = b[bi++];  
  26.             k--;  
  27.         }  
  28.         return kth;  
  29.     }  

本算法是对算法一的改进,用一个临时变量保存K值,而不需要讲新合并的数组单独存储,节省了存储空间。

其 时间复杂度为O(m+n), 空间复杂度为O(1).


到此都是线性时间复杂度,已经是非常高效了,但又没有更加高效的方法进一步降低时间复杂度呢?

这里注意到原数组有序特性,利用二分特性可以将复杂度降至对数级别。


思路三:递归二分

[java]  view plain  copy
  1. /** 
  2.      * 递归二分查找K值 
  3.      *  
  4.      * @param a 
  5.      *            有序数组a 
  6.      * @param b 
  7.      *            有序数组b 
  8.      * @param k 
  9.      *            K值位置,0<=k<=m+n-1 
  10.      * @param aStart 
  11.      *            数组a初始查找位置 
  12.      * @param aEnd 
  13.      *            数组a结束查找位置 
  14.      * @param bStart 
  15.      *            数组b初始查找位置 
  16.      * @param bEnd 
  17.      *            数组b结束查找位置 
  18.      * @return k值 
  19.      */  
  20.     public static int findKth(int a[], int b[], int k, int aStart, int aEnd,  
  21.             int bStart, int bEnd) {  
  22.   
  23.         int aLen = aEnd - aStart + 1;  
  24.         int bLen = bEnd - bStart + 1;  
  25.   
  26.         // 递归结束条件  
  27.         if (aLen == 0) {  
  28.             return b[bStart + k];  
  29.         }  
  30.         if (bLen == 0) {  
  31.             return a[aStart + k];  
  32.         }  
  33.         if (k == 0) {  
  34.             return a[aStart] < b[bStart] ? a[aStart] : b[bStart];  
  35.         }  
  36.   
  37.         // 将k按比例分配到a和b中,(k+1)=ka+kb,  
  38.         int ka = (k + 1) * aLen / (aLen + bLen);  
  39.         int kb = (k + 1) - ka;  
  40.         ka += aStart;  
  41.         kb += bStart;  
  42.   
  43.         // 因为a和b有序,aStart-ka , bStart-kb yi  
  44. <span style="white-space:pre;">     </span>// 最大值进行比较         
  45.         if (a[ka] > b[kb]) {  
  46.             k = k - (kb - bStart); // bStart - kb 这段应当排除,调整k值  
  47.             aEnd = ka; // 新k值可能存在于 aStart - ka   
  48.             bStart = kb; // 新k值可能存在于 kb - bEnd 之间  
  49.         } else {  
  50.             k = k - (ka - aStart);  
  51.             bEnd = kb;  
  52.             aStart = ka;  
  53.         }  
  54.         return findKth(a, b, k, aStart, aEnd, bStart, bEnd);  
  55.     }  
本方法计算中值每次将范围缩小一半,故而 其  时间复杂度为 lg(m+n).


3. 测试算法

[java]  view plain  copy
  1. public static void main(String[] args) {  
  2.         int A[] = { 010304050808999101 };  
  3.         // int A[]={};  
  4.         int B[] = { -133365680839798200 };  
  5.         // int B[] = {};  
  6.         int k = 0;  
  7.         int kth = 0;  
  8.   
  9.         k = (A.length + B.length - 1) / 2;  
  10.         System.out.println("A.length=" + A.length + "\t" + Arrays.toString(A));  
  11.         System.out.println("B.length=" + B.length + "\t" + Arrays.toString(B));  
  12.         System.out.println("k-index = " + k);  
  13.   
  14.         kth = findKthByMerge(A, B, k);  
  15.         System.out.println(kth);  
  16.   
  17.         kth = findKthByCounter(A, B, k);  
  18.         System.out.println(kth);  
  19.   
  20.         System.out.println("递归查找");  
  21.         kth = findKth(A, B, k, 0, A.length - 10, B.length - 1);  
  22.         System.out.println(kth);  
  23.     }  

输出结果如下:

[plain]  view plain  copy
  1. A.length=9  [0, 10, 30, 40, 50, 80, 89, 99, 101]  
  2. B.length=9  [-1, 33, 36, 56, 80, 83, 97, 98, 200]  
  3. k-index = 8  
  4. Find kth by merge array first  
  5. [-1, 0, 10, 30, 33, 36, 40, 50, 56, 80, 80, 83, 89, 97, 98, 99, 101, 200]  
  6. 56  
  7. Find kth by counter  
  8. 56  
  9. 递归查找  
  10. 56  
<think>我们有一个数组:[12,21,34,50,67],需要找到23附近的值,即23的前一个值(比23小的最大值)和后一个值(比23大的最小值)。在这个例子中,23介于21和34之间,因此我们想要得到21和34。思路:1.由于数组是有序的(题目中的数组是升序),我们可以利用二分查找来快速定位。2.但也可以使用遍历,因为数组不大,但为了效率,我们考虑二分查找。步骤:假设数组是升序排列(如果不是,先排序)。使用二分查找,找到第一个大于等于23的元素的位置。如果这个位置大于0,则这个位置的前一个元素就是小于23的最大值,而这个位置的元素就是大于23的最小值。注意:如果23在数组中存在,那么我们可以找到它的位置,但题目要求是接近23的前后两个值,即使23不存在。具体算法:设数组为arr,长度为n。使用二分查找,查找第一个大于等于23的元素的索引,设为idx。情况:-如果idx=0,说明所有元素都大于23,那么就没有比23小的值,只有比23大的最小值arr[0]。-如果idx=n(即23比所有元素都大),那么就没有比23大的值,只有比23小的最大值arr[n-1]。-否则,arr[idx-1]就是小于23的最大值,arr[idx]就是大于23的最小值。在Delphi中实现二分查找(使用TArray,或者自己实现)。由于Delphi没有直接提供二分查找函数来返回第一个大于等于的位置,我们可以自己写一个。我们写一个函数,返回第一个大于等于目标值的索引,如果所有元素都小于目标值,则返回n。示例数组:[12,21,34,50,67],查找23:-第一个大于等于23的是34,索引为2(从0开始)。-因此,前一个元素索引1(值为21),后一个元素索引2(值为34)。代码示例:注意:Delphi中动态数组的索引从0开始。步骤:1.初始化low=0,high=n-12.当low<=high时:mid=(low+high)div2如果arr[mid]<目标值,则low=mid+1否则,high=mid-1,并记录候选位置mid3.最后返回low(因为循环结束时,low指向第一个大于等于目标值的位置,如果目标值大于所有元素,则low=n)或者另一种写法(返回第一个大于等于目标值的索引):functionBinarySearchGreaterOrEqual(constA:arrayofInteger;Value:Integer):Integer;varlow,high,mid:Integer;beginlow:=0;high:=Length(A)-1;Result:=Length(A);//默认设置为长度,表示没有大于等于的元素(即所有元素都小于)whilelow<=highdobeginmid:=low+(high-low)div2;ifA[mid]<Valuethenlow:=mid+1elsebeginResult:=mid;//找到一个大于等于的,但可能前面还有,所以继续向左找high:=mid-1;end;end;end;但是注意,这个函数返回的是第一个大于等于Value的索引,如果没找到则返回Length(A)。然后我们就可以用这个索引来获取前后值。代码示例:假设我们有一个数组:arr:arrayofInteger;目标值:target:=23;调用:idx:=BinarySearchGreaterOrEqual(arr,target);然后:ifidx=0then//只有后值elseifidx=Length(arr)then//只有前值else//前值:arr[idx-1],后值:arr[idx]但是注意,如果数组中有23,那么idx指向第一个23,此时前值就是arr[idx-1](如果idx>0)和arr[idx](即23本身)。但是题目要求是接近23的前后两个值,如果存在23,那么前后值应该是23本身和23前一个?还是按原意(比23小的最大值和比23大的最小值)?题目没有明确,但通常我们理解的是不包括23本身,而是23两侧的值。所以,即使存在23,我们仍然取比23小的最大值(可能是23前面的一个数,如果23重复,则取最后一个23?)和比23大的最小值(可能是23后面的一个数,如果重复则取第一个大于23的数?)。但根据要求,我们只需要23的前后值,所以如果23存在,我们可以忽略23本身,而取它之前的一个和之后的一个?但是题目要求是“接近23的前后两个值”,所以更合理的应该是取23左右两边的值,即比23小的最大值和比23大的最小值,即使23存在,也不包括23。因此,我们的方法就是取:前值:小于23的最大值(在数组中最后一个小于23的元素)后值:大于23的最小值(数组中第一个大于23的元素)注意:我们二分查找的条件是第一个大于等于23的索引,那么如果存在23,这个索引指向第一个23。那么前一个元素(idx-1)就是小于23的(因为数组有序,且是第一个23,所以arr[idx-1]一定小于23)。而arr[idx]等于23,但我们想要的是大于23的最小值,所以应该继续往后找到第一个大于23的?但是数组有序,如果23重复,那么我们需要跳过所有23,找到第一个大于23的。但是,题目没有说重复的情况,所以我们先不考虑重复。即使有重复,我们取第一个大于等于23的索引,这个位置的值可能是23,那么大于23的最小值应该是这个位置之后第一个大于23的值。但是这样就需要向后遍历,效率可能不高。另一种思路:我们可以分别查找小于23的最大值和大于23的最小值。修改算法:查找小于23的最大值:即最后一个小于23的元素。查找大于23的最小值:即第一个大于23的元素。我们可以通过两次二分查找来实现。但是,我们也可以在一次二分查找中同时得到这两个值。实际上,第一次二分查找我们找到了第一个大于等于23的位置idx,那么:小于23的最大值:如果idx>0,则是arr[idx-1];否则不存在。大于23的最小值:如果idx<Length(arr),则我们需要从idx开始向后找到第一个大于23的值?不,因为数组有序,第一个大于等于23的位置idx,如果arr[idx]等于23,那么大于23的最小值应该是从idx开始向后找到第一个不等于23的值(即大于23)?这样需要遍历,不好。所以,我们改为:查找最后一个小于23的位置:即小于23的最大值。查找第一个大于23的位置:即大于23的最小值。定义两个函数:functionFindLastLessThan(constA:arrayofInteger;Value:Integer):Integer;//返回索引,如果不存在返回-1functionFindFirstGreaterThan(constA:arrayofInteger;Value:Integer):Integer;//返回索引,如果不存在返回-1或者,我们可以通过一次二分查找同时得到两个位置?实际上,我们可以用一次二分查找来得到分界点,然后分别取分界点前一个和后一个。为了简单,我们使用两次二分查找:1.最后一个小于23的元素:可以转化为查找第一个大于等于23的位置,然后减1。因为第一个大于等于23的位置为idx,那么idx-1就是最后一个小于23的元素(如果idx>0)。2.第一个大于23的元素:查找第一个大于23的位置,这可以通过修改二分查找条件,将条件改为大于(而不是大于等于)来实现。所以,我们先找到第一个大于等于23的位置idx1,那么小于23的最大值就是arr[idx1-1](如果idx1>0)。然后,我们再找到第一个大于23的位置idx2,那么大于23的最小值就是arr[idx2](如果idx2<Length(arr))。注意:第一个大于23的位置,我们可以通过查找大于等于24的位置?不行,因为23和24之间可能有小数?但数组是整数,所以大于23的最小整数就是23+1=24?不对,因为数组元素是整数,所以大于23的最小值可能是24,也可能是25等,所以不能这样。我们可以写一个查找第一个大于目标值的函数:functionBinarySearchGreater(constA:arrayofInteger;Value:Integer):Integer;varlow,high,mid:Integer;beginlow:=0;high:=Length(A)-1;Result:=Length(A);//默认whilelow<=highdobeginmid:=low+(high-low)div2;ifA[mid]<=Valuethenlow:=mid+1elsebeginResult:=mid;high:=mid-1;end;end;end;这个函数返回第一个大于Value的元素的索引。那么:小于23的最大值:通过第一个大于等于23的位置idx1,如果idx1>0,则取arr[idx1-1];否则不存在。大于23的最小值:通过第一个大于23的位置idx2,如果idx2<Length(arr),则取arr[idx2];否则不存在。但是,注意:如果数组中有多个23,那么第一个大于等于23的位置idx1指向第一个23,而第一个大于23的位置idx2则指向第一个大于23的元素(跳过所有23)。因此,我们得到:前值(小于23的最大值):如果idx1>0,则arr[idx1-1];否则不存在。后值(大于23的最小值):如果idx2<Length(arr),则arr[idx2];否则不存在。注意:如果数组中没有23,那么idx1和idx2指向同一个位置吗?例如:数组[12,21,34,50,67],查找23:第一个大于等于23的位置:2(因为34>=23)第一个大于23的位置:也是2(因为34>23)所以,前值:arr[1]=21,后值:arr[2]=34。如果数组中有23,比如[12,21,23,34,50,67]:第一个大于等于23的位置:2(arr[2]=23)第一个大于23的位置:3(arr[3]=34)所以前值:arr[1]=21,后值:arr[3]=34。如果数组是[23,34,50,67]:第一个大于等于23的位置:0第一个大于23的位置:1所以前值不存在(因为idx1=0,所以idx1-1=-1无效),后值:arr[1]=34。如果数组是[12,21,23]:第一个大于等于23的位置:2第一个大于23的位置:3(超出数组范围,所以不存在)所以前值:arr[1]=21,后值不存在。因此,我们编写两个函数,然后根据返回值判断。代码示例:我们使用System.Generics.Collections中的TArray.BinarySearch?但该函数只能查找等于的值,不能直接用于大于等于,所以自己写。实现:函数1:FindFirstGreaterOrEqual函数2:FindFirstGreater然后:idx1:=FindFirstGreaterOrEqual(arr,23);ifidx1>0thenfrontValue:=arr[idx1-1]elsefrontValue:=?//不存在,我们可以用某种方式标记,比如返回一个布尔值或者使用异常,或者用out参数指示是否存在。idx2:=FindFirstGreater(arr,23);ifidx2<Length(arr)thenbackValue:=arr[idx2]elsebackValue:=?//不存在为了完整,我们返回两个值,并指示它们是否存在。下面我们写一个完整的函数:functionFindNearestValues(constA:arrayofInteger;Value:Integer;outFrontValue,BackValue:Integer;outFrontFound,BackFound:Boolean);varidx1,idx2:Integer;begin//初始化FrontFound:=False;BackFound:=False;//查找第一个大于等于Value的位置idx1:=FindFirstGreaterOrEqual(A,Value);ifidx1>0thenbeginFrontValue:=A[idx1-1];FrontFound:=True;end;//查找第一个大于Value的位置idx2:=FindFirstGreater(A,Value);ifidx2<Length(A)thenbeginBackValue:=A[idx2];BackFound:=True;end;end;然后,我们实现FindFirstGreaterOrEqual和FindFirstGreater。注意:数组为空的情况。下面给出完整的Delphi代码示例(控制台程序):</think>在 Delphi 中查找数组中接近指定值的前后两个元素,可以通过以下步骤实现: ### 解决方案 ```pascal function FindClosestValues(const Arr: array of Integer; Target: Integer; out Lower, Upper: Integer; out Found: Boolean): Boolean; var i, idx: Integer; begin Found := False; Lower := Low(Integer); Upper := High(Integer); // 查找目标值的位置或插入点 idx := -1; for i := 0 to High(Arr) do begin if Arr[i] = Target then begin idx := i; Break; end else if Arr[i] > Target then begin idx := i; // 第一个大于目标的位置 Break; end; end; // 未找到有效位置(所有元素都小于目标) if idx = -1 then idx := Length(Arr); // 获取前后值 if (idx > 0) and (idx <= High(Arr)) then begin Lower := Arr[idx - 1]; // 目标前的值 Upper := Arr[idx]; // 目标后的值 Found := True; end else if idx = 0 then // 目标小于所有元素 begin Upper := Arr[0]; Found := True; end else if idx = Length(Arr) then // 目标大于所有元素 begin Lower := Arr[High(Arr)]; Found := True; end; Result := Found; end; ``` ### 使用示例 ```pascal var Data: array of Integer; LowerVal, UpperVal: Integer; Success: Boolean; begin // 示例数组 [12, 21, 34, 50, 67] Data := TArray<Integer>.Create(12, 21, 34, 50, 67); if FindClosestValues(Data, 23, LowerVal, UpperVal, Success) then begin if (LowerVal <> Low(Integer)) and (UpperVal <> High(Integer)) then ShowMessage(Format('小于23的最大值: %d, 大于23的最小值: %d', [LowerVal, UpperVal])) else if LowerVal = Low(Integer) then ShowMessage(Format('所有值都大于23, 最小值: %d', [UpperVal])) else ShowMessage(Format('所有值都小于23, 最大值: %d', [LowerVal])); end else ShowMessage('数组为空'); end; ``` ### 输出结果 ``` 小于23的最大值: 21, 大于23的最小值: 34 ``` ### 算法说明 1. **查找逻辑**: - 遍历数组找到目标值位置或第一个大于目标值的位置 - 若目标值存在,直接定位;若不存在,定位到其应插入的位置 2. **边界处理**: - 目标小于所有元素:返回第一个元素作为上界 - 目标大于所有元素:返回最后一个元素作为下界 - 目标在数组范围内:返回前一个元素(下界)和后一个元素(上界) 3. **返回值**: - 通过 `out` 参数返回下界(`Lower`)和上界(`Upper`) - 函数返回 `Boolean` 指示是否找到有效值 ### 优化建议 - 对于**有序大数组**,可将遍历改为二分查找提升效率 - 需要处理**重复值**时,可修改比较逻辑为 `>=` 和 `<=` - 若需返回**精确匹配**,可额外添加标志位
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值