剑指offer所有题都刷完了一遍,大部分都是比较基础的,只是有一些是值得记录下来反复领会的,在这里做一个记录。
-
- 1. LCA问题(树中两个节点的最小公共父节点)
- 2. 巧妙的位运算
- 3. 二分查找
- 4. 掷骰子
- 5. 和为s的连续正数序列
- 6. 第一次只出现一次的字符位置
- 7. 最长不含重复字符的子字符串
- 8. 丑数
- 9. 面试题12- 矩阵中的路径:
- 10. 面试题13-机器人的运动范围
- 11. 面试题14- 剪绳子
- 12. P99- 微软EXCEL列转换;
- 13. 位运算,计算某二进制表达式里1的个数(输入int型整数n);
- 14. 面试题16-数值的整数次方运算
- 15. 面试题17-打印从1到最大的n位数
- 16. 面试题18-删除链表中重复的节点
- 17. 面试题19-正则表达式匹配
- 18. 面试题11-旋转数组的最小数字
- 19. 面试题26-树的子结构
- 20. 面试题29-打印旋转矩阵
1. LCA问题(树中两个节点的最小公共父节点)
这个问题分为两种情况:二叉搜索树和普通二叉树;
- 二叉搜索树
TreeNode lowestCommonAnscestor(TreeNode root, TreeNode p1, TreeNode p2) {
if (root == null) return root;
if (root.val < p1.val && root.val < p2.val) return lowestCommonAnscestor(root.right, p1, p2);
else if (root.val > p1.val && root.val > p2.val) return lowestCommonAnscestor(root.left, p1, p2);
return root;
}
- 普通二叉树
TreeNode lowestCommonAnscestor(TreeNode root, TreeNode p1, TreeNode p2) {
if (root == null || root == p1 || root == p2) return root;
TreeNode left = lowestCommonAnscestor(root.left, p1, p2);
TreeNode right = lowestCommonAnscestor(root.right, p1, p2);
if (left == null) return right;
else if (right == null) return left;
else return root;
}
- 如果是排序二叉树的话,可以利用val值的大小顺序来判断该往那边遍历;如果是普通二叉树,则必须都左右子树都遍历一遍;如果遍历到某个节点是两个目标节点之一,则返回非空,这样在依次往下遍历的时候,在回溯过程中,发现某个节点的left和right都非非空,那么该节点就是最小公共父节点;
2. 巧妙的位运算
2.1 不用加减乘除做加法
两个数a,b相加,二进制里体现出来的可以这么看,如果两个数在某一位互异,则和在该位就是1,正好这个特性可以用 a^b 来体现,这个得出的结果是所有互异的位上变成1,而相同的位上则都变成了0,可以如果两位都是 1 怎么办?为了将所有进位的信息都保留下来,这里需要再利用 a&b 这个运算,只有两个数某一位上数都是1,那么运算得出的结果里这一位就是1,否则都是0,不过进位是体现到上一位,所以得进位,左移一位,得到的就是包含了进位信息的数,两者结合起来就是最终结果;代码如下:
//这个写的麻烦了些,为了更好的看里面的效果,其实可以只用一行代码就可;
// return b == 0 ? a : a ^ b : (a & b) << 1;
private static int help(int a, int b) {
System.out.printf("%7s\n", Integer.toBinaryString(a));
System.out.printf("%7s\n", Integer.toBinaryString(b));
if (b == 0) return a;
int c = a ^ b;
int d = (a & b) << 1;
return help(c, d);
}
2.2 数组中出现一次的数字
一个数组中除了两个数,其他数都出现两次,(类似的一个题是除了一个数),那么在这里如何找到仅出现一次的那两个数?
private static void help(int[] nums) {
int diff = 0;
for (int k : nums)
diff ^= k;
diff &= -diff;
int l = 0, r = 0;
for (int i : nums) {
if ((i & diff) == 1) l ^= i;
else r ^= i;
}
System.out.println(l+"," + r);
}
这里涉及到的位运算包括,
- 一个数和它的相反数做与运算,得到的结果仅包含其最右边不为0的那一位。
- 两个相同数异或,得到的结果为0;
- 异或运算具有交换律
3. 二分查找
排序数组中的二分查找是很考察能力的问题,有以下许多情况可以用来考察:
- 可能包含重复数字的数组中找到第一个目标值的下标:
private static int help(int[] arr, int left, int right, int target) {
if (left > right) return -1;
int mid = left + (right - left) / 2;
int midData = arr[mid];
if (midData == target) {
if (mid == 0 || (mid > 0 && arr[mid-1] != target)) return mid;
else return help(arr, left, mid-1, target);
} else if (midData < target) left = mid + 1;
else right = mid - 1;
return help(arr, left, right, target);
}
依照这个模板,写出最后一个数值应该可不成问题,这个算法时间复杂度是O(logN)级别;
3.1 数字在排序数组中出现的次数
这道题可以使用通过借鉴上面的找第一个和最后一个数字的下标可以得出答案,复杂度也是2O(logn),但还有个更tricky的做法,也是jdk里使用的一个做法,那就是对于任意一个可能不存在与数组里的查询目标数都给你返回你如果插入到数组里的位置(这就包含了相同数首位的意思);而且这里查最后一个位置时巧妙的使用了查询目标数+1时返回的位置,下面是代码:
private static void help(int[] a, int target) {
int left = binarySearch(a, target);
int right = binarySearch(a, target+1); //tricky
System.out.println(right - left); // 出现的次数
}
private static int binarySearch(int[] a, int target) {
int l = 0, r = a.length-1;
while (l < r) {
int m = l + (r - l) / 2;
if (a[m] >= target) r = m;
else l = m + 1;
}
return l;
}
4. 掷骰子
- 深搜算法
很容易想到深搜,但这样递归好像会超时,时间复杂度比较大;
public static void main(String[] args) throws IOException {
int n = 2; //骰子数量
// 最大和为6*n,最小是n,那么中间所有的情况有 6n-n+1种;
double[] p = new double[5*n+1]; // 所有情况的概率
int[] num = new int[5*n+1];
int total = (int) Math.pow(6, n); // 总和的情况数
dfs(num, n, 1, 0, 6);
System.out.println(Arrays.toString(num));
}
/**
* 递归主算法
* @param num 每一种和的数量
* @param base n个骰子的话就是n,意味着n-1作为和的情况为 0
* @param i 当前第 i 个骰子(从 1 到 n )
* @param sum 叠加到当前这颗骰子的和大小
* @param k 骰子每颗最大数,6;
*/
private static void dfs(int[] num, int base, int i, int sum, int k) {
if (i > base) {
num[sum-base] ++;
return;
}
for (int j=1; j<=k; j++)
dfs(num, base,i+1, sum+j, k);
}
- 动态规划
public static void main(String[] args) {
int n = 2, face = 6, num = n * face; //骰子数量,骰子面数,n个骰子和的所有情况种类(从1开始)
int[][] dp = new int[n+1][num+1]; //状态数组,dp[i][j]表示从第1到第i个骰子累加起来,和为j的出现次数
for (int i = 1; i <= face; i++)
dp[1][i] = 1;
for (int i=1; i<=n; i++) {
for (int j=i; j<=i*face; j++) {
for (int k=1; k<=face && k<=j; k++) {
dp[i][j] += dp[i-1][j-k];
}
}
}
System.out.println(Arrays.toString(dp[n]));
//结果:[0, 0, 1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
}
优化以后的时间复杂度是O(n^2),比之前大为提高;
5. 和为s的连续正数序列
找出所有连续的正数序列,使得其和为s;这个题非常有意思,很容易想到类似于通过遍历有序数组的长度(1-n),然后找平均数,并在周左右拓展的做法,但是这样很麻烦!
下面是一个非常清爽干净的解法:
/**
* 将所有情况都存在lists中,按照长度排列
* @param lists 所有排列情况的数组
* @param s 和是 s;
*/
private static void help(List<List<Integer>> lists, int s) {
if (s < 3) return;
int l = 1, r = 2, cur = 3;
while (r < s) {
if (cur == s) {
List<Integer> list = new ArrayList<>();
for (int i=l; i<=r; i++)
list.add(i);
lists.add(list);
cur -= l;
l ++;
r ++;
cur += r;
} else if (cur < s) {
r ++;
cur += r;
} else {
cur -= l;
l ++;
}
}
}
看完让人心旷神怡~!
6. 第一次只出现一次的字符位置
求出一个字符串中找到第一个只出现一次的字符,常规很容易想到使用hashmap等解法,再进一步可以仅考虑 int[256]数组里存数量等,下面是使用bitmap算法的一个典型应用,大大减少了空间复杂度;
private static void help(String s) {
BitSet b1 = new BitSet(256);
BitSet b2 = new BitSet(256);
for (char c : s.toCharArray()) {
if (!b1.get(c) && !b2.get(c))
b1.set(c);
else if (b1.get(c) && !b2.get(c)) b2.set(c);
}
for (char c : s.toCharArray()) {
if (b1.get(c) && !b2.get(c)) {
System.out.println(c);
return;
}
}
}
稍微查一下bitmap及bitset的api和源码就可以知道这里面省去了多少空间了,bitmap算法常用类处理海量数据;
7. 最长不含重复字符的子字符串
非常简单的算法,运用动态规划的思想,下面是代码实现
private static int help(String s) {
int curLen = 0;
int maxLen = 0;
int[] a = new int[26];
Arrays.fill(a, -1);
for (int i=0; i<s.length(); i++) {
int c = s.charAt(i) - 'a';
int preIndex = a[c];
// -1代表之前还未出现过该字符,后面判断条件是中间被其他重复字符给打断过,否则会相等(如果该字符冲突的话)
if (preIndex == -1 || i-preIndex > curLen)
curLen ++;
else {
maxLen = Math.max(maxLen, curLen);
curLen = i - preIndex;
}
a[c] = i;
}
maxLen = Math.max(maxLen, curLen);
return maxLen;
}
8. 丑数
只包含质因子2,3,5的数叫做丑数,并且习惯将第一个丑数定义为1,求第 n 个丑数;
- 解题思路
只需要用一个长n的数组保存所有丑数(从小到大),并且从1到n初始化求即可,其中需要借用里面的规律:第i个丑数是前面某一个丑数乘以2,3,或者是5的结果;
其中使用到的思想是三指针思想; - 代码实现
static int getUglyNum(int n) {
if (n < 2) return 1;
int[] num = new int[n];
int i=0, j=0, k=0;
num[0] = 1;
for (int t=1; t<n; t++) {
int n1 = num[i] * 2;
int n2 = num[j] * 3;
int n3 = num[k] * 5;
int mn = Math.min(n1, Math.min(n2, n3));
num[t] = mn;
if (mn == n1) i ++;
if (mn == n2) j ++;
if (mn == n3) k ++;
}
return num[n-1];
}
9. 面试题12- 矩阵中的路径:
static char[][] ch = {{'a', 'e', 't', 'g'}, {'c', 'f', 'c', 's'}, {'j', 'd', 'e', 'h'}};
static String s = "bfce";
static boolean[][] b;
static int M, N;
public static void main(String[] args) throws IOException {
M = ch.length;
N = ch[0].length;
System.out.println(help(ch, s));
}
private static boolean help(char[][] ch, String s) {
b = new boolean[M][N];
for (int i=0; i<M; i++) {
for (int j=0; j<N; j++) {
if (check(i, j, 0)) return true;
}
}
return false;
}
private static boolean check(int i, int j, int k) {
if (i < M && i >= 0 && j < N && j >= 0 && !b[i][j] && ch[i][j] == s.charAt(k)) {
if (k == s.length()-1) return true;
b[i][j] = true;
boolean flag = check(i-1, j, k+1) || check(i+1, j, k+1) || check(i, j-1, k+1) || check(i, j+1, k+1);
if (flag) return flag;
b[i][j] = false;
}
return false;
}
10. 面试题13-机器人的运动范围
static boolean[][] b;
static int M, N;
public static void main(String[] args) throws IOException {
M = 8;
N = 7;
b = new boolean[M][N];
int k = 4;
System.out.println(help(0, 0, k));
}
private static int help(int i, int j, int k) {
int count = 0;
if (i < M && i >= 0 && j < N && j >= 0 && !b[i][j] && getNum(i) + getNum(j) <= k) {
count ++;
b[i][j] = true;
count = count + help(i-1, j, k) + help(i+1, j, k) + help(i, j-1, k) + help(i, j+1, k);
return count;
}
return count;
}
private static int getNum(int i) {
int sum = 0;
while (i != 0) {
sum += i % 10;
i /= 10;
}
return sum;
}
11. 面试题14- 剪绳子
动态规划的思想体现;
private static int help(int len) {
if (len <=2) return 0;
else if (len == 2) return 1;
else if (len == 3) return 2;
int[] product = new int[len+1];
product[0] = 0;
product[1] = 1;
product[2] = 2;
product[3] = 3;
for (int i=4; i<=len; i++) {
int max = 0;
for (int l=1; l<=i/2; l++) {
System.out.println(i + "-" + l);
max = Math.max(product[l] * product[i-l], max);
}
product[i] = max;
}
System.out.println(Arrays.toString(product));
return product[len];
}
12. P99- 微软EXCEL列转换;
//将第几列的数字转化为字符串,如1-A,17-AA;
private static String change(int num) {
StringBuilder sb = new StringBuilder();
while (num != 0) {
sb.append((char)((num-1) % 26 + 65));
num = (num-1) / 26;
}
return sb.reverse().toString();
}
//将字符串转化为第几列数字,如B-2,AB-28;
private static int help(String s) {
int sum = 0;
for (int i=0; i<s.length(); i++) {
sum = sum * 26 + s.charAt(i) - 64;
}
return sum;
}
13. 位运算,计算某二进制表达式里1的个数(输入int型整数n);
//输入int整数n,求算n的二进制表达式里1的个数,可以通过一下函数查看;
// System.out.println(Integer.toBinaryString(n));
private static int help(int n) {
int sum = 0;
while (n != 0) {
sum ++;
n = (n-1) & n;
}
return sum;
}
- 思考?如何判别n是否是2的整数次方?即为证明n是否其二进制只含有1个1,可以将n和n-1做与运算,如果结果为0,即证明;
- 求算两个正整数m,n中,需要改变m的二进制里的多少位才能得到n?也即为m和n多少位不同?可以将m和n进行异或运算,得到的结果进行1统计即为所求;
14. 面试题16-数值的整数次方运算
/**
* 求幂运算,无大数出现,若出现除数为0,则报自定义的Exception;
* 如果出现0的0次方,无意义,直接打印一句话无意义,并返回0;
* @param d
* @param i
* @return
* @throws Exception
*/
private static double pow(double d, int i) throws Exception {
if (i == 0) {
if (Math.abs(d - 0) < 0.0001) {
System.out.println("0的0次方,无意义!");
return 0;
} else return d;
} else if (i == 1) return d;
if (Math.abs(d - 0) < 0.0001 && i < 0) {
throw new Exception("除数不能为0...");
}
double result = count(d, Math.abs(i));
if(i < 0) result = 1.0 / result;
return result;
}
/**
* 分治法求幂运算,奇数和偶数的判别使用位运算,除2也是;
* 位运算比传统运算高效的多!
* @param d
* @param i
* @return
*/
private static double count(double d, int i) {
if (i == 0) return 1;
else if (i == 1) return d;
double result = count(d, i >> 1);
result *= result;
if ((i & 0x1) == 1) result *= d;
return result;
}
15. 面试题17-打印从1到最大的n位数
- 两个思路,第一个是模拟10进制数值的加法,一个一个网上增加,然后打印直到n位数字9,另一个是万能是深搜,n个袋子,每个袋子填数字0-9,你遍历所有结果,即为答案;
模拟加法
public static void main(String[] args) throws IOException {
int n = 4;
char[] ch = new char[n];
Arrays.fill(ch, '0');
do {
print(ch);
increase(ch, 0, n-1);
} while (!check(ch));
}
/**
* 用来加1的函数,只在最低位加1,其他位使用进位加1;
* @param ch
* @param up
* @param k
*/
private static void increase(char[] ch, int up, int k) {
if (k == -1) return;
int num = ch[k] - '0' + up;
if (k == ch.length-1) num ++;
if (num == 10) {
ch[k] = '0';
up = 1;
increase(ch, up, k-1);
} else {
ch[k] = (char)('0' + num);
}
}
/**
* 用来打印的函数,如果前面的0就不必打印;
* @param ch
*/
private static void print(char[] ch) {
int i = 0;
while (i < ch.length-1 && ch[i] == '0') i++;
for (; i<ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println();
}
/**
* 检查ch[]是否每一位都是0的函数?
* @param ch
* @return
*/
private static boolean check(char[] ch) {
for (int i=0; i<ch.length; i++)
if (ch[i] != '0') return false;
return true;
}
深搜算法
public static void main(String[] args) throws IOException {
int n = 4;
int[] arr = new int[n];
dfs(arr, 0, n);
}
private static void dfs(int[] arr, int depth, int max) {
if (depth == max) {
print(arr);
return;
}
for (int i=0; i<10; i++) {
arr[depth] = i;
dfs(arr, depth+1, max);
}
}
private static void print(int[] ch) {
int i = 0;
while (i < ch.length-1 && ch[i] == 0) i++;
for (; i<ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println();
}
- 两种算法都能够很好的实现,但第二种深搜算法更加简洁优美;
16. 面试题18-删除链表中重复的节点
static private class Node {
int val;
Node next;
public Node(int val) {
this.val = val;
}
}
private static void deleteDupulication(Node head) {
Node pre = head;
Node temp = head.next;
while (temp != null) {
if (temp.val == pre.val) {
temp = temp.next;
pre.next = temp;
} else {
pre = temp;
temp = temp.next;
}
}
}
- 因为题目已经要求了链表元素是非递减排列的,所以这样的话一次遍历即可完成;时间复杂度是O(n);
17. 面试题19-正则表达式匹配
public boolean match(String str, String pattern) {
if (str == null || pattern == null) return false;
else return help(str, pattern, 0, 0);
}
private boolean help(String a, String b, int i, int j) {
if (i == a.length() && j == b.length()) return true;
if (i != a.length() && j == b.length()) return false;
if (j+1 < b.length() && b.charAt(j+1) == '*') {
if (b.charAt(j) == a.charAt(i) || b.charAt(j) == '.')
return help(a, b, i+1, j+2) || help(a, b, i+1, j) || help(a, b, i, j+2);
else return help(a, b, i, j+2);
}
if (a.charAt(i) == b.charAt(j) || (i < a.length() && b.charAt(j) == '.'))
return help(a, b, i+1, j+1);
return false;
}
18. 面试题11-旋转数组的最小数字
这个题困扰我好久,看了牛客网上一个回答,顿觉非常清爽,粘贴过来学习一个;
附上原答案链接
以下是分析:
采用二分法解答这个问题,
mid = low + (high - low)/2
需要考虑三种情况:
(1)array[mid] > array[high]:
出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。
low = mid + 1
(2)array[mid] == array[high]:
出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边
还是右边,这时只好一个一个试 ,
high = high - 1
(3)array[mid] < array[high]:
出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左
边。因为右边必然都是递增的。
high = mid
注意这里有个坑:如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字
比如 array = [4,6]
array[low] = 4 ;array[mid] = 4 ; array[high] = 6 ;
如果high = mid - 1,就会产生错误, 因此high = mid
但情形(1)中low = mid + 1就不会错误
public class Solution {
public int minNumberInRotateArray(int [] array) {
int low = 0 ; int high = array.length - 1;
while(low < high){
int mid = low + (high - low) / 2;
if(array[mid] > array[high]){
low = mid + 1;
}else if(array[mid] == array[high]){
high = high - 1;
}else{
high = mid;
}
}
return array[low];
}
}
19. 面试题26-树的子结构
public class Solution {
public boolean HasSubtree(TreeNode r1,TreeNode r2) {
if (r1 == null || r2 == null) return false;
return help(r1, r2) || help(r1.left, r2) || help(r1.right, r2);
}
boolean help(TreeNode r1,TreeNode r2) {
if (r1 == null && r2 == null) return true;
else if (r1 == null && r2 != null) return false;
else if (r1 != null && r2 == null) return true;
else {
if (r1.val == r2.val) return help(r1.left, r2.left) && help(r1.right, r2.right);
else return false;
}
}
}
20. 面试题29-打印旋转矩阵
import java.util.ArrayList;
public class Solution {
public ArrayList<Integer> printMatrix(int [][] matrix) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(matrix == null || matrix.length == 0)
return list;
int m = matrix.length, n = matrix[0].length;
int min = (Math.min(m, n) + 1) / 2;
for(int i=0; i<min; i++) {
int lastRow = m - i - 1;
int lastCol = n - i - 1;
if(i == lastRow) {
for(int j=i; j<=n-1-i; j++)
list.add(matrix[i][j]);
} else if(i == lastCol) {
for(int j=i; j<=m-1-i; j++)
list.add(matrix[j][lastCol]);
} else {
for(int j=i; j<n-1-i; j++)
list.add(matrix[i][j]);
for(int j=i; j<=m-1-i; j++)
list.add(matrix[j][lastCol]);
for(int j=n-2-i; j>=i; j--)
list.add(matrix[lastRow][j]);
for(int j=m-2-i; j>i; j--)
list.add(matrix[j][i]);
}
}
return list;
}
}