算法進階面試題02——BFPRT算法、找出最大/小的K個數、雙向隊列、生成窗口最大值數組、最大值減最小值小於或等於num的子數組數量、介紹單調棧結構(找出臨近的最大數)

第二課主要介紹第一課餘下的BFPRT算法和第二課部份內容算法

 

一、BFPRT算法詳解與應用

找到第K小或者第K大的數。api

普通作法:先經過堆排序而後取,是n*logn的代價。數組

    // O(N*logK)
    public static int[] getMinKNumsByHeap(int[] arr, int k) { if (k < 1 || k > arr.length) { return arr; } int[] kHeap = new int[k];//存放第k小的數
        for (int i = 0; i != k; i++) {//把k個數造成大頂堆
 heapInsert(kHeap, arr[i], i); } //剩餘的數,逐個檢查是否有小於堆頂的數
        for (int i = k; i != arr.length; i++) { if (arr[i] < kHeap[0]) { kHeap[0] = arr[i]; heapify(kHeap, 0, k); } } return kHeap; } public static void heapInsert(int[] arr, int value, int index) { arr[index] = value; while (index != 0) { int parent = (index - 1) / 2; if (arr[parent] < arr[index]) { swap(arr, parent, index); index = parent; } else { break; } } } public static void heapify(int[] arr, int index, int heapSize) { int left = index * 2 + 1; int right = index * 2 + 2; int largest = index; while (left < heapSize) { if (arr[left] > arr[index]) { largest = left; } if (right < heapSize && arr[right] > arr[largest]) { largest = right; } if (largest != index) { swap(arr, largest, index); } else { break; } index = largest; left = index * 2 + 1; right = index * 2 + 2; } }

 

基於荷蘭國旗問題,能夠實現o(N)的代價。ide

 

 

每次都分小於等於大於區域,再判斷是拿大於區域仍是小於區域繼續劃分,等於的話就直接出答案。函數

 

選劃分值是關鍵。最差狀況可能致使o(n²)spa

由於每一個位置都是等機率的,機率累加,數學上的長期指望是o(n)的。3d

 

BFPRT算法,是嚴格o(n)的。code

流程:blog

 

 

bfprt得到劃分的中位數排序

 

 

例子:

 

目的:達到第三步若是還超過5個,要繼續遞歸調用劃分,直到少於5個找到劃分值。

 

爲何選這個值?

複雜度分析:

 

 

按照最差狀況最多多少個數比P要小、最多多少個數比P要大。

 

 

 

至少有3N/10個比你大。最多7N/10個比你小。

 

一、邏輯分組

二、組內排序

三、全部數組成一個數組

四、BFPRT調用本身

五、作劃分值

六、若是沒命中,左右兩邊只走一側,左/右部分最多7n/10。

 

應用:在一個數組中,找出最大/小的K個數的問題。

大部分用堆作o(n*logn),最優解是BFPRT。

    // O(N)
    public static int[] getMinKNumsByBFPRT(int[] arr, int k) { if (k < 1 || k > arr.length) { return arr; } //得到第K小的數
        int minKth = getMinKthByBFPRT(arr, k); int[] res = new int[k];//答案數組
        int index = 0; for (int i = 0; i != arr.length; i++) { if (arr[i] < minKth) {//把小於K的加入到數組
                res[index++] = arr[i]; } } for (; index != res.length; index++) {//不足補K
            res[index] = minKth; } return res; } public static int getMinKthByBFPRT(int[] arr, int K) { int[] copyArr = copyArray(arr); return bfprt(copyArr, 0, copyArr.length - 1, K - 1); } public static int[] copyArray(int[] arr) { int[] res = new int[arr.length]; for (int i = 0; i != res.length; i++) { res[i] = arr[i]; } return res; } //得到第i小的數
    public static int bfprt(int[] arr, int begin, int end, int i) { if (begin == end) { return arr[begin]; } //得到中位數的中位數,做爲劃分值
        int pivot = medianOfMedians(arr, begin, end); int[] pivotRange = partition(arr, begin, end, pivot); if (i >= pivotRange[0] && i <= pivotRange[1]) {//命中
            return arr[i]; } else if (i < pivotRange[0]) { return bfprt(arr, begin, pivotRange[0] - 1, i); } else { return bfprt(arr, pivotRange[1] + 1, end, i); } } //得到中位數的中位數
    public static int medianOfMedians(int[] arr, int begin, int end) { int num = end - begin + 1;//總數
        int offset = num % 5 == 0 ? 0 : 1;//不夠五個自成一組
        int[] mArr = new int[num / 5 + offset];//中位數組成的數組
        for (int i = 0; i < mArr.length; i++) { int beginI = begin + i * 5;//開始位置
            int endI = beginI + 4; mArr[i] = getMedian(arr, beginI, Math.min(end, endI)); } //複用bfprt,找出數組中第中間小的數,即中位數。
        return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2); } public static int[] partition(int[] arr, int begin, int end, int pivotValue) { int small = begin - 1; int cur = begin; int big = end + 1; while (cur != big) { if (arr[cur] < pivotValue) { swap(arr, ++small, cur++); } else if (arr[cur] > pivotValue) { swap(arr, cur, --big); } else { cur++; } } int[] range = new int[2]; range[0] = small + 1;//等於區域的最左
        range[1] = big - 1;//最右
        return range; } public static int getMedian(int[] arr, int begin, int end) { insertionSort(arr, begin, end); int sum = end + begin; System.out.println(end + " " + begin + " " + sum); int mid = (sum / 2) + (sum % 2);//取中位數
        System.out.println(mid + "----" + arr[mid]); return arr[mid]; } public static void insertionSort(int[] arr, int begin, int end) { for (int i = begin + 1; i != end + 1; i++) { for (int j = i; j != begin; j--) { if (arr[j - 1] > arr[j]) { swap(arr, j - 1, j); } else { break; } } } } public static void swap(int[] arr, int index1, int index2) { int tmp = arr[index1]; arr[index1] = arr[index2]; arr[index2] = tmp; } public static void printArray(int[] arr) { for (int i = 0; i != arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } public static void main(String[] args) { int[] arr = {6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9}; // sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
        printArray(getMinKNumsByHeap(arr, 10)); printArray(getMinKNumsByBFPRT(arr, 10)); }

 

 第二課內容

一、介紹窗口及窗口內最大值或最小值的更新結構(單調雙向隊列)

雙端隊列結構。

 

 

加數邏輯:進來的數若是比他前面的數要大,那就把前面比他小的所有彈出。

減數邏輯:L向前移動,說明某一下標過時,到隊列中檢查頭部節點是否過時,過時就彈出。

隊列中留的數,在L縮的時候,都有可能成爲最大值。

 

 

這個狀況,3進來幹掉了前面進去的1,由於減數是左到右的,1不可能再成爲最大值了,因此若是6進來,前面的均可以幹掉了。

 

 

總規則:L\R不回退、L不能超過R。

下標必需要,下面的狀況如下標大爲主。

 

 

應用

一、生成窗口最大值數組

有一個整型數組arr和一個大小爲w的窗口從數組的最左邊滑到最右邊,窗口每次向右邊滑一個位置。

例如,數組爲[4,3,5,4,3,3,6,7],窗口大小爲3時:

 

[4 3 5] 4 3 3 6 7 窗口中最大值爲5

4 [3 5 4] 3 3 6 7 窗口中最大值爲5

4 3 [5 4 3] 3 6 7 窗口中最大值爲5

4 3 5 [4 3 3] 6 7 窗口中最大值爲4

4 3 5 4 [3 3 6] 7 窗口中最大值爲6

4 3 5 4 3 [3 6 7] 窗口中最大值爲7

 

若是數組長度爲n,窗口大小爲w,則一共產生n-w+1個窗口的最大值。

請實現一個函數,給定一個數組arr,窗口大小w。

返回一個長度爲n-w+1的數組res,res[i]表示每一種窗口狀態下的最大值。以本題爲例,結果應該返回[5,5,5,4,6,7]。

詳見代碼...

public class Code_01_SlidingWindowMaxArray { public static int[] getMaxWindow(int[] arr, int w) { if(arr == null || w < 1 || arr.length < w){ return null; } LinkedList<Integer> qmax = new LinkedList<Integer>();//雙向鏈表,雙端隊列
        int[] res = new int[arr.length - w + 1]; int index = 0; for (int i = 0; i < arr.length; i++) { //當隊列不是空且入隊列的數大於原隊列的最後一個數
            while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]) { qmax.pollLast(); } qmax.addLast(i); //每次加一個,縮一個的時候就看看頭節點是否過時。
            ///當窗口向右移動,原來在窗口中的最左端數字失效了好比1234,w=3,移動到了4,3-3=0,因此下標爲0的就失效,退出隊列
            if(qmax.peekFirst() == i - w){ qmax.pollFirst(); } //當i大於等於窗口大小時纔開始計算窗口裏的最大值
            if (i >= w - 1) { res[index++] = arr[qmax.peekFirst()]; } } return res; } public static void main(String[] args) { int[] arr={2,3,4,2,6,2,5,1}; int[] maxWindow = getMaxWindow(arr, 3); for (int i = 0; i < maxWindow.length; i++) { System.out.print(maxWindow[i]+"-"); } } }

 

 

二、最大值減去最小值小於或等於num的子數組數量

給定數組arr和整數num,返回有多少個子數組知足以下狀況:

max(arr[i..j]) - min(arr[i..j]) <= num

max(arr[i..j])表示子數組arr[i..j]中的最大值,min(arr[i..j])表示子數組arr[i..j]中的最小值。

要求:

若是數組長度爲 N,請實現時間複雜度爲 O(N)的解法。

子數組的數量。(子數組是連續的)

 

 

暴力方法o(n^3),不必看了。

    //暴力的方法o(n^3),不必看了
    public static int getNum1(int[] array,int num) {
        int count = 0;
        for (int start = 0; start != array.length; start++) {
            for (int end = start; end != start; end++) {
                if (isValid(array,start,end,num))
                    count++;
            }
        }
        return count;
    }

    public static boolean isValid(int[] array, int s, int e, int num) {
        int MAX = Integer.MAX_VALUE;
        int MIN = Integer.MIN_VALUE;
        for (int n = s; n != e; n++) {//找出最大最小
            MAX = Math.max(array[n], MAX);
            MIN = Math.min(array[n], MIN);
        }
        return MAX - MIN <= num;
    }
暴力解法

 

 

 

先說幾個結論:

一、一個數組L~R達標,內部的子數組必定達標。

縮小範圍只可能讓MAX變小、MIN變大

 

 

二、L~R不達標,數組往外擴確定不達標。

 

 

利用這個性質,加上雙端隊列。

 

使用窗口最大/小值,在擴充下一個後是不達標的位置停下。

假設是0~X,那麼就獲得了X+1個以0開頭的達標數組。(再日後都是不達標的,因此以0開頭的所有找出res += x+1)

 

 

接着,L向前移動,R能夠繼續向前擴,同理到了不能擴的地方就停,L所在的位置就所有計算出。

 

    public static int getNum(int[] arr, int num) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        //準備最大/小值的更新結構
        LinkedList<Integer> qmin = new LinkedList<Integer>();
        LinkedList<Integer> qmax = new LinkedList<Integer>();
        int L = 0;
        int R = 0;
        int res = 0;
        while (L < arr.length) {
            while (R < arr.length) {//R擴到不能再擴,停
                //最小值結構更新
                while (!qmin.isEmpty() && arr[qmin.peekLast()] >= arr[R]) {
                    qmin.pollLast();
                }
                qmin.addLast(R);
                ////最大值結構更新
                while (!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[R]) {
                    qmax.pollLast();
                }
                qmax.addLast(R);
                //不達標
                if (arr[qmax.getFirst()] - arr[qmin.getFirst()] > num) {
                    break;
                }
                R++;
            }
            //L向前推進,雙端隊列進行調整
            if (qmin.peekFirst() == L) {
                qmin.pollFirst();
            }
            if (qmax.peekFirst() == L) {
                qmax.pollFirst();
            }
            res += R - L;
            L++;//換一個開頭
        }
        return res;
    }

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        int[] arr_test = {1, 2, 3, 4, 5, 6, 7, 8, 9};
        int num_test = 4;
        int res_test;
        res_test = getNum(arr_test, num_test);
        System.out.printf("res = %d", res_test);
    }

 

 

三、介紹單調棧結構

在一個數組中,全部的數左/右邊距離最近的比他大的數。

能不能o(n)作到

 

 

彈出的時候生成信息。

讓他彈出的是右邊比他大的,他下面的是左邊最近比他大的。

 

 

若是數組遍歷完,就單獨處理棧內的信息。單獨彈出的右邊爲null,左邊是底下的數。

 

 

特殊狀況:

相同的數,壓在一塊兒。

 

 

 

相等狀況,不會有影響,下標壓在一塊兒,共同結算便可。

 

 

 

這個流程爲何對?

由於咱們的邏輯是遇到大的就彈出,因此a在碰到c以前,確定沒有遇到比本身大的數,纔會等到c出現才彈出的。

 

b確定在a的左邊,爲何b在a的下面?確定是由於b大於a,若是b和a之間存在數,確定是小於a的,那並不影響咱們的查找左邊最近大於a的數的邏輯。也不會存在a<x<b的數,存在的話,就輪不到a挨着b了,會變成a x b。

因此流程證實完畢。

 

相關文章
相關標籤/搜索