2lgN for ditonic array search

    • 2lgN for ditonic array search

      I have solutions for both the 3lgN and 2lgN algorithms for finding an element in a bitonic array.  So if you are still working on this, don't read further.  I've coded these algorithms up and tested them, both for correctness and complexity, so I know they work.

      First, the 3lgN is pretty simple conceptually.  Find the peak in lgN, and then use binary search on the left and right half.  

      The only tricky part is finding the peak with only lgN compares.  This requires a single comparison for each time you divide the range in half. Most algorithms I've seen do multiple comparison operations with a[mid], a[mid-1] and a[mid+1] to determine what to do next.  If you do more than one comparison, you kill the ~1 lgN complexity guarantee.

      So, here is my ~lgN code for finding the peak:
      public int find_peak()
      {
          int lo = 0, hi = a.length-1;
          // Invariant: lo <= peak <= hi 
          while (lo < hi) {
              int mid = (lo+hi)/2;
              if (a[mid] < a[mid+1]) lo = mid+1;
              else hi = mid;
          }
          return lo;
      }

      Then the finder is very simple:
      public boolean simple_search(int key)
      {
          int lo = 0, hi = a.length-1;
          int peak = find_peak();
          return left_search(key, lo, peak+1) || 
                 right_search(key, peak, hi);
      }
      The "peak+1" is because my code for the left_search and right_search are on the open ranges (see the next post for details), so we add 1 to the first one to make sure we include the peak itself.

      So, lgN for finding the peak, then lg(N/2) for each side, which (since lg(N/2) ~ lg(N)) gives us a total of ~3lgN.

      The 2lgN solution follows...
      0
      · flag
      • Now for the signing bonus.  :)  The 2lgN solution requires doing the splitting without knowing exactly where the peak is.  

        The key insight is that if you know a[lo] <= key < a[hi] (The open inequality for a[hi] is important!), then you can use a binary search on the semi-open range [lo,hi), even if a is bitonic.  

        Proof: If a is bitonic in the range [lo,hi), then we can divide up the range into [lo, k), [k, peak), [peak, hi) where k is the first index with a[k] > a[hi].  We know the answer can only fall into the first region, [lo,k), since key < a[hi] < a[k], so everything in the second two regions are necessarily > key. 

        Thus, if we find key < a[mid], it is safe to set hi = mid, which maintains the invariant a[lo] <= key < a[hi].
        Likewise, if key >= a[mid], it is safe to set lo = mid, which also maintains the invariant.

        The same is true for the right search if a[lo] > key >= a[hi].


        When we start the algorithm for the bitonic search, we have the condition (key is not in array) or (key >= a[lo] and key >= a[hi]).

        So when we split this array in half, we check if key < a[mid].  If so, we have the conditions we need to do the binary searches, so we just do so on both the left and the right.

        But if not, we need to figure out which half the key might be in.  We want to search whichever half has the peak, since the other half is all below the key value.  So we use the same check we used for find_peak: a[mid] < a[mid+1] and search the appropriate half.  Here is the complete algorithm:
        public boolean left_search(int key, int lo, int hi)
        {
            // Invariant: a[lo] <= key < a[hi]
            while (lo < hi-1) {
                int mid = (lo + hi) / 2; 
                if (key < a[mid]) hi = mid;
                else lo = mid;
            }
            return (key == a[lo]);
        }
            
        public boolean right_search(int key, int lo, int hi)
        {
            // Invariant: a[lo] > key >= a[hi]
            while (lo < hi-1) {
                int mid = (lo + hi) / 2; 
                if (key < a[mid]) lo = mid;
                else hi = mid;
            }
            return (key == a[hi]);
        }
        
        public boolean bitonic_search(int key)
        {
            int lo = 0, hi = a.length-1;
            // Invariants: a is bitonic from lo..hi,
            //             key >= a[lo]. and
            //             key >= a[hi]
            while (lo < hi-1) {
                int mid = (lo + hi) / 2;
                if (key < a[mid]) {
                    return left_search(key, lo, mid) ||
                           right_search(key, mid, hi);
                } else {
                    if (a[mid] < a[mid+1]) lo = mid;
                    else hi = mid;
                }
            } 
            return (key == a[lo]) || (key == a[hi]);
        } 
        Each split on the main function only does up to 2 compares.  And if it spawns off into the left_search and right_search functions, each of them does 1 compare per split.  So the total complexity is only ~2lgN.

        I confirmed this by adding code that counts how many compares are done along the way, and the results for  104  runs with  N=105  are:
        Fast algorithm (bitonic_search) for N = 100000:
            Average number of compares = 32.905 = 1.9355882352941176 lg N
            Max number of compares = 35 = 2.0588235294117645 lg N
        Simple algorithm (simple_search) for N = 100000:
            Average number of compares = 47.7897 = 2.811158823529412 lg N
            Max number of compares = 51 = 3.0 lg N
        I also checked for correctness by comparing against the trivial linear search to make sure both algorithms found the key when they were supposed to.  It did take a while to get all the details right.  As Prof. Sedgewick said, binary search is notoriously hard to get all the details correct, and the bitonic search was probably even trickier still.   For example, I couldn't find a way to have the while loop in bitonic_search be "while (lo < hi)" in order to only have a single check "(key == a[lo])" at the end.  I think you have to stop when the range is 2 and check both by hand at that point.  So I end up with a worst case complexity of 2lgN+1 rather than 2lgN.  Still ~2lgN, but I was hoping to get rid of that extra compare.

        Anyway, thanks to the professors for an interesting challenge.  I quite enjoyed this problem.  I don't think I would have been able to come up with the second one while under pressure in a job interview.  :)
    • 0
      点赞
    • 1
      收藏
      觉得还不错? 一键收藏
    • 1
      评论

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

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

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值