秋招接近尾声,我总结了 牛客
、WanAndroid
上,有关笔试面经的帖子中出现的算法题,结合往年考题写了这一系列文章,全部文章均与 LeetCode 进行核对、测试。欢迎食用css
本文将覆盖 「字符串处理」 + 「动态规划」 方面的面试算法题,文中我将给出:java
仓库地址:超级干货!精心概括视频、归类、总结
,各位路过的老铁支持一下!给个 Star !
android
如今就让咱们开始吧!git
字符串普遍应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String
类来建立和操做字符串。面试中的字符串处理问题,主要是对于字符串各类方法的灵活应用。下面结合实例,讲讲常见的考点:github
给定 n
,表示有 n
对括号, 请写一个函数以将其生成全部的括号组合,并返回组合结果。面试
例如算法
给出 n = 3,生成结果为:
[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
复制代码
使用 回溯法数据库
只有在咱们知道序列仍然保持有效时才添加 '(' or ')',而不是像 方法一 那样每次添加。咱们能够经过跟踪到目前为止放置的左括号和右括号的数目来作到这一点,编程
若是咱们还剩一个位置,咱们能够开始放一个左括号。 若是它不超过左括号的数量,咱们能够放一个右括号。数组
public List<String> generateParenthesis(int n) { List<String> res = new ArrayList<>(); helper(n, n, "", res); return res; } // DFS private void helper(int nL, int nR, String parenthesis, List<String> res) { // nL 和 nR 分别表明左右括号剩余的数量 if (nL < 0 || nR < 0) { return; } if (nL == 0 && nR == 0) { res.add(parenthesis); return; } helper(nL - 1, nR, parenthesis + "(", res); if (nL >= nR) { return; } helper(nL, nR - 1, parenthesis + ")", res); } 复制代码
给定一个正整数,返回相应的列标题,如Excel表中所示。如: 1 -> A, 2 -> B ... 26 -> Z, 27 -> AA
示例 :
输入: 28 输出: "AB" 复制代码
n
多人的方法,感受不少都作麻烦了。大多数人都困在这个 ‘A’
或者说 n = 0
上26
,咱们通常会直接把它 %26
这样获得的就是一个 0
‘A’ = 'A'
,正确答案固然是 ‘Z’
,因而加了一堆判断n--
就能搞定.public String convertToTitle (int n) { StringBuilder str = new StringBuilder(); while (n > 0) { n--; str.append ( (char) ( (n % 26) + 'A')); n /= 26; } return str.reverse().toString(); } 复制代码
给定一个只包含两种字符的字符串:+
和-
,你和你的小伙伴轮流翻转"++"变成"--"。当一我的没法采起行动时游戏结束,另外一我的将是赢家。编写一个函数,计算字符串在一次有效移动后的全部可能状态。
示例 :
输入:s = "++++"
[
"--++",
"+--+",
"++--"
]
复制代码
+
,和以前那个字母是否为+
public List<String> generatePossibleNextMoves (String s) { List list = new ArrayList(); // indexOf 方法使用 看下方拓展 for (int i = -1; (i = s.indexOf ("++", i + 1)) >= 0;) { list.add (s.substring (0, i) + "--" + s.substring (i + 2)); } return list; } 复制代码
Java中字符串中子串的查找共有四种方法,以下:
int indexOf(String str)
:返回第一次出现的指定子字符串在此字符串中的索引。int indexOf(String str, int startIndex)
:从指定的索引处开始,返回第一次出现的指定子字符串在此字符串中的索引。int lastIndexOf(String str)
:返回在此字符串中最右边出现的指定子字符串的索引。int lastIndexOf(String str, int startIndex)
:从指定的索引处开始向后搜索,返回在此字符串中最后一次出现的指定子字符串的索引。substring()
方法返回字符串的子字符串。
public String substring(int beginIndex)
返回 beginIndex
后的字符串public String substring(int beginIndex, int endIndex)
返回 beginIndex
到 endIndex
之间的字符串给定一个字符串,逐个翻转字符串中的每一个单词。
示例 :
输入: "a good example" 输出: "example good a" 解释: 若是两个单词间有多余的空格,将反转后单词间的空格减小到只含一个。 复制代码
split
方法,以 “ ” 为标识符为基准拆分字符串public String reverseWords(String s) { if(s.length() == 0 || s == null){ return " "; } //按照空格将s切分 String[] array = s.split(" "); StringBuilder sb = new StringBuilder(); //从后往前遍历array,在sb中插入单词 for(int i = array.length - 1; i >= 0; i--){ if(!array[i].equals("")) { // 为防止字符串首多一个 “ ” 判断当前是否是空字符串 // 是字符串第一个就不输出空格 if (sb.length() > 0) { sb.append(" "); } sb.append(array[i]); } } return sb.toString(); } 复制代码
实现atoi
这个函数,将一个字符串转换为整数。若是没有合法的整数,返回0
。若是整数超出了32
位整数的范围,返回 INT_MAX(2147483647)
若是是正整数,或者 INT_MIN(-2147483648)
若是是负整数。
示例 :
输入: "4193 with words"
输出: 4193
解释: 转换截止于数字 '3' ,由于它的下一个字符不为数字。
示例 4:
输入: "words and 987"
输出: 0
解释: 第一个非空字符是 'w', 但它不是数字或正、负号。
所以没法执行有效的转换。
复制代码
trim()
去掉空格‘+’
‘-’
其余falg
叫作 sign
默认值为一,若是监测到 ‘-’
则设为 -1
num
值用于保存答案数值for
循环从头至尾访问字符串-+
号,根据题意直接退出循环sum
的值 *10
倍,再将其加入 sum
中MAX_VALUE
跳出循环*sigh
输出正负值,或者 MAX_VALUE
或 MIN_VALUE
便可public int myAtoi(String str) { if(str == null) { return 0; } str = str.trim(); if (str.length() == 0) { return 0; } int sign = 1; int index = 0; if (str.charAt(index) == '+') { index++; } else if (str.charAt(index) == '-') { sign = -1; index++; } long num = 0; for (; index < str.length(); index++) { if (str.charAt(index) < '0' || str.charAt(index) > '9') { break; } num = num * 10 + (str.charAt(index) - '0'); if (num > Integer.MAX_VALUE ) { break; } } if (num * sign >= Integer.MAX_VALUE) { return Integer.MAX_VALUE; } if (num * sign <= Integer.MIN_VALUE) { return Integer.MIN_VALUE; } return (int)num * sign; } 复制代码
注:trim()
函数是去掉String
字符串的首尾空格;
编写一个函数来查找字符串数组中的最长公共前缀。
若是不存在公共前缀,返回空字符串 ""
。
示例 :
输入: ["flower","flow","flight"] 输出: "fl" 复制代码
标签:链表 当字符串数组长度为 0
时则公共前缀为空,直接返回 令最长公共前缀 ans
的值为第一个字符串,进行初始化 遍历后面的字符串,依次将其与 ans
进行比较,两两找出公共前缀,最终结果即为最长公共前缀 若是查找过程当中出现了 ans
为空的状况,则公共前缀不存在直接返回 s
为全部字符串的长度之和
public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) { return ""; } String prefix = strs[0]; for(int i = 1; i < strs.length; i++) { int j = 0; while (j < strs[i].length() && j < prefix.length() && strs[i].charAt(j) == prefix.charAt(j)) { j++; } if( j == 0) { return ""; } prefix = prefix.substring(0, j); } return prefix; } 复制代码
判断一个正整数是否是回文数。回文数的定义是,将这个数反转以后,获得的数仍然是同一个数。
示例 :
输入: 121 输出: true 复制代码
经过取整和取余操做获取整数中对应的数字进行比较。
举个例子:1221
这个数字。
经过计算 1221 / 1000
, 得首位1
经过计算 1221 % 10
, 可得末位 1
进行比较 再将 22
取出来继续比较
public boolean palindromeNumber(int num) { // Write your code here if(num < 0){ return false; } int div = 1; while(num / div >= 10){ div *= 10; } while(num > 0){ if(num / div != num % 10){ return false; } num = (num % div) / 10; div /= 100; } return true; } 复制代码
动态规划
经常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间每每远少于朴素解法。其背后的基本思想很是简单。大体上,若要解一个给定问题,咱们须要解其不一样部分
(即子问题),再根据子问题的解以得出原问题的解。
一般许多子问题很是类似,为此动态规划法试图仅仅解决每一个子问题一次,从而减小计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次须要同一个子问题解之时直接查表。这种作法在重复子问题的数目关于输入的规模呈指數增長
时特别有用。
给定字符串 s
和单词字典 dict
,肯定 s
是否能够分红一个或多个以空格分隔的子串
,而且这些子串都在字典中存在。
示例 :
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 由于 "applepenapple" 能够被拆分红 "apple pen apple"。
注意你能够重复使用字典中的单词。
复制代码
这个方法的想法是对于给定的字符串 s 能够被拆分红子问题 s1
和 s2
。若是这些子问题均可以独立地被拆分红符合要求的子问题,那么整个问题 s
也能够知足。也就是,若是 能够拆分红两个子字符串 "
" 和 "
" 。子问题 "
" 能够进一步拆分红 "
" 和 "
" ,这两个独立的部分都是字典的一部分,因此 "
" 知足题意条件,再往前, "
" 和 "
" 也分别知足条件,因此整个字符串 "
" 也知足条件。
如今,咱们考虑 数组求解的过程:
2
个下标指针 s1'
和 s2'
(注意 i
如今指向 s2'
的结尾)。s1′
是否知足题目要求。若是知足,咱们接下来检查 s2′
是否在字典中。若是包含,咱们接下来检查 s2′
是否在字典中,若是两个字符串都知足要求,咱们让 public boolean wordBreak(String s, List<String> wordDict) { Set<String> wordDictSet=new HashSet(wordDict); boolean[] dp = new boolean[s.length() + 1]; dp[0] = true; for (int i = 1; i <= s.length(); i++) { for (int j = 0; j < i; j++) { if (dp[j] && wordDictSet.contains(s.substring(j, i))) { dp[i] = true; break; } } } return dp[s.length()]; } 复制代码
时间复杂度: 。求出
数组须要两重循环。
空间复杂度:。
数组的长度是
。
假设你正在爬楼梯。须要 n
阶你才能到达楼顶。
每次你能够爬 1
或 2
个台阶。你有多少种不一样的方法能够爬到楼顶呢?
注意:给定 n
是一个正整数。
示例 :
输入: 3
输出: 3
解释: 有三种方法能够爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶
复制代码
感受这题相似斐波那契数列。不难发现,这个问题能够被分解为一些包含最优子结构的子问题,即它的最优解能够从其子问题的最优解来有效地构建,咱们可使用动态规划来解决这一问题。
第 阶能够由如下两种方法获得:
在第 阶后向上爬
1
阶。
在第 阶后向上爬
2
阶。
因此到达第 阶的方法总数就是到第
阶和第
阶的方法数之和。
令 表示能到达第
阶的方法总数:
public int climbStairs(int n) { if (n == 0) return 0; int[] array = new int[n + 1]; array[0] = 1; if (array.length > 1) { array[1] = 1; } for(int i = 2; i < array.length; i++) { array[i] = array[i - 1] + array[i - 2]; } return array[n]; } 复制代码
假设你是一个专业的窃贼,准备沿着一条街打劫房屋。每一个房子都存放着特定金额的钱。你面临的惟一约束条件是:相邻的房子装着相互联系的防盗系统,且 当相邻
的两个房子同一天被打劫时,该系统会自动报警。给定一个非负整数列表,表示每一个房子中存放的钱, 算一算,若是今晚去打劫,在不触动
报警装置的状况下, 你最多能够获得多少钱 。
示例 :
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
复制代码
考虑全部可能的抢劫方案过于困难。一个天然而然的想法是首先从最简单的状况开始。记:
从前
k
个房屋中能抢劫到的最大数额,= 第
i
个房屋的钱数。
首先看 n = 1
的状况,显然 f(1) = 。
再看 n = 2
,。
对于 n = 3
,有两个选项:
抢第三个房子,将数额与第一个房子相加。
不抢第三个房子,保持现有最大数额。
显然,你想选择数额更大的选项。因而,能够总结出公式:
咱们选择 为初始状况,这将极大地简化代码。
答案为 。能够用一个数组来存储并计算结果。不过因为每一步你只须要前两个最大值,两个变量就足够用了。
public long houseRobber(int[] A) { if (A.length == 0) return 0; long[] res = new long[A.length + 1]; res[0] = 0; res[1] = A[0]; for (int i = 2; i < res.length; i++) { res[i] = Math.max(res[i - 2] + A[i - 1], res[i - 1]); } return res[A.length]; } 复制代码
时间复杂度:。其中
n
为房子的数量。 空间复杂度:。
给出两个单词word1
和word2
,计算出将 word1
转换为word2
的最少操做次数。你总共三种操做方法:插入一个字符、删除一个字符、替换一个字符。
示例 :
输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
复制代码
咱们的目的是让问题简单化,好比说两个单词 horse
和 ros
计算他们之间的编辑距离 D
,容易发现,若是把单词变短会让这个问题变得简单,很天然的想到用 D[n][m]
表示输入单词长度为 n
和 m
的编辑距离。
具体来讲,D[i][j]
表示 word1
的前 i
个字母和 word2
的前 j
个字母之间的编辑距离。
当咱们得到 D[i-1][j],D[i][j-1] 和 D[i-1][j-1] 的值以后就能够计算出 D[i][j]。
每次只能够往单个或者两个字符串中插入一个字符
那么递推公式很显然了
若是两个子串的最后一个字母相同,word1[i] = word2[i] 的状况下:
不然,word1[i] != word2[i] 咱们将考虑替换最后一个字符使得他们相同:
因此每一步结果都将基于上一步的计算结果,示意以下:
同时,对于边界状况,一个空串和一个非空串的编辑距离为 D[i][0] = i
和 D[0][j] = j
。
综上咱们获得了算法的所有流程。
舒适提示,若是思惟很差理解的话,把解题思路记清楚就行
public int minDistance(String word1, String word2) { // write your code here int n = word1.length(); int m = word2.length(); int[][] dp = new int[n + 1][m + 1]; for (int i = 0; i < n + 1; i++){ dp[i][0] = i; } for (int j = 0; j < m + 1; j++){ dp[0][j] = j; } for (int i = 1; i< n + 1; i++){ for (int j = 1; j < m + 1; j++){ if (word1.charAt(i - 1) == word2.charAt(j - 1)){ dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])); } } } return dp[n][m]; } 复制代码
时间复杂度 :,两层循环显而易见。 空间复杂度 :
,循环的每一步都要记录结果。
给定一个整数数组 nums
,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数)。
示例 :
输入: [-2,0,-1] 输出: 0 解释: 结果不能为 2, 由于 [-2,-1] 不是子数组。 复制代码
imax = max(imax * nums[i], nums[i])
imin
,imin = min(imin * nums[i], nums[i])
public int maxProduct(int[] nums) { int max = Integer.MIN_VALUE, imax = 1, imin = 1; for(int i=0; i<nums.length; i++){ if(nums[i] < 0){ int tmp = imax; imax = imin; imin = tmp; } imax = Math.max(imax*nums[i], nums[i]); imin = Math.min(imin*nums[i], nums[i]); max = Math.max(max, imax); } return max; } 复制代码
本片文章篇幅总结越长。我一直以为,一片过长的文章,就像一堂超长的 会议/课堂,体验很很差,因此我打算再开一篇文章
在后续文章中,我将继续针对链表
栈
队列
堆
动态规划
矩阵
位运算
等近百种,面试高频算法题,及其图文解析 + 教学视频 + 范例代码
,进行深刻剖析有兴趣能够继续关注 _yuanhao 的编程世界
不求快,只求优质,每篇文章将以 2 ~ 3
天的周期进行更新,力求保持高质量输出
仓库地址:超级干货!精心概括视频、归类、总结
,各位路过的老铁支持一下!给个 Star !