目录
剑指 Offer 32 - II. 从上到下打印二叉树 II
剑指 Offer 32 - III. 从上到下打印二叉树 III
剑指 Offer 56 - II. 数组中数字出现的次数 II
剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
解题思路:
- 话不多说,这里的做法很多题都可以用到
- 定义一个长度为n的数组temp,位置 i 就代表 0 - n-1 中的数
- 遍历一遍原数组nums,将对应位置上的数字+1,最终位置上的值代表总的出现次数
- 题目要找到出现多次的,也就是大于1的,遍历temp,遇到满足的就返回
class Solution {
public int findRepeatNumber(int[] nums) {
int[] res = new int[nums.length];
for (int n:nums){
res[n]++;
}
for (int i=0;i<res.length;i++){
if (res[i] > 1)return i;
}
return -1;
}
}
剑指 Offer 04. 二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解题思路:
题目从左至右递增,从上之下递增
根据这个特征可以从右上开始遍历,向左下遍历,分为行和列
如果target比当前值要大,因为从左至右递增,所以右边都是小值,这一行跳过
如果target比当前值要小,因为从上之下递增,所以下边都是大值,这一列跳过
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
// 根据题目从左至右递增,从上之下递增
// 根据这个特征可以从右上开始遍历
// 分为行和列
// 如果target比当前值要大,因为从左至右递增,所以右边都是小值,这一行跳过
// 如果target比当前值要小,因为从上之下递增,所以下边都是大值,这一列跳过
if (matrix.length == 0)return false;
int row = 0;
int col = matrix[0].length - 1;
while (row < matrix.length && col >= 0){
if (matrix[row][col] == target)return true;
if (matrix[row][col] < target){
row++;
}else if (matrix[row][col] > target){
col--;
}
}
return false;
}
}
剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
解题思路:
- 把字符串转换成char数组
- 遍历数组,判断当前是否为空
- 如果为空,则用“%20”替代
- 结果StringBuffer拼接起来
class Solution {
public String replaceSpace(String s) {
char[] s_c = s.toCharArray();
StringBuffer str = new StringBuffer();
for (char c : s_c){
if (c == ' '){
str.append("%20");
}else{
str.append(c+"");
}
}
return str.toString();
}
}
剑指 Offer 06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
解题思路:
- 正好拿这道题复习下反转链表(只是想练习下)
- 将链表反转后,再从头遍历到尾,拿到对应的值
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
ListNode node = head;
ListNode pre = null;
int size = 0;
while (node != null){
ListNode next = node.next;
node.next = pre;
pre = node;
node = next;
size++;
}
int[] res = new int[size];
for (int i=0;i<size;i++){
res[i] = pre.val;
pre = pre.next;
}
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) {
return dfs(preorder,0,preorder.length-1,inorder,0,inorder.length-1);
}
public TreeNode dfs(int[] preorder,int p_start,int p_end,int[] inorder,int i_start,int i_end){
if (p_start > p_end)return null;
if (i_start > i_end)return null;
// 获取前序遍历的开始节点,作为根节点
TreeNode node = new TreeNode(preorder[p_start]);
// 找到根节点在中序遍历的位置
int index = 0;
for (int i=0;i<inorder.length;i++){
if (preorder[p_start] == inorder[i]){
index = i;
}
}
// 找到中间值后,将中序遍历分为左树和右数
// 计算左子树的长度,去前序遍历确定位置,这样右子树的位置也确定了
node.left = dfs(preorder,p_start+1,p_start+index-i_start,inorder,i_start,index-1);
node.right = dfs(preorder,p_start+1+index-i_start,p_end,inorder,index+1,i_end);
return node;
}
}
剑指 Offer 09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail
和 deleteHead
,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead
操作返回 -1 )
解题思路:
- 两个栈,一个用来放入,一个用来弹出
- 因为是弹出队列头部,所以要将放入栈整个颠倒,也就是挨个从放入栈取出到弹出栈中,弹出时就直接从弹出栈中拿就行
- 放入还是照常放入,当发现弹出栈没有值的时候,可以做一次转换
- 当需要弹出元素,但弹出栈没值的时候可以做一次转换,如果发现双方都没值则返回-1
class CQueue {
private static Stack<Integer> queue1;
private static Stack<Integer> queue2;
public CQueue() {
queue1 = new Stack<Integer>();
queue2 = new Stack<Integer>();
}
public void appendTail(int value) {
queue1.push(value);
if (queue2.isEmpty()){
stack1MoveToStack2();
}
}
public int deleteHead() {
if (queue2.isEmpty() && queue1.isEmpty()){
return -1;
}
if (queue2.isEmpty()){
stack1MoveToStack2();
}
return queue2.pop();
}
public void stack1MoveToStack2(){
while (!queue1.isEmpty()){
queue2.push(queue1.pop());
}
}
}
剑指 Offer 10- I. 斐波那契数列
写一个函数,输入 n
,求斐波那契(Fibonacci)数列的第 n
项(即 F(N)
)。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1 F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路:
- 动态规划
- 矩阵快速幂,满足时间复杂度:O(logn)
- 所以如果要用上述关系式,n 对应的就是 F(n+1),n-1 对应的就是 F (n)
- 最终目的就是 n-1 个 M 相乘,快速运算下
- n - 1 等于 1, 一个 M
- n - 2 等于 2, 两个 M
- n - 3 等于 3, 三个 M
- 可以用右移的方式判断1的个数
- 1 的时候最右端就是1,所以 M*单位矩阵=M
- 2 的时候最右端是0,所以M不用乘任何东西,但右移动后就需要乘M*M
- 3 的时候最右端是1,为ret = M,右移也是1,ret = ret * M * M
- 可以看出除了第一位如果是1,只需要乘一次,后续右移动,都是在2的基础上翻倍
- 最终满足 n-1 个 M 相乘
- 代入矩阵方程:因为 F (1) = 1 , F (0) = 0 ,所以最终 F (n) = res [0][0]
class Solution {
public int fib(int n) {
if (n == 0)return 0;
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for (int i=2;i<=n;i++){
dp[i] = (dp[i-1] + dp[i-2])%1000000007;
}
return dp[n];
}
}
class Solution {
public int fib(int n) {
if (n < 2)return n;
int[][] p = new int[][]{{1,1},{1,0}};
int[][] res = computed(p,n-1);
return res[0][0];
}
public int[][] computed(int[][] a,int n){
int[][] ret = new int[][]{{1,0},{0,1}};
while (n > 0){
if ((n&1)==1){
ret = multiple(ret,a);
}
n>>=1;
a = multiple(a,a);
}
return ret;
}
public int[][] multiple(int[][] a,int[][] b){
int[][] ret = new int[2][2];
for (int i=0;i<2;i++){
for (int j=0;j<2;j++){
ret[i][j] = (int)(((long)a[i][0]*b[0][j] + (long)a[i][1]*b[1][j]) %1000000007);
}
}
return ret;
}
}
剑指 Offer 10- II. 青蛙跳台阶问题
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n
级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路:
- 动态规划:每次都可选1或者选2,每一层的台阶都可从前一层或者前两层跳上来,方法的和就是这两种方法之前的和,进行个累加
class Solution {
public int numWays(int n) {
if (n == 0)return 1;
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
for (int i=2;i<=n;i++){
dp[i] = (dp[i-1] + dp[i-2])%1000000007;
}
return dp[n];
}
}
剑指 Offer 11. 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers
,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2]
为 [1,2,3,4,5]
的一次旋转,该数组的最小值为 1。
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
解题思路:
- 二分查找
- 目的是查找旋转后数组的最小值
- 而最小值出现在旋转点,在数组中旋转点往右是升序,但都是小值,肯定比左边来的小
- 以此来进行二分查找
- 当发现当前中间值比右边大的时候,当前值就是属于左边大值得范围,所以直接缩小左边界就行,left 可以直接是中间值的后一位开始
- 当发现当前中间值比右边小的时候,说明当前值是属于右边小值那一区域,属于是旋转点后的值,所以当前值往右都是升序,最小值肯定在当前值开始往左
- 当发现当前中间值等于右边值的时候,说明是重复的值,直接缩小一位就行。这里为什么不直接把右边界直接缩小到当前值呢?举个例子:[3,3,1,3],当前值是3,又边界也是3,如果直接把右边界拉到当前值,就直接pass掉了1,所以每次 - 1
class Solution {
public int minArray(int[] numbers) {
// 旋转后,从旋转点到右边界得都是小值,且从右往左肯定是降序,所以当前值和右边值比较就行了
int left = 0;
int right = numbers.length - 1;
while (left < right){
int mid = left + (right - left)/2;
// 当前值比右边大,说明当前值不属于旋转后得小值那部分,缩小左边界
if (numbers[mid] > numbers[right]){
left = mid + 1;
// 当前值比右边要小,说明处于一个小值范围区域,当前值也有可能是最小值,所以缩小右边界到当前值
}else if (numbers[mid] < numbers[right]){
right = mid;
// 相等的情况,缩小右边界,减去一个相同值
}else{
right--;
}
}
return numbers[right];
}
}
剑指 Offer 12. 矩阵中的路径
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
- 首先初步筛选从字母的第一位开始
- 每个字母占了一个方格,它的下一位可能在它的上下左右,所以都得尝试,有一个满足就说明能找到
- 找不到的条件:
- 上下左右搜索的时候超出了边界,直接返回
- 你用过的格子不能反复去用,所以有个boolean去记录格子使用情况,每次用了就占住,最后这个格子前进失败了就释放掉,所以遇到已经被用得格子直接返回
- 如果当前格子不是想要得下一个字母,直接返回
- 找下一个字母,找不到直接返回
class Solution {
public boolean exist(char[][] board, String word) {
int n = board.length;
int m = board[0].length;
boolean[][] used = new boolean[n][m];
for (int i=0;i<n;i++){
for (int j=0;j<m;j++){
if (board[i][j] == word.charAt(0)){
if (dfs(board,n,m,i,j,word,0,used)){
return true;
}
}
}
}
return false;
}
public boolean dfs(char[][] board,int n,int m,int i,int j,String word,int index,boolean[][] used){
if (index > word.length()-1 || i < 0 || j < 0 || i > n-1 || j > m-1 || used[i][j]){
return false;
}
if (index == word.length()-1){
if (board[i][j] == word.charAt(index)){
return true;
}
return false;
}
if (board[i][j] == word.charAt(index)){
used[i][j] = true;
if (dfs(board,n,m,i+1,j,word,index+1,used) || dfs(board,n,m,i-1,j,word,index+1,used) || dfs(board,n,m,i,j+1,word,index+1,used) || dfs(board,n,m,i,j-1,word,index+1,used)){
return true;
}else{
used[i][j] = false;
}
}
return false;
}
}
剑指 Offer 14- I. 剪绳子
给你一根长度为 n
的绳子,请把绳子剪成整数长度的 m
段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m-1]
。请问 k[0]*k[1]*...*k[m-1]
可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
解题思路:
- 看到分几段就想用动态规划
- 绳子长度可以从小到大慢慢扩大,每次分段也可以从小到大慢慢扩大
- 假设在固定绳子长度 i 下,最后一段 j 可以从长度1开始切,剩下长度就是 i - j
- 随着 j 的增大,j 也可以分为好几段,求该长度的最大解
- 每个线段都时可分可不分,选更大的
- 举例:n = 5
- 绳子长从2开始,j 只能为1,因为不可再分 dp [2] = 1
- 绳子长为3,j 为 1,剩下绳长为2,2再分就是1+1,1*1 < 2,所以判断当前线段不如不分来的好。j 为 2,本身2也可再分,和上述一样,不分比分来的好,所以最终dp[3]=2
- 绳子长为4,j为1,不可再分,剩下身长为3,判断绳长为3时,dp[3],能产生的最大值,该值和不分该线段3比较,发现dp[3]=2<3,所以不分比分来的好。j为2,可分可不分,选择不分2,剩下也是2,选择不分,dp[4] =2*2=4
- 绳子长为5,j为1,不分再分,剩下4,dp[4]=4,不分时线段长也为4,取4。j为2,不分好,剩下3,不分好,取 2 * 3 = 6。j 为 3,剩下为2,取6。再往后都经历过了
- 所以取 j 的时候只要取到一半就行,剩下的另一半都经历过
class Solution {
public int cuttingRope(int n) {
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for (int i=2;i<=n;i++){
dp[i] = i-1;
for (int j=1;j<i/2+1;j++){
dp[i] = Math.max(dp[i],Math.max(dp[i-j],(i-j))*Math.max(dp[j],j));
}
}
return dp[n];
}
}
剑指 Offer 14- II. 剪绳子 II
给你一根长度为 n
的绳子,请把绳子剪成整数长度的 m
段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]...k[m - 1]
。请问 k[0]*k[1]*...*k[m - 1]
可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
解题思路:
- 2 取 1*1
- 3 取 1*2
- 4 取 2*2
- 5 取 2*3
- 6 取 3*3
- 7 取 3*2*2
- 8 取 3*3*2
- 9 取 3*3*3
- 发现规律,能尽量多的取3
- 当取到不能取的时候,剩下有这几种情况
- 留下0,则直接返回
- 留下1,例如4-3=1,但是这种情况下2*2>3*1,所以回退一个3,乘一个4
- 留下2,例如5-3=2,直接*2
class Solution {
public int cuttingRope(int n) {
// 动态规划max超出则没有意义
// 尽量取3
// 取3后 剩余0 则返回
// 取3后 剩余2 则*2返回
// 取3后 剩余1 则/3*4 2*2 > 1*3
if (n <= 3)return n-1;
long res = 1;
while (n > 4){
res = (res*3)%1000000007;
n -= 3;
}
// n = 1,3,4
return (int)((res*n)%1000000007);
}
}
剑指 Offer 15. 二进制中1的个数
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为 汉明重量).)。
解题思路:
- 判断原有的1的个数
- n&1:检测最右边是否是1
- n>>>1:无符号右移,移动之前比较过的1
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res = 0;
while (n != 0){
res += n&1;
n = n>>>1;
}
return res;
}
}
剑指 Offer 16. 数值的整数次方
实现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
解题思路:
- 二进制角度分析,一个 n 可以拆分,x^n 也可以继续拆分
- n 的拆分
- n = 1, 0000 0001, x
- n = 2, 0000 0010, x*x
- n = 3, 0000 0011, x*x*x
- n = 4, 0000 0100, x*x*x*x
- 可以看出
- 遇到 0,x = x*x 翻倍
- 遇到 1,res = res * x
- Java中 int 在 [−2147483648,2147483647] ,所以当 n = -2147483648 , n = -n 会越界,所以计算的时候,要先用换个类型,用 long 来计算
class Solution {
public double myPow(double x, int n) {
double res = 1;
long N = n;
if (N < 0){
N = -N;
}
while (N > 0){
if ((N&1)==1){
res = res*x;
}
N>>=1;
x = x*x;
}
return n > 0 ? res : 1/res;
}
}
剑指 Offer 17. 打印从1到最大的n位数
输入数字 n
,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。
解题思路:
- 根据 n 判断数组大小,10的n次方 - 1
- 给数组挨个赋值从1开始,正好对应 i -1 位
class Solution {
public int[] printNumbers(int n) {
int len = 1;
while (n-- != 0){
len *= 10;
}
int[] res = new int[len-1];
for (int i=1;i<len;i++){
res[i-1] = i;
}
return res;
}
}
剑指 Offer 18. 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
解题思路:
- 定义一个头节点pre,头节点的下一位就是head
- 判断当前位的下一位是否还有值,有值就判断是否等于value,如果等于就直接删除该节点,如果不等于,就接着判断下一位
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode node = pre;
while (node.next != null){
if (node.next.val == val){
node.next = node.next.next;
return pre.next;
}else{
node = node.next;
}
}
return node;
}
}
剑指 Offer 19. 正则表达式匹配
请实现一个函数用来匹配包含'. '
和'*'
的正则表达式。模式中的字符'.'
表示任意一个字符,而'*'
表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"
与模式"a.a"
和"ab*ac*a"
匹配,但与"aa.a"
和"ab*a"
均不匹配。
解题思路:
- 动态规划
- 随着s长度的增加,在不同长度下,p能否匹配
- 先判断当前位置p是不是‘*’,如果不是则表示无特殊情况,直接看最后两位是否能匹配起来,如果p的最后一位是‘.’,则表示可以,否则不相同不能匹配
- 这里注意,首先需要判断当前 i 是否是0,0 表示当前s为空。当p的所在位置是‘*’,就是说如果是‘*',还能删除前面的值和空匹配起来,已知不是’*‘也就不能和空匹配,所以在不是'*'的情况下,i = 0,都不能匹配
- 如果当前位置p是'*',就有多种情况可以考虑
- ’*‘前面的一位能否和当前s的最后一位对应上
- 如果可以,'*'可以表示为删除前一位,'*'保留前一位,'*'表示前一位的多位
- 分别代表 a* = '',a* = a,a* = aaaaa
- 所以当匹配起来的时候
- 取 删除p的后两位和s当前位置再比较 的结果
- 或者
- 取 删除p的后两位和删除s的后一位再比较 的结果
- 或者
- 取 删除s的后一位和p当前保持不变 的结果(a*表示多个,所以不用动)
class Solution {
public boolean isMatch(String s, String p) {
int n = s.length();
int m = p.length();
boolean[][] dp = new boolean[n+1][m+1];
// 两个空字符串也能匹配
dp[0][0] = true;
// i代表s长度的增加
for (int i=0;i<=n;i++){
//j代表p长度的增加
for (int j=1;j<=m;j++){
// 分为有'*'和无'*'的情况
// 有*,可以匹配0,1,多次
if (p.charAt(j-1) == '*'){
// 首先判断s最后一位和p中*前的一位相同不相同
if (isSame(s,p,i,j-1)){
// 如果*前面的字符和i最后的字符相等
// 可以把p的后两位删除,也就是*重复0次,之后看i和j-2是否一样
// 也可以把p的后两位删除,把s的后一位删除
// 也可以p不动,表示多次,把s的后一位删除
dp[i][j] = dp[i][j-2] || dp[i-1][j-2] || dp[i-1][j];
}else{
// 如果不相等直接删除p的后两位,表示*重复0次,然后再看
dp[i][j] = dp[i][j-2];
}
}else{
if (isSame(s,p,i,j)){
dp[i][j] = dp[i-1][j-1];
}
}
}
}
return dp[n][m];
}
public boolean isSame(String s,String p,int i,int j){
if (i == 0){
return false;
}
if (p.charAt(j-1) == '.'){
return true;
}
return s.charAt(i-1) == p.charAt(j-1);
}
}
剑指 Offer 20. 表示数值的字符串
请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。
数值(按顺序)可以分成以下几个部分:
- 若干空格
- 一个 小数 或者 整数
- (可选)一个
'e'
或'E'
,后面跟着一个 整数 - 若干空格
小数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(
'+'
或'-'
) - 下述格式之一:
- 至少一位数字,后面跟着一个点
'.'
- 至少一位数字,后面跟着一个点
'.'
,后面再跟着至少一位数字 - 一个点
'.'
,后面跟着至少一位数字
- 至少一位数字,后面跟着一个点
整数(按顺序)可以分成以下几个部分:
- (可选)一个符号字符(
'+'
或'-'
) - 至少一位数字
部分数值列举如下:
["+100", "5e2", "-123", "3.1416", "-1E-16", "0123"]
部分非数值列举如下:
["12e", "1a3.14", "1.2.3", "+-5", "12e+5.4"]
解题思路:
- 分情况讨论
- 首先判断'+','-',应该处在第一位或者跟在e或者E后面
- 其次判断数值,在 0 - 9 之间就是数值
- 其次判读点,点只能出现一次,就是跟在数值后面,不能跟在e或E后面
- 其次按段e或E,也只能出现一次,且跟在数值后面,e或E得后面还得跟数值
class Solution {
public boolean isNumber(String s) {
char[] c = s.trim().toCharArray();
boolean hasE = false;
boolean hasDot = false;
boolean isNumber = false;
for (int i=0;i<c.length;i++){
// 0-9说明是数值
if (c[i]>= '0' && c[i] <= '9'){
isNumber = true;
}
// 正负号的时候,只能出现在第一位,或者跟在e或E的后面
else if (c[i] == '+' || c[i] == '-'){
if (i != 0 && c[i-1] != 'e' && c[i-1] != 'E')return false;
}
// 当出现e或者E,得之前没出现过
else if (c[i] == 'e' || c[i] == 'E'){
// 如果已经出现过,或者e 前面没有数值
if (hasE || !isNumber){
return false;
}
// e或E后面一定要跟数值
hasE = true;
isNumber = false;
}
// 当出现.,允许出现一次,但是不能跟在e或E后面
else if (c[i] == '.'){
if (hasDot || hasE){
return false;
}
hasDot = true;
}else {
return false;
}
}
return isNumber;
}
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数在数组的前半部分,所有偶数在数组的后半部分。
解题思路:
- 左右双指针,左指针移动到偶数,又指针移动到奇数
- 左指针先移动到偶数,然后移动右指针直到找到奇数,将两者交换,前提 left < right
class Solution {
public int[] exchange(int[] nums) {
int left = 0;
int right = nums.length-1;
while (left <= right){
if (nums[left]%2==0){
while (left < right && nums[right]%2==0){
right--;
}
if (right >= 0){
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
right--;
}
}
left++;
}
return nums;
}
}
剑指 Offer 22. 链表中倒数第k个节点
难度简单385收藏分享切换为英文接收动态反馈
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6
个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6
。这个链表的倒数第 3
个节点是值为 4
的节点。
解题思路:
- 快慢指针,让快指针先走k步,然后快慢指针一起走,当快指针走到null的时候,slow就是答案。这里倒推一下,倒数k个节点,设置最后k个节点的左右边界,左边界往前移动到开始,左右边界正好差了k。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head;
ListNode slow = head;
while (k-- > 0){
fast = fast.next;
}
while (fast != null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
剑指 Offer 24. 反转链表
难度简单473收藏分享切换为英文接收动态反馈
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
解题思路:
- 老生常谈,反转链表
- 设置一个虚拟节点prev,让当前节点的next指向prev,循环这个过程
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
while (head != null){
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
}
剑指 Offer 25. 合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
解题思路:
- 类似于合并两个数组,谁小就选谁,谁先选完就停止,剩下的没选完的直接连上即可
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode head = new ListNode(0);
ListNode node = head;
while (l1 != null && l2 != null){
if (l1.val < l2.val){
node.next = l1;
l1 = l1.next;
}else {
node.next = l2;
l2 = l2.next;
}
node = node.next;
}
node.next = l1 == null ? l2 : l1;
return head.next;
}
}
剑指 Offer 26. 树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
例如:
给定的树 A:
3
/ \
4 5
/ \
1 2
给定的树 B:
4
/
1
返回 true,因为 B 与 A 的一个子树拥有相同的结构和节点值。
解题思路:
- 当A和B都为null的时候直接返回false
- 只有当前节点相等,且左左相等,右右相等的时候才是子树
- 否则在当前节点找不到,只能去当前节点的左子树或者右子树查找
- 递归找的时候,如果当前B为null,说明已经找完了可以返回true。如果A为null,则表示在B不为null的时候,A已经为空,说明就不是了。其次如果A,B两个当前值就不一样,那就直接返回 false,当条件都满足的时候,递归的判断它们的左左和右右
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
if (A == null || B == null){
return false;
}
if (A.val == B.val && dfs(A.left,B.left) && dfs(A.right,B.right)){
return true;
}
return isSubStructure(A.left,B) || isSubStructure(A.right,B);
}
public boolean dfs(TreeNode A, TreeNode B){
if (B == null){
return true;
}
if (A == null){
return false;
}
if (A.val != B.val){
return false;
}
return dfs(A.left,B.left) && dfs(A.right,B.right);
}
}
剑指 Offer 27. 二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
例如输入:
4
/ \
2 7
/ \ / \
1 3 6 9
镜像输出:
4
/ \
7 2
/ \ / \
9 6 3 1
解题思路:
- 递归的获取左,获取右,把左给右,把右给左,最后返回当前节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null)return null;
TreeNode node1 = mirrorTree(root.right);
TreeNode node2 = mirrorTree(root.left);
root.left = node1;
root.right = node2;
return root;
}
}
剑指 Offer 28. 对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:
1
/ \
2 2
\ \
3 3
解题思路:
- 递归条件:左左等于右右,左右等于右左
- 终止条件:都为null
- 其他都是不符合:
- 一边为null,一边不为null
- 两个节点的值不相同
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null)return true;
return dfs(root.left,root.right);
}
public boolean dfs(TreeNode left,TreeNode right){
if (left == null && right == null)return true;
if (left == null && right != null)return false;
if (left != null && right == null)return false;
if (left.val != right.val)return false;
return dfs(left.left,right.right) && dfs(left.right,right.left);
}
}
剑指 Offer 29. 顺时针打印矩阵
输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
解题思路:
- 按照顺序,从左至右遍历,当遇到超出边界,或访问过的情况则掉转方向,直至数组满
class Solution {
int curr = 0;
int[][] direction = {{0,1},{1,0},{0,-1},{-1,0}};
public int[] spiralOrder(int[][] matrix) {
int n = matrix.length;
if (n == 0)return new int[0];
int m = matrix[0].length;
int[] res = new int[n*m];
boolean[][] used = new boolean[n][m];
dfs(res,used,matrix,n,m,0,0,0);
return res;
}
public void dfs (int[] res,boolean[][] used,int[][] matrix,int n,int m,int i,int j,int size){
if (size > n*m-1){
return;
}
if (i < 0 || j < 0 || i > n-1 || j > m-1 || used[i][j]){
if (curr == 0){
curr = 1;
dfs(res,used,matrix,n,m,i+direction[curr][0],j-1+direction[curr][1],size);
}else if (curr == 1){
curr = 2;
dfs(res,used,matrix,n,m,i-1+direction[curr][0],j+direction[curr][1],size);
}else if (curr == 2){
curr = 3;
dfs(res,used,matrix,n,m,i+direction[curr][0],j+1+direction[curr][1],size);
}else if (curr == 3){
curr = 0;
dfs(res,used,matrix,n,m,i+1+direction[curr][0],j+direction[curr][1],size);
}
}else{
res[size] = matrix[i][j];
used[i][j] = true;
dfs(res,used,matrix,n,m,i+direction[curr][0],j+direction[curr][1],size+1);
}
}
}
剑指 Offer 30. 包含min函数的栈
难度简单373收藏分享切换为英文接收动态反馈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
解题思路:
- 辅助栈用来存放最小值,每次都会和栈顶比较,最小的值入栈,弹出则同时弹出
class MinStack {
Stack<Integer> stack1;
Stack<Integer> stack2;
/** initialize your data structure here. */
public MinStack() {
stack1 = new Stack();
stack2 = new Stack();
}
public void push(int x) {
if (stack1.isEmpty()){
stack2.push(x);
}else{
if (stack2.peek() > x){
stack2.push(x);
}else{
stack2.push(stack2.peek());
}
}
stack1.push(x);
}
public void pop() {
stack1.pop();
stack2.pop();
}
public int top() {
return stack1.peek();
}
public int min() {
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();
*/
剑指 Offer 31. 栈的压入、弹出序列
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。
解题思路:
- 循环一边pushed,当判断当前需要push的值和pop值相同时,就无需放入,双方都继续下一位的判断,直到不同为止。当产生不同后,有可能是需要把之前的值弹出,所以用一个栈去存放需要放入的值,当发现栈顶元素是当前需要弹出的值,则弹出栈,同时poped位置向后移动,while循环判断,直到发现不是弹出的元素。当什么都不是的时候,这就是一个新值,需要放入到栈中,最终遍历结束,可能会存在最后一位没有及时判断的情况,所以最终在把栈顶元素和当前弹出位比较,弹出需要弹出的元素,最终检查栈中元素是否都弹出了,都弹出则表示是正确的弹出顺序
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
int n = pushed.length;
Stack<Integer> stack = new Stack();
int i = 0;
int j = 0;
for (;i<n;i++){
while (i < n && pushed[i] == popped[j]){
j++;
i++;
}
while (!stack.isEmpty() && stack.peek() == popped[j]){
stack.pop();
j++;
}
if (i < n){
stack.push(pushed[i]);
}
}
while (!stack.isEmpty() && stack.peek() == popped[j]){
stack.pop();
j++;
}
return stack.isEmpty();
}
}
剑指 Offer 32 - I. 从上到下打印二叉树
难度中等228收藏分享切换为英文接收动态反馈
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
解题思路:
- 层序遍历,挨个插入到list中,最终list转数组输出
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int[] levelOrder(TreeNode root) {
List<Integer> res = new ArrayList();
if (root == null)return new int[0];
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
for (int i=0;i<size;i++){
root = queue.poll();
res.add(root.val);
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
}
}
int[] re = new int[res.size()];
for (int i=0;i<res.size();i++){
re[i] = res.get(i);
}
return re;
}
}
剑指 Offer 32 - II. 从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
解题思路:
- 常规的层序遍历,从左至右按顺序即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList();
if (root == null)return res;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()){
int size = queue.size();
List<Integer> list = new ArrayList();
for (int i=0;i<size;i++){
root = queue.poll();
list.add(root.val);
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
}
res.add(list);
}
return res;
}
}
剑指 Offer 32 - III. 从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
解题思路:
- 层序遍历的变形,一层从左到右,一层从右到左
- 根据层数判断奇偶确定左右顺序,由于每层的节点个数已知,就可以构建数组,插入的时候根据左右顺序决定是从前往后插入,还是从后往前插入
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList();
if (root == null)return res;
Queue<TreeNode> queue = new LinkedList();
queue.add(root);
int depth = 0;
while (!queue.isEmpty()){
int size = queue.size();
depth++;
Integer[] temp = new Integer[size];
for (int i=0;i<size;i++){
root = queue.poll();
if (depth % 2 == 0){
temp[size-i-1] = root.val;
}else{
temp[i] = root.val;
}
if (root.left != null){
queue.add(root.left);
}
if (root.right != null){
queue.add(root.right);
}
}
res.add(Arrays.asList(temp));
}
return res;
}
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true
,否则返回 false
。假设输入的数组的任意两个数字都互不相同。
解题思路:
- 二叉搜索树:左节点 < 根节点 < 右节点
- 数组中最后一位就是根节点,所以从头开始找,比根节点小的就是左子树,找到中间的界限mid,从left 到 mid-1 为左子树,从 mid 到 right 为右子树,之前只检验了左边小值的为左子树,右树也需要继续检验是否都大于根节点,如不满足则fan'hui
- 递归着左子树和右子树,判断是不是二叉搜索树
class Solution {
public boolean verifyPostorder(int[] postorder) {
// 二叉搜索树,左节点 < 根节点 < 右节点
int n = postorder.length;
int left = 0;
int right = n-1;
return dfs(postorder,left,right);
}
public boolean dfs(int[] postorder,int left,int right){
// 最后一位就是根节点,所以比根节点小的就是左子树,比根节点大的就是右子树
if (left >= right)return true;
// 先找左子树
int i = left;
while (postorder[i] < postorder[right]){
i++;
}
// 从 left 到 mid-1 为左子树
int mid = i;
// 先判读剩下的右边的数是不是都大于根节点
while (postorder[mid] > postorder[right]){
mid++;
}
if (postorder[mid] != postorder[right])return false;
// 递归遍历左和右
return dfs(postorder,left,mid-1) && dfs(postorder,mid,right);
}
}
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root
和一个整数目标和 targetSum
,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
解题思路:
- 从根节点出发一直到叶子节点为止,找到对应和为target的路径
- 叶子节点,所以最终当target满足的时候,也要看下当前是否是叶子节点,也就是没有左子树和右子树,当满足时才是答案之一
- 总体递归从左至右,前序遍历树就行
- 用个栈存放遍历过的节点,在遍历到的时候插入,当遍历结束的时候弹出
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> pathSum(TreeNode root, int target) {
List<List<Integer>> res = new ArrayList();
Stack<Integer> stack = new Stack();
dfs(root,target,res,stack);
return res;
}
public void dfs(TreeNode root,int target,List<List<Integer>> list,Stack<Integer> stack){
if (root == null){
return;
}
target -= root.val;
stack.push(root.val);
dfs(root.left,target,list,stack);
dfs(root.right,target,list,stack);
if (root.left == null && root.right == null){
if (target == 0){
list.add(new ArrayList(stack));
}
}
target += root.val;
stack.pop();
}
}
剑指 Offer 35. 复杂链表的复制
请实现 copyRandomList
函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next
指针指向下一个节点,还有一个 random
指针指向链表中的任意节点或者 null
。
解题思路:
- 因为存在下一个节点和随机节点,所以随机节点有可能会是之前的节点
- 所以要用个map储存下节点
- 创建第一个节点,是head的复制,接着递归着去查找它的next和random,如果之前没有过就创建新的,如果有直接返回
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
HashMap<Node,Node> map = new HashMap();
public Node copyRandomList(Node head) {
if (head == null)return null;
if (!map.containsKey(head)){
Node node = new Node(head.val);
map.put(head,node);
node.next = copyRandomList(head.next);
node.random = copyRandomList(head.random);
}
return map.get(head);
}
}
剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化为双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下图展示了上面的二叉搜索树转化成的链表。“head” 表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
解题思路:
- 链表的顺序就是中序遍历的顺序,所以大框架还是中序遍历
- 最后一个节点和第一个节点得相互连接,定义一个头节点,定义一个尾部节点,尾部节点只需要遍历得时候跟着移动就行
- 当头部节点为空得时候,赋值头部节点
- pre始终作为前一个节点,当前节点得左边即是pre,pre得右边即是当前节点,赋值完毕后,将当前节点赋值给pre,然后开始下一个节点,最终当下一个节点为null得时候,pre就恰好停留在最后一个节点,最后只需要head.left = pre; pre.right = head;
/*
// Definition for a Node.
class Node {
public int val;
public Node left;
public Node right;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val,Node _left,Node _right) {
val = _val;
left = _left;
right = _right;
}
};
*/
class Solution {
Node head; // 定义第一个
Node pre; // 遍历的时候到最后一个
public Node treeToDoublyList(Node root) {
// 中序遍历,获取节点顺序
if (root == null)return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
public void dfs(Node root){
if (root == null)return;
dfs(root.left);
if (head == null){
head = root;
}else{
pre.right = root;
}
root.left = pre;
pre = root;
dfs(root.right);
}
}
剑指 Offer 37. 序列化二叉树
请实现两个函数,分别用来序列化和反序列化二叉树。
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
解题思路:
- 应用题:按照前序遍历的方式,用字符串把它们连接起来,用 ’,‘ 分隔,注意None
- 反序列化的时候,就是先分割还原成数组,转为list,一个个取,分别按照前序的样子就构造
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
public class Codec {
// Encodes a tree to a single string.
public String serialize(TreeNode root) {
return serializeToString(root,"");
}
public String serializeToString(TreeNode root,String s){
if (root == null){
s += "None,";
}else{
s += "" + root.val + ",";
s = serializeToString(root.left,s);
s = serializeToString(root.right,s);
}
return s;
}
// Decodes your encoded data to tree.
public TreeNode deserialize(String data) {
String[] d = data.split(",");
List<String> list = new ArrayList(Arrays.asList(d));
return redeserialize(list);
}
public TreeNode redeserialize(List<String> list){
if (list.get(0).equals("None")){
list.remove(0);
return null;
}
TreeNode node = new TreeNode(Integer.parseInt(list.get(0)));
list.remove(0);
node.left = redeserialize(list);
node.right = redeserialize(list);
return node;
}
}
剑指 Offer 38. 字符串的排列
输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
解题思路:
- 回溯剪枝,相同得剪掉
- 为了使得相同得在一起,所以先要给char数组排个序
- 每个遍历都可以从头开始取,只不过每次用过得不能再用
- 注意剪枝得条件,当前仅当当前值和前一个值相同,且上一个值刚刚被撤回使用
- 例如: aab
- 一开始取第一个a,递归发现第一个a取过了,取第二个a,判断第二个a和第一个相同,但是第一个a是正在使用中得,说明符合选用条件,可以选择,最后选择b,结果为 aab
- 当一开始取第二个,答案会和取一个 a 重复,所以这种情况就得剪枝,唯一不一样得点在于,第一个 a 这时候并没有选用状态,说明是外层的重复,不是里面选取的重复,所以判断条件得加一个有没有已经被使用
class Solution {
public String[] permutation(String s) {
char[] c = s.toCharArray();
Arrays.sort(c);
boolean[] used = new boolean[c.length];
List<String> list = new ArrayList();
dfs(c,used,list,"");
String[] res = new String[list.size()];
for (int i=0;i<list.size();i++){
res[i] = list.get(i);
}
return res;
}
public void dfs(char[] c,boolean[] used,List<String> list,String s){
if (s.length() == c.length){
list.add(s);
return;
}
for (int i=0;i<c.length;i++){
if (i > 0 && c[i] == c[i-1] && !used[i-1]){
continue;
}
if (!used[i]){
used[i] = true;
dfs(c,used,list,s+c[i]);
used[i] = false;
}
}
}
}
剑指 Offer 39. 数组中出现次数超过一半的数字
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
解题思路:
- 数组中随机查找一个数,因为是多数的数,所以大概率能取到
- 取到的数,遍历一遍看出现的次数,如果出现的次数超过长度一半,说明就是答案
- 否则再次随机选取
class Solution {
public int majorityElement(int[] nums) {
Random random = new Random();
int max = nums.length/2;
while (true){
int curr = nums[randomNum(random,0,nums.length)];
if (countNum(nums,curr) > max){
return curr;
}
}
}
public int countNum(int[] nums,int curr){
int count = 0;
for (int i=0;i<nums.length;i++){
if (nums[i] == curr)count++;
}
return count;
}
public int randomNum (Random random,int left,int right){
return random.nextInt(right-left)+left;
}
}
剑指 Offer 40. 最小的k个数
输入整数数组 arr
,找出其中最小的 k
个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
解题思路:
- 优先队列,使用保持k个最小数在队列中,最后返回队列
- 先随机插入k个数,k个数在队列中已经排序好了,最大的数在队列顶部
- 从k+1开始,每插入一个数都和顶部数比较,谁小谁进队列
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
int[] res = new int[k];
if (k == 0) { // 排除 0 的情况
return res;
}
PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new Comparator<Integer>() {
public int compare(Integer num1, Integer num2) {
return num2 - num1;
}
});
for (int i = 0; i < k; ++i) {
queue.offer(arr[i]);
}
for (int i = k; i < arr.length; ++i) {
if (queue.peek() > arr[i]) {
queue.poll();
queue.offer(arr[i]);
}
}
for (int i = 0; i < k; ++i) {
res[i] = queue.poll();
}
return res;
}
}
剑指 Offer 41. 数据流中的中位数
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
- void addNum(int num) - 从数据流中添加一个整数到数据结构中。
- double findMedian() - 返回目前所有元素的中位数。
解题思路:
- 求中位数,自然而然把数组分为两部分A、B,可以用两个优先队列
- 如果A.size = B.size,中位数就是两端值相加除以2.0
- 如果A.size ≠ B.size,中位数就是取多的那部分的顶端
- A 保存较大的值,队列顶端是最小的值
- B 保存较小的值,队列顶端是最大的值
- 例如:[5,4,3,2,1]
- A:[3,4,5] ,从小大到排序,
- B:[2,1] ,从大到小排序
- 中位数就是 3
- 例如:[7,6,5,3,2,1]
- A:[5,6,7]
- B:[3,2,1]
- 中位数就是 ( 5+3 ) / 2.0
- 当 A.size = B.size,新插入一个值,肯定要给A,但是新值不知道大小,会打乱原来的顺序,所以既然注定有个值去A,而且要保证A的值都比B的大,新值可能是小值,所以先插入到B中,由B排序,推个新的最大值出来去A,这样始终保证了A>B
- 当 A.size ≠ B.size,新插入一个值,肯定要给B,但是新值可能很大,所以先去A,排序后推个相对最小的给B,这样也保证了A>B
- 注意最终结果如果相等要取两队列顶端之和再除以 2.0,2.0的原始是要保留小数部分
class MedianFinder {
/** initialize your data structure here. */
PriorityQueue<Integer> A; // 保存数大的部分,堆顶是小的数
PriorityQueue<Integer> B; // 保存数小的部分,堆顶是大的数
public MedianFinder() {
A = new PriorityQueue<>((o1,o2) -> (o2-o1));
B = new PriorityQueue<>();
}
public void addNum(int num) {
// 两边数量相等,如果还有值插入,需要进入大的那部分,但是新的值会调整整个队列顺序,所以为了保证大值,需要先插入B,把B中最大的值给A
if (A.size() == B.size()){
B.offer(num);
A.offer(B.poll());
}else{
// 两边不相等,A比B多,所以肯定插入B,但是新值数影响排序,所以先插入A,确保较小的数去B
A.offer(num);
B.offer(A.poll());
}
}
public double findMedian() {
return A.size() != B.size() ? A.peek() : (A.peek()+B.peek())/2.0;
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
剑指 Offer 42. 连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
解题思路:
- 遍历一遍数组
- 从头开始累加,如果累加的和还没有当前值大,说明之前累加的值很小,题目求的是最大值,那就重新从当前值开始累加。遍历的过程中不断更新最大值
class Solution {
public int maxSubArray(int[] nums) {
int sum = 0;
int max =Integer.MIN_VALUE;
for (int i=0;i<nums.length;i++){
sum += nums[i];
sum = Math.max(sum,nums[i]);
max = Math.max(sum,max);
}
return max;
}
}
剑指 Offer 43. 1~n 整数中 1 出现的次数
输入一个整数 n
,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
解题思路:
- 假设 n = 32104, 要计算每一位1出现的次数
- 现在计算下十位出现的次数,cur就表示所在的位数
- 每100位,会出现10次,所以321个百位出现321*10次
- 当前cur = 0,正好是整数,结果就是321*10
- 如果当前cur = 1,即除去321*10,还有32110,32111,32112,32113,32114,就是结果321*10+4+1
- 如果当前cur > 1,即除去321*10,还有32110,32111,32112,32113,32114,32115,32116,32117,32118,32119,供十个数,结果为321*10+10
- 每一位的结果有了,最终就是每一位累加的过程
- 将 321 用 high 来代替,将 4 用 low 来代替,将当前位 0 用 cur 代替
- 初始化:
- cur = n%10; 当前位从个位开始,取最后一位
- high = n/10; 比cur高的一位开始
- low = 0; 没有更低位
- digit = 1; 当前个位为 1
- 移动过程:
- 计算累加结果
- low、cur、high 都向前移动一位
- low += cur * digit; 把当 cur 作为 low 的一部分
- cur = high%10; cur 需要进位,把 high 的最后一位给 cur
- high = high/10; high 需要缩小一位,把最后一位去除
- digit = digit*10; 进位
class Solution {
public int countDigitOne(int n) {
int high = n/10; // 除最后一位前面都是高值
int low = 0; // low 还没有值
int curr = n%10; // 当前值从最后一位开始
int digit = 1; // 从个位开始累加
int res = 0; // 结果
while (high != 0 || curr != 0){
if (curr == 0){
res += high*digit;
}else if (curr == 1){
res += high*digit+low+1;
}else {
res += (high+1)*digit;
}
low += curr*digit; //low每次往前面挪动1位,即把curr移进来
curr = high%10; // curr也是往前移动一位,变成high的最后一位
high = high/10; // high也往前移动一位,把最后一位剔除,给curr
digit = digit*10; // 每次进位需要*10
}
return res;
}
}
剑指 Offer 44. 数字序列中某一位的数字
数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
解题思路:
- 01234567891011121314151617181920212223242526......
- 所以根据 n 不同,你可以知道之前的构成
- 1 位数 1 - 9,共 9 个,占了 9 个位置
- 2 位数 10 - 99,共 90 个,占了 90 * 2 个位置
- 3 位数 100 - 999,共 900 个,占了 900 * 3 个位置
- 例如 n = 12,取第12位,1位数的取满了,取到了两位数中
- 例如 189 位 就是 99 最后一个 9,189 - 9 = 180,180 > 180 不满足,所以还在两位数的范围内,start 从 10 开始,(180-1)/ 2 = 89 余 1,第 180 位是从 9 位开始算的,10+89 = 99,取第 (n-1)%digit 位
class Solution {
public int findNthDigit(int n) {
int digit = 1;
long start = 1;
long count = 9;
while (n > count){
n -= count;
digit += 1;
start *= 10;
count = 9 * start * digit;
}
long num = start + (n-1)/digit;
return Long.toString(num).charAt((n-1)%digit) - '0';
}
}
剑指 Offer 45. 把数组排成最小的数
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。
解题思路:
- a+b > b+a,交换两者保证小数在前,剩下的就是一个排序
class Solution {
public String minNumber(int[] nums) {
for (int i=0;i<nums.length;i++){
for (int j=i+1;j<nums.length;j++){
if (compare(nums[i]+"",nums[j]+"")){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
}
StringBuffer s = new StringBuffer();
for (int n:nums){
s.append(n+"");
}
return s.toString();
}
public boolean compare(String a,String b){
if (Long.parseLong(a+b) > Long.parseLong(b+a)){
return true;
}
return false;
}
}
剑指 Offer 46. 把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
解题思路:
- 动态规划:取一个还是取两个,取两个要判断在10-25之间,如果超过就说明本次不行,只能取一位;如果在范围内,表示本次可以取两位,也可以取一位,是两种的结果相加
class Solution {
int res = 0;
public int translateNum(int num) {
String s = num + "";
int len = s.length();
int[] dp = new int[len+1];
dp[0] = 1;
dp[1] = 1;
for (int i=2;i<=len;i++){
String temp = s.substring(i-2,i);
System.out.println(temp);
if (temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0){ // ”06“这种不行
dp[i] = dp[i-1] + dp[i-2];
}else{
dp[i] = dp[i-1];
}
}
return dp[len];
}
}
剑指 Offer 47. 礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
解题思路:
- 动态规划:每次向右和向下,所以对于每个格子的上一步可以选择左或者上,只要选择更大的那一方就行了,进行一个累加,最终走到最后一格
class Solution {
public int maxValue(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
for (int i=1;i<n;i++){
dp[i][0] = grid[i][0] + dp[i-1][0];
}
for (int j=1;j<m;j++){
dp[0][j] = grid[0][j] + dp[0][j-1];
}
for (int i=1;i<n;i++){
for (int j=1;j<m;j++){
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[n-1][m-1];
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
解题思路:
- 维护一个左指针,同时记录每个字符出现的位置
- 如果遇到重复的字符,就更新下左指针移动到最新出现字符的位置,但是有可能目前left已经移动到更远的位置,则取更远的
- 比如abcdba,当第二次出现 b,left 已经移动到 第一个出现 b 的位置,当出现第二个 a 的时候不可能 left 反过来移动到 第一次出现 a 的位置,因为中间已经夹了 b ,所以取更远的那个
- 每次移动无论之前有没有出现,都要更新下字符出现时的坐标
- 同时每次都要计算当前位置到左指针的距离 i - left
- 注意,由于类似”ab“,字符,i 最大为 1,实际应该是 2,所以 初始值 left = -1
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s.length() == 0)return 0;
HashMap<Character,Integer> map = new HashMap();
char[] c = s.toCharArray();
int left = -1;
int max_len = 1;
for (int i=0;i<c.length;i++){
if (map.containsKey(c[i])){
left = Math.max(left,map.get(c[i]));
}
map.put(c[i],i);
max_len = Math.max(i-left,max_len);
}
return max_len;
}
}
剑指 Offer 49. 丑数
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
解题思路:
- 动态规划:2 3 5的倍数,每生成一个新的数,它的2 3 5倍也是丑数,丑数只会在这些数中产生,所以不能 2 3 5 不能单纯的乘 1 - n,类似的 7 就不是 2 3 5的倍数,所以不能加入到里面,为了区分,2 3 5 只能去乘之前出现过的数
- 2*1、3*1、5*1,2、3、5 中取 2,a++,2 去乘第二位 2,dp[1] = 2
- 2*2、3*1、5*1,4、3、5 中取 3,b++,3 去乘第二位 2,dp[2] = 3
- 2*2、3*2、5*1,4、6、5 中取 4,a++,2 去乘第三位 3,dp[3] = 4
- 2*3、3*2、5*1,6、6、5 中取 5,c++,5 去乘第二位 2,dp[4] = 5
- 2*3、3*2、5*2,6、6、10 中取 6,dp[5] = 6
- a++,2 去乘第四位 4
- b++,3 去乘第三位 3
class Solution {
public int nthUglyNumber(int n) {
int a = 0, b = 0, c = 0;
int[] dp = new int[n];
dp[0] = 1;
for (int i=1;i<n;i++){
int n1 = 2*dp[a];
int n2 = 3*dp[b];
int n3 = 5*dp[c];
dp[i] = Math.min(Math.min(n1, n2), n3);
if(dp[i] == n1) a++;
if(dp[i] == n2) b++;
if(dp[i] == n3) c++;
}
return dp[n-1];
}
}
剑指 Offer 50. 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
解题思路:
- 用个长度为26的数组代表26个字母,遍历一遍s,记录出现的字母次数,再次遍历一遍数组找出次数等于1的字母,直接返回
class Solution {
public char firstUniqChar(String s) {
int[] res = new int[26];
char[] c = s.toCharArray();
for (char cc : c){
res[cc - 'a']++;
}
for (char cc : c){
if (res[cc - 'a'] == 1){
return cc;
}
}
return ' ';
}
}
剑指 Offer 51. 数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
解题思路:
- 归并排序中加入计算逆序对
- 例如 [5 7],[4 6] 做归并
- 5和4比较,5>4,这就是一组逆序对,由于组内已经是排过序的,所以5之后的数都和4组成逆序对,逆序对的个数就是当前到最后的个数。接着5和6做比较,正常情况。接着7和6做比较,是一组逆序对
- 一方全部取完为止,另一方剩下的自动归入
class Solution {
int count = 0;
public int reversePairs(int[] nums) {
divide(nums,0,nums.length-1);
return count;
}
public void divide(int[] nums,int left,int right){
int mid = left + (right-left)/2;
// 不停拆分,拆到最小开始合并
if (left < right){
divide(nums,left,mid);
divide(nums,mid+1,right);
merge(nums,left,mid,right);
}
}
public void merge (int[] nums,int left,int mid,int right){
// 定义一个用来装合并结果的数组
// 例如 [5 7] [4 6]
// 最终按照顺序合并成 [4 5 6 7]
int[] temp = new int[right-left+1];
// index 是 temp数组的下角标,每次+1
int index = 0;
// temp1 是左边数组 [5 7],left是在nums原数组开始的位置
int temp1 = left;
// temp2 是右边数组 [4 6],mid+1是在nums原数组开始的位置
int temp2 = mid + 1;
// 只要元素没有取完,两个组 每个挨个比较
while (temp1 <= mid && right >= temp2){
// 如果temp1<temp2 正常情况,把小的那个值放入temp数组,++
if (nums[temp1] <= nums[temp2]){
temp[index++] = nums[temp1++];
}else{
// 逆序对
temp[index++] = nums[temp2++];
// 例如 [5 7] [4 6]
// 4小于5 ,必定也小于5之后的数,所以数量是末尾mid-当前位置temp1+1
count += mid-temp1+1;
}
}
// 哪一放先结束,剩下的自动放入数组
while (temp1 <= mid){
temp[index++] = nums[temp1++];
}
while (right >= temp2){
temp[index++] = nums[temp2++];
}
// 把新数组的值返回给老数组,用作下次合并
for (int i=left;i<=right;i++){
nums[i] = temp[i-left];
}
}
}
剑指 Offer 52. 两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
如下面的两个链表:
在节点 c1 开始相交。
解题思路:
- 假设相交
- A = a + c
- B = b + c
- A + b = a + b + c,A走完,走B
- B + a = b + c + a,B走完,走A
- 最终两节点在公共节点相遇
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA;
ListNode p2 = headB;
while (p1 != p2){
p1 = p1 == null ? headB : p1.next;
p2 = p2 == null ? headA : p2.next;
}
return p1;
}
}
剑指 Offer 53 - I. 在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
解题思路:
- 二分法,查找左边界和右边界,左右边界中间的就是重复的数字
- target > nums[mid]:收紧左边界,left = mid + 1
- target < nums[mid]:收紧右边界,right = mid - 1
- target == nums[mid]:
- 查找右边界,终止条件 left 往右移动直到 left > right,所以等于的情况移动 left
- 查找左边界,终止条件 right 往左移动 直到 right < left,所以等于的情况移动 right
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
// 探查右边界、结束条件left<right,这时候left跳出右边界,因为要跳出,所以肯定是不包含相等的情况
while (left <= right){
int mid = (right+left)/2;
if (target >= nums[mid]){
left = mid + 1;
}else{
right = mid - 1;
}
}
int R = left;
// 此时left已经跳出边界,所以right指向值
if(right >= 0 && nums[right] != target) return 0;
left = 0;
right = nums.length-1;
// 探查左边界,结束条件left<right,这时候right跳出左边界
while (left <= right){
int mid = (right+left)/2;
if (target <= nums[mid]){
right = mid - 1;
}else{
left = mid + 1;
}
}
return R-right-1;
}
}
剑指 Offer 53 - II. 0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
解题思路:
- 二分查找:当nums[mid] = mid,表示左边的元素都没有缺失,所以 left = mid + 1,反之有了缺失,right = mid - 1,所以当越界的时候,left 就正好停留在超出正常范围的下一个元素位置,它的位置就是缺失的数字
class Solution {
public int missingNumber(int[] nums) {
int left = 0;
int right = nums.length-1;
while (left <= right){
int mid = (right+left)/2;
if (nums[mid] == mid){
left = mid + 1;
}else{
right = mid - 1;
}
}
return left;
}
}
剑指 Offer 54. 二叉搜索树的第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 res = 0;
int k = 0;
public int kthLargest(TreeNode root, int k) {
// 二叉搜索树,左 < 根 < 右,中序遍历
// 找最大的就是, 左 根 右,中序遍历,左右颠倒
this.k = k;
dfs(root);
return res;
}
public void dfs(TreeNode root){
if (root == null)return;
dfs(root.right);
if (--k == 0){
res = root.val;
return;
}
dfs(root.left);
}
}
剑指 Offer 55 - I. 二叉树的深度
输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。
例如:
给定二叉树 [3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回它的最大深度 3 。
解题思路:
- 层序遍历,获取最大层数
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
int res = 0;
int k = 0;
public int kthLargest(TreeNode root, int k) {
// 二叉搜索树,左 < 根 < 右,中序遍历
// 找最大的就是, 左 根 右,中序遍历,左右颠倒
this.k = k;
dfs(root);
return res;
}
public void dfs(TreeNode root){
if (root == null)return;
dfs(root.right);
if (--k == 0){
res = root.val;
return;
}
dfs(root.left);
}
}
剑指 Offer 55 - II. 平衡二叉树
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
解题思路:
- 计算每个节点的左右子树的深度,恰巧是后序遍历,左右根
- 如果遇到null,则返回0,每个节点取左右树最深的那颗树
- 如果左树和右数深度差值超过1,那就返回-1,然后-1都向上返回,最终判断-1即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null)return true;
return dfs(root) != -1;
}
public int dfs(TreeNode root){
if (root == null)return 0;
int left = dfs(root.left);
if (left == -1)return -1;
int right = dfs(root.right);
if (right == -1)return -1;
return Math.abs(left-right) < 2 ? Math.max(left,right) + 1 : -1;
}
}
剑指 Offer 56 - I. 数组中数字出现的次数
一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
解题思路:
- 异或思想:相同数异或结果为0
- 一组数中只有两个数不同,把它们全部异或一遍,剩下的结果 z = x^y,且 x 和 y 不同
- 既然 x 和 y 不同,那它们二进制的位数肯定有一个不一样,利用这一点,找出第一个不一样的点。用 m & z 处理,m 不停的左移1,最终得到第一个为 1 的点,也就是不一样的点。利用这个 m 去和原数组再进行 & 处理,也就可以把 x 和 y 分成了两组,当然组里还有其他数,这时候在分组后的组里再进行异或处理,就可以挑出 x 和 y
class Solution {
public int[] singleNumbers(int[] nums) {
// 异或的思想,相同数异或为0,所以不同的两个数异或一定不为0
// z = x^y
int z = 0;
int x = 0;
int y = 0;
for (int num : nums){
z^=num;
}
int m = 1;
// m 就是为了来区分两个数的
while ((z & m) == 0){
m <<= 1;
}
for (int num : nums){
if ((num & m) == 0){
x ^= num;
}else{
y ^= num;
}
}
return new int[]{x,y};
}
}
剑指 Offer 56 - II. 数组中数字出现的次数 II
在一个数组 nums
中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
解题思路:
- 1、1、1、2
- 1、1、1、2、3、3、3
- 1、2、2、2
- 就这三种情况,可以从第三个数开始,往前倒推两个数,看是否相等,如果不相等,则倒推的数就是答案。相等则往后移动三位。这里注意第一种情况,往后移动可能越界了,如果最终没有发现单个数,则表示最后一位就是答案
class Solution {
public int singleNumber(int[] nums) {
Arrays.sort(nums);
int res = Integer.MAX_VALUE;
if (nums.length == 1)return nums[0];
for (int i=2;i<nums.length;i++){
if (nums[i] == nums[i-2]){
i+=2;
}else{
return nums[i-2];
}
}
return res == Integer.MAX_VALUE ? nums[nums.length-1] : res;
}
}
剑指 Offer 57. 和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
解题思路:
- 这道题有个明确的条件,递增,所以可以用双指针的方式
- 先从右边过滤到所有大于target 的数
- 将左右两数相加
- 如果大于target,因为左端 left 已经最小了,只能缩小右端值 right--
- 如果小于target,因为右端 right 已经最大了,只能扩大左端值 left++
class Solution {
public int[] twoSum(int[] nums, int target) {
// 递增排序,左右指针
int left = 0;
int right = nums.length - 1;
while (nums[right] > target){
right--;
}
while (left < right){
int sum = nums[left] + nums[right];
if (sum > target){
right--;
}else if (sum < target){
left++;
}else if (sum == target){
return new int[]{nums[left],nums[right]};
}
}
return new int[0];
}
}
剑指 Offer 57 - II. 和为s的连续正数序列
输入一个正整数 target
,输出所有和为 target
的连续正整数序列(至少含有两个数)。
序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。
解题思路:
- 累加,如果 sum == target ,即左边界开始到右边界之前的连续数组就是答案
- 如果 sum > target,说明当前值过大,缩小左边界,sum对应的要减去值
- 如果 sum < target,说明当前值过小,增大右边界,sum对应要增加值
class Solution {
public int[][] findContinuousSequence(int target) {
List<int[]> list = new ArrayList();
int i = 1;
int j = 1;
int sum = 0;
while (i <= target/2){
if (sum < target){
sum += j;
j++;
}else if (sum > target){
sum -= i;
i++;
}else if (sum == target){
int[] r = new int[j-i];
for (int x=i;x<j;x++){
r[x-i] = x;
}
list.add(r);
// 左边界向右移动,sum变小后再移动
// 右边界向右移动,sun先变大后再移动
sum += j;
j++;
}
}
return list.toArray(new int[list.size()][]);
}
}
剑指 Offer 58 - I. 翻转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. ",则输出"student. a am I"。
解题思路:
- 分割后,反过来取,注意 pass 空格
class Solution {
public String reverseWords(String s) {
String[] res = s.split(" ");
StringBuffer str = new StringBuffer();
for (int i=res.length-1;i>=0;i--){
if (!res[i].trim().equals("")){
str.append(res[i] + " ");
}
}
return str.toString().trim();
}
}
剑指 Offer 58 - II. 左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
解题思路:
- 运用 substring ( i , j ),从 i 开始到 n 之前
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n,s.length()) + s.substring(0,n);
}
}
剑指 Offer 59 - I. 滑动窗口的最大值
给定一个数组 nums
和滑动窗口的大小 k
,请找出所有滑动窗口里的最大值。
解题思路:
- 维护一个双端队列,里面的值分布规律从大到小
- 初始化第一个窗口,队列中挨个塞入值,循环判断后进入的值如果比队列中的值要大则要弹出,要小就直接放入。
- 例如: 1、 3、 1,第一个窗口,先放入 1 ,放入 3 时,由于1比3小,将1弹出,3进入,最后一个1判断比3小,跟在3的后面,这时候队列:3 、1,队列头部是该区间的最大值
- 滑动的时候,判断出去的值是不是队首元素,是则弹出,不是则不用弹出。同时插入的值也要向前面一样操作。例如 1、3、1、5,窗口为3,1滑出,5滑入。由于之前窗口最大为3,所以不用操作,新加入的5,把之前的1、3都弹出了,所以队列只剩下5且为最大值
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int count = n-k+1; // 可以形成窗口数量
int[] res = new int[count];
Deque<Integer> deque = new LinkedList(); // 双端队列
int index = 0;
// 形成最开始的窗口
for (int i=0;i<k;i++){
if (deque.isEmpty()){
deque.addFirst(nums[i]);
}else{
while (!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.pollLast();
}
deque.addLast(nums[i]);
}
}
// 队列中只包含窗口中从大到小的数,队首就是最大值
res[index++] = deque.peekFirst();
// 移动窗口
for (int i=k;i<n;i++){
// 移除最开始的数字,如果最开始的数字就是队首,也就是最大值,就一起弹出
if (nums[i-k] == deque.peekFirst()){
deque.pollFirst();
}
// 将新的值加入
while (!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.pollLast();
}
deque.addLast(nums[i]);
res[index++] = deque.peekFirst();
}
return res;
}
}
剑指 Offer 59 - II. 队列的最大值
请定义一个队列并实现函数 max_value
得到队列里的最大值,要求函数max_value
、push_back
和 pop_front
的均摊时间复杂度都是O(1)。
若队列为空,pop_front
和 max_value
需要返回 -1
解题思路:
- 维护一个双端队列,用来存放最大值。另一个队列正常使用
- 当插入值的时候,循环判断双端队列从后往前,把之前小的值都顶出
- 例如: 插入 1 1 1 2
- 当前面的1插入时候,双端队列全是1,就算取出一个1,最大值还是1。但是当插入2的时候,最大值都是2,取出一个1,结果还是2,所以干脆就把前面的1全部顶出,保留个2,当需要最大值的时候,直接返回队列顶端2
- 例如:插入 3 1 1 2
- 插入2的时候,会把前面的1都顶掉,双端队列剩余 3 2,当3移出的时候,最大值就是2了
- 当弹出值的时候,判断是不是双端队列队首元素,是的话一起弹出
class MaxQueue {
Queue<Integer> queue1;
Deque<Integer> queue2;
public MaxQueue() {
queue1 = new LinkedList();
queue2 = new LinkedList();
}
public int max_value() {
if (queue1.isEmpty())return -1;
return queue2.peekFirst();
}
public void push_back(int value) {
while (!queue2.isEmpty() && queue2.peekLast() < value){
queue2.pollLast();
}
queue1.offer(value);
queue2.offerLast(value);
}
public int pop_front() {
if (queue1.isEmpty())return -1;
int ans = queue1.poll();
if (ans == queue2.peekFirst())queue2.pollFirst();
return ans;
}
}
剑指 Offer 60. n个骰子的点数
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
解题思路:
- 动态规划
class Solution {
public double[] dicesProbability(int n) {
double[] dp = new double[6];
Arrays.fill(dp, 1.0 / 6.0);
// n = 1 1/6 1/6 1/6 1/6 1/6 1/6
// n = 2 1/6*1/6 1/6*1/6+1/6*1/6 1/6*1/6+1/6*1/6+1/6*1/6 1/6*1/6+1/6*1/6+1/6*1/6+1/6*1/6 .....
// 代表n个筛子
for (int i=2;i<=n;i++){
// 表示i个筛子时,总共有 i ~ 5*i+1
double[] temp = new double[5*i+1];
// 每个元素都得赋值
for (int j=0;j<temp.length-dp.length+1;j++){
for (int k=0;k<dp.length;k++){
temp[j+k] += dp[k]/6;
}
}
dp = temp;
}
return dp;
}
}
剑指 Offer 61. 扑克牌中的顺子
从若干副扑克牌中随机抽 5
张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
解题思路:
- 看 0 的个数,也就是万能牌的个数
- 从第一个不为0数字开始,如果和后面的数有间隔就拿0去填充,填充完的0就被消耗掉了,如果填不满则表示不构成顺子
- 这道题还可以直接取最大值和最小值,它们的差值小于5就是顺子,前提无重复
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int zero = 0;
int i = 0;
for (;i<nums.length;i++){
if (nums[i] == 0)zero++;
else break;
}
i++;
for (;i<nums.length;i++){
if (nums[i-1] == nums[i])return false;
if (nums[i-1]+1 != nums[i]){
if (nums[i]-nums[i-1]-1 <= zero){
zero -= nums[i]- nums[i-1] - 1;
}else{
return false;
}
}
}
return true;
}
}
剑指 Offer 62. 圆圈中最后剩下的数字
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
解题思路:
- 假设第n次,最终留下得数字得位置在 (m-1)%n
- 例如 0 1 2 3 4 ,n = 5,m = 3
- 第一次删除 (m-1)%n = 2
- 第n-1次,由于n次的时候删除了一个,下一位计算从m%n开始,删除第 m 个数
- 第二次原来的顺序变成 3 4 0 1,删除第 m 个
- 第三次顺序变成 1 3 4,删除第m个
- 第四次顺序变成 1 3,删除第m个
- 第五次只剩下 3
- n = 1,长度为1,只有0,剩下的就是0
- 假设 x 是最终留下的值,例子中就是 3 ,没往下走一步,3 往前位移了 m 步,也就是从后往前推,不断-m。从前往后推,不断 + m。x 表示一开始长度为1的时候,就只剩下一个就是0,长度为2开始直到n每次往前推m,因为怕超过最大长度,所以取模
class Solution {
public int lastRemaining(int n, int m) {
int x = 0;
for (int i=2;i<=n;i++){
x = (x + m) % i;
}
return x;
}
}
剑指 Offer 63. 股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
解题思路:
- 动态规划
- 每天都有两种选择,持有或者不持有
- 不持有:保持和昨天一样的金额或者昨天持有今天卖掉了
- 持有:保持和昨天一样的金额或者之前从来没有持有,今天持有了
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0)return 0;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i=1;i<n;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1] + prices[i]); // 可以是维持上一次,也可以是上一次拥有这次卖掉了
dp[i][1] = Math.max(dp[i-1][1],-prices[i]); // 可以是维持上一次,也可以是从来每买过,这次购买
}
return dp[n-1][0];
}
}
剑指 Offer 64. 求1+2+…+n
求 1+2+...+n
,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
解题思路:
- 求和,想到的就是累加,但是不能写for循环,只能用递归处理,但终止条件不能有 if
- 首先直到递归终止条件 n = 1,如何终止递归?
- 利用 && 的特性,当前一个判断条件为false的时候,后面直接不执行,利用这一点
- n > 1 作为前置条件,满足才执行后面,n = 1的时候,为fasle,自动终止后面
- 由于光写条件判断不成立,需要随便拿个boolean 变量接收一下
class Solution {
int res = 0;
public int sumNums(int n) {
boolean x = (n > 1) && (sumNums(n-1) > 0);
res += n;
return res;
}
}
剑指 Offer 65. 不用加减乘除做加法
写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。
解题思路:
- n = a ^ b (无进位和)
- c = a & b (进位和)
- z = a + b = n + c (无进位+进位和)
- c 代表的就是进位符号,不为0就说明还需要进位,所以当没有进位的时候a^b就是答案,当有进位的时候就要不停的和进位相加,相加后的结果可能还要进位所以有这样的一个循环,直到加到没有进位为止
class Solution {
public int add(int a, int b) {
while (b!=0){
int c = (a&b)<<1;
a^=b;
b = c;
}
return a;
}
}
剑指 Offer 66. 构建乘积数组
给定一个数组 A[0,1,…,n-1]
,请构建一个数组 B[0,1,…,n-1]
,其中 B[i]
的值是数组 A
中除了下标 i
以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]
。不能使用除法。
解题思路:
- 每个都是左边的乘积 x 右边的乘积
- 所以两遍循环,分别计算该值左边的乘积,再计算该值右边的乘积,最后相乘
class Solution {
public int[] constructArr(int[] a) {
int n = a.length;
if (n == 0)return new int[0];
int[] dp = new int[n];
dp[0] = 1;
int temp = 1;
// 左乘
for (int i=1;i<n;i++){
temp *= a[i-1];
dp[i] = temp;
}
temp = 1;
// 右乘
for (int i=n-2;i>=0;i--){
temp *= a[i+1];
dp[i] *= temp;
}
return dp;
}
}
剑指 Offer 67. 把字符串转换成整数
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
解题思路:
- 先去除首尾空格
- 转换成数组挨个取
- 先判断第一位有无符号,如果是负号,需要记录一下,正负号最后返回用,其他除了正号外,默认都是数字字母
- 开始循环判断,如果不在字符0~9范围内说明遇到非数字了,直接返回之前的计算结果
- 正确的连续数字字符处理,转换成整数就是 res = res*10 + (c[ j ] - '0')
- 这里判断一下,res 有没有超出数值范围,有两种情况:
- 因为下面的计算res需要*10,而res又不能超过整数范围,也就是没*10之前的res不能超过214748364,不然*10后最少是2147483650 > 2147483647,就得根据正负号输出MAX or MIN
- 其次如果正巧等于214748364,就要看最后一位有没有超过7,一样得道理,超过就出界了,就得是MAX or MIN
class Solution {
public int strToInt(String str) {
int sign = 1;
char[] c = str.trim().toCharArray();
if(c.length == 0) return 0;
int i = 1; // 如果第一位是符号位,后面数字从1开始
if (c[0] == '-')sign=-1;
else if (c[0] != '+')i=0; // 如果第一位不是符号位,i从0开始
int res = 0; // 返回值
int bndry = Integer.MAX_VALUE/10;
for (int j=i;j<c.length;j++){
if (c[j] < '0' || c[j] > '9')break; //后面的就不是数字
// res有范围大小,不能超过MAX, 而下面式子res = res*10 + ?,所以res不能大于max/10
// res == bndry && chars[j] > '7' 的意思是,当res == bndry时,即:214748364
// 此时res * 10 变成 2147483640 此时没越界,但是还需要 + chars[j],
// 而int最大值为 2147483647,所以当chars[j] > 7 时会越界
if(res > bndry || res == bndry && c[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
res = res*10 + (c[j]-'0');
}
return sign*res;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
解题思路:
- 二叉搜索树,左右节点大小有规律
- 如果 p q 一个大于root,一个小于root,说明在异侧,root为父节点
- 如果 p q 都小于root,说明都在左侧,递归左侧再判断
- 如果 p q 都大于root,说明都在右侧,递归右侧再判断
- 如果在同侧,先遇到谁,谁就是父节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 二叉搜索树,左右节点大小有规律
// 如果 p q 一个大于root,一个小于root,说明在异侧,root为父节点
// 如果 p q 都小于root,说明都在左侧,递归左侧再判断
// 如果 p q 都大于root,说明都在右侧,递归右侧再判断
if (p.val == root.val)return p;
if (q.val == root.val)return q;
if (p.val > root.val && q.val > root.val){
return lowestCommonAncestor(root.right,p,q);
}
if (p.val < root.val && q.val < root.val){
return lowestCommonAncestor(root.left,p,q);
}
return root;
}
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
解题思路:
- 不是二叉搜索树,所以没有规律
- 向下寻找左子树和右子树,遇到p、q就直接向上返回
- 合并得时候,如果两侧都没找到p、q就返回null;如果一侧找到了就返回有值得那一侧;如果两侧都有值说明该合并得节点就是第一个公共父节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null)return null;
if (root.val == p.val)return p;
if (root.val == q.val)return q;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if (left == null && right == null)return null;
if (left == null)return right;
if (right == null)return left;
return root;
}
}