大廠勸退,面試高頻^_^
node
查找字符串問題:例如咱們有一個字符串str="abc1234efd"和match="1234"。咱們如何查找str字符串中是否包含match字符串的子串?面試
暴力解思路:循環str和match,挨個對比,最差狀況爲O(NM)。時間複雜度爲O(NM)算法
KMP算法,在N大於M時,能夠在時間複雜度爲O(N)解決此類問題數組
咱們對str記錄字符座標前的前綴後綴最大匹配長度,例如str="abcabck"less
一、對於k位置前的字符,先後綴長度取1時,前綴爲"a"後綴爲"c"不相等dom
二、對於k位置前的字符,先後綴長度取2時,前綴爲"ab"後綴爲"bc"不相等ide
三、對於k位置前的字符,先後綴長度取3時,前綴爲"abc"後綴爲"abc"相等測試
四、對於k位置前的字符,先後綴長度取4時,前綴爲"abca"後綴爲"cabc"不相等優化
五、對於k位置前的字符,先後綴長度取5時,前綴爲"abcab"後綴爲"bcabc"不相等code
注意先後綴長度不可取k位置前的總體長度6。那麼此時k位置前的最大匹配長度爲3
因此,例如"aaaaaab","b"的指標爲6,那麼"b"座標前的先後綴最大匹配長度爲5
咱們對match創建座標先後綴最大匹配長度數組,概念不存在的設置爲-1,例如0位置前沒有字符串,就爲-1,1位置前只有一個字符,先後綴沒法取和座標前字符串相等,規定爲0。例如"aabaabc",nextArr[]爲[-1,0,1,0,1,2,3]
暴力方法之因此慢,是由於每次比對,若是match的i位置前都和str匹配上了,可是match的i+1位置沒匹配成功。那麼str會回退到第一次匹配的下一個位置,match直接回退到0位置再次比對。str和match回退的位置太多,以前的信息所有做廢,沒有記錄
而KMP算法而言,若是match的i位置前都和str匹配上了,可是match的i+1位置沒匹配成功,那麼str位置不回跳,match回跳到當前i+1位置的最大先後綴長度的位置上,去和當前str位置比對。
原理是若是咱們當前match位置i+1比對失敗了,咱們跳到最大先後綴長度的下一個位置去和當前位置比對,若是能匹配上,因爲i+1位置以前都匹配的上,那麼match的最大後綴長度也比對成功,能夠被咱們利用起來。替換成match的前綴長度上去繼續對比,起到加速的效果
那麼爲何str和match最後一個不相等的位置,以前的位置沒法配出match,能夠反證,若是能夠配置出來,那麼該串的頭信息和match的頭信息相等,得出存在比match當前不等位置最大先後綴還要大的先後綴,矛盾
Code:
public class Code01_KMP { // O(N) public static int getIndexOf(String s, String m) { if (s == null || m == null || m.length() < 1 || s.length() < m.length()) { return -1; } char[] str = s.toCharArray(); char[] match = m.toCharArray(); int x = 0; // str中當前比對到的位置 int y = 0; // match中當前比對到的位置 // match的長度M,M <= N O(M) int[] next = getNextArray(match); // next[i] match中i以前的字符串match[0..i-1],最長先後綴相等的長度 // O(N) // x在str中不越界,y在match中不越界 while (x < str.length && y < match.length) { // 若是比對成功,x和y共同往各自的下一個位置移動 if (str[x] == match[y]) { x++; y++; } else if (next[y] == -1) { // 表示y已經來到了0位置 y == 0 // str換下一個位置進行比對 x++; } else { // y還能夠經過最大先後綴長度往前移動 y = next[y]; } } // 一、 x越界,y沒有越界,找不到,返回-1 // 二、 x沒越界,y越界,配出 // 三、 x越界,y越界 ,配出,str的末尾,等於match // 只要y越界,就配出了,配出的位置等於str此時所在的位置x,減去y的長度。就是str存在匹配的字符串的開始位置 return y == match.length ? x - y : -1; } // M O(M) public static int[] getNextArray(char[] match) { // 若是match只有一個字符,人爲規定-1 if (match.length == 1) { return new int[] { -1 }; } // match不止一個字符,人爲規定0位置是-1,1位置是0 int[] next = new int[match.length]; next[0] = -1; next[1] = 0; int i = 2; // cn表明,cn位置的字符,是當前和i-1位置比較的字符 int cn = 0; while (i < next.length) { if (match[i - 1] == match[cn]) { // 跳出來的時候 // next[i] = cn+1; // i++; // cn++; // 等同於 next[i++] = ++cn; // 跳失敗,若是cn>0說明能夠繼續跳 } else if (cn > 0) { cn = next[cn]; // 跳失敗,跳到開頭仍然不等 } else { next[i++] = 0; } } return next; } // for test public static String getRandomString(int possibilities, int size) { char[] ans = new char[(int) (Math.random() * size) + 1]; for (int i = 0; i < ans.length; i++) { ans[i] = (char) ((int) (Math.random() * possibilities) + 'a'); } return String.valueOf(ans); } public static void main(String[] args) { int possibilities = 5; int strSize = 20; int matchSize = 5; int testTimes = 5000000; System.out.println("test begin"); for (int i = 0; i < testTimes; i++) { String str = getRandomString(possibilities, strSize); String match = getRandomString(possibilities, matchSize); if (getIndexOf(str, match) != str.indexOf(match)) { System.out.println("Oops!"); } } System.out.println("test finish"); } }
例如Str1="123456",對於Str1的旋轉詞,字符串自己也是其旋轉詞,Str1="123456"的旋轉詞爲,"123456","234561","345612","456123","561234","612345"。給定Str1和Str2,那麼判斷這個兩個字符串是否互爲旋轉詞?是返回true,不是返回false
暴力解法思路:把str1的全部旋轉詞都列出來,看str2是否在這些旋轉詞中。挨個便利str1,循環數組的方式,和str2挨個比對。O(N*N)
KMP解法:str1拼接str1獲得str',"123456123456",咱們看str2是不是str'的子串
給定兩顆二叉樹頭結點,node1和node2,判斷node2爲頭結點的樹,是否是node1的某個子樹?
面試常見
情形:在一個無序數組中,怎麼求第k小的數。若是經過排序,那麼排序的複雜度爲O(n*logn)。問,如何O(N)複雜度解決這個問題?
思路1:咱們利用快排的思想,對數組進行荷蘭國旗partion過程,每一次partion能夠獲得隨機數m小的區域,等於m的區域,大於m的區域。咱們看咱們m區域是否包含咱們要找的第k小的樹,若是沒有根據比較,在m左區間或者m右區間繼續partion,直到第k小的數在咱們的的中間區域。
快排是左右區間都會再進行partion,而該問題只會命中大於區域或小於區域,時間複雜度獲得優化。T(n)=T(n/2)+O(n),時間複雜度爲O(N),因爲m隨機選,機率收斂爲O(N)
思路2:bfprt算法,不使用機率求指望,複雜度仍然嚴格收斂到O(N)
經過上文,利用荷蘭國旗問題的思路爲:
一、隨機選一個數m
二、進行荷蘭國旗,獲得小於m區域,等於m區域,大於m區域
三、index命中到等於m區域,返回等於區域的左邊界,不然比較,進入小於區域,或者大於區域,只會進入一個區域
bfprt算法,再此基礎上惟一的區別是,第一步,如何選擇m。快排的思想是隨機選擇一個
bfprt如何選擇m?
T(N) = T(N/5) + T(?) + O(N)
建議畫圖分析:
T(?)在咱們隨機選取m的時候,是不肯定的,可是在bfprt中,m的左側範圍最多有多少個數,等同於m右側最少有幾個數。
假設咱們通過分組拿到的m數組有5個數,中位數是咱們的m,在m[]數組中,大於m的有2個,小於m的有2個。對於整的數據規模而言,m[]的規模是n/5。大於m[]中位數的規模爲m[]的一半,也就是總體數據規模的n/10。
因爲m[]中的每一個數都是從小組中選出來的,那麼對於總體數據規模而言,大於m的數總體爲3n/10(每一個n/10規模的數回到本身的小組,大於等於的每小組有3個)
那麼最少有3n/10的規模是大於等於m的,那麼對於總體數據規模而言最多有7n/10的小於m的。同理最多有7n/10的數據是大於m的
可得:
T(N) = T(N/5) + T(7n/10) + O(N)
數學證實,以上公式沒法經過master來算複雜度,可是數學證實複雜度嚴格O(N),證實略(算法導論第九章第三節)
bfprt算法在算法上的地位很是高,它發現只要涉及到咱們隨便定義的一個常數分組,獲得一個表達式,最後收斂到O(N),那麼就能夠經過O(N)的複雜度測試
public class Code01_FindMinKth { public static class MaxHeapComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } } // 利用大根堆,時間複雜度O(N*logK) public static int minKth1(int[] arr, int k) { PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new MaxHeapComparator()); for (int i = 0; i < k; i++) { maxHeap.add(arr[i]); } for (int i = k; i < arr.length; i++) { if (arr[i] < maxHeap.peek()) { maxHeap.poll(); maxHeap.add(arr[i]); } } return maxHeap.peek(); } // 改寫快排,時間複雜度O(N) public static int minKth2(int[] array, int k) { int[] arr = copyArray(array); return process2(arr, 0, arr.length - 1, k - 1); } public static int[] copyArray(int[] arr) { int[] ans = new int[arr.length]; for (int i = 0; i != ans.length; i++) { ans[i] = arr[i]; } return ans; } // arr 第k小的數: process2(arr, 0, N-1, k-1) // arr[L..R] 範圍上,若是排序的話(不是真的去排序),找位於index的數 // index [L..R] // 經過荷蘭國旗的優化,機率指望收斂於O(N) public static int process2(int[] arr, int L, int R, int index) { if (L == R) { // L == R ==INDEX return arr[L]; } // 不止一個數 L + [0, R -L],隨機選一個數 int pivot = arr[L + (int) (Math.random() * (R - L + 1))]; // 返回以pivot爲劃分值的中間區域的左右邊界 // range[0] range[1] // L ..... R pivot // 0 1000 70...800 int[] range = partition(arr, L, R, pivot); // 若是咱們第k小的樹正好在這個範圍內,返回區域的左邊界 if (index >= range[0] && index <= range[1]) { return arr[index]; // index比該區域的左邊界小,遞歸左區間 } else if (index < range[0]) { return process2(arr, L, range[0] - 1, index); // index比該區域的右邊界大,遞歸右區間 } else { return process2(arr, range[1] + 1, R, index); } } public static int[] partition(int[] arr, int L, int R, int pivot) { int less = L - 1; int more = R + 1; int cur = L; while (cur < more) { if (arr[cur] < pivot) { swap(arr, ++less, cur++); } else if (arr[cur] > pivot) { swap(arr, cur, --more); } else { cur++; } } return new int[] { less + 1, more - 1 }; } public static void swap(int[] arr, int i1, int i2) { int tmp = arr[i1]; arr[i1] = arr[i2]; arr[i2] = tmp; } // 利用bfprt算法,時間複雜度O(N) public static int minKth3(int[] array, int k) { int[] arr = copyArray(array); return bfprt(arr, 0, arr.length - 1, k - 1); } // arr[L..R] 若是排序的話,位於index位置的數,是什麼,返回 public static int bfprt(int[] arr, int L, int R, int index) { if (L == R) { return arr[L]; } // 經過bfprt分組,最終選出m。不一樣於隨機選擇m做爲劃分值 int pivot = medianOfMedians(arr, L, R); int[] range = partition(arr, L, R, pivot); if (index >= range[0] && index <= range[1]) { return arr[index]; } else if (index < range[0]) { return bfprt(arr, L, range[0] - 1, index); } else { return bfprt(arr, range[1] + 1, R, index); } } // arr[L...R] 五個數一組 // 每一個小組內部排序 // 每一個小組中位數拿出來,組成marr // marr中的中位數,返回 public static int medianOfMedians(int[] arr, int L, int R) { int size = R - L + 1; // 是否須要補最後一組,例如13,那麼須要補最後一組,最後一組爲3個數 int offset = size % 5 == 0 ? 0 : 1; int[] mArr = new int[size / 5 + offset]; for (int team = 0; team < mArr.length; team++) { int teamFirst = L + team * 5; // L ... L + 4 // L +5 ... L +9 // L +10....L+14 mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4)); } // marr中,找到中位數,原問題是arr拿第k小的數,這裏是中位數數組拿到中間位置的數(第mArr.length / 2小的數),相同的問題 // 返回值就是咱們須要的劃分值m // marr(0, marr.len - 1, mArr.length / 2 ) return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2); } public static int getMedian(int[] arr, int L, int R) { insertionSort(arr, L, R); return arr[(L + R) / 2]; } // 因爲肯定是5個數排序,咱們選擇一個常數項最低的排序-插入排序 public static void insertionSort(int[] arr, int L, int R) { for (int i = L + 1; i <= R; i++) { for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) { swap(arr, j, j + 1); } } } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) (Math.random() * maxSize) + 1]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) (Math.random() * (maxValue + 1)); } return arr; } public static void main(String[] args) { int testTime = 1000000; int maxSize = 100; int maxValue = 100; System.out.println("test begin"); for (int i = 0; i < testTime; i++) { int[] arr = generateRandomArray(maxSize, maxValue); int k = (int) (Math.random() * arr.length) + 1; int ans1 = minKth1(arr, k); int ans2 = minKth2(arr, k); int ans3 = minKth3(arr, k); if (ans1 != ans2 || ans2 != ans3) { System.out.println("Oops!"); } } System.out.println("test finish"); } }
題目:求一個數組中,拿出全部比第k小的數還小的數
能夠經過bfprt拿到第k小的數,再對原數組遍歷一遍,小於該數的拿出來,不足k位的,補上第k小的數
對於這類問題,筆試的時候最好選擇隨機m,進行partion。而不是選擇bfprt。bfprt的常數項高。面試的時候能夠選擇bfprt算法