1. 程式人生 > >LintCode二分查詢題總結

LintCode二分查詢題總結

LC上二分查詢那一章有這麼些題:



二分查詢的題經常用於考,因為它雖然看似簡單,但其實要完全正確卻不容易,很容易寫出死迴圈的程式。一個二分查詢的程式可以很容易判斷出一個人功底扎不紮實。

這是一道非常經典的二分查詢題,給出一個有序陣列以及一個目標值target,要求返回target在陣列中的位置,若數組裡不存在target,則返回-1。套用經典的二分查詢模板即可:

    public int findPosition(int[] A, int target) {
        if (A == null || A.length == 0) {
            return -1;
        }
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] == target) {
                return mid;
            } else if (A[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        if (A[start] == target) {
            return start;
        }
        if (A[end] == target) {
            return end;
        }
        return -1;
    }

14. First Position of Target

給定一個有序陣列和一個目標值,要求在O(logn)的時間內找到那個目標值第一個出現的位置(陣列中可能存在多個相同的目標值)

    public int binarySearch(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int start = 0, end = nums.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (nums[mid] >= target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        
        if (nums[start] == target) {
            return start;
        }
        if (nums[end] == target) {
            return end;
        }
        return -1;
    }

需要注意的是,當找到一個數和target相等時,要把end指標,即右指標指向target,並繼續迴圈搜尋,直到整個區間的長度<=2,這個時候就跳出迴圈,看看這最後兩個數哪個是第一個就返回它。

458. Last Position of Target

給定一個有序陣列和一個目標值,要求在O(logn)的時間內找到那個目標值最後1個出現的位置(陣列中可能存在多個相同的目標值)。跟上道題類似,需要注意的是,當找到一個數和target相等時,要把start指標,即左指標指向target,並繼續迴圈搜尋,直到整個區間的長度<=2,這個時候就跳出迴圈,看看這最後兩個數哪個是最後一個就返回它。

    public int lastPosition(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return -1;
        }
        int start = 0, end = nums.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (nums[mid] <= target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        
        if (nums[end] == target) {
            return end;
        }
        if (nums[start] == target) {
            return start;
        }
        return -1;
    }

在一個有序陣列中,給定一個target,要求在陣列中找到離target最近的數。

Given [1, 4, 6] and target = 3, return 1.

Given [1, 4, 6] and target = 5, return 1 or 2.

Given [1, 3, 3, 4] and target = 2, return 0 or 1 or 2.

還是用標準的二分查詢模板,因為它最後始終會把區間縮小為長度為2的區間內,這時候再去比較就可以得到答案了。
    public int closestNumber(int[] A, int target) {
        if (A == null || A.length == 0) {
            return -1;
        }
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] == target) {
                return mid;
            } else if (A[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        int left = Math.abs(A[start] - target);
        int right = Math.abs(A[end] - target);
        return left < right ? start : end;
    }

462. Total Occurrence of Target

要在一個有序陣列中求某個target出現了多少次,這道題可以在上上一道題的基礎上來做,找到陣列中第一個出現target的位置,然後再數它出現了幾次就行。

    public int totalOccurrence(int[] A, int target) {
        if (A == null || A.length == 0) {
            return 0;
        }
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        int count;
        if (A[start] == target) {
            count = 0;
            for (int i = start; i < A.length; i++) {
                if (A[i] == target) {
                    count++;
                }
            }
            return count;
        }
        if (A[end] == target) {
            count = 0;
            for (int i = end; i < A.length; i++) {
                if (A[i] == target) {
                    count++;
                }
            }
            return count;
        }
        return 0;
    }
60. Search Insert Position

給定一個排序陣列和一個目標值,如果在陣列中找到目標值則返回索引。如果沒有,返回到它將會被按順序插入的位置。你可以假設在陣列中無重複元素。

[1,3,5,6],5 → 2

[1,3,5,6],2 → 1

[1,3,5,6],7 → 4

[1,3,5,6],0 → 0

    public int searchInsert(int[] nums, int target) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        int start = 0, end = nums.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        if (target <= nums[start]) {
            return start;
        }
        if (target <= nums[end]) {
            return end;
        }
        return end + 1;
    }

要求在一個二維陣列中找到某個數target。這個矩陣具有以下特性:每行中的整數從左到右是排序的。每行的第一個數大於上一行的最後一個整數。

由於這個陣列是從左到右有序、從上到下有序的。所以可以把它重排成一維的有序陣列。利用除法和取模運算,把它對映到一位陣列上,然後再用一位陣列的二分搜尋來做。

    public boolean searchMatrix(int[][] matrix, int target) {
        // write your code here
        if (matrix == null || matrix.length == 0) {
            return false;
        }
        int m = matrix.length;
        int n = matrix[0].length;
        if (n == 0) {
            return false;
        }
        
        int left = 0, right = m * n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (matrix[mid/n][mid%n] == target) {
                return true;
            } else if (matrix[mid/n][mid%n] < target) {
                left = mid + 1;
            } else if (matrix[mid/n][mid%n] > target) {
                right = mid - 1;
            }
        }
        return false;
    }

求兩個陣列的交集,最後的交集必須是唯一的,不得出現重複元素。有多種方法可以求解。我先只列出HashSet的解法:

    public int[] intersection(int[] nums1, int[] nums2) {
        if (nums1 == null || nums2 == null) {
            return null;
        }
        
        HashSet<Integer> set1 = new HashSet<Integer>();
        for (int i = 0; i < nums1.length; i++) {
            set1.add(nums1[i]);
        }
        
        HashSet<Integer> res = new HashSet<Integer>();
        for (int i = 0; i < nums2.length; i++) {
            if (set1.contains(nums2[i]) && !res.contains(nums2[i])) {
                res.add(nums2[i]);
            }
        }
        
        int[] result = new int[res.size()];
        int index = 0;
        for (Integer tmp : res) {
            result[index] = tmp;
            index++;
        }
        
        return result;
    }
解法一:用HashSet,掃描第一個陣列,加進HashSet1中,得到的HashSet1是唯一的。然後掃描第二個陣列,如果第二個陣列的元素在HashSet1中存在,則加進HashSet2中。最後得到的HashSet2就是答案了。

解法二:先排序,然後掃描第二個陣列,在掃描第二個陣列的過程中使用binarySearch,binarySearch即在第一個陣列中找第二個陣列的某個元素,如果找到了則加入HashSet,這樣能保證答案是唯一的。

跟上道題類似,但是不同的是最後的答案是可以出現重複元素的,即在原陣列中出現了幾次,最後就得出現幾次。這道題得用一個HashMap,把第一個陣列的元素以及出現次數存進HashMap中,然後再對第二個陣列進行掃描。掃描的過程中,如果發現有匹配的,就加進res裡,並且把HashMap中對應的出現次數減1。

    public int[] intersection(int[] nums1, int[] nums2) {
        if (nums1 == null || nums2 == null) {
            return null;
        }
        HashMap<Integer, Integer> map = new HashMap();
        for (int i = 0; i < nums1.length; i++) {
            if (map.containsKey(nums1[i])) {
                map.put(nums1[i], 1 + map.get(nums1[i]));
            } else {
                map.put(nums1[i], 1);
            }
        }
        
        ArrayList<Integer> res = new ArrayList<Integer>();
        for (int i = 0; i < nums2.length; i++) {
            if (map.containsKey(nums2[i]) && map.get(nums2[i]) > 0) {
                res.add(nums2[i]);
                map.put(nums2[i], map.get(nums2[i]) - 1);
            }
        }
        
        int[] result = new int[res.size()];
        for (int i = 0; i < result.length; i++) {
            result[i] = res.get(i);
        }
        return result;
    }

141. Sqrt(x)

給一個整數,要求它的平方數,當然不能直接呼叫庫函式,這道題得用二分來模擬平方的操作。記得要用long來處理,因為可能會出現溢位。

    public int sqrt(int x) {
        long start = 0, end = x;
        while (start + 1 < end) {
            long mid = start + (end - start) / 2;
            if (mid * mid > x) {
                end = mid;
            } else if (mid * mid == x) {
                return (int)mid;
            } else {
                start = mid;
            }
        }
        if (start * start <= x && end * end > x) {
            return (int)start;
        }
        return (int)end;
    }

要求出一個數的開根號,只不過從整數換成了小數。當然小數是沒辦法精確地計算根號值的,只能近似的去模擬,比如誤差不大於多少1e-10。

有2種方法可以解決這個問題,第一種是二分法。因為根號值的數肯定是大於0的,如果那個數大於1,那麼它的根號值就肯定也大於1,如果那個數小於1,那麼他的根號值肯也小於1.這樣我們就有了查詢的初始區間,也就是[0, n]。然後我們去用二分法逼近,直到區間的長度縮小到了一定範圍,這樣我們就能得到近似值了。

第二種方法是牛頓法逼近,這篇部落格講的很詳細:點選開啟連結 可以在紙上畫圖體驗一下牛頓逼近法的美妙之處。

public class Solution {
    /**
     * @param x a double
     * @return the square root of x
     */
    public double sqrt(double x) {
        return binary_search_sqrt(x);
    }
    public double newton_sqrt(double n) {
        double result = 1.0;
        double eps = 1e-12;
        while (Math.abs(result * result - n) > eps) {
            result = (result + n / result) / 2;
        }
        return result;
    }
    public double binary_search_sqrt(double n) {
        double start = 0, end = n > 1 ? n : 1.0;
        while (end - start > 1e-12) {
            double mid = (start + end) / 2;
            if (mid * mid > n) {
                end = mid;
            } else {
                start = mid;
            }
        }
        return start;
    }
}

要自己實現一個求冪函式。

最直觀容易想到的方法就是用遞迴方法求n個x的乘積,注意考慮n的正負號,時間複雜度為O(n)

    public double myPow(double x, int n) {
        if (n == 0) {
            return 1.0;
        } 
        if (n < 0) {
            return 1.0/myPow(x, -n);
        }
        return x * myPow(x, n - 1);
    }

考慮到n個x相乘式子的對稱關係,可以對上述方法進行改進,從而得到一種時間複雜度為O(logn)的方法,遞迴關係可以表示為pow(x,n) = pow(x,n/2)*pow(x,n-n/2)
    public double myPow(double x, int n) {
        if (n == 0) {
            return 1.0;
        } 
        if (n < 0) {
            return 1.0/myPow(x, -n);
        }
        double half = myPow(x, n / 2);
        if (n % 2 == 0) {
            return half * half;
        }
        return x * half * half;
    }

74. First Bad Version

二分法變種題,類似於first postion of target那道題

    public int findFirstBadVersion(int n) {
        int start = 1, end = n;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (!SVNRepo.isBadVersion(mid)) {
                start = mid;
            } else {
                end = mid;
            }
        }
        if (SVNRepo.isBadVersion(start)) {
            return start;
        } else {
            return end;
        }
    }

75. Find Peak Element

找一個數組的峰值,峰值也就是他這個點比左右兩邊的元素都要大。只要找到一個峰值就行。所以根據定義,陣列的第一個元素和最後一個元素不可能是峰值。

二分的時候會有四種情況:

1)mid落在了上升區間,那這個時候的峰值肯定在mid右邊的區間

2)mid落在了下降區間,那這個時候的峰值肯定在mid左邊的區間

3)mid就在峰值,那就直接return

4)mid在極小值,那就隨便往右邊或者往左邊走都行

    public int findPeak(int[] A) {
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid - 1] <= A[mid] && A[mid] <= A[mid + 1]) {
                start = mid;
                continue;
            }
            if (A[mid - 1] >= A[mid] && A[mid] >= A[mid + 1]) {
                end = mid;
                continue;
            }
            if (A[mid - 1] <= A[mid] && A[mid] >= A[mid + 1]) {
                return mid;
            }
            end = mid;
        }
        if (A[start] < A[end]) {
            return end;
        }
        return start;
    }

61. Search for a Range

在一個有序數組裡,找到一個數出現的範圍。跟那道total occurrence of target是一樣的思想。就是求一個數出現了幾次,只不過這道題是要返回第一個出現的位置以及最後一個出現的位置:

    public int[] searchRange(int[] A, int target) {
        int[] res = new int[2];
        if (A == null || A.length == 0) {
            return new int[]{-1, -1};
        }
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        if (A[start] == target) {
            res[0] = start;
            for (int i = start; i < A.length; i++) {
                if (A[i] == target) {
                    res[1] = i;
                }
            }
            return res;
        }
        if (A[end] == target) {
            res[0] = end;
            for (int i = end; i < A.length; i++) {
                if (A[i] == target) {
                    res[1] = i;
                }
            }
            return res;
        }
        return new int[]{-1, -1};
    }

在一個很大的全都是正數的有序陣列流裡面找target,你只能通過API來獲取特定位置的元素。由於二分法是需要邊界才能進行的,而這個陣列流來說,我們需要找到它的右邊界,即第一個 ≥ target的元素的位置。這裡要用到倍增法。只要當前get到的元素比target小,我就把區間乘以2,一直到get到一個比target大的元素為止。這時候我們就在Ologn的時間內把一個無限的陣列流轉換成了一個有限有序陣列。然後再用有序數組裡二分就好了。

不過有一個地方需要注意的是,這個不是真正無限的陣列,他有一定邊界,但是他沒有告訴你邊界,但是如果你通過API get到的那個值為-1的時候,就說明越界了,所以在倍增的同時需要保持不越界。

    public int searchBigSortedArray(ArrayReader reader, int target) {
        int index = 1;
        while (reader.get(index - 1) < target && reader.get(index - 1) != -1) {
            index *= 2;
        }
        int start = 0, end = index - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (reader.get(mid) == -1) {
                end = mid;
                continue;
            }
            if (reader.get(mid) < target) {
                start = mid;
            } else {
                end = mid;
            }
        }
        
        if (reader.get(start) == target) {
            return start;
        }
        if (reader.get(end) == target) {
            return end;
        }
        return -1;
    }
159. Find Minimum in Rotated Sorted Array

在一個旋轉有序數組裡找到最小值。旋轉有序的陣列的意思是:(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2)。數組裡的元素都是唯一的,沒有重複。

總之這個陣列一定是有序的,要麼是整段有序,要麼就是由2段有序陣列組成。並且沒有重複元素的存在。

假如是整段有序,那就直接返回第一個元素就是最小值了。假如是後面那種分2段有序的情況,那就用二分查詢找那個0的位置。比前一個元素小,比後一個元素要小的就是最小值了。我們需要指定一個target,找到第一個小於等於target的數,在這裡我們需要指定陣列的最後一個元素為target。

    public int findMin(int[] num) {
        if (num == null || num.length == 0) {
            return -1;
        }
        int start = 0, end = num.length - 1;
        if (num[end] > num[start]) {
            return num[start];
        }
        int target = num[end];
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (num[mid] <= target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        if (num[start] <= target) {
            return num[start];
        } else {
            return num[end];
        }
    }

跟上道題類似,但是陣列可能會出現重複元素。比如{1,1,1,1,1,1}。這個時候,就沒法用二分來做了,所以只有從頭到尾遍歷所有元素才可以。

在一個旋轉有序數組裡找到某個target。旋轉有序的陣列的意思是:(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2)。並且裡面沒有重複元素。

總之這個陣列一定是有序的,要麼是整段有序,要麼就是由2段有序陣列組成。如果是整段有序,那用二分查詢很好做。但是關鍵是處理後面那種分段有序的情況稍微棘手一點。

如果最後一個元素比第一個元素大,那就說明這個是單調遞增的有序陣列,可以直接用二分查詢模板來做。假如最後一個元素比第一個元素小的話,那麼則首先看它是在哪段區間裡,如果start對應的元素比mid對應的元素小,則是左邊那段上升區間。如果start對應的元素比mid對應的元素大,則是右邊那段上升區間。

    public int search(int[] A, int target) {
        if (A == null || A.length == 0) {
            return -1;
        }
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] == target) {
                return mid;
            }
            // 左邊上升區間
            if (A[start] < A[mid]) {
                if (A[start] <= target && target <= A[mid]) {
                    end = mid;
                } else {
                    start = mid;
                }
            } else { // 右邊上升區間
                if (A[mid] <= target && target <= A[end]) {
                    start = mid;
                } else {
                    end = mid;
                }
            }
        }
        if (A[start] == target) {
            return start;
        }
        if (A[end] == target) {
            return end;
        }
        return -1;
    }

跟上道題類似,但是陣列可能會出現重複元素。比如{1,1,1,1,1,1}。這個時候,就沒法用二分來做了,所以只有從頭到尾遍歷所有元素才可以。

在一個二維數組裡找給定值出現了幾次,二維陣列的每一行都是有序的,每一列都是有序的。每一行、每一列中都沒有重複元素。

[
  [1, 3, 5, 7],
  [2, 4, 7, 8],
  [3, 5, 9, 10]
]
比如這個數組裡找3,則返回3出現的次數:2次。

有個巧妙的方法,就是從左下角開始搜尋,如果左下角元素比target小,則往右移動;若比target大,則往上移動;若相等,則count++,並同時x--,y++

    public int searchMatrix(int[][] matrix, int target) {
        if (matrix == null || matrix.length == 0) {
            return 0;
        }
        int m = matrix.length;
        int n = matrix[0].length;
        if (n == 0) {
            return 0;
        }
        int x = m - 1;
        int y = 0;
        int count = 0;
        while (x >= 0 && y < n) {
            if (matrix[x][y] < target) {
                y++;
            } else if (matrix[x][y] > target) {
                x--;
            } else {
                count++;
                x--;
                y++;
            }
        }
        return count;
    }

要求在一個數組中找到K個離target最近的數。我們可以先找到第一個大於等於target的數在陣列中的位置,然後再從這個位置往兩邊用雙指標同時掃描,掃描到K個數的時候,就停止掃描,這樣就得到了K個最近的數了。

    // find the first index of element >= target
    private int firstIndex(int[] A, int target) {
        int start = 0, end = A.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (A[mid] >= target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        
        if (A[start] >= target) {
            return start;
        }
        if (A[end] >= target) {
            return end;
        }
        return A.length;
    }

    public int[] kClosestNumbers(int[] A, int target, int k) {
        if (A == null || A.length == 0) {
            return A;
        }
        if (k > A.length) {
            return A;
        }
        int index = firstIndex(A, target);
        
        int start = index - 1, end = index;
        int[] res = new int[k];
        for (int i = 0; i < k; i++) {
            if (start < 0) {
                res[i] = A[end++];
            } else if (end >= A.length) {
                res[i] = A[start--];
            } else {
                if (target - A[start] <= A[end] - target) {
                    res[i] = A[start--];
                } else {
                    res[i] = A[end++];
                }
            }
        }
        return res;
    }

切木頭問題,給定幾塊木頭,和一個需要的木頭塊數目,你要如何砍這些木頭,使得在達到需要的木頭塊數目的同時又保證每塊木頭的長度一樣並且最大。

For L=[232, 124, 456], k=7, return 114. 比如給了你三塊長度分別為232、124、456的木頭,你需要把他們均分成7塊等長的木頭,並且保證木頭長度最大。

在這裡的話其實是一個二分問題,114就是我們要找的值。而區間則是[0, 最長的木頭的長度]。然後我在這個區間內進行二分,113短了,115大了,只有114才是滿足條件的。

    private int count(int[] L, int n) {
        int res = 0;
        for (int i = 0; i < L.length; i++) {
            res += L[i] / n;
        }
        return res;
    }
    public int woodCut(int[] L, int target) {
        int max = 0;
        for (int i = 0; i < L.length; i++) {
            max = Math.max(max, L[i]);
        }
        int start = 1, end = max;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (count(L, mid) < target) {
                end = mid;
            } else {
                start = mid;
            }
        }
        if (count(L, end) >= target) {
            return end;
        }
        if (count(L, start) >= target) {
            return start;
        }
        return 0;
    }

這道題最好用線段樹來做,不過暫時還沒接觸線段樹,就可以先用排序+二分查詢的方法來做。先對陣列排序,然後對於每個查詢,用二分查詢在陣列中進行查詢,找到原陣列中第一個比查詢數大的數,然後再從後往前統計。

    private int count(int[] num, int target) {
        if (num == null || num.length == 0) {
            return 0;
        }
        int start = 0, end = num.length - 1;
        while (start + 1 < end) {
            int mid = start + (end - start) / 2;
            if (target <= num[mid]) {
                end = mid;
            } else {
                start = mid;
            }
        }
        if (num[start] >= target) {
            return start;
        }
        if (num[end] >= target) {
            return end;
        }
        return 0;
    }
    public ArrayList<Integer> countOfSmallerNumber(int[] A, int[] queries) {
        Arrays.sort(A);
        ArrayList<Integer> res = new ArrayList<Integer>();
        for (int i = 0; i < queries.length; i++) {
            int n = count(A, queries[i]);
            res.add(n);
        }
        return res;
    }

解法:時間複雜度O(n * log n) 二分查詢(Binary Search)+ 鴿籠原理(Pigeonhole Principle) 參考維基百科關於鴿籠原理的詞條連結:https://en.wikipedia.org/wiki/Pigeonhole_principle “不允許修改陣列” 與 “常數空間複雜度”這兩個限制條件意味著:禁止排序,並且不能使用Map等資料結構 小於O(n2)的執行時間複雜度可以聯想到使用二分將其中的一個n化簡為log n 參考LeetCode Discuss:https://leetcode.com/discuss/60830/python-solution-explanation-without-changing-input-array 二分列舉答案範圍,使用鴿籠原理進行檢驗 根據鴿籠原理,給定n + 1個範圍[1, n]的整數,其中一定存在數字出現至少兩次。 假設列舉的數字為 n / 2: 遍歷陣列,若陣列中不大於n / 2的數字個數超過n / 2,則可以確定[1, n /2]範圍內一定有解, 否則可以確定解落在(n / 2, n]範圍內。

可以考慮構造出邊界條件陣列:[5,6,6,6] 來幫助確定二分查詢的邊界條件

public class Solution {
    /**
     * @param nums an array containing n + 1 integers which is between 1 and n
     * @return the duplicate one
     */
    public int findDuplicate(int[] nums) {
        int start = 1, end = nums.length - 1;
        while (start + 1 < end) {
            int mid = (start + end) / 2;
            if (checkSmaller(nums, mid) <= mid) {
                start = mid;
            } else if (checkSmaller(nums, mid) > mid) {
                end = mid;
            }
        }
        if (checkSmaller(nums, start) > start) {
            return start;
        }
        return end;
    }
    
    public int checkSmaller(int[] nums, int target) {
        int count = 0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] <= target) {
                count++;
            }
        }
        return count;
    }
}

617. Maximum Average Subarray

一個數組既有正數也有負數,要求出長度大於等於K的最大平均子陣列的平均長度是多少。也就是說需要找出一個長度大於等於K的子陣列,這個子陣列的平均數是最大的。只需要返回那個平均數即可,不需要求出整個子陣列是什麼。比如說一個數組是[1, 12, -5, -6, 50, 3], k = 3。那麼就返回15.667。因為 (-6 + 50 + 3) / 3 = 15.667

如果是要把整個子陣列列印輸出,那可能這道題就沒啥可以優化的地方了,直接就迴圈比較,找出平均數最大的子陣列輸出。但是問題是這道題並不要求把整個陣列打印出來,而是需要你找到一個最大的平均數。要找一個最大的數,那其實可以想到或許可以用二分搜尋去搜索這個數。

而平均數是一個小數,我們只要滿足精度的要求就好了,那麼二分搜尋的迴圈中止條件就可以是左右區間的精度小於某個可以接受的範圍。

假設陣列是A,最後需要返回的平均數是avg。假如子陣列{A[1], A[2], A[3]}是我們想要的結果的話,那sum[3] = A[1] + A[2] + A[3] - avg - avg - avg應該是大於等於0的。

假設陣列的最大值和最小值分別是max和min,那麼avg肯定是介於max和min之間的。所以二分搜尋初始化的時候可以吧left和right賦值為min和max。搜尋的時候,假如遍歷陣列發現有sum值是大於0的,那麼就可以肯定目標avg值是會比當前的mid要大的。反之則比mid小。

checksum的時候只需要檢查長度大於等於K的sum和就好,如果發現前面有badAss拖後腿,則直接刪掉badAss。然後badAss是要隨著遍歷陣列的進行而不斷更新的。

    public boolean checkRest(int[] nums, int k, double mid) {
        double[] sum = new double[nums.length + 1];
        sum[0] = 0;
        double badAss = 0.0;
        for (int i = 1; i <= nums.length; i++) {
            sum[i] = sum[i - 1] + nums[i - 1] - mid;
            if (i >= k) {
                if (sum[i] - badAss >= 0) {
                    return true;
                }
                badAss = Math.min(badAss, sum[i - k + 1]);
            }
        }
        return false;
    }
    public double maxAverage(int[] nums, int k) {
        double min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] >= max) {
                max = nums[i];
            }
            if (nums[i] <= min) {
                min = nums[i];
            }
        }
        
        while (max - min > 1e-6) {
            double mid = (min + max) / 2.0;
            if (checkRest(nums, k, mid)) {
                min = mid;
            } else {
                max = mid;
            }
        }
        
        return min;
    }

這道題給我們一個二維矩陣,表示一個圖片的資料,其中1代表黑畫素,0代表白畫素,現在讓我們找出一個最小的矩陣可以包括所有的黑畫素,還給了我們一個黑畫素的座標。這裡需要注意的一點是,所有黑色畫素都是連通的。

那我們需要找的就是最小矩陣的上邊界、下邊界、左邊界、右邊界。

再來通過如下的圖來理解一下,假設有如下的矩陣:

"000000111000000"
"000000101000000"
"000000101100000"
"000001100100000"
那其實投影后就變成了如下的樣子:
"000001111100000"
由於二維矩陣裡黑畫素是連通的,所以投影到一維的時候肯定是連續的。

row search為例, 所有含有black pixel的column,對映到row x上時,必定是連續的。這樣我們可以使用binary search,在0到y裡面搜尋最左邊含有black pixel的一列。接下來可以繼續搜尋上下和右邊界。

public class Solution {
    /**
     * @param image a binary matrix with '0' and '1'
     * @param x, y the location of one of the black pixels
     * @return an integer
     */
    public int minArea(char[][] image, int x, int y) {
        if (image == null || image.length == 0) {
            return 0;
        }
        int left = binarySearchLeft(image, 0, y);
        int right = binarySearchRight(image, y, image[0].length - 1);
        int high = binarySearchHigh(image, x, image.length - 1);
        int low = binarySearchLow(image, 0, x);
        return (right - left + 1) * (high - low + 1);
    }
    
    public int binarySearchLeft(char[][] image, int left, int right) {
        while (left + 1 < right) {
            int mid = (left + right) / 2;
            boolean black_flag = false;
            for (int i = 0; i < image.length; i++) {
                if (image[i][mid] == '1') {
                    black_flag = true;
                    break;
                }
            }
            if (black_flag) {
                right = mid;
            } else {
                left = mid;
            }
        }
        for (int i = 0; i < image.length; i++) {
            if (image[i][left] == '1') {
                return left;
            }
        }
        return right;
    }
    
    public int binarySearchRight(char[][] image, int left, int right) {
        while (left + 1 < right) {
            int mid = (left + right) / 2;
            boolean black_flag = false;
            for (int i = 0; i < image.length; i++) {
                if (image[i][mid] == '1') {
                    black_flag = true;
                    break;
                }
            }
            if (black_flag) {
                left = mid;
            } else {
                right = mid;
            }
        }
        for (int i = 0; i < image.length; i++) {
            if (image[i][right] == '1') {
                return right;
            }
        }
        return left;
    }
    
    public int binarySearchHigh(char[][] image, int left, int right) {
        while (left + 1 < right) {
            int mid = (left + right) / 2;
            boolean black_flag = false;
            for (int i = 0; i < image[0].length; i++) {
                if (image[mid][i] == '1') {
                    black_flag = true;
                    break;
                }
            }
            if (black_flag) {
                left = mid;
            } else {
                right = mid;
            }
        }
        for (int i = 0; i < image[0].length; i++) {
            if (image[right][i] == '1') {
                return right;
            }
        }
        return left;
    }
    
    public int binarySearchLow(char[][] image, int left, int right) {
        while (left + 1 < right) {
            int mid = (left + right) / 2;
            boolean black_flag = false;
            for (int i = 0; i < image[0].length; i++) {
                if (image[mid][i] == '1') {
                    black_flag = true;
                    break;
                }
            }
            if (black_flag) {
                right = mid;
            } else {
                left = mid;
            }
        }
        for (int i = 0; i < image[0].length; i++) {
            if (image[left][i] == '1') {
                return left;
            }
        }
        return right;
    }
    
}

390. Find Peak Element II

在二維數組裡面找peak,peak的定義是它比上下左右四個方向的數字都要小。而且只要找到一個peak就可以返回了。首先對每行進行二分,找到一行,然後用二分法找到這行裡面最大的元素,如果這個元素比上面的小,那就繼續往上進行二分。如果這個元素比下面的小,那就往下面進行二分。如果這個元素比上下都大,那就可以返回了。由於根據題意,每行每列都是滿足先變大後變小的,所以方法是奏效的。

    public List<Integer> findPeakII(int[][] A) {
        List<Integer> res = new ArrayList<Integer>();
        int low = 1, high = A.length - 2;
        while (low <= high) {
            int mid = (low + high) / 2;
            int col = findPeak(A, mid);
            if (A[mid][col] < A[mid - 1][col]) {
                high = mid - 1;
            } else if (A[mid][col] < A[mid + 1][col]) {
                low = mid + 1;
            } else {
                res.add(mid);
                res.add(col);
                return res;
            }
        }
        return res;
    }
    public int findPeak(int[][] A, int row) {
        int col = 0;
        for (int i = 0; i < A[row].length; i++) {
            if (A[row][i] > A[row][col]) {
                col = i;
            }
        }
        return col;
    }
假設輸入是n*n的矩陣,findPeak函式的時間複雜度是n,而findPeak被執行了logn次,所以最壞的時間複雜度是logn * n, 最好的情況是logn + n(橫豎各掃一遍就找到peak了)