1. 程式人生 > >《資料結構與演算法之美》專欄閱讀筆記4——二分查詢

《資料結構與演算法之美》專欄閱讀筆記4——二分查詢

找呀找呀找朋友

文章目錄

1、二分查詢

二分查詢也叫折半查詢,是一種針對有序資料集合的查詢演算法。
原理:押大押小呀?

複雜度分析
2^k = N,O(logn)

適用場景

  • 有序資料
  • 依賴於順序表結構,用陣列效能最高,因為陣列查詢的時間複雜度為O(1),用連結串列複雜度為O(n) = n/2 + n/4 + …
  • 資料量較少或較大都不合適:資料量小,優勢不明顯(哦。。。),資料量大的情況下,因為要把查詢的關鍵字都放到數組裡(陣列佔用的是連續的記憶體哦~),貧窮限制了我的發展?

作業~【求一個數的平方根,精確到小數點後6位】
看到題目的第一反應是,啥?啥叫平方根?精確到小數點後是幾個意思?小數點單獨算?(訊號處理專業中數學最差的說的大概就是我……)反應過來啥叫平方根後,腦補了一下小數點的限制的方法,然後就……想起來作者之前的忠告:

public static double sqrt1(double value) {
        if (value <= 0)
            return 0;

        double eps = 1e-7;
        double left,right,mid;
        left = 0;
        right = value;
        while (Math.abs(left*left - value) > eps) {
            mid = (left + right) / 2;
            if (mid * mid < value) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }

強迫症般糾結的我發現36的平方根居然不是6的時候,面臨崩潰,才發現不是我的鍋。
科普一下牛頓-拉弗森法(喜歡講著麼清楚的文~)

用來求平方根的話,公式大概就是:

實現就是

public static double sqrt2(double value) {
        if (value <= 0)
            return 0;

        double c = value;
        double old = 0.0;
        double eps = 1e-7;
        while (Math.abs(old - c) > eps) {
            old = c;
            c = (c + value/c) / 2;
        }
        return c;
    }

36的平方根就是6了呢~
(還是學數學的厲害呢~)

2、變形的二分查詢

常見的四個變形問題

  • 查詢第一個值等於給定值的元素
  • 查詢最後一個值等於給定值的元素
  • 查詢第一個大於等於給定值的元素
  • 查詢最後一個小於等於給定值的元素
2.1、查詢第一個、最後一個值等於給定值的元素

下面是查詢第一個等於給定值的元素的實現程式碼。
原始寫法

public static int firstEqual(int[] values, int target) {
        int left = 0;
        int right = values.length - 1;
        int mid = left + ((right - left) >> 1);
        while (left < right) {
            if (values[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
            mid = left + ((right - left) >> 1);
        }

        while (mid >= 0 && values[mid] == target) {
            mid--;
        }

        return mid+1;
    }

跟作者給出來的說是不好理解的寫法差不多的呢,就是看到下面的while之後就發現我可能是瞬間腦抽了。不過看到“標準答案”後也明白了這樣好像更優雅,同時也說明我雖然“不小心”處理了等於的情況,但是沒有理解等於的情況交給右邊界其實可以保證左邊界收縮到第一個值的位置。

int left = 0;
        int right = values.length - 1;
        int mid = left + ((right - left) >> 1);
        while (left < right) {
            if (values[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
            mid = left + ((right - left) >> 1);
        }
        if (left < values.length && values[left] == target)
            return left;
        return -1;

另外一種更好理解的寫法,其實是把等於的情況單獨拿出來顯式宣告。跟我最開始的寫法一樣,不過比我寫的好地方在於把這個從mid往前查詢的動作放在了二分的迴圈裡面。

	   int left = 0;
       int right = values.length - 1;
       int mid = left + ((right - left) >> 1);
       while (left < right) {
           if (values[mid] < target) {
               left = mid + 1;
           } else if (values[mid] > target) {
               right = mid - 1;
           } else {
               if (mid == 0 || (values[mid-1] != target))
                   return mid;
               else
                   right = mid - 1;
           }
           mid = left + ((right - left) >> 1);
       }
       
       return -1;

查詢最後一個值等於給定值的元素是類似的,明白等於的情況交給誰處理會出現什麼樣的結果,那就很好辦了,找最後一個值就是把等於的情況交給左邊界來處理,往右壓縮,最後一個值等於給定值的元素會被壓縮到右邊界。

public static int lastEqual(int[] values, int target) {
        int left = 0;
        int right = values.length - 1;
        int mid = left + ((right - left) >> 1);
        while (left < right) {
            if (values[mid] <= target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
            mid = left + ((right - left) >> 1);
        }
        if (right < values.length && values[right] == target)
            return right;
        return -1;
    }

對應的,這樣更好理解呢~

        int left = 0;
        int right = values.length - 1;
        int mid = left + ((right - left) >> 1);
        while (left < right) {
            if (values[mid] < target) {
                left = mid + 1;
            } else if (values[mid] > target) {
                right = mid - 1;
            } else {
                if (mid == 0 || values[mid + 1] != target)
                    return mid;
                else
                    left = mid + 1;
            }
            mid = left + ((right - left) >> 1);
        }

        return -1;
2.2、查詢第一個大於等於、最後一個小於等於給定值的元素

找第一個大於等於給定值 = 找一個最小右邊界。
找最後一個小於等於給定值 = 找一個最大左邊界。

public static int lastMin(int[] values, int target) {
        int left = 0;
        int right = values.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (values[mid] <= target) {
                if (mid == values.length-1 || values[mid + 1] > target)
                    return mid;
                else
                    left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return -1;
    }
    public static int firstMax(int[] values, int target) {
        int left = 0;
        int right = values.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (values[mid] >= target) {
                if (mid == 0 || values[mid - 1] < target)
                    return mid;
                else
                    right = mid - 1;
            } else {
                left = mid + 1;
            }
        }
        return -1;
    }
2.3、思考題

在一個迴圈有序陣列中查詢值等於給定值的情況,如在[4,5,6,1,2,3]中查詢2。

public static int findTargetInCircle(int[] values, int target) {
        int length = values.length;
        int left = 0;
        int right = length - 1;
        int mid = -1;
        while (left <= right) {
            mid = left + ((right - left) >> 1);
            if (values[mid] == target) {
                return mid;
            } else if (values[mid] > target) {
                if (values[mid] >= values[left] && values[left] > target) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            } else {
                if (values[mid] <= values[right] && values[right] < target) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }
        }

        return -1;
    }

炫耀,leetcode上的第33題。

同理,對迴圈的陣列求上面四個二分的變形問題,只需要把values[mid] == target時的處理加到對應的位置即可~