剑指 Offer 03. 数组中重复的数字
可以用map,set等方法,但是消耗大。时间空间都是O(n)
也可以先排序,再遍历.时间O(n*logn)
用辅助数组标记对应位置上出现的次数
因为限制了数字出现的范围,所以辅助数组的大小也可以确定
时间空间都是O(n)
class Solution {
public int findRepeatNumber(int[] nums) {
// 辅助数组
int[] arr = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
arr[nums[i]]++;
if (arr[nums[i]] > 1) {
return nums[i];
}
}
return -1;
}
}
上面利用了一个辅助数组用于数字nums[i]和放入到arr[nums[i]]的位置上,进而判断arr数组索引为nums[i]的出现的次数来判断重复元素
也可以不借助辅助数组,因为可以利用原数组nums的索引:
逐步遍历元素,如果它的值没有出现再对应的索引上,则放到索引上
这样,就会把第一次出现的值全部和索引对应
如果再出现值已经出现再了对应索引上,说明重复了
一开始如图所示,上面的是初始数组,下面的是索引
i=0时,已经值和索引是对应的,就跳过
i=1时,不对应,但是值2对应的索引为2的值时5,说明没有重复,则把它放在索引为2的上面
此时到i=1,把nums[1]的5再放到nums[5]上
得到0 0 2 3 2 5
此时i=1,num[1]=1,它和对应索引上的值一样,说明已经出现过了,重复
class Solution {
public int findRepeatNumber(int[] nums) {
int i = 0;
while (0 < nums.length) {
//值和索引是本来就是有序的,跳过
if (nums[i] == i) {
i++;
continue;
}
//出现别的索引上的值(没拍好的)和值对应的索引上的值(排好了的)一样,重复
if (nums[i] == nums[nums[i]]) {
return nums[i];
}
//没发生重复,也没有本来就排好,手动把它放到对应索引的位置上
int temp = nums[i];
nums[i] = nums[temp];
nums[temp] = temp;
}
return -1;
}
}
剑指 Offer 04. 二维数组中的查找
剑指 Offer 05. 替换空格
方法一:利用list或者StringBuffer遍历并逐个加入
时间复杂度可空间复杂度都是O(n)
class Solution {
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
char[] c = s.toCharArray();
for(char ch : c){
if(ch!=' '){
sb.append(ch);
}else{
sb.append("%20");
}
}
return sb.toString();
}
}
方法二:原地拓展数组,双指针处理。空间复杂度就变成了O(1)——只有C++可以原地拓展。但是JAVA也可以实现,不过空间复杂度还是O(n),不过不再是从前往后一个一个填充了,而是从后往前
class Solution {
public String replaceSpace(String s) {
//有多少个空格,扩充数组
int count = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == ' ') {
count++;
}
}
char[] arr = new char[s.length() + 2 * count];
//从后面开双指针替换
for (int i = s.length() - 1, j = arr.length - 1; i >=0; i--, j--) {
if (s.charAt(i) == ' ') {
arr[j] = '0';
arr[j - 1] = '2';
arr[j - 2] = '%';
j-=2;
} else {
arr[j] = s.charAt(i);
}
}
return new String(arr);
}
}
剑指 Offer 06. 从尾到头打印链表
主要就是用LinkedList
很简单,就是学习以下List转为int[]
public int[] reversePrint(ListNode head) {
//直接插入到list的头部
// LinkedList<Integer> list = new LinkedList<>();
// while (head != null) {
// list.addFirst(head.val);
// head = head.next;
// }
//这个转换用的时间多,所以不如下面直接遍历来的块
// int[] res = list.stream().mapToInt(Integer::intValue).toArray();
// return res;
//利用栈(其实和上面是一样的)
LinkedList<Integer> stack = new LinkedList<Integer>();
while(head != null) {
stack.addLast(head.val);
head = head.next;
}
int[] res = new int[stack.size()];
for(int i = 0; i < res.length; i++)
res[i] = stack.removeLast();
return res;
}
剑指 Offer 07. 重建二叉树
这个题已经可以独立做出来了
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
//把中序遍历的值和位置存入map
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildHelp(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1,map);
}
private TreeNode buildHelp(int[] preorder, int pl, int pr, int[] inorder, int il, int ir,HashMap<Integer, Integer> map) {
if (pl > pr || il > ir) {
return null;
}
TreeNode root = new TreeNode(preorder[pl]);
Integer index = map.get(root.val);
int countLeft = index - il;
root.left = buildHelp(preorder, pl + 1, pl + countLeft, inorder, il, index - 1, map);
root.right = buildHelp(preorder, pl + countLeft + 1, pr, inorder, index + 1, ir, map);
return root;
}
}
剑指 Offer 09. 用两个栈实现队列
class CQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
public CQueue() {
stack1 = new ArrayDeque<>();
stack2 = new ArrayDeque<>();
}
public void appendTail(int value) {
stack1.push(value);
}
public int deleteHead() {
if (stack2.isEmpty()) {
if (stack1.isEmpty()) {
return -1;
}
while (!stack1.isEmpty()) {
stack2.push(stack1.poll());
}
}
Integer poll = stack2.poll();
return poll;
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
剑指 Offer 10- I. 斐波那契数列
class Solution {
public int fib(int n) {
int a = 0, b = 1, sum;
for (int i = 0; i < n; i++) {
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
剑指 Offer 10- II. 青蛙跳台阶问题
class Solution {
public int numWays(int n) {
int a =1,b=1,sum;
for(int i =0;i<n-1;i++){
sum = (a+b)%1000000007;
a=b;
b=sum;
}
return b;
}
}
剑指 Offer 11. 旋转数组的最小数字
因为是两部分有序数组——二分
数组的最后一个是第二个数组的最大值,也就是说
1.它<=第一个数组的所有数
2.它>=第二个数组所有的数
所以可以使用二分法和他比较,逐步缩小第二个数组第一个元素所在的区间
举例:345123
mid为5,5》3,所以所在区间一定再[mid+1,j]
mid为2,2<3,所以所在区间一定再[i,mid]注意这个包含mid
mid为1,小于2,所以所在区间一定再[i,mid]
此时i==j,退出循环,返回i的值
注意,有一种清空,当num[mid]==num[j]
比如0,1,1或者1,0,0
第一种清空,在mid的左区间[i,mid],第二种情况是[mid,j]。所以不确定该怎么办
这就应当执行j-1,因为即使把num[j]删除了,还有num[mid]保存了他的信息,防止num[j]就是最小值的情况
class Solution {
public int minArray(int[] numbers) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int mid = i + (j - i)/2;
if (numbers[mid] > numbers[j]) {
i = mid + 1;
} else if (numbers[mid] < numbers[j]) {
j = mid;
} else {
j--;
}
}
return numbers[i];
}
}
剑指 Offer 12. 矩阵中的路径
明显使用递归逐步判断是否符合
思路:遍历从二维数组从i,j开始和word的每一位位进行比较
1.比较不成功,直接false
2.比较成功,且比较到了最后一位,全部成功,返回true
3.成功,但没比较晚,递归i,j的上下左右和word的下一位进行比较
(1)如果新位置越界了,false
(2)每越界,但是已经访问过了的就不能再访问(访问辅助数组)
class Solution {
public boolean exist(char[][] board, String word) {
int h = board.length;//高
int w = board[0].length;//宽
boolean[][] visited = new boolean[h][w];//记录i,j是否被访问过
//遍历每一个ij,看从他们开始是否有符合的
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
//判断从i,j开始,校验从0开始包括0之后的字符是否符合board
boolean check = check(board, i, j, visited, word, 0);
if (check) {
return true;
}
}
}
return false;
}
/**
* 从i,j开始,校验从k开始包括k之后的字符是否符合board
* @param board
* @param i
* @param j
* @param visited
* @param word
* @param k 该校验的word的位置
* @return
*/
private boolean check(char[][] board, int i, int j, boolean[][] visited, String word, int k) {
//1.比较i,j上的元素和k的元素匹配不成功:失败,从下一个i,开始匹配
if (board[i][j] != word.charAt(k)) {
return false;
} else if (k == word.length() - 1) {//2.匹配成功,且匹配到了word的最后一个元素:成功
return true;
}
//3.匹配成功,继续匹配i,j的上下左右是否和k+1匹配
visited[i][j] = true;//记录当前的i,j访问过滤,防止之后的递归探测访问
int[][] dirs = new int[][]{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (int[] dir : dirs) {
int newi = i + dir[0];
int newj = j + dir[1];
//不越界说明新的方法可探测
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
//新坐标没有被探测过
if (!visited[newi][newj]) {
boolean check = check(board, newi, newj, visited, word, k + 1);
//当前i,j开始不光k匹配成功,K+1之后的全部匹配成功
if (check) {
//恢复访问
visited[i][j] = false;
return true;
}
}
}
}
//i,j虽然和k匹配成功,但是之后的没有成功
visited[i][j] = false;
return false;
}
}
面试题13. 机器人的运动范围
思路:广度遍历
1.因为从0,0开始走,所以只能走右边和下,所以只需要一直向这两个方向走就可以了
2.访问数组,用于记录走过的格子
3.把走过的格子放入队列,然后出队,进行向右、下两个方向走,如果新位置合理,则加入队列
4.判断新位置合理:
(1)位数和
(2)是否越界
class Solution {
public int movingCount(int m, int n, int k) {
if (k == 0) {
return 1;
}
boolean[][] visited = new boolean[m][n];
int res = 1;
//广度优先遍历需要的数组
Queue<int[]> queue = new LinkedList<>();
//向右和向下的方向数组
//1,0和0,1是因为后边向右和向下走的时候好分开进行走
//因为算法从0,0开始走,所以只有向下和向上就行
int[] right = new int[]{1, 0};
int[] down = new int[]{0, 1};
//初始坐标进入
queue.offer(new int[]{0, 0});
visited[0][0] = true;
while (!queue.isEmpty()) {
int[] poll = queue.poll();
//向右和向下走
int x = poll[0];
int y = poll[1];
for (int i = 0; i < 2; i++) {
int tx = right[i] + x;
int ty = down[i] + y;
//判断走后的新位置是否合适
if (tx < 0 || tx >= m || ty < 0 || ty >= n || get(tx) + get(ty) > k || visited[tx][ty]) {
continue;
}
queue.offer(new int[]{tx, ty});
visited[tx][ty] = true;
res++;
}
}
return res;
}
//传入一个数,得到他的位数和
public int get(int x) {
int res = 0;
while (x > 0) {
res += x % 10;
x = x / 10;
}
return res;
}
}
剑指 Offer 14- I. 剪绳子
(1)数学推导
public int cuttingRope(int n) {
//1.n=2,1*1;n=3,1*2
if (n <= 3) {
return n - 1;
}
int a = n / 3;//能切分的长度为3的最大段数
int b = n % 3;//切分之后,最后一段的长度
//2.最后一段长度为1,把前面一个长度为3的分出来个1给它,变成2
if (b == 1) {
return (int) (Math.pow(3, a - 1) * 2 * 2);
}
//3.最后一段长度为2,直接计算
if (b == 2) {
return (int) (Math.pow(3, a) * 2);
} else {
//4.最后一段长度为3
return (int) Math.pow(3, a);
}
}
(2)动态规划
长度i可以分解为i和i-j
dp[i]表示将正整数 ii 拆分成至少两个正整数的和之后,这些正整数的最大乘积。特别地,0 不是正整数,1 是最小的正整数,0 和 1 都不能拆分,因此 dp[0]=dp[1]=0。
public int cuttingRope(int n) {
//dp[i]存储i拆分后的最大成绩,即dp[n]为所求
int[] dp = new int[n + 1];
//从n=2开始拆
for (int i = 2; i <= n; i++) {
int max = 0;//n=i时,最大乘积,即dp[i]=max
// i分成j和i-j
for (int j = 1; j < i; j++) {
max = Math.max(max, Math.max(j * (i-j), j * dp[i - j]));
}
dp[i] = max;
}
return dp[n];
}
剑指 Offer 15. 二进制中1的个数
位运算
(1)位运算的方法
一个数字n,底层表示的是一串二进制。
所以当n&1进行运算时是n的二进制表示的最右位和1进行&运算
若为1,则得到的也是1
然后将n>>1做向右移一位的操作(右边第二位变成右边第一位)进行比较即可
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if ((n & 1) == 1) {
ret++;
}
n = n >> 1;
}
return ret;
}
把整数右移一位和把整数除以2在数学上是等价的,那上面的代码中可以把右移运算换成除以2吗?
不能。
位移运算比除法效率低得多
(2)优化
在(1)中是让n不断的右移,但是这种方法在面对有符号数时的负数时会出现问题
负数的最高位永远是1
所以当右移的时候,最高位永远会补1,也就会陷入循环
为了避免死循环,我们可以不右移输入的数字i。首先把i和1做与运算,判断i的最低位是
不是为1。接着把1左移一位得到2,再和i做与运算,就能判断i的次低位是不是1……这样反复
左移,每次都能判断i的其中一位是不是1。
1<<0:得到1,1和n做&判断最右位是不是1
1<<1:得到10,10和n做&判断右边第二位是不是1
1<<2:得到100,。。。。。。。
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int ret = 0;
for (int i = 0; i < 32; i++) {
if ((n & (1 << i)) != 0) {
ret++;
}
}
return ret;
}
(3)利用位&计算的技巧
当用n-1,若最右位为1.则最右位变为0;若第m位为1,右边全为0,则变为m位为0,右边全为1
此时发现,n-1得到的是从最右侧的第一个1开始(m),m和其右侧的全变为取反
所以若n&(n-1)得到的是将n的最右侧的第一个1变为0(右边全部都是0)
即能做几次n&(n-1),就有几个1
public int hammingWeight(int n) {
int res = 0;
while (n != 0) {
n = n & (n - 1);
res++;
}
return res;
}
剑指 Offer 16. 数值的整数次方
public double myPow(double x, int n) {
if (n == 0) {
return 1;
}
if (n == 1) {
return x;
}
if (n == -1) {
return 1 / x;
}
double half = myPow(x, n / 2);
double mod = myPow(x, n % 2);
return half * half * mod;
}
剑指 Offer 17. 打印从1到最大的n位数
这道题就是打印1-10^n-1,很简单
但是需要考虑大数(先没看)
此处先不考虑解决
class Solution {
public int[] printNumbers(int n) {
//最大的数
int end = (int)Math.pow(10,n)-1;
int[] res = new int[end];
for(int i =0;i<end;i++){
res[i]=i+1;
}
return res;
}
}
剑指 Offer 18. 删除链表的节点
//创建空头节点
class Solution {
public ListNode deleteNode(ListNode head, int val) {
ListNode MyHead = new ListNode(-1);
MyHead.next = head;
ListNode pre = MyHead;
ListNode p = head;
while(p!=null){
if(p.val==val){
pre.next =p.next;
break;
}
pre = p;
p = p.next;
}
return MyHead.next;
}
}
//先判断头节点,就不用创建空头节点了
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if(head.val == val) return head.next;
ListNode pre = head, cur = head.next;
while(cur != null && cur.val != val) {
pre = cur;
cur = cur.next;
}
if(cur != null) pre.next = cur.next;
return head;
}
}
剑指 Offer 19. 正则表达式匹配
使用动态规划
dp[0][0]表示两个字符串都是空的时候是否匹配,自然是匹配的.设为1
dp[i][j]表示s的第i-1和p的j-1以及之前的是否匹配
初始化:
dp[0][0]为0
因为s为空,p必须是值*值*
的这种形式
所以必须初始化dp第一行,当j=2,4等偶数时,必须为*
即dp[0][j] = dp[0][j - 2] && p[j - 1] = ‘*’
判别思路:
明确dp[i][j]对应的是字符s[i-1]和p[j-1]
class Solution {
public boolean isMatch(String s, String p) {
//定义dp
int m = s.length() + 1;
int n = p.length() + 1;
boolean[][] dp = new boolean[m][n];
//1.初始化dp
//1.1空字符串匹配成功
dp[0][0] = true;
//1.2s为空时,p能否匹配成功(填充第一行)
//从2开始即从p的第二个字符(p.charAt(1))是因为p的第一个肯定不匹配
for (int j = 2; j < n; j += 2) {
dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
}
//2.正式比较
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = p.charAt(j - 1) == '*' ?
dp[i][j - 2] || (dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) || (dp[i - 1][j] && p.charAt(j - 2) == '.') :
(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) || (dp[i - 1][j - 1] && p.charAt(j - 1) == '.');
}
}
return dp[m - 1][n - 1];
}
}
剑指 Offer 20. 表示数值的字符串
确定有限状态自动机
1.列出所有满足业务的内部状态,得到状态集合
2.每一个新的字符加入,看能否到达列出来的状态。如果可以,接着下一个字符;如果不行,直接false
3.如果遍历字符完毕,需要判断当前状态是否是接收状态:最终满足条件的状态
,如果是,则结束,不然false
如何定义状态集合
根据每一步的操作,定义可能出现的状态
注意,只要满足业务的可能状态都要列举
注意:本题规定允许4.5、.6、4.这四个状态的小数出现
画出状态转换图
public boolean isNumber(String s) {
Map[] states = {
//数组0:从状态0可以到的别的状态为:0还是空格、2整数部分、1符号位、4左边无整数的小数点
new HashMap<Character, Integer>() {{
put(' ', 0);
put('s', 1);
put('d', 2);
put('.', 4);
}},
//数组1:从状态1可以到的状态:2整数、4左边没有整数的小数点
new HashMap<Character, Integer>() {{
put('d', 2);
put('.', 4);
}},
//数组2:从状态2可到的状态:2整数、3左侧有整数的小数点、6字符e、9空格末尾
new HashMap<Character, Integer>() {{
put('d', 2);
put('.', 3);
put('e', 6);
put(' ', 9);
}},
//数组3:从状态3可到的状态:5小数部分、6字符e、9空格末尾
new HashMap<Character, Integer>() {{
put('d', 5);
put('e', 6);
put(' ', 9);
}},
//数组4:从状态4可到的状态:5小数部分
new HashMap<Character, Integer>() {{
put('d', 5);
}},
//数组5:从状态5可到的状态:5小数部分、6字符e、9空格末尾
new HashMap<Character, Integer>() {{
put('d', 5);
put('e', 6);
put(' ', 9);
}},
//数组6:从状态6可到的状态:7指数符号位、8指数部分的整数部分
new HashMap<Character, Integer>() {{
put('s', 7);
put('d', 8);
}},
//数组7:从状态7可到的状态:8指数部分的整数部分
new HashMap<Character, Integer>() {{
put('d', 8);
}},
//数组8:从状态8可到的状态:8指数部分的整数部、9空格末尾
new HashMap<Character, Integer>() {{
put('d', 8);
put(' ', 9);
}},
//数组9:从状态9可到的状态:9空格末尾
new HashMap<Character, Integer>() {{
put(' ', 9);
}}
};
int p = 0;//初始状态0
char t;
for (char c : s.toCharArray()) {
if (c >= '0' && c <= '9') t = 'd';
else if (c == '+' || c == '-') t = 's';
else if (c == 'e' || c == 'E') t = 'e';
else if (c == '.' || c == ' ') t = c;
else t = '?';
//如果当前状态下没有执行操作t可到达的状态,false
if (!states[p].containsKey(t)) return false;
//到达可到达的状态
p = (int) states[p].get(t);
}
//最终状态是2,3,6,8,9处于接收状态
return p == 2 || p == 3 || p == 5 || p == 8 || p == 9;
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
双指针:
前后指针一起搜索,各找到属于对方的数,再一起交换
public int[] exchange(int[] nums) {
int i = 0, j = nums.length - 1;
while (i < j) {
while (i < j && nums[i] % 2 != 0) {
i++;
}
while (i < j && nums[j] % 2 == 0) {
j--;
}
swap(nums, i, j);
}
return nums;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
剑指 Offer 22. 链表中倒数第k个节点
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode q = head;
ListNode p = head;
while (k > 1) {
q = q.next;
k--;
}
while (q.next != null) {
p = p.next;
q = q.next;
}
return p;
}
}
剑指 Offer 24. 反转链表
从前往后反转:
记录当前节点的下一个节点
当前节点指向前一个节点
前一个节点更新
当前节点更新
class Solution {
public ListNode reverseList(ListNode head) {
ListNode cur = head;
ListNode pre = null;
while (cur != null) {
ListNode p = cur.next;
cur.next = pre;
pre = cur;
cur = p;
}
return pre;
}
}
递归
从后往前进行反转
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}
剑指 Offer 25. 合并两个排序的链表
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(-1);
ListNode pre = head;
ListNode p = l1;
ListNode q = l2;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
pre.next = l1;
pre = l1;
l1 = l1.next;
} else {
pre.next = l2;
pre = l2;
l2 = l2.next;
}
}
if (l1 != null) {
pre.next = l1;
}
if (l2 != null) {
pre.next = l2;
}
return head.next;
}
}
剑指 Offer 26. 树的子结构
先判断两个树的根节点是否一样
- 如果一样,判断这两个树的子树是否一样(知道B为null)
- 如果不一样,判断A.left和B的根节点是否一样
- 再不一样,判断A.right和B的根节点是否一样
public boolean isSubStructure(TreeNode A, TreeNode B) {
boolean res = false;
if (A != null && B != null) {
if (A.val == B.val) {
//如果根节点一样,判断子树们是否一样
res = leftAndRight(A, B);
}
if (!res) {
//A的根几点改变
res = isSubStructure(A.left, B);
}
if (!res) {
res = isSubStructure(A.right, B);
}
}
return res;
}
private boolean leftAndRight(TreeNode A, TreeNode B) {
//成功条件
if (B == null) {
return true;
}
if (A == null) {
return false;
}
if (A.val != B.val) {
return false;
}
return leftAndRight(A.left, B.left) && leftAndRight(A.right, B.right);
}
剑指 Offer 27. 二叉树的镜像
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
mirrorTree(root.left);
mirrorTree(root.right);
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
return root;
}
}
剑指 Offer 28. 对称的二叉树
要注意,不是判断别左右子树是否一样, 而是对称!!!
所以就是判断当前节点的左子树和比对节点的右子树是否一样
public boolean isSymmetric(TreeNode root) {
return check(root, root);
}
private boolean check(TreeNode p,TreeNode q) {
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
return p.val == q.val && check(p.left, q.right) && check(q.left, q.right);
}
6.剑指 Offer 29. 顺时针打印矩阵
1.从左向右遍历第一行,在向下——》第一行排除
2.从上到下遍历最后一行,再向左——》最后一列排除
3.从右到左遍历最后一行,再向上——》最后一行排除
4.从下到上遍历第一列,再向右——》第一列排除
可以看出,周围一圈在不断缩小,所以指定四个遍历表示第一行、最后一列、最后一行、第一列,进行循环即可
public int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0) {
return new int[0];
}
//返回的数组
int[] res = new int[matrix.length * matrix[0].length];
//填充res的位置
int i = 0;
//最后一行
int height = matrix.length-1;
//最后一列
int wide = matrix[0].length - 1;
//第一行
int x = 0;//行的位置
//第一列
int y = 0;//列的位置
while (true) {
//从左到右遍历行
for (int j = y; j <= wide; j++) {
res[i++] = matrix[x][j];
}
//向下移动一行
if (++x > height) {
break;
}
//从上到下遍历列
for (int j = x; j <= height; j++) {
res[i++] = matrix[j][wide];
}
//向左移动一列
if (--wide < y) {
break;
}
//从右向左移动
for (int j = wide; j >=y ; j--) {
res[i++] = matrix[height][j];
}
//向上移动一行
if (--height < x) {
break;
}
for (int j = height; j >=x; j--) {
res[i++] = matrix[j][y];
}
//向右移动一行
if (++y > wide) {
break;
}
}
return res;
}
剑指 Offer 30. 包含min函数的栈
辅助栈来维护栈中的最小值
辅助站的栈顶元素永远是栈的最小值
- push的x比辅助站顶元素小,加入
- 否则继续加入栈顶元素
class MinStack {
Deque<Integer> stack;
Deque<Integer> helpStack;
/**
* initialize your data structure here.
*/
public MinStack() {
stack = new ArrayDeque<>();
helpStack = new ArrayDeque<>();
//放入一个最大值
helpStack.push(Integer.MAX_VALUE);
}
public void push(int x) {
stack.push(x);
helpStack.push(Math.min(helpStack.peek(), x));
}
public void pop() {
stack.poll();
helpStack.poll();
}
public int top() {
return stack.peek();
}
public int min() {
return helpStack.peek();
}
}
上面的方法是每次加入的数小于栈顶元素时候,才加入,否则加入栈顶元素进行占位,方便两个栈同时出栈。
也可以每次当x<=栈顶元素的时候就入栈,然而x>栈顶元素的时候就什么都不入,则这样在出栈的时候判断一下就可以了。两种都可以。注意x<=这个条件主要是为了针对0,1,0这种情况导致的空指针
class MinStack {
Deque<Integer> stack1;
Deque<Integer> stack2;
/** initialize your data structure here. */
public MinStack() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void push(int x) {
stack1.push(x);
if (stack2.isEmpty() || x <= stack2.peek()) {
stack2.push(x);
}
}
public void pop() {
Integer pop = stack1.pop();
if (stack2.peek().equals(pop)) {
stack2.pop();
}
}
public int top() {
return stack1.peek();
}
public int min() {
if(stack2.isEmpty()){
return -1;
}
return stack2.peek();
}
}
/**
* Your MinStack object will be instantiated and called as such:
* MinStack obj = new MinStack();
* obj.push(x);
* obj.pop();
* int param_3 = obj.top();
* int param_4 = obj.min();
*/
7.剑指 Offer 31. 栈的压入、弹出序列
public boolean validateStackSequences(int[] pushed, int[] popped) {
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0, j = 0; i < pushed.length; i++) {
stack.push(pushed[i]);
while (!stack.isEmpty() && stack.peek() == popped[j]) {
stack.pop();
j++;
}
}
return stack.isEmpty();
}
8.剑指 Offer 32 - 从上到下打印二叉树
(1)普通的层次遍历打印
public int[] levelOrder(TreeNode root) {
if (root == null) {
return new int[0];
}
Deque<TreeNode> queue = new ArrayDeque<>();
ArrayList<Integer> list = new ArrayList<>();
queue.addLast(root);
while (!queue.isEmpty()) {
TreeNode p = queue.removeFirst();
list.add(p.val);
if (p.left != null) {
queue.addLast(p.left);
}
if (p.right != null) {
queue.addLast(p.right);
}
}
//把list给数组
int[] res = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
res[i] = list.get(i);
}
return res;
}
}
(2)按每一行打印
就是需要在外层遍历的时候,记录队列内的数量,一次性把当前队列内的(这一层的)全部取出
关键在于:如果把这一层全部取出是按while (!queue.isEmpty())
取,则新放入的下一层放到哪?
所以取的时候应当记录现有的个数,按while (size != 0)
取
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return new ArrayList<>();
}
List<List<Integer>> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> list1 = new ArrayList<>();
while (size != 0) {
TreeNode p = queue.poll();
size--;
list1.add(p.val);
if (p.left != null) {
queue.offer(p.left);
}
if (p.right != null) {
queue.offer(p.right);
}
}
list.add(list1);
}
return list;
}
(3)不同层不同顺序打印
1.需要有1个遍历记录层数
2.内层的list采用双端队列,奇数层从后面插入,偶数层从前面插入。最后在转为list类型
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
if (root == null) {
return list;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int i = 1;
while (!queue.isEmpty()) {
Deque<Integer> list1 = new LinkedList<>();
int size = queue.size();
while (size != 0) {
TreeNode p = queue.poll();
size--;
if (i % 2 != 0) {
list1.addLast(p.val);
} else {
list1.addFirst(p.val);
}
if (p.left != null) {
queue.offer(p.left);
}
if (p.right != null) {
queue.offer(p.right);
}
}
i++;
list.add((List<Integer>) list1);
}
return list;
}
9.剑指 Offer 33. 二叉搜索树的后序遍历序列
重点是,不需要你判读是不是后序遍历
就默认是后序,也默认是搜索树,判断是否满足即可1.划分左右树
2.判断是否满足是搜索树
难点在于如何根据后序遍历
划分左右子树,去递归的判断是否符合搜索树?
- 如何划分?
- root为根的树若是搜索树,他的后序遍历一定是[左孩子的一堆值,有孩子的一堆值,root]所以从头遍历,当遇到第一个比root大的,就是右子树的最左下角,便可以划分左右子树
- 划分之后如何判断是否是搜索树?
- 搜索树后序遍历一定是[左孩子的一堆值,有孩子的一堆值,root],所以当按照前面的划分出左右子树之后,继续向后遍历,一直找到不大于root的数。若是搜索树,这个点一定就是他自己。
public boolean verifyPostorder(int[] postorder) {
boolean verify = verify(postorder, 0, postorder.length - 1);
return verify;
}
/**
* 验证局部树是否正确
*
* @param postorder 后根次序
* @param left 左
* @param right 右
*/
private boolean verify(int[] postorder, int left, int right) {
if (left >= right) {
return true;
}
//1.划分左右子树
int mid = left;
while (postorder[mid] < postorder[right]) {
mid++;//(1)mid是右子树的第一个;(2)mid是right
}
int p = mid;
while (postorder[p] > postorder[right]) {
p++;
}
//此时无论那种情况,只要是搜索树,都该是p==right
boolean leftIs = verify(postorder, left, mid - 1);
boolean rightIs = verify(postorder, mid, right - 1);
//2.判断是否满足搜索二叉树
return leftIs && rightIs && p == right;
}
剑指 Offer 34. 二叉树中和为某一值的路径
List<List<Integer>> res = new LinkedList<>();
List<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
getPath(root, target);
return res;
}
private void getPath(TreeNode root, int target) {
if (root == null) {
return;
}
path.add(root.val);
target -= root.val;
if (root.left == null && root.right == null && target == 0) {
// res.add(path);
// 细节:为什么我要通过构造方法传入path,不能直接res.add(path)
// 因为直接加入,加入的是引用(指向的堆中数据会变化),而此时的path是个全局变量,需要克隆一份加入
res.add(new LinkedList<Integer>(path));
}
getPath(root.left, target);
getPath(root.right, target);
path.remove(path.size()-1);
}
剑指 Offer 35. 复杂链表的复制
思路:
不可能利用循环或者递归,逐步的把当前节点的next和random创建和连接
因为这样会重复创建,所以需要map保存关系,那么保存什么关系
如果保存random关系?
问题:不可以先连接next,然后从map中取出来random节点进行连接
因为再进行next连接(第一次遍历复制)的时候,每一次循环,保存的都是原来链表的random关系(因为赋值链表还没有完全创建)
所以再第二次遍历的时候,想要给复制链表连接random关系是不行的
因为:通过原来链表的random关系,找到当前创建好的链表对应的random节点还要再遍历一次
所以,需要保存的是原来节点和当前节点
第一次遍历可以把新节点们都创建出来,并且map保存对应关系
这样第二次遍历就可以连接next和random,因为新节点的next和random的节点可以通过map.(对应旧节点.next/random)得到
/*
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
Map<Node,Node> map = new HashMap<>();
Node p = head;
//存储新旧节点关系
while (p != null) {
map.put(p, new Node(p.val));
p = p.next;
}
//第二次遍历,进行连接
p = head;
while (p != null) {
map.get(p).next = map.get(p.next);
map.get(p).random = map.get(p.random);
p = p.next;
}
return map.get(head);
}
}
剑指 Offer 36. 二叉搜索树与双向链表
问题1:如何连接左右指针?
中序遍历、使用pre记录中序遍历的前驱节点,逐步连接
问题2:如何把head指向最左节点?
设置前驱节点pre,初始伟null,指向当前节点的前一个节点,如果是最左节点,那么此时pre为null
问题3:头尾指针的关系怎么指向?
递归过程中,并不能解决最后的头为节点的连接,所以需要手动连接。因为最后一次递归,pre指向的是尾指针,就可以方便连接了
//pre表示前驱节点,head表示头节点
Node pre, head;
public Node treeToDoublyList(Node root) {
if (root == null) {
return null;
}
inOrder(root);
//配置头尾节点的关系
head.left = pre;
pre.right = head;
return head;
}
/**
* 中序遍历
* 形成一个链表
*
* @param root
*/
private void inOrder(Node root) {
if (root == null) {
return;
}
//中序遍历
inOrder(root.left);
//找到了最左节点,配置head节点
if (pre == null) {
head = root;
} else {//不是最左的情况,配置pre
pre.right = root;
root.left = pre;
}
pre = root;
inOrder(root.right);
}
12.剑指 Offer 37. 序列化二叉树
注意点:
1.Node的left是一个成员变量,所以本来就会给一个初始化null。所以在执行queue.add(poll.left);
的啥时候放入的是一个Node类型的null。一般的层序遍历要考虑为左右为NULL的时候不把子节点放入队列,是因为层序遍历不需要。而这个题需要放入NULL的情况。
2.如何反序列化?
如果遍历层序遍历数组,会不知道给哪个节点弄左右了。所以需要用队列保存父节点,把队列里的左右节点一个个补齐。null的就不用管了,会自动初始化(成员变量)。
3.如果用了层序遍历,逐个填充,如何知道填充的是数组的第几个?
用变量记录
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
if (root == null) {
return "[]";
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
TreeNode poll = queue.poll();
if (poll == null) {
stringBuilder.append("null" + ",");
} else {
stringBuilder.append(poll.val+",");
queue.add(poll.left);
queue.add(poll.right);
}
}
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
stringBuilder.append("]");
return stringBuilder.toString();
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
if(data.equals("[]")) return null;
String[] vals = data.substring(1, data.length() - 1).split(",");
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
Queue<TreeNode> queue = new LinkedList<>() {{ add(root); }};
int i = 1;
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!vals[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.left);
}
i++;
if(!vals[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.right);
}
i++;
}
return root;
}
}
13.剑指 Offer 38. 字符串的排列
思想:
1.第一个位置确定的可能n种,第二个n-1种。。。。
2.先确定x位,然后递归的确定n+1位
3.可以用交换的方式实现不同情况的排序
简单逻辑模拟,abc为例子
1.最外层,确定第一位:a和a交换,a和b交换,a和c交换,三种情况
2.内层,同理的确定第2,3位
在第一位为a的情况,剩下bc,b和b交换,b和c交换
即可得到abc,acb
问题:
aab这种情况
确定第一个位置时候,a先和本身交互,再和第二个a交换,此时aab就出现了两次,所系在每次确定第x位置的时候,需要判断当前要交换的元素是否已经之前进行了交换
class Solution {
char[] c;
List<String> res = new ArrayList<>();//结果集
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);
}
/**
* 确定第x位,并递归确定x以后的位
* @param x
*/
private void dfs(int x) {
//有一种可能的结果确定了
if (x == c.length - 1) {
res.add(String.valueOf(c));
return;
}
//防止有重复的字符交换
HashSet<Character> set = new HashSet<>();
//从x位开始和x,x+1...进行交换
for (int i = x; i < c.length; i++) {
//修剪操作,防止重复元素造成的冗余
if (set.contains(c[i])) {
continue;
}
set.add(c[i]);
swap(x, i);//交换,交换完相当于已经确定了第x位
dfs(x + 1);//递归的确定x+1位
/**
* 此时第x位的第一种确定情况的所有情况已经得到
* eg:abc,acb已经得到了
* 现在要恢复交换,以便x和新的i(x+1)进行交换
* 即确定以b开头的
*/
swap(x, i);
}
}
private void swap(int x, int i) {
char temp = c[x];
c[x] = c[i];
c[i] = temp;
}
}
14.剑指 Offer 39. 数组中出现次数超过一半的数字
hash
排序
投票
这三个方法里,投票是最有效率的,一个循环+两个变量的维护
思想:
1.当票数0,暂时没有最多的数,当前数即最多的数
2.如果当前数最多的数,票数+1,否则-1
摩尔投票法找的其实不是众数,而是占一半以上的数。当数组没有超过一半的数,则可能返回非众数,比如[1, 1, 2, 2, 2, 3, 3],最终返回3。
投票法简单来说就是不同则抵消,占半数以上的数字必然留到最后。
public int majorityElement(int[] nums) {
int x = 0;//所求的众数
int v = 0;//票数
for (int num : nums) {
if (v == 0) {
x = num;
}
v += x == num ? 1 : -1;
}
return x;
}
15.剑指 Offer 40. 最小的k个数
这道题主要考研快排
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
quickSort(arr, 0, arr.length - 1);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
//快排
private void quickSort(int[] arr, int left, int right) {
if (left < right) {
int p = partition(arr, left, right);
quickSort(arr, left, p - 1);
quickSort(arr, p + 1, right);
}
}
private int partition(int[] arr,int left, int right) {
int t = arr[left];
while (left < right) {
while (left < right && arr[right] >= t) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] <= t) {
left++;
}
arr[right] = arr[left];
}
arr[left] = t;
return left;
}
}
上面是直接全部排序出来,但是题目只要前k个得到出来即可,并不要求有序。所以,只要快排的哨兵p>k时,则想要的前k个数,就继续快排左半部分即可。若p<k,则说明想要的在右边,快排右边即可。当p== k时,想要的就是左边+p,已经得到了,但此时不一定时有序的(不要求有序),如果要求有序,把==的情况时也继续左边快排即可。
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k >= arr.length) {
return arr;
}
quickSort(arr, 0, arr.length - 1,k);
int[] res = new int[k];
for (int i = 0; i < k; i++) {
res[i] = arr[i];
}
return res;
}
//快排
private void quickSort(int[] arr, int left, int right,int k) {
if (left < right) {
int p = partition(arr, left, right);
if (p > k ) {
quickSort(arr, left, p - 1, k);
}
if (p < k) {
quickSort(arr, p+1, right, k);
}
//不需要有序,当等于的之后结束
return;
}
}
private int partition(int[] arr,int left, int right) {
int t = arr[left];
while (left < right) {
while (left < right && arr[right] >= t) {
right--;
}
arr[left] = arr[right];
while (left < right && arr[left] <= t) {
left++;
}
arr[right] = arr[left];
}
arr[left] = t;
return left;
}
}
16剑指 Offer 41. 数据流中的中位数
这道题主要就是需要在得到中位数的时候,把数据进行排序,但是每次得到的时候都排序,消耗就很大
所以利用优先队列在加入数据的时候就维护数据的顺序
但是一个优先队列在取中位数的时候不好取(消耗大)
就可以利用两个优先队列
思想:
q1维护小于等于中位数的从大到小的队列
q2维护大于中位数的从小到大的队列
当为奇数的时候,直接取出q1的头,当为偶数的时候取出两个的头的平均数即可
怎么在加入数据的时候维护两个队列?
当q1为空的时候,说明当前数就是中位数,所以加入q1
当q1的队头大于等于num的时候,说明比中位数小,所以加入q1,但是加入之后可能会存在,q2.size+1<q1.size的情况,此时q1的队头就不是中位数了,第二个数才是中位数,即把queue2.add(queue1.poll());
q2的维护也是同理
package com.example.concurrent;
public class MedianFinder {
/**
* initialize your data structure here.
*/
PriorityQueue<Integer> queue1;//queue1是保存小于等于中位数的,队头是最大值
PriorityQueue<Integer> queue2;//queue2是保存大于中位数的,队头是最小值
public MedianFinder() {
queue1 = new PriorityQueue<>((a, b) -> b - a);
queue2 = new PriorityQueue<>((a, b) -> a - b);
}
public void addNum(int num) {
if (queue1.isEmpty() || num <= queue1.peek()) {
queue1.add(num);
if (queue2.size() + 1 < queue1.size()) {
queue2.add(queue1.poll());
}
} else {
queue2.add(num);
if (queue2.size() > queue1.size()) {
queue1.add(queue2.poll());
}
}
}
public double findMedian() {
if (queue1.size() > queue2.size()) {
return queue1.peek();
} else {
return (queue1.peek() + queue2.peek()) / 2.0;
}
}
public static void main(String[] args) {
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.add(5);
queue.add(1);
queue.add(8);
queue.add(3);
while (!queue.isEmpty()) {
System.out.println(queue.poll());
}
}
}
剑指 Offer 42. 连续子数组的最大和
思考:
我首先想到了滑动窗口,看滑动窗口能否找到最大数组和的窗口
但是遇到问题:
如何扩大和缩小窗口?
一开始-2,遇到1的时候肯定是扩大,在遇到-3,是不懂还是继续扩大?
在一个-2肯定要缩小,那1什么时候缩?
所以滑动窗口不行
采用动态规划
dp[i]表示i之前包括i的最大值
这个例子中,dp数组就是:-2,-1,-3,4…
可以看出,求取dp[i]的时候,只要dp[i-1]<=0,说明前面的没有帮助,就不要了,dp[i]=nums[i];否则,dp[i]=dp[i-1]+nums[i]
剑指 Offer 43. 1~n 整数中 1 出现的次数
想要得到1的个数,即找到所给范围内每个位上的1的个数之和
eg:12
- 个位为2,把他设为1,则这个1出现的次数取决于他的高位十位。高位为1,则可能取的值为0,1.所以个位为1的次数为2*1(digit=1)
- 十位为1,高位没有所以十位出现的次数取决于地位个位。个位为2,可能的取值为0,1,2.所以十位出现的次数为3.
- 共计为2+3=5
为了总结出结论,选取2304作为例子
十位为当前位。digit=10,23位高位,4为低位。求cur当前位1的个数
- 当cur=0时:
因为是求当前位为1的个数,所以令cur=1,所以高位就不能取23了,只能取[0,22]。低位为4,但是因为高位降维了,所以可以取[0,9]。此时当前位1的个数取决于高位和低位。
res=23*10=230
,总结成公式就是res=high*digit
- 当cur=1时:
此时就要分两种情况:
1.当高位取值为【0,22】时,低位可以取【0,9】
2.当高位为23时,低位只能取到【0,4】
所以,需要把这两种情况加在一起。
res = 23*10+5=235
,总结公式就是res=high*digit+low+1
- 当cur=2,3,4,5,6,7,8,9时:
此时也要零cur=1,则高位可以取【0,23】,低位可以取到【0,9】.
res = (23+1)*10
,总结公式就是(high+1)*digit
public int countDigitOne(int n) {
int res = 0;//1的个数
int digit = 1;//因子
int high = n / 10;//当前为的高位(初始是个位的高位)
int low = 0;//低位,个位没有低位
int cur = n % 10;//当前位
while (high != 0 || cur != 0) {
if (cur == 0) res += high * digit;
else if (cur==1) res += high * digit + low + 1;
else res += (high + 1) * digit;
//当前位生位
low += cur*digit;
cur = high % 10;
high = high / 10;
digit *= 10;
}
return res;
}
18 剑指 Offer 44. 数字序列中某一位的数字
因为位数是从0开始数的,所以可以不用管0,直接从1开始计算
public int findNthDigit(int n) {
if (n < 10) {
return n;
}
//因为start和count可能会超出int范围,所以用long
long start = 1;//起始值
int digit = 1;//位数
long count = 9;//位数
//计算出digit
while (n > count) {
n -= count;
start *= 10;//1,10,100
digit += 1;//1,2,3
count = 9 * start * digit;//9,180...
}
//此时的n表示了从start开始的第几位
//第n位的数字时num
long num = start + (n - 1) / digit;
//index表示第n位在所求数的哪一位
int index = (n - 1) % digit;//0,1;0,1,2;0,1,2,3
return Long.toString(num).charAt(index)-'0';
}
19.剑指 Offer 45. 把数组排成最小的数
思想:
1.首先应该把第一位最小的放在最前面
2.当第一位有多个一样的时候,就应该考虑拼接的情况。即拼起来最小的
所以总结下来就是需要将拼接起来最小的情况进行排序
若x+y>y+x,则说明x>y,按照y,x排序,得到的拼接情况最小
因此需要排序,排序判断大小的规则即拼接比较。
具体实现的方式有两种,第一种是使用优先队列,传入比较器。第二中是自己写快排
//优先队列
class Solution {
public String minNumber(int[] nums) {
String[] strings = new String[nums.length];
for (int i = 0; i < nums.length; i++) {
strings[i] = String.valueOf(nums[i]);
}
PriorityQueue<String> queue = new PriorityQueue<>((a, b) -> {
return (a + b).compareTo(b + a);
});
for (String str : strings) {
queue.offer(str);
}
StringBuffer stringBuffer = new StringBuffer();
while (!queue.isEmpty()) {
stringBuffer.append(queue.poll());
}
return stringBuffer.toString();
}
}
//手写快排
class Solution {
public String minNumber(int[] nums) {
String[] strings = new String[nums.length];
for (int i = 0; i < nums.length; i++) {
strings[i] = String.valueOf(nums[i]);
}
quickSort(strings,0,strings.length-1);
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
stringBuilder.append(strings[i]);
}
return new String(stringBuilder);
}
private void quickSort(String[] strings, int left, int right) {
if (left >= right) {
return;
}
int p = partition(strings, left, right);
quickSort(strings, left, p - 1);
quickSort(strings, p + 1, right);
}
private int partition(String[] strings, int left, int right) {
String privot = strings[left];
while (left < right) {
while (left < right && (strings[right]+privot).compareTo(privot+strings[right]) >= 0) {
right--;
}
strings[left] = strings[right];
while (left < right && (strings[left] + privot).compareTo(privot + strings[left]) <= 0) {
left++;
}
strings[right] = strings[left];
}
strings[left] = privot;
return left;
}
}
20.剑指 Offer 46. 把数字翻译成字符串
设动态规划列表 dp ,dp[i]代表以 Xi 为结尾的数字的翻译方案数量。
所以初始化,dp[0] = dp[1] = 1,即 “无数字” 和 “第 1 位数字” 的翻译方法数量均为 1 ;
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int[] dp = new int[s.length()+1];//+1是因为有dp[0]表示没有数时候的排序情况。
dp[0] = 1;//没有数的排序有1种
dp[1] = 1;//第一个数的排序有一种
for (int i = 2; i <= s.length(); i++) {
String c = s.substring(i - 2, i);//拿到第Xi-1和第Xi数的组合
//如果Xi-1和第Xi可以联合翻译
if (c.compareTo("10") >= 0 && c.compareTo("25") <= 0) {
dp[i] = dp[i - 1] + dp[i-2];
} else {
dp[i] = dp[i - 1];
}
}
return dp[s.length()];
}
}
21.剑指 Offer 47. 礼物的最大价值
这种一步一步寻找最优的——动态规划
这道题的关键是只能向右或者向下,所以比如:在填充dp[0][2]的时候,一定是dp[0][1]+当前值
class Solution {
public int maxValue(int[][] grid) {
int[][] dp = new int[grid.length][grid[0].length];
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(i==0&&j==0){
dp[0][0] = grid[0][0];
}
else if (i == 0) {
dp[i][j] = dp[i][j - 1] + grid[i][j];
} else if (j == 0) {
dp[i][j] = dp[i - 1][j] + grid[i][j];
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
}
return dp[grid.length - 1][grid[0].length - 1];
}
}
22.剑指 Offer 48. 最长不含重复字符的子字符串
方法一:滑动窗口
遍历每一个开头的最长长度
这个方法空间和时间的消耗都是极大的,但是方便想出来
class Solution {
public int lengthOfLongestSubstring(String s) {
int res = 0;
int r = -1;//右指针控制窗口
for (int i = 0; i < s.length(); i++) {
r = i - 1;
Set<Character> set = new HashSet<>();
while (r+1 < s.length() && !set.contains(s.charAt(r+1))) {
set.add(s.charAt(r+1));
r++;
}
res = Math.max(res, r - i + 1);
}
return res;
}
}
方法二:动态规划+Map
使用Map来拿到i的位置
如果不能使用Map来记录各个位置的话,就可以得到j之后,往前遍历到i,但是这样时间复杂度就是O(n2)
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length()==0){
return 0;
}
if(s.length()==1){
return 1;
}
//map存放字符的位置,方便找出确定有边界j时,最近的相同字符的位置i
Map<Character, Integer> map = new HashMap<>();
int res = 1;
int[] dp = new int[s.length()];
dp[0] = 1;
map.put(s.charAt(0), 0);
for (int p = 1; p < s.length(); p++) {
int j = p;
//得到i
Integer i = map.getOrDefault(s.charAt(j), -1);
map.put(s.charAt(p), p);
//这里要注意,如果i=-1,则j-1一定>dp[p-1],所以就可以在这里一起判断,不用再判断map是否包含了
dp[p] = dp[p - 1] < j - i ? dp[j - 1] + 1 : j - i;
res = Math.max(res, dp[p]);
}
return res;
}
}
23.剑指 Offer 49. 丑数
思想:
只包含质因子的才是丑数
所以不能逐个判定,对2,3,5取余数,比如14质因子有7
所以
1.一个丑数,一定是一个较小的丑数质因子产生的
2.同样,一个丑数质因子一定是一个丑数
所以就可以根据之前的丑数推出新的丑数——动态规划
一个丑数可以*2,*3,*5得到3个丑数,但是最新的丑数是最小的那个
一开始的丑数为1,则dp[0]=1
为了得到三种质因子产生的丑数,就需要为他们创造三个索引a,b,c指向乘以2,3,5能创建的最小丑数的之前丑数的位置。
当第一个丑数*三个质因子,得到三个新的丑数,第二个丑数应当是他们最小的这个,代表这个质因子的索引就应该++(到下一个丑数)
class Solution {
public int nthUglyNumber(int n) {
/**
* dp[0]表示第一个丑数
* dp[n-1]表示第n个丑数
* a表示要成质因子2的较小丑数的索引,所以dp[a]*2就是有可能的最新丑数
* b,c同理
*
*/
int[] dp = new int[n];
int a = 0, b = 0, c = 0;//初始化索引位置
dp[0] = 1;
//遍历,把dp补充起来
for (int i = 1; i < n; i++) {
int n1 = dp[a] * 2;//n1,n2,n3表示三个质因子算出来的新的丑数
int n2 = dp[b] * 3;
int n3 = dp[c] * 5;
dp[i] = Math.min(Math.min(n1, n2), n3);//dp[i]为三者的最小
//判断最新的丑数是哪个质因子的,为其更新最新的位置
//注意当同时满足的时候,都应该++
//所以不能用ifelse
if (dp[i] == n1) {
a++;
}
if (dp[i] == n2) {
b++;
}
if (dp[i] == n3) {
c++;
}
}
return dp[n - 1];
}
}
剑指 Offer 50. 第一个只出现一次的字符
方法一:这道题很简单,就是使用Map遍历两遍
不过要找到第第一个出现一次的,所以第二次遍历还要遍历字符串s
方法二:主要学习使用有序哈希表,第二次遍历就可以遍历Map了
在哈希表的基础上,有序哈希表中的键值对是 按照插入顺序排序 的。基于此,可通过遍历有序哈希表,实现搜索首个 “数量为 1 的字符”。
Java 使用 LinkedHashMap 实现有序哈希表。
剑指 Offer 51. 数组中的逆序对
暴力求解,超时
归并排序
分成小的分组,然后两个分组合并有序的,得到他们的逆序对
class Solution {
public int reversePairs(int[] nums) {
return mergeSort(nums, 0, nums.length - 1);
}
private int mergeSort(int[] nums, int l, int r) {
if (l < r) {
int mid = l + ((r - l) >> 1);
//得到子递归的逆序对
int res = mergeSort(nums, l, mid) + mergeSort(nums, mid + 1, r);
//计算两个子递归组合的逆序对,并排列有序
if (nums[mid] <= nums[mid + 1]) {
return res;
}
//排序
int[] help = new int[r - l + 1];
int i = 0;
int p1 = l;
int p2 = mid + 1;
while (p1 <= mid && p2 <= r) {
if (nums[p1] <= nums[p2]) {
help[i++] = nums[p1++];
} else {
//res++;错
//res += p1-l+1;错
//这里比较两个有序分组的逆序对,比较容易出错
res += mid-p1+1;
help[i++] = nums[p2++];
}
}
while (p1 <= mid) {
help[i++] = nums[p1++];
}
while (p2 <= r) {
help[i++] = nums[p2++];
}
//把排序好的help放回nums
for (int j = 0; j < help.length; j++) {
nums[l + j] = help[j];
}
return res;
}
return 0;
}
}
剑指 Offer 52. 两个链表的第一个公共节点
先到统一起点,再一起走
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//计算表长,让统一位置开始
ListNode longHead, shortHead;
int dist = 0;//长度差
int len1 = getLongth(headA);
int len2 = getLongth(headB);
if (len1 > len2) {
longHead = headA;
shortHead = headB;
dist = len1 - len2;
}else {
longHead = headB;
shortHead = headA;
dist = len2 - len1;
}
//到统一起始点
while (dist != 0) {
longHead = longHead.next;
dist--;
}
while (longHead != null) {
if (longHead == shortHead) {
return longHead;
} else {
longHead = longHead.next;
shortHead = shortHead.next;
}
}
return null;
}
public int getLongth(ListNode listNode) {
int longth = 0;
while (listNode != null) {
longth++;
listNode = listNode.next;
}
return longth;
}
}
剑指 Offer 53 - I. 在排序数组中查找数字 I
先用二分查找,然后找到之后往前后遍历
class Solution {
public int search(int[] nums, int target) {
int i = EFsearch(nums, 0, nums.length - 1, target);
if (i == -1) {
return 0;
}
int index = i-1;
int res = 1;
//越界判断要放到前面,不然比较时候会越界
while (index >= 0 && nums[index] == target) {
res++;
index--;
}
index = i+1;
while (index < nums.length && nums[index] == target) {
res++;
index++;
}
return res;
}
private int EFsearch(int[] nums, int l, int r, int target) {
while (l <= r) {
int mid = l + ((r - l) >> 1);
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
return -1;
}
}
剑指 Offer 53 - II. 0~n-1中缺失的数字
二分查找,数字和位置进行比较
class Solution {
public int missingNumber(int[] nums) {
int i = ef(nums, 0, nums.length - 1);
return i;
}
private int ef(int[] nums, int left, int right) {
while (left <= right) {
int mid = (right + left) / 2;
if (mid == nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
}
25 剑指 Offer 54. 二叉搜索树的第k大节点
这个题要注意的是对k不能设置为局部变量,传入进行递归。那样子递归对k-操作返回之后,父程序的k还是操作之前的
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int k = 0;
int res = 0;
public int kthLargest(TreeNode root, int k) {
this.k = k;
test(root);
return res;
}
private void test(TreeNode root) {
if (root == null) {
return;
}
test(root.right);
k--;
if (k == 0) {
res = root.val;
}
test(root.left);
}
}
剑指 Offer 55 - I. 二叉树的深度
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
剑指 Offer 55 - II. 平衡二叉树
public boolean isBalanced(TreeNode root) {
if (root == null) {
return true;
}
int leftHigh = getHigh(root.left);
int rightHigh = getHigh(root.right);
return (Math.abs(leftHigh - rightHigh) <2)&&isBalanced(root.left)&&isBalanced(root.right);
}
//得到二叉树高度
private int getHigh(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(getHigh(root.left), getHigh(root.right) + 1);
}
优化
上面的方法是把判断子树平衡不平衡(isBalanced
)和以当前为节点的树平不平衡(判断子树高度差
)分开了
但是其实可以在得到子树长度的时候就判断平衡不平衡
也就是说在得到以当前节点为根的高度的时候,如果它为根的树不是平衡的,就返回-1
这种方法因为少了很多多余的递归,所以时间上有明显提升
class Solution {
public boolean isBalanced(TreeNode root) {
return getHigh(root) != -1;
}
private int getHigh(TreeNode root) {
if (root == null) {
return 0;
}
//得到子树的高度
int leftH = getHigh(root.left);
//子树不是平衡的
if (leftH == -1) {
return -1;
}
int rightH = getHigh(root.right);
//子树不是平衡的
if (rightH == -1) {
return -1;
}
//加上root是不是平衡的
if (Math.abs(rightH - leftH) > 1) {
return -1;
}
//都是平衡的,返回真正高度
return Math.max(getHigh(root.left), getHigh(root.right)) + 1;
}
}
26.剑指 Offer 56 - I. 数组中数字出现的次数
思想:
如果是只有一个单独的数,其他都是两个一样的数,那么做异或^操作,得到的就是这个单独的数
所以:把两个不同的数,拆分成两个子数组,每个数组进行异或,得到的分别就是所求的两个数
怎么分?
把nums异或,可以得到结果z = x^y.因为x,y肯定是不同的,所以,他们俩的二进制位肯定至少有一位是不同的。那么异或操作得到的z肯定至少有一位是1.可以利用m=1。m不断左移和z进行&操作,判断哪一位是1.
找到之后
遍历整个nums,若和m做&操作,有两个结果:
1.num&m==0——num 的m位为0
2.num&m!=0——num的m位不为0
这就划分成m位不同的两个组,也就把x和y划分开了
class Solution {
public int[] singleNumbers(int[] nums) {
//nums全部做异或运算,得到z=x^y
int z = 0;
for (int num : nums) {
z ^= num;
}
//得到x和y的第m位是不一样的
int m = 1;//000001
while ((z & m) == 0) {
m <<= 1;//左移1
}
//按照m划分两个组(m位相同的组,m位不同的组)
int x = 0, y = 0;//划分两个组后,每个组异或之后的结果
for (int num : nums) {
if ((m & num) == 0) {
x ^= num;
} else {
y ^= num;
}
}
return new int[]{x, y};
}
}
27.剑指 Offer 56 - II. 数组中数字出现的次数 II
思想:
只要时出现m次的数字,他们的二进制位1的个数加起来对m取余数,都是0
所以,此时那一个只出现1次的数,二进制位1也加进去,取余得到的就是那个数
class Solution {
public int singleNumber(int[] nums) {
//把nums所有数的每一位的二进制的1加起来
int[] count = new int[32];//32位
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < 32; j++) {
count[j] += nums[i] & 1;//count从0-32保存所有数从低到高位1的个数
nums[i] >>= 1;
}
}
//对每一位的1得到个数对3取余,得到唯一一个不是三个相同数的数
int res = 0;
for (int i = 0; i < 32; i++) {
res <<= 1;
res |= count[31 - i] % 3;
}
return res;
}
}
28.剑指 Offer 57. 和为s的两个数字
双指针
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
return new int[]{nums[left], nums[right]};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return new int[0];
}
}
因为这个题说的是递增的,所以不会出现一样的。但是,当是非递减的时候,就可以优化掉相同的情况
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
int leftNum = nums[left];
int rightNum = nums[right];
if (sum == target) {
return new int[]{nums[left], nums[right]};
} else if (sum < target) {
while (left < right && leftNum == nums[left]) {
left++;
}
} else {
while (left < right && rightNum == nums[right]) {
right--;
}
}
}
return new int[0];
}
}
29.剑指 Offer 57 - II. 和为s的连续正数序列
滑动窗口
class Solution {
//滑动窗口
public int[][] findContinuousSequence(int target) {
List<int[]> list = new ArrayList<>();
//滑动窗口,i表示左窗口,j表示右窗口,s表示窗口内和
//i为开区间,j为闭区间,因为最少为2个数,所以一开始就是1,2
int i = 1, j = 2, s = 3;
while (i < j) {
if (s == target) {
int[] ans = new int[j - i + 1];
for (int k = i; k <= j; k++) {
ans[k - i] = k;
}
list.add(ans);
s-=i;
i++;
} else if (s > target) {
s -= i;
i++;
} else {
j++;
s += j;
}
}
return list.toArray(new int[0][]);
}
}
剑指 Offer 58 - I. 翻转单词顺序
1.不用双指针进行交换,直接从后面进行倒序遍历使用StringBuilder进行连接即可
2.使用String[] s1 = s.split(" ");
进行划分之后,原来里面的>=2的空格全部被分成""
了,所以使用if (s1[i].equals("")
进行判断
class Solution {
public String reverseWords(String s) {
String[] s1 = s.split(" ");
StringBuffer stringBuffer = new StringBuffer();
for (int i = s1.length - 1; i >= 0; i--) {
if (s1[i].equals("")) {
continue;
}
stringBuffer.append(s1[i]).append(" ");
}
return new String(stringBuffer).trim();
}
}
上面是将字符串s划分之后,使用额外的空间得到一个新的逆转后的字符串。所以空间复杂度是O(n)。那么如何原地进行逆转?
划分之后,先对所有的字符进行逆转,然后再将每一个单词进行逆转就可以实现原地逆转了。但是对于Java来说,划分之后还是需要额外的空间来存储划分之后的字符串,所以空间复杂度还是n。
另外,如何自己实现划分的代码,而不使用API?
class Solution {
public String reverseWords(String s) {
//去除前后空格和单词之间多余的空格
StringBuilder sb = myTrim(s);
//反转整个sb字符
reverse(sb, 0, sb.length() - 1);
//反转每个单词
reverseEveryWord(sb);
return new String(sb);
}
private void reverseEveryWord(StringBuilder sb) {
int start = 0;
int end = 1;
int n = sb.length();
while (start < n) {
//遍历得到每个单词的结尾
while (end < n && sb.charAt(end) != ' ') {
end++;
}
//反转每个单词
reverse(sb, start, end - 1);
start = end + 1;
end = start + 1;
}
}
/**
* 反转字符串
* @param sb
* @param start
* @param end
*/
private void reverse(StringBuilder sb, int start, int end) {
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
}
/**
* 自定义去除空格
* @param s
* @return
*/
private StringBuilder myTrim(String s) {
//双指针先把前后空格去掉
int start = 0;
int end = s.length() - 1;
while (s.charAt(start) == ' ') start++;
while (s.charAt(end) == ' ') end--;
StringBuilder sb = new StringBuilder();
while (start <= end) {
char c = s.charAt(start);
//后面的条件是把两个单词之间多余的空格变成一个
if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') {
sb.append(c);
}
start++;
}
// System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]");
return sb;
}
}
剑指 Offer 58 - II. 左旋转字符串
简单做法:
1.切片:return s.substring(n, s.length()) + s.substring(0, n);
2.遍历:先遍历[n,len),用StringBuilder连接,再把前[0,n)遍历,连接
好做法:
先将[0,n)逆转
再将[n,len)逆转
再将[0,len)逆转
class Solution {
public String reverseLeftWords(String s, int n) {
char[] arr = s.toCharArray();
reversed(arr, 0, n - 1);
reversed(arr, n, s.length() - 1);
reversed(arr, 0, s.length() - 1);
return new String(arr);
}
public void reversed(char[] arr, int left, int right) {
while (left < right) {
char temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
}
剑指 Offer 59 - I. 滑动窗口的最大值
单调队列
参考剑指 Offer 30. 包含min函数的栈
要求实现一个栈,可以返回栈中元素的最小值,则栈1保存正常的栈数据,栈2保存栈中可能的最小值。即不保存所有的值,只有新加入的值更小的时候才入栈。
所以,单调队列也是一样
1.当对头保存的最大值不再是窗口内的元素时,出队
2.当新加入的元素比对尾元素大的时候,队尾元素出队==(队头永远保存最大值)==
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
//单调队列,保存的是索引
Deque<Integer> queue = new ArrayDeque<>();
//结果数组
int[] res = new int[nums.length - k + 1];
//结果数组的索引
int index = 0;
//开始遍历
for (int i = 0; i < nums.length; i++) {
//两种情况需要出队:
// 1.队头元素不在索引为[i-k+1,i]的范围
while (!queue.isEmpty() && queue.peek() < i - k + 1) {
queue.poll();
}
// 2.新加入的元素比队尾的元素大,队尾一直出队
while (!queue.isEmpty() && nums[i] > nums[queue.peekLast()]) {
queue.removeLast();
}
//新元素入队
queue.offer(i);
//把结果放入res
if (i + 1 >= k) {
res[index++] = nums[queue.peek()];
}
}
return res;
}
}
面试题59 - II. 队列的最大值
和59-Ⅰ一样,都是求一个时间序列的最大值(最小值)。所以依旧想到构造一个
递减队列
class MaxQueue {
Deque<Integer> queue;
Deque<Integer> helpQueue;
public MaxQueue() {
queue = new LinkedList<>();
helpQueue = new LinkedList<>();
}
public int max_value() {
if (helpQueue.size() == 0) {
return -1;
}
return helpQueue.peek();
}
public void push_back(int value) {
queue.addLast(value);
while (!helpQueue.isEmpty()&&helpQueue.peekLast() < value) {
helpQueue.removeLast();
}
helpQueue.addLast(value);
}
public int pop_front() {
if (queue.size() == 0) {
return -1;
}
if (queue.peek().equals(helpQueue.peek())) {
helpQueue.poll();
}
return queue.poll();
}
}
/**
* Your MaxQueue object will be instantiated and called as such:
* MaxQueue obj = new MaxQueue();
* int param_1 = obj.max_value();
* obj.push_back(value);
* int param_3 = obj.pop_front();
*/
注意:使用链表进行构造比使用数组要省时间的多
复习:这个是维护队列的最大值,那维护栈的最小值那?第30题
剑指 Offer 60. n个骰子的点数
思想
f(n,x)表示n个骰子,和为x的概率
一个骰子得到的6种情况概率记为f(1,1-6)
两个骰子得到的11种情况f(2,2-12)
如果想计算f(n,x),那么
- 当第n个骰子为1是,前n-1个骰子和要为x-1,概率为f(n-1,x-1)。此时f(n,x)=f(n-1,x-1)*1/6
- 当第n个骰子为2是,前n-1个骰子和要为x-2,概率为f(n-1,x-2)。此时f(n,x)=f(n-1,x-2)*1/6
- 当第n个骰子为3是,前n-1个骰子和要为x-3,概率为f(n-1,x-3)。此时f(n,x)=f(n-1,x-3)*1/6
- 当第n个骰子为4是,前n-1个骰子和要为x-4,概率为f(n-1,x-4)。此时f(n,x)=f(n-1,x-4)*1/6
- 当第n个骰子为5是,前n-1个骰子和要为x-5,概率为f(n-1,x-5)。此时f(n,x)=f(n-1,x-5)*1/6
- 当第n个骰子为6是,前n-1个骰子和要为x-6,概率为f(n-1,x-6)。此时f(n,x)=f(n-1,x-6)*1/6
所以总的f(n,x)就是把他们加起来。
观察发现,以上递推公式虽然可行,但 f(n - 1, x - i)f(n−1,x−i) 中的 x - ix−i 会有越界问题。例如,若希望递推计算 f(2, 2)f(2,2) ,由于一个骰子的点数和范围为 [1, 6][1,6] ,因此只应求和 f(1, 1)f(1,1) ,即 f(1, 0)f(1,0) , f(1, -1)f(1,−1) , … , f(1, -4)f(1,−4) 皆无意义。此越界问题导致代码编写的难度提升。
为了让不越界,就需要换一种思维。之前是求一个f(n,x),往前推6个加起来
现在可以,遍历所有的f(n-1),看他们对谁有贡献,逐个把他们的共享加起来。
以此类推,把贡献们都加载一起,就得到了对应的概率。这样也不会越界
class Solution {
public double[] dicesProbability(int n) {
//本来是dp[i][j]表示i个骰子和为j的概率。但是只需要最后一组概率,所以用一个数组dp,逐步和temp数组替换就可以了
// 初始为6,是因为n=1,就是6种情况
double[] dp = new double[6];
Arrays.fill(dp, 1.0 / 6.0);
//此时dp[0]代表1个骰子和为1的概率
//从2个骰子遍历到n个骰子
for (int i = 2; i <= n; i++) {
//temp表示i个骰子的dp。最小和为i,最大为6*i。个数和为(6*i)-i+1=5*i+1
double[] temp = new double[5 * i + 1];
//填充temp
for (int j = 0; j < dp.length; j++) {//遍历dp,填充的每个temp[j]
for (int k = 0; k < 6; k++) {
//dp[j]对temp[j~j+6]有贡献
temp[j + k] += dp[j] / 6.0;//还要乘以1/6,因为这是当前第i个骰子的概率
}
}
dp = temp;
}
return dp;
}
}
31.剑指 Offer 61. 扑克牌中的顺子
分析题目:
说是扑克牌,但是实际上是就是看一个数组,里面5个数字,0可以代表任何数,数字的取值是[0~13]。这里面数字出现的次数都可以是多次的。
自己的写法
思想:用变量wn记录万能牌0的个数,数组arr记录[1-13]出现的次数,这样就可以通过遍历确定导致不连续的牌。因为填充好arr之后,只有最多5个位置有数字,所以缩小到第一个牌和最后一个牌的位置。然后看他们里面有几个位置是0,则有几个不连续的,看万能牌0能否够填充他们。
因为牌可以多次出现,所以万能牌0的个数不止两张。且只要有别的牌出现两次,必然不是连续的,直接false
public static boolean isStraight(int[] nums) {
//记录0的个数
int wn = 0;
int[] arr = new int[14];
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) {
wn++;
continue;
}
arr[nums[i]] += 1;
}
//此时得到的arr,可以通过遍历它来看是否有序
int i = 0, j = arr.length - 1;
while (i < j && (arr[i] == 0 || arr[j] == 0)) {
if (arr[i] == 0) {
i++;
}
if (arr[j] == 0) {
j--;
}
}
while (i <= j) {
if (arr[i] > 1) {
return false;
}
if (arr[i] == 0) {
wn--;
}
i++;
}
if (wn < 0) {
return false;
} else {
return true;
}
}
改进方法一
改进思路:
1.上面是通过数组的方式将nums的最大最小值放在数组arr中,然后再遍历缩小arr的范围,得到最小最大值的位置,再遍历得到他们之间不连续的位置数,看万能牌0能否够填充。其实这样太麻烦,可以利用比较的方法得到最大最小值这样就不用再用数组了。
2.上面是还要通过遍历记录0的次数,但是实际上得到最大最小值,max-min<5那么就是可以填充的,因为一个数要么是在最大最小之间,要么就是0.如果有不连续,最大最小确定了是<5的。肯定有对应个数的0来填充。比如0,0,3,5,7.max-min=4<5,必然有对应的0去补它
3.上面使用数组记录的个数来判断重复,因为没有数组了,就要用set
改进二:
1.上面使用比较来确定最大最小值,也可以使用排序,然后遍历,跳过==0的,就可以得到最大最小值
2.判断num[i]和num[i+1]是否相等就可以判断是否有重复的
实际上三个方法效率差不多,但是我自己写的方法因为要多次遍历,就感觉很傻
32.剑指 Offer 62. 圆圈中最后剩下的数字
class Solution {
public int lastRemaining(int n, int m) {
//f(1,m)=0
int x = 0;
//从f(2,m)开始循环推导
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x;
}
}
剑指 Offer 63. 股票的最大利润
只需要记录
之前最低价
和目前最大利润
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int res = 0;
int min = prices[0];
for (int i = 1; i < prices.length; i++) {
if (prices[i] > min) {
res = Math.max(prices[i] - min, res);
}
min = Math.min(min, prices[i]);
}
return res;
}
}
33.剑指 Offer 64. 求1+2+…+n
考虑递归
//使用if的递归
if (n == 1) {
return 1;
}
n += sumNums(n - 1);
return n;
但是不能用if
考虑短路运算符&&
n > 1 && n += sumNums(n - 1)
当n==1的时候,就不会执行后面的递归,直接返回n
public int sumNums(int n) {
//前面是boolen,后面也的是,所以加一个">0"的永远成立的条件,防止报错
// x也是防止报错
boolean x = (n > 1) && (n += sumNums(n - 1)) > 0;
return n;
}
34.剑指 Offer 65. 不用加减乘除做加法
class Solution {
public int add(int a, int b) {
//当有进位的时候一直进行无进位和的操作,直到得到无进位时候的所以无进位和
while (b != 0) {
int c = (a & b) << 1; //&操作可以得到两个数进位的值,左移1为得到进位之后的进位值
//a 无进位和 异或操作可以得到两个数的无进位和
a ^= b;
//b为最新的进位
b = c;
}
return a;
}
}
35.剑指 Offer 66. 构建乘积数组
class Solution {
public int[] constructArr(int[] a) {
if(a.length==0){
return new int[0];
}
int[] b = new int[a.length];
int temp = 1;//计算上半部分三角时候的辅助变量
b[0] = 1;//初始计算下半部分三角时候b[0]=1
// 计算下三角,从B[1]开始,因为B[0]左边部分第一个就是1
for (int i = 1; i < a.length; i++) {
b[i] = b[i - 1] * a[i - 1];
}
//计算上三角,从B[len-2]开始
for (int i = a.length - 2; i >= 0; i--) {
temp *= a[i + 1];
b[i] *= temp;
}
return b;
}
}
面试题67. 把字符串转换成整数
如果知识简单的把字符串变成数字:
1.trim去掉空格
2.判断正负号,使用标记位记录,最后乘上
直接截取字符串进行拼接在转成数字
但是要考虑:
1.越界
怎么判断越界很重要,如果已经得到了结果在和最值比较,则是不对的,因为已经越界了(越界的值时Integer.MAX_VALUE=2147483647)
所以应当把res每次准备和x进行计算时和Integer.MAX_VALUE/10=214748364
进行比较
(1)如果大于则,res10至少为2147483650直接越界
(2)如果不大于,但是x>7,则res10+x得到的值至少为2147483648也越界
2.有可能有"123abc"这种不是数字的存在
这种情况就不能直接字符串截取了,就必须一个一个遍历
class Solution {
public int strToInt(String str) {
String s = str.trim();
if(s.length()==0){
return 0;
}
char[] chars = s.toCharArray();
//正负数标记
int sign = 1;
//结果
int res = 0;
//遍历的索引,默认一开始有正负号
int i = 1;
if (chars[0] == '-') {
sign = -1;
} else if (chars[0] != '+') {
//第一位不是正负号,所以从0开始遍历
i = 0;
}
//越界判断
int binary = Integer.MAX_VALUE / 10;
for (; i < chars.length; i++) {
//1。判断是否是数字
if (chars[i] < '0' || chars[i] > '9') {
break;
}
//2.判断是否越界(题目要求越界返回最值)
if (res > binary || (res == binary && chars[i] > '7')) {
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
//计算
res = res * 10 + (chars[i] - '0');
}
return sign*res;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
注意:是找最近的公共父节点
思路:
对于二叉搜索树
可以分为三种情况:
- p,q位于root的两个子树内,则root就是所求节点
- p,q位于左子树内,向root的左子树寻找,直到找到公共
- 同上的右子树
所以只要找到这个公共的交叉节点即可
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
while (root != null) {
//向左子树找
if (root.val > p.val && root.val > q.val) {
root = root.left;
}
//向右子树找
else if (root.val < p.val && root.val < q.val) {
root = root.right;
} else {
//找到了
break;
}
}
return root;
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
因为这个不是搜索树了,所以无法使用比较值来判断:1.公共节点在哪个子树。2.什么时候找到公共节点
因此就要左右子树都找,直到找到
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
/**
* 1.当一开始p/q就是公共节点,直接返回
* 2.往子树遍历,只要找到p/q,就返回
*/
if (root == null || root == p || root == q) {
return root;
}
//子树递归,返回找到的p,q
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//如果找到了,则返回他们的最近公共节点
if (left != null && right != null) {
return root;
}
//没有全都找到,或者是返回的left/right内全都找到了,即向上一层递归返回找到的最终结果
return left == null ? right : left;
}