Q15 三数之和
three pointer
// my solution
import java.util.*;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
Arrays.sort(nums);
for(int cur = 0; cur < nums.length; cur++){
if(cur != 0 && nums[cur]== nums[cur-1]){
continue;
}
int l = cur + 1, r = nums.length-1;
while(l < r){
int sum = nums[l] + nums[r]+nums[cur];
if( sum == 0){
ArrayList<Integer> each = new ArrayList<Integer>();
each.add(nums[cur]);
each.add(nums[l]);
each.add(nums[r]);
res.add(each);
l++;
r--;
while(l < r && nums[l] == nums[l-1]){
l++;
}
while(l < r && nums[r] == nums[r+1]){
r--;
}
}else if(sum < 0){
l++;
}else{
r--;
}
}
}
return res;
}
}
Q1139 最大的以1为边界的正方形
preprocess matrix
暴力解法的时间复杂度是O(nmmin(n,m)min(n,m)),使用预处理矩阵后的时间复杂度是O(nm*min(n,m))
Q14 最长公共前缀
longest-common-prefix 没有找到总结性的题解。
总结了一下这道题目的最优解法应该有两种,假设字符串个数为n,字符串中字符的个数为m。
最优解1
分治法。每次将字符串数组划分为两部分,不断分治问题直到子问题中包含两个字符串时可以直接求解最长公共子串。如果子问题中只有一个字符串,那么这种特殊情况直接返回字符串。时间复杂度为O(mlogn)
最优解2
二分法。首先找到最短的那个字符串ss,对它进行二分[low, mid, hi],如果ss[low: mid]是所有字符串的前缀,那么真正最长公共前缀的最后一个字符的位置一定在mid和hi的右边,否则在其左边(包括mid)。时间复杂度O(x*n*logm),其中x是判断二分的字符串是否是一个字符串前缀的时间复杂度,x不好估算,这种解法相比最优解1更加适合m比n大得多的情况。
Q297 二叉树的序列化和反序列化
二叉树的序列化有四种方法:先,中,后,层序。它们就是二叉树的四种遍历方法。但是二叉树的反序列化可行的只有三种方法:先,后,层序。中序无法完成反序列化的原因在于:对于中序序列你无法找到一棵树的根,先,后序反序列化本质还是递归算法(先找到一个根节点,反序列化这个根节点,然后递归的反序列化根的左子树和右子树)。代码上先/后/层序反序列化的代码在逻辑上都是一样的,进行X序反序列化就是重做X序遍历,层序反序列化中需要注意设置两个队列childQueue和parentQueue,并且空节点不要放入到队列中。
Q1014 最佳观光组合
https://leetcode.com/problems/best-sightseeing-pair/discuss/260850/JavaC%2B%2BPython-One-Pass
解法的思路借鉴题解,但是题解中的说明并不清晰。
/**
* max(A[i] + A[j] + i -j ) and i < j
* = max(A[i] + i -j + A[j])
* => max(A[j]) + max(A[i] + i - j ) 相当于对于每个位置j,找前面最大的A[i] + i -j
* 所以如果想在one-pass复杂度中得到结果,需要优化"找前面最大的A[i] + i -j"的过程,这个过程在代码中对应的就是maxpart2
*/
public class Q1014 {
public static void main(String[] args) {
int[] arr = new int[] {8,1,5,2,6};
int res = maxScoreSightseeingPair(arr);
System.out.println(res);
}
public static int maxScoreSightseeingPair(int[] A) {
int maxpart2= 0;
int res = Integer.MIN_VALUE;
for(int i = 0; i< A.length; i++){
if( i == 0){
maxpart2 = A[0] - 1;
}else{
res = Math.max(res, A[i] + maxpart2);
// 减1的原因在于:
// 1.若果下一个景点i前面最优搭配的景点是景点i-1前面最优搭配的景点的化,距离会增加一个
// 2.如果下一个景点i前面最优搭配的景点是它前一个位置,距离为1
maxpart2 = Math.max(maxpart2 - 1, A[i] - 1);
}
}
return res;
}
}
Q221 最大正方形
https://leetcode.com/problems/maximal-square/discuss/61803/C%2B%2B-space-optimized-DP
坐标型动态规划
Q93 复原IP地址
一种解法(回溯法)的多种写法。约束条件较多的回溯问题如何解决。
// most votes 题解
public class Solution {
public List<String> restoreIpAddresses(String s) {
List<String> res = new ArrayList<String>();
int len = s.length();
for(int i = 1; i<4 && i<len-2; i++){
for(int j = i+1; j<i+4 && j<len-1; j++){
for(int k = j+1; k<j+4 && k<len; k++){
String s1 = s.substring(0,i), s2 = s.substring(i,j), s3 = s.substring(j,k), s4 = s.substring(k,len);
if(isValid(s1) && isValid(s2) && isValid(s3) && isValid(s4)){
res.add(s1+"."+s2+"."+s3+"."+s4);
}
}
}
}
return res;
}
public boolean isValid(String s){
if(s.length()>3 || s.length()==0 || (s.charAt(0)=='0' && s.length()>1) || Integer.parseInt(s)>255)
return false;
return true;
}
}
// My code
import java.util.*;
public class Q93 {
public static void main(String[] args) {
//System.out.println(String.join("-", new String[]{"a","b"}));
List<String> res = new Q93().restoreIpAddresses("010010");
System.out.println(res);
}
public List<String> restoreIpAddresses(String s) {
ArrayList<String> res = new ArrayList<>();
ArrayList<String> each = new ArrayList<>();
backtrack(s,res,each,0);
return res;
}
public void backtrack(String s, ArrayList<String> res, ArrayList<String> each, int start){
if(each.size() == 3){
String substr = s.substring(start);
if(isValid(substr)){
each.add(substr);
res.add(String.join(".", each));
each.remove(each.size() -1);
}
return;
}
for(int point = start + 1; point <= start + 3 && point <= s.length(); point++ ){
String substr = s.substring(start, point);
if(isValid(substr)){
each.add(substr);
backtrack(s,res,each,point);
each.remove(each.size() -1 );
}
}
}
public boolean isValid(String s){
if(s.length() == 0){
return false;
}
if(s.charAt(0) == '0' && s.length() != 1){
return false;
}
if(s.length() > 3){
return false;
}
if(Integer.valueOf(s) > 255){
return false;
}
return true;
}
}
Q11盛最多水的容器
和Q42接雨水进行区分,注意两者盛水的方式是不同的,但是都可以使用双指针解决
class Solution {
public int maxArea(int[] height) {
int l = 0, r = height.length -1;
int res = 0;
int curarea = 0;
while(l < r){
if(height[l] <= height[r]){
curarea = (r - l)* height[l];
res = Math.max(res, curarea);
//因为[l] < [r],所以此时把r向左移动只会减少盛水量
l++;
}else{
curarea = (r - l)* height[r];
res = Math.max(res, curarea);
r--;
}
}
return res;
}
}
Q42接雨水
class Solution {
public int trap(int[] height) {
if(height.length == 0){
return 0;
}
int l = 0, r = height.length - 1;
int lmax = height[l], rmax = height[r];
int res = 0;
int cur = 0;
while(l < r){
if(height[l] < height[r]){
cur = Math.max(0, lmax - height[l]);
res += cur;
lmax = Math.max(lmax, height[l]);
l ++;
}else{
cur = Math.max(0, rmax - height[r]);
res += cur;
rmax = Math.max(rmax, height[r]);
r --;
}
}
return res;
}
}
Q48 旋转图像
class Solution {
public void rotate(int[][] matrix) {
int tr = 0, tc = 0, dr = matrix.length-1, dc = matrix.length-1;
while(tr < dr){
rotateALoop(matrix, tr,tc,dr,dc);
tr++; tc++;
dr--; dc--;
}
}
void rotateALoop(int[][] mat, int tr, int tc, int dr, int dc){
//tr,tc为左上顶点的行和列坐标;dr,dc为右下顶点的行和列的坐标
if(tr==dr) {// mat中有条件是nxn
return;
}
int[] temp = {Integer.MAX_VALUE};
for(int i =0 ; i < dc - tc; i++){ // 仔细思考这里i从0循环的意图
swap(mat, tr,tc+i,temp); // i再col上增加
swap(mat, tr + i, dc,temp); //i再row上增加
swap(mat, dr, dc-i,temp); //i在col上减少
swap(mat, dr-i,tc,temp); //i在row上减少
swap(mat, tr,tc+i,temp); // 补一次
}
return;
}
void swap(int[][] mat, int sr, int sc, int[] temp){
int _temp = mat[sr][sc];
mat[sr][sc] = temp[0];
temp[0] = _temp;
}
}
Q50 pow(x, n)
我估计牛客面试不会考这题,太简单了。
class Solution {
public double myPow(double x, long n) { // 注意这里n的类型为long,虽然题目中表示n是32位有符号整数,但是如果n = Integer.MIN_VALUE;第10行代码中-n其实就会发生int溢出。造成的结果就是负数取反后还是负数,从而引发无限递归造成栈溢出。
if(n == 0){
return 1;
}
if(x == 1){
return 1;
}
if(n < 0){
return 1.0/myPow(x,-n);
}
if(n % 2 == 0){
double ret = myPow(x, n/2);
return ret * ret;
}else{
double ret = myPow(x, n/2);
return ret * ret * x;
}
}
}
Q57 插入区间
https://www.youtube.com/watch?v=jeKvO1JS-4w
class Solution {
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> listRes = new ArrayList<>();
for(int [] it : intervals){
if( newInterval == null || it[1] < newInterval[0]){
listRes.add(it);
}else if(newInterval[1] < it[0]){
listRes.add(newInterval);
newInterval = null;
listRes.add(it);
}else{ // newInterval[0] <= it[1] && newInterval[1] >= it[0] 这是newInterval和it区间有交集的情况。
newInterval[0] = Math.min(newInterval[0], it[0]);
newInterval[1] = Math.max(newInterval[1], it[1]);
}
}
if(newInterval == null) return listToArray(listRes);
listRes.add(newInterval);
return listToArray(listRes);
}
int[][] listToArray(List<int[]> list){
int[][] res = new int[list.size()][2];
int idx = 0;
for(int[] it: list){
res[idx++] = it;
}
return res;
}
}
Q60 第k个排列
class Solution {
private HashMap<Integer, Integer> memo = new HashMap<>();
public String getPermutation(int n, int k) {
ArrayList<Integer> candidates = new ArrayList<>();
ArrayList<Integer> res = new ArrayList<>();
for(int i = 1; i<=n; i++){
candidates.add(i);
}
getPerm(candidates, res, k-1); // k做offset的原因见33、34行注释
StringBuilder sb = new StringBuilder();
for(int i: res){
sb.append(i);
}
return sb.toString();
}
private void getPerm(ArrayList<Integer> candidates, ArrayList<Integer> res, int remainOfK){
if(candidates.size()==1){ // 此时remainOfK == 0
res.add(candidates.remove(0));
return;
}
int nSubGroup = computePermNum(candidates.size() - 1);
int idx = remainOfK / nSubGroup;
remainOfK = remainOfK % nSubGroup;
//注意因为idx从0开始计数,而remainOfK从1开始计数,所以要对上面的取余运算做一些调整
//当然也可以不做调整,在传入remainOfK参数时直接offset做-1的处理,这里的代码使用这种方法。
res.add(candidates.remove(idx));
getPerm(candidates, res, remainOfK);
return;
}
private int computePermNum(int n){
if(n == 1){
return 1;
}
if(memo.containsKey(n)){
return memo.get(n);
}
int res = n * computePermNum(n-1);
memo.put(n, res);
return res;
}
}
这题含有一个非常普遍的处理技巧:
int n; // 从1开始计数
int k = n / div; // 这里的k其实是从0开始计数
int r = n % div; // 这里的r其实也是从0开始计数
如果我们需要将r的值赋给n进行多次迭代,那么这其实是会出现错误的:比如n = 5,div = 5,计算后r = 0,实际上对应从1开始计数的第一个数1。解决这个错误的办法可以从两个角度来解决:
- 每次计算后对k和r进行修正:
//修正代码
k = k + 1;
if(r == 0){
k = k - 1;
r = 1;
}else{
r = r + 1;
}
- 直接修正n,在传入参数时选择传入n-1;
显然第二种方法更简洁。
Q139 单词拆分
序列型动态规划+遍历型(划分型)动态规划
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i]表示s中前0个字符是否满足题目条件
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] && wordDict.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
Q116 填充每个节点的下一个右侧节点指针
也就是要构造上图中的红色指针,本质上还是层序遍历的一个变式,解答略。
Q136 只出现一次的数字
某个元素只出现一次,其他元素都出现两次,找出这个出现一次的元素。
类似题目:
Q137某个元素出现一次,其他元素出现三次,找出出现一次的元素。
对于这种某个元素出现一次,其他元素出现k次,找出出现一次的元素的题目。有统一的解法:
逐位操作,如果所有数在当前位上0的个数可以整除k,那么目标数在当前位上的二进制值为1,否则为0
class Solution {
public int singleNumber(int[] nums) {
int [] zeroCount = new int[32];
for(int n: nums){
for(int i = 0; i< 32; i++){
if(((n >> i) & 1) == 0){
zeroCount[i]++;
}
}
}
int res = 0;
for(int i = 0; i< 31; i++){
if(zeroCount[i] % 3 == 0){
res+= Math.pow(2,i);
}
}
if(zeroCount[31] % 3 == 0){
res += -Math.pow(2,31);
}
return res;
}
}
Q260 只出现一次的数字III
首先求数组的xor sum,这个值肯定是要找的两个数的异或和。因为这两个元素不同,所以异或和中其中一位肯定为1,找到这个位置,也就是说两个数在这一位上有不同的二进制值,通过这个可以将原数组分为两个部分,两个目标数在不同部分中,从而将问题转化两个Q136的子问题。
Q76 最小覆盖子串
import java.util.HashMap;
import java.util.Map;
/**
* 最小覆盖子串
*/
class Q76 {
public String minWindow(String s, String t) {
int[] remainMap = new int[128]; // 覆盖字母、数字的ascii范围
char [] ss = s.toCharArray();
char[] tt = t.toCharArray();
for(char c: tt){
remainMap[c]++;
}
int nremain = tt.length; // 窗口内没有覆盖的字符的个数
int begin = 0, end = 0; // 窗口的左右边界
int minlen = Integer.MAX_VALUE; // 能覆盖的最小子串的长度
int startPos = 0; // 能覆盖的最小子串的起始位置
while(end < ss.length){
if(remainMap[ss[end]] > 0){
remainMap[ss[end]]--;
nremain--;
}else{
remainMap[ss[end]]--;
}
end++;
while(nremain == 0){
if(end - begin < minlen){
minlen = end - (startPos= begin);
}
if(remainMap[ss[begin]] == 0){
remainMap[ss[begin]]++;
begin++;
nremain++;
}else{
remainMap[ss[begin]]++;
begin++;
}
}
}
return minlen==Integer.MAX_VALUE ? "" : s.substring(startPos, startPos + minlen);
}
}
删除重复元素系列问题
Q26删除排序数组中的重复项
原地修改数组让每个元素都只出现一次。
Q80删除排序数组中的重复项II
原地修改数组让每个元素最多出现两次(出现超过两次的元素变为出现两次,未超过的保持原来的出现次数)
上面两题原地删除排序数组中的重复元素达到指定次数的问题通用解法就是读写双指针。
Q83删除排序链表中的重复元素
删除所有重复元素,使得每个元素只出现一次。
Q82删除排序链表中的重复元素II
只保留没有重复出现的元素。
Q287寻找重复数
题目中的条件数字都在1到n之间(包括1和n)所以可以将数组看成是图来做。
https://leetcode.com/problems/find-the-duplicate-number/discuss/72846/My-easy-understood-solution-with-O(n)-time-and-O(1)-space-without-modifying-the-array.-With-clear-explanation./75491
这个解释中注重的是证明快慢指针找到环入口点的正确性,但是对于本题为何可以转换为找环的入口点的问题所述不详。
我的理解:首先图中的节点有n+1个为数组的所有的index,每个数组中的元素值可以在图中建立一条边,比如a[0] = 1,可以建立一条边从0节点到1节点的边。这样有n+1个元素的数组构成的图就包含了n+1个节点和n+1条边。根据前面构成边的方法,可知如果数组中存在重复的元素那么这个重复的数字代表的节点的入度一定是大于1的,重复的元素代表的节点就是环的入口点。那么这个问题就转换为用快慢指针的方法来找环的入口点。证明到重复元素代表节点的入度一定是大于1的这里,后面仍不好证明如何转换为找环入口点的问题,这是可以考虑化抽象为具体的思考方法,举几个典型的例子,可以发现重复元素确实是环的入口点。
只出现一次的数字系列
Q136只出现一次的数字
除了某个元素只出现一次外,其他元素都出现两次。
解法:求异或和。
Q137只出现一次的数字 II
除了某个元素只出现一次外,其他每个元素都出现三次。
解法:位操作。转换为二进制数后,每一位上0和1中只有一个的出现次数会被3整除,另一个除3的余数为1,这样的话我们就可以确定只出现一次元素的每一位上的值是0还是1。
Q260只出现一次的数字III
有两个元素只出现一次,其他元素都出现两次,找出只出现一次的那两个元素。
还是位操作。求所有元素的异或和为xorsum,然后找到xorsum中一个为1的位,这表示要求的两个数字在这位上是不同的,所有我们可以根据二进制数在这位上是1还是0将原始的数组分为两个部分。每个部分分别包含要求的数字的一个,然后可以转换为Q136来求解。
Q139单词拆分
动态规划。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
// dp[i]表示s中前0个字符是否满足题目条件
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] && wordDict.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
Q202快乐数
首先可能的猜想有三种:
- 最终会得到1
- 最终会进入循环。
- 值会越来越大,最后接近无穷大。
但是第三个猜测通过数学证明是可以否定掉的,见 https://leetcode-cn.com/problems/happy-number/solution/kuai-le-shu-by-leetcode-solution/
解法:使用set记录变换过程中得到的数字,一旦计算的数字在set中出现过,那么就会发生循环,就一定不是快乐数。
Q165比较版本号
class Solution {
public int compareVersion(String version1, String version2) {
String[] v1s = version1.split("\."); // split函数传入的是正则表达式,注意!!!,差点没debug出来。
String[] v2s = version2.split("\.");
int [] v1 = new int[v1s.length];
int [] v2 = new int[v2s.length];
for(int i = 0; i< v1s.length; i++){
v1[i] = Integer.valueOf(v1s[i]);
}
for(int i = 0; i< v2s.length; i++){
v2[i] = Integer.valueOf(v2s[i]);
}
int i = 0;
for(i = 0; i< v1.length && i < v2.length; i++){
if(v1[i] < v2[i]){
return -1;
}else if(v1[i] == v2[i]){
continue;
}else{
return 1;
}
}
int sum1 = 0, sum2 = 0;
for(int j = i; j< v1.length; j++){
sum1 += v1[j];
}
for(int j = i; j< v2.length;j++){
sum2 += v2[j];
}
if(sum1 == sum2){
return 0;
}else if(sum1 < sum2){
return -1;
}else{
return 1;
}
}
}