大厂劝退,面试高频^_^
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算法