剑指 Offer 09. 用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
解析:先初始化两个栈,appendTail方法负责添加元素到栈1;调用deleteHead方法时,先把栈1的值转移到栈2中,然后取栈2 栈顶值出栈返回,就是结果,注意记录后还需把栈2的值转移回栈1中去。
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void appendTail(int value) {
stack1.push(value); //添加元素
}
public int deleteHead() {
//当队列中没有元素,返回-1
if(stack1.isEmpty() && stack2.isEmpty()) return -1;
//将栈1中的元素转移到栈2中
while(!stack1.isEmpty()){
int temp = stack1.pop();
stack2.push(temp);
}
//记录要删除的值
int result = stack2.pop();
//把栈2的值再转移到栈1中
while(!stack2.isEmpty()){
int temp = stack2.pop();
stack1.push(temp);
}
return result;
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
解法二:优化上面解法
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
public void appendTail(int value) {
stack1.push(value); //添加元素
}
public int deleteHead() {
//当队列中没有元素,返回-1
if(stack1.isEmpty() && stack2.isEmpty()) return -1;
//如果栈2不为空,则可直接返回;否则,需要将栈1的元素转移到栈2中;
if(!stack2.isEmpty()){
return stack2.pop();
}else {
while(!stack1.isEmpty()){
int temp = stack1.pop();
stack2.push(temp);
}
}
return stack2.pop();
}
}
/**
* Your CQueue object will be instantiated and called as such:
* CQueue obj = new CQueue();
* obj.appendTail(value);
* int param_2 = obj.deleteHead();
*/
剑指 Offer 30. 包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
解析:借助辅助栈来存放递减元素
class MinStack {
private Stack<Integer> stack;
private Stack<Integer> minStack;
/** initialize your data structure here. */
public MinStack() {
stack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
stack.push(x);
//如果最小栈为空 或 最小栈的栈顶元素大于等于x,就添加元素
if(minStack.isEmpty() || minStack.peek() >= x){
minStack.push(x);
}
}
public void pop() {
//主栈出栈,然后比较,如果主栈出栈的元素 等于 最小栈的栈顶元素,那么最小栈也要出栈
if(stack.pop().equals(minStack.peek())){
minStack.pop();
}
}
public int top() {
return stack.peek();
}
public int min() {
return minStack.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 06. 从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
解法一:遍历链表时,用list存起来,然后反转list,再转为int数组返回。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
List<Integer> list = new ArrayList<>();
while(head != null){
list.add(head.val); //把元素添加进list
head = head.next; //下一个
}
//反转集合
Collections.reverse(list);
//list转int数组
int[] arr = list.stream().mapToInt(Integer::valueOf).toArray();
return arr;
}
}
解法二:先计算出链表长度,然后定义数组,从尾到头开始赋值。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
int len = 0;
ListNode temp = head;
//统计链表长度
while (temp != null) {
temp = temp.next;
len++;
}
int[] ans = new int[len];
//从尾到头 开始赋值
for (int i = len - 1; i >= 0; i--) {
ans[i] = head.val;
head = head.next;
}
return ans;
}
}
剑指 Offer 24. 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
解析:双指针法
/**
* 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 pre = null; //前一个节点
ListNode cur = head; //当前节点
ListNode temp = null; //临时节点
while(cur != null){
temp = cur.next; //保存cur的指向
cur.next = pre; //反转链表
pre = cur; //移动
cur = temp; //移动
}
return pre;
}
}
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
解析:使用哈希map
来构建从旧节点到新节点的映射关系,再重新构建指向。
/*
// 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 {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;
Map<Node,Node> map = new HashMap<>();
//构建新旧节点的映射关系
while(cur != null){
map.put(cur, new Node(cur.val)); //构建键值对
cur = cur.next; //移动
}
//重置
cur = head;
//构建新链表的 next 和 random 指向
while(cur != null){
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
剑指 Offer 05. 替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
解法一:先扩容空间,空格数量的两倍,再定义左指针:指向原始字符串最后一个位置,右指针:指向扩展字符串的最后一个位置,进行判断赋值操作。
class Solution {
public String replaceSpace(String s) {
if(s == null || s.length() == 0){
return s;
}
//扩充空间,空格数量2倍
StringBuilder str = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ' '){
str.append(" ");
}
}
//若是没有空格直接返回
if(str.length() == 0){
return s;
}
//有空格情况 定义两个指针
int left = s.length() - 1;//左指针:指向原始字符串最后一个位置
s += str.toString();
int right = s.length()-1;//右指针:指向扩展字符串的最后一个位置
char[] chars = s.toCharArray();
while(left>=0){
if(chars[left] == ' '){
chars[right--] = '0';
chars[right--] = '2';
chars[right] = '%';
}else{
chars[right] = chars[left];
}
left--;
right--;
}
return new String(chars);
}
}
*解法二:
class Solution {
public String replaceSpace(String s) {
if(s == null || s.length() == 0){
return s;
}
StringBuilder sb = new StringBuilder();
for(int i=0; i<s.length(); i++){
//如果是空格,就追加 %20 ;否则就追加原来的字符。
if(s.charAt(i) == ' '){
sb.append("%20") ;
}else{
sb.append(s.charAt(i));
}
}
return new String(sb);
}
}
剑指 Offer 58 - II. 左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
解法一:将字符串分成两部分,然后各自反转,最后再整个反转即可。如abcde,k=2,分别反转得ba edc ,再整体反转得 cdeab。
class Solution {
public String reverseLeftWords(String s, int n) {
int len=s.length();
StringBuilder sb=new StringBuilder(s);
reverseString(sb,0,n-1);
reverseString(sb,n,len-1);
return sb.reverse().toString();
}
//反转字符串
public void reverseString(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--;
}
}
}
解法二:创建一个StringBuilder,先添加后半部分字符串,再添加前半部分字符串。
class Solution {
public String reverseLeftWords(String s, int n) {
if(s == null) return null;
StringBuilder sb = new StringBuilder();
//字符串转字符数组
char[] ch = s.toCharArray();
//先添加后半部分
for(int i=n; i<s.length(); i++){
sb.append(ch[i]);
}
//再添加前半部分
for(int i=0; i<n; i++){
sb.append(ch[i]);
}
return new String(sb);
}
}
剑指 Offer 03. 数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
解析:使用map存储元素,然后查找。
class Solution {
public int findRepeatNumber(int[] nums) {
if(nums.length < 0) return 0;
Map<Integer, Integer> map = new HashMap<>();
for(int i=0; i<nums.length; i++){
//如果map里有num[i],说明重复了,直接返回
if(map.containsKey(nums[i])){
return nums[i];
}else{
map.put(nums[i],1);
}
}
return 0;
}
}
剑指 Offer 53 - I. 在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
解法一:使用二分查找,对比中间值,找到后分别统计左右两边与目标值相等的次数。
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if(len < 0) return 0;
int left = 0;
int right = len-1;
int count = 1;
while(left <= right){ //二分查找
int mid = (left + right) //中间值
if(target > nums[mid]){ //如果目标值 大于 当前中间值,移动左边界
left = mid+1;
}else if(target < nums[mid]){ //如果目标值 小于 当前中间值,移动右边界
right = mid-1;
}else{ //相等
int l=mid-1,r=mid+1;
while(r < len){ //统计右边
if(target == nums[r]){
count++;
}
r++;
}
while(l>=0){ //统计左边
if(target == nums[l]){
count++;
}
l--;
}
return count;
}
}
return 0;
}
}
解法二:二分查找
二分查找模板
class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if(len < 0) return 0;
//左边界
int left = searchLeft(nums,target);
//右边界
int right = searchRight(nums,target);
//判断是否合法
if(left<=right && right<len && nums[left] == target && nums[right]== target){
return right-left+1;
}
return 0;
}
//找左边界的二分查找
public int searchLeft(int[] nums, int target){
int left = -1, right = nums.length;
while(left+1 != right){
int mid = (left+right)/2;
if(nums[mid] >= target){
right = mid;
}else{
left = mid;
}
}
return right;
}
//找右边界的二分查找
public int searchRight(int[] nums, int target){
int left = -1, right = nums.length;
while(left+1 != right){
int mid = (left+right)/2;
if(nums[mid] <= target){
left = mid;
}else{
right = mid;
}
}
return left;
}
}
剑指 Offer 53 - II. 0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
解析:如果当前元素与当前元素的下标不符合时,说明缺失了数字,返回当前元素-1后的值
class Solution {
public int missingNumber(int[] nums) {
int len = nums.length;
if(len < 0) return 0;
for(int i=0; i<len; i++){
//如果缺失0,则直接返回0
if(nums[0] != 0){
return 0;
}
//如果当前元素与当前元素的下标不符合时,说明缺失了数字,返回当前元素-1后的值
if(nums[i] != i){
return nums[i]-1;
}
}
return nums[len-1]+1;
}
}
剑指 Offer 04. 二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
解法一:暴力查找,两重循环遍历
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix == null || matrix.length == 0) return false;
int x = matrix.length; //列
int y = matrix[0].length; //行
for(int i=0; i<x; i++){
for(int j=0; j<y; j++){
//找到相等的整数,返回true
if(matrix[i][j] == target){
return true;
}
}
}
return false;
}
}
解法二:从二维数组的右上角开始查找。如果当前元素等于目标值,则返回 true。如果当前元素大于目标值,则移到左边一列。如果当前元素小于目标值,则移到下边一行。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) return false;
int rows = matrix.length; //行
int columns = matrix[0].length; //列
int row = 0, column = columns-1;
while(row < rows && column >= 0){
int num = matrix[row][column]; //当前元素
if(num == target){ //相等直接返回
return true;
}else if(num > target){ //如果当前元素大于目标元素,则向左查找
column--;
}else{ //如果当前元素小于目标元素,则向下查找
row++;
}
}
return false;
}
}
剑指 Offer 11. 旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。
给你一个可能存在 重复 元素值的数组 numbers ,它原来是一个升序排列的数组,并按上述情形进行了一次旋转。请返回旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一次旋转,该数组的最小值为1。
解析:从后往前遍历,当发现当前元素小于上一个元素时,就返回当前元素
class Solution {
public int minArray(int[] numbers) {
int len = numbers.length;
if(len <= 0) return 0;
if(len == 1) return numbers[0];
for(int i=len-1; i>0; i--){
//从后往前遍历,当发现当前元素小于上一个元素时,就返回当前元素
if(numbers[i] < numbers[i-1]){
return numbers[i];
}
}
//已经是排好序时,返回第一个元素
return numbers[0];
}
}
剑指 Offer 50. 第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
解析:题目说明都是小写字母,可以用数组来统计次数,再按字符串的字符顺序查找出现一次的字符返回。
class Solution {
public char firstUniqChar(String s) {
if("".equals(s)) return ' ';
int[] arr = new int[26];
char[] ch = s.toCharArray();
for(char c : ch){ //统计字符出现的次数
int index = c - 'a';
arr[index] += 1;
}
for(char c : ch){ //按字符串的字符顺序查找出现一次的字符
int index = c - 'a';
if(arr[index] == 1){
return c;
}
}
return ' ';
}
}
剑指 Offer 32 - I. 从上到下打印二叉树
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
解析:层序遍历
/**
* 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) {
int[] arr = {};
if(root == null) return new int[0];
List<Integer> list = new ArrayList<>();
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//遍历
while(!queue.isEmpty()){
int len = queue.size();
while(len > 0){
//获取元素
TreeNode node = queue.poll();
//添加元素的值
list.add(node.val);
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
len--;
}
}
//list转int数组
int[] res = new int[list.size()];
for(int i = 0; i < list.size(); i++)
res[i] = list.get(i);
return res;
}
}
剑指 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> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
//保存每一层的结果
List<Integer> list = new ArrayList<>();
int len = que.size();
while(len > 0){
TreeNode node = que.poll();
list.add(node.val);
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
len--;
}
//添加每一层
res.add(list);
}
return res;
}
}
剑指 Offer 32 - III. 从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
解析:层序遍历时,统计层数,如果当前是奇数层,就直接添加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 List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null) return res;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int num = 1; //记录层数
while(!que.isEmpty()){
//保存每一层的结果
List<Integer> list = new ArrayList<>();
int len = que.size();
while(len > 0){
TreeNode node = que.poll();
list.add(node.val);
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
len--;
}
//添加每一层
if(num % 2 != 0){
//奇数行直接添加
res.add(list);
}else{
//如果是偶数行,先反转后再添加
Collections.reverse(list);
res.add(list);
}
num++;//层数加1
}
return res;
}
}
解析:递归
/**
* 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;
//在A树中查找与B树一样的子结构
//1.直接考虑A和B是否是一样的
if(isSame(A, B)){
//如果直接在根节点处,就找到A和B一样的子结构,那直接返回true
return true;
}
//2.考虑A的左子树是否和B一样,或者A的右子树是否和B一样
return isSubStructure(A.left, B) || isSubStructure(A.right, B);
}
public boolean isSame(TreeNode t1, TreeNode t2){
//假如t1和t2是两棵树的根节点
//当t2为空时,说明已经查找完子树t2了,返回true
if(t2==null) return true;
//当t2不为空,t1为空时,说明t1不含有t2子树,返回false
if(t1==null) return false;
//当子树的节点不相同,说明不相等,返回false
if(t1.val!=t2.val) return false;
//否则,继续往下比较,注意这里是 &&
return isSame(t1.left, t2.left) && isSame(t1.right, t2.right);
}
}
剑指 Offer 27. 二叉树的镜像
解析:后序遍历-递归,遍历左右节点后,反转左右节点。
/**
* 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;
//后序遍历
mirrorTree(root.left); //左
mirrorTree(root.right); //右
reverseTree(root); //根 反转
return root;
}
//反转左右节点
private void reverseTree(TreeNode root){
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
}
}
剑指 Offer 28. 对称的二叉树
解析:递归分别比较里外两侧是否相等。
/**
* 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 compare(root.left, root.right);
}
private boolean compare(TreeNode left, TreeNode right) {
if (left == null && right != null) {
return false;
}
if (left != null && right == null) {
return false;
}
if (left == null && right == null) {
return true;
}
if (left.val != right.val) {
return false;
}
// 比较外侧
boolean compareOutside = compare(left.left, right.right);
// 比较内侧
boolean compareInside = compare(left.right, right.left);
return compareOutside && compareInside;
}
}
解析:动态规划,构建dp数组,初始化然后推导出递推表达式。
class Solution {
public int fib(int n) {
if(n <= 1) return n;
int[] dp = new int[n+1]; //dp数组
dp[0] = 0; //初始化
dp[1] = 1;
for(int i=2; i<=n; i++){ //注意这里是 <=n
//递推表达式
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
}
return dp[n];
}
}
剑指 Offer 10- II. 青蛙跳台阶问题
解析:动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2);依次类推。
class Solution {
public int numWays(int n) {
if(n <= 1) return 1;
if(n == 2) return 2;
int sum = 0;
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
//动态规划,当n = 3时,f(3)=f(2)+f(1) 当n = 4时,f(4) = f(3) +f(2)
for(int i=3; i<=n; i++){
dp[i] = (dp[i-1] + dp[i-2]) % 1000000007;
}
return dp[n];
}
}
剑指 Offer 63. 股票的最大利润
解析:动态规划,动态取买入最小值和卖出最大值
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
if(len <= 1) return 0;
int minCost = Integer.MAX_VALUE;
int max = 0;
for(int price : prices){
minCost = Math.min(minCost, price); //取买入最小价格
max = Math.max(max, price-minCost); //取卖出最大值
}
return max;
}
}
剑指 Offer 42. 连续子数组的最大和
解析:
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
//dp[i]表示以元素 nums[i] 为结尾的连续子数组最大和。
int[] dp = new int[len+1];
dp[0] = nums[0];
//存储较大的 子数组最大和
int res = dp[0];
for(int i=1; i<=len-1; i++){
dp[i] = Math.max(nums[i], dp[i-1] + nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
}
解析:dp(i,j) 代表从棋盘的左上角开始,到达单元格 (i,j)(i,j) 时能拿到礼物的最大累计价值。
class Solution {
public int maxValue(int[][] grid) {
int rows = grid.length; //行
int cols = grid[0].length; //列
int[][] dp = new int[rows][cols];
dp[0][0] = grid[0][0];
//行赋值
for(int i=1; i<rows; i++){
dp[i][0] = dp[i-1][0] + grid[i][0];
}
//列赋值
for(int j=1; j<cols; j++){
dp[0][j] = dp[0][j-1] + grid[0][j];
}
//从上到下,从左到右的顺序
for(int i=1; i<rows; i++){
for(int j=1; j<cols; j++){
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[rows-1][cols-1];
}
}
剑指 Offer 46. 把数字翻译成字符串
解析:先将数字转为字符串,然后定义dp[i]数组,表示前i个数字一共有多少种不同的翻译方法。
参考:把数字翻译成字符串 | 图解DP | 最清晰易懂的题解
class Solution {
public int translateNum(int num) {
//将数字转为字符串
String s = String.valueOf(num);
int n = s.length();
//dp[i]表示 前i个数字共有多少种不同的翻译
int[] dp = new int[n+1];
dp[0] = 1;
for(int i=1; i<=n; i++){
//单独翻译s[i]
dp[i] = dp[i-1];
if(i > 1){
int temp = (s.charAt(i-2) - '0')*10 + (s.charAt(i-1) - '0');
if(temp >= 10 && temp <= 25){
dp[i] = dp[i] + dp[i-2]; //组合翻译
}
}
}
return dp[n];
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
解法一:滑动窗口
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s == null || s.length()<=0) return 0;
int leftIndex = -1; //定义左边界,注意这里是-1
int res = 0;
Map<Character, Integer> map = new HashMap<>();
char[] ch = s.toCharArray();
for(int i=0; i<s.length(); i++){
if(map.containsKey(ch[i])){ //当前数字 在之前遍历时出现过,就更新左边界
leftIndex = Math.max(leftIndex, map.get(ch[i]));
}
map.put(ch[i], i); //添加元素
res = Math.max(res, i-leftIndex); //计算当前最大长度
}
return res;
}
}
解法二:动态规划+哈希表
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
dic.put(s.charAt(j), j); // 更新哈希表
// dp[j - 1] -> dp[j]
tmp = tmp < j - i ? tmp + 1 : j - i;
// max(dp[j - 1], dp[j])
res = Math.max(res, tmp);
}
return res;
}
}
剑指 Offer 18. 删除链表的节点
解析:设置一个虚拟头结点再进行删除操作。
/**
* 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 dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
while(cur.next != null){
if(cur.next.val == val){ //如果相等,就跳到下一个,就是删除
cur.next = cur.next.next;
}else{
cur = cur.next; //移动到下一个
}
}
return dummy.next;
}
}
剑指 Offer 22. 链表中倒数第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; //慢指针
for(int i=0; i<k; i++){
fast = fast.next; //先让快指针移动k个节点
}
while(fast != null){ //再同时移动,最后返回慢指针即可
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
解析:双指针,比较两个链表的节点值,哪个小就把新链表指向它,最后把剩下的直接拼接到新链表末尾。
/**
* 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) {
if(l1 == null) return l2;
if(l2 == null) return l1;
ListNode dummy = new ListNode(-1); //创建虚拟头节点
ListNode res = dummy;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){ //如果l1的值小于等于l2的值,就添加l1
res.next = l1; //赋值
l1 = l1.next; //添加元素后的链接移动
}else{ //如果l1的值大于l2的值,就添加l2
res.next = l2;
l2 = l2.next;
}
res = res.next; //新链表移动
}
//剩下的直接拼接到新链表后面
if(l1 == null) res.next = l2;
if(l2 == null) res.next = l1;
return dummy.next;
}
}
剑指 Offer 52. 两个链表的第一个公共节点
解析:使用双指针,先计算出两个链表的长度,然后让长的链表先移动“差值”,然后再一起移动,一起移动时碰到两个节点相同
就返回,否则就一起移动到下一个节点。
注意:是
节点
相等,而不是值相等。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null && headB == null) return null;
ListNode curr1 = headA;
ListNode curr2 = headB;
//算出两个链表的长度
int len1=0, len2=0;
while(curr1 != null){
++len1;
curr1 = curr1.next;
}
while(curr2 != null){
++len2;
curr2 = curr2.next;
}
//重置头指针
curr1 = headA;
curr2 = headB;
//如果链表1比链表2长
if(len1 > len2){
//链表1先移动
for(int i=0; i<(len1-len2); i++){
curr1 = curr1.next;
}
//再同时移动,如果发现结点相等,就返回
for(int j=0; j<len2; j++){
if(curr1 == curr2){
return curr1;
}else{
curr1 = curr1.next;
curr2 = curr2.next;
}
}
}else{
//链表2先移动
for(int i=0; i<(len2-len1); i++){
curr2 = curr2.next;
}
//再同时移动,如果发现结点相等,就返回
for(int j=0; j<len2; j++){
if(curr2 == curr1){
return curr2;
}else{
curr1 = curr1.next;
curr2 = curr2.next;
}
}
}
return null;
}
}
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
解析:双指针,分别从左右两边开始,判断如果是 偶奇,交换后都移动;奇偶,都移动;奇奇,移动左;偶偶,移动右。
class Solution {
public int[] exchange(int[] nums) {
if(nums == null || nums.length <= 0) return nums;
int left = 0;
int right = nums.length-1;
while(left < right){
if(nums[left] % 2 == 0 && nums[right] % 2 != 0){ //偶奇,交换后都移动
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}else if(nums[left] % 2 != 0 && nums[right] % 2 == 0){ //奇偶,都移动
left++;
right--;
}else if(nums[left] % 2 != 0 && nums[right] % 2 != 0){ //奇奇,移动左
left++;
}else { //偶偶,移动右
right--;
}
}
return nums;
}
}
解析:双指针
class Solution {
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length-1;
while(left < right){
if(nums[left] + nums[right] == target){ //相等,直接返回
return new int[]{nums[left],nums[right]};
}else if(nums[left] + nums[right] < target){ //和 小于 目标值,移动左
left++;
}else{ //和 大于 目标值,移动右
right--;
}
}
return new int[]{};
}
}
解法一:分割+倒序
class Solution {
public String reverseWords(String s) {
String[] new_s = s.trim().split(" "); // 删除首尾空格,分割字符串
StringBuilder res = new StringBuilder();
//从后往前添加单词数组
for(int i=new_s.length-1; i>=0; i--){
if(new_s[i].equals("")) continue; //如果遇到空单词,就跳过
if(i != 0){
res.append(new_s[i] + " ");
}else{
res.append(new_s[i]); //添加最后一个时,不需要加空格
}
}
return new String(res);
}
}
解法二:双指针
class Solution {
public String reverseWords(String s) {
s = s.trim(); // 删除首尾空格
int j = s.length() - 1, i = j;
StringBuilder res = new StringBuilder();
while(i >= 0) {
while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
j = i; // j 指向下个单词的尾字符
}
return res.toString().trim(); // 转化为字符串并返回
}
}
解析:
参考:矩阵中的路径( DFS + 剪枝 ,清晰图解)
class Solution {
public boolean exist(char[][] board, String word) {
//将word转换成字串数组
char[] words = word.toCharArray();
//遍历图
for(int i=0; i<board.length; i++){
for(int j=0; j<board[0].length; j++){
//如果找到了,就返回true,否则继续找
if(dfs(board,words,i,j,0)) return true;
}
}
//遍历结束还没找到返回false
return false;
}
//i,j是元素位置下标,k是传入字符串当前索引
private boolean dfs(char[][] board, char[] words, int i, int j, int k){
//判断传入参数的可行性 i 与图行数row比较,j与图列数col比较
//如果board[i][j] == word[k],则表明当前找到了对应的数,就继续执行(标记找过,继续dfs 下上右左)
if(i>=board.length || i<0 || j>=board[0].length || j<0 || board[i][j] != words[k]) return false;
// 表示找完了,每个字符都找到了
// 一开始k=0,而word.length肯定不是0,所以没找到,就执行dfs继续找。
if(k == words.length-1) return true;
// 访问过的标记空字符串,“ ”是空格 '\0'是空字符串,不一样的!
// 比如当前为A,没有标记找过,且A是word中对应元素,则此时应该找A下一个元素,假设是B,在dfs(B)的时候还是-
// ->要搜索B左边的元素(假设A在B左边),所以就是ABA(凭空多出一个A,A用了2次,不可以),如果标记为空字符串->
// 就不会有这样的问题,因为他们值不相等AB != ABA。
board[i][j] = '\0';
//顺序是 下上右左, 上面找到了对应索引的值所以k+1
boolean res = dfs(board,words,i+1,j,k+1) || dfs(board,words,i-1,j,k+1) ||
dfs(board,words,i,j+1,k+1) || dfs(board,words,i,j-1,k+1);
// 还原找过的元素,因为之后可能还会访问到(不同路径)
board[i][j] = words[k];
// 返回结果,如果false,则if(dfs(board, words, i, j, 0)) return true;不会执行,就会继续找
return res;
}
}
解析:DFS
参考:机器人的运动范围( 回溯算法,DFS / BFS ,清晰图解)
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n]; //辅助数组
return dfs(m,n,k,0,0,visited);
}
public int dfs(int m, int n, int k, int i, int j, boolean[][] visited){
//下标越界 或 行坐标和列坐标的数位之和大于k 或 标志位为false
if(i>=m || j>=n || k<getNumSum(i)+getNumSum(j) || visited[i][j]){
return 0;
}
//可以到达的格子 设置为true
visited[i][j] = true;
//结果 = 1 + 向下可达的格子数 + 向右可达的格子数
return 1 + dfs(m,n,k,i+1,j,visited) + dfs(m,n,k,i,j+1,visited);
}
//数的各个位数之和
private int getNumSum(int a){
int sum = a % 10;
int tmp = a / 10;
while(tmp > 0){
sum += tmp % 10;
tmp /= 10;
}
return sum;
}
}
剑指 Offer 34. 二叉树中和为某一值的路径
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
解析:回溯
/**
* 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 {
LinkedList<List<Integer>> res = new LinkedList<>(); //结果
LinkedList<Integer> path = new LinkedList<>(); //路径
public List<List<Integer>> pathSum(TreeNode root, int target) {
recall(root,target);
return res;
}
public void recall(TreeNode root, int target){
if(root == null) return;
path.add(root.val); //单条路径添加值
target -= root.val; //做减法
//如果做减法后结果为0 并且 当前为叶子节点,就添加路径
if(target == 0 && root.left == null && root.right == null){
res.add(new LinkedList<>(path)); //添加路径
}
//回溯调用,查找左右子树
recall(root.left, target);
recall(root.right, target);
//撤销已处理的节点
path.removeLast();
}
}
剑指 Offer 36. 二叉搜索树与双向链表
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
解析:
参考:二叉搜索树与双向链表(中序遍历,清晰图解)
/*
// 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 pre, head; //pre为前一个节点,head为头节点
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
//经过dfs处理之后,pre就有指向了,然后进行头尾相连
head.left = pre;
pre.right = head;
//返回头节点
return head;
}
public void dfs(Node cur){
//递归结束条件
if(cur == null) return;
//左
dfs(cur.left);
//如果pre为空,就说明是第一个节点,头结点,然后用head保存头结点,用于之后的返回
if(pre == null) head = cur;
//如果pre不为空,那就说明是中间的节点。并且pre保存的是上一个节点,让上一个节点的右指针指向当前节点
else pre.right = cur;
//再让当前节点的左指针指向父节点,也就连成了双向链表
cur.left = pre;
//保存当前节点,用于下层递归创建
pre = cur;
//右
dfs(cur.right);
}
}
解法一:先使用中序遍历存储元素,然后返回倒数第k大的节点值。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int kthLargest(TreeNode root, int k) {
if(root == null) return 0;
List<Integer> res = new ArrayList<>();
inOrder(root,res);
return res.get(res.size()-k); //获取第k大的节点值
}
//中序遍历
public void inOrder(TreeNode root, List<Integer> res){
if(root == null) return;
inOrder(root.left,res);
res.add(root.val);
inOrder(root.right,res);
}
}
优化后:遍历右根左,到倒数第k大值时就直接返回。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
private int res = 0, n = 0;
public int kthLargest(TreeNode root, int k) {
this.n = k;
inOrder(root);
return res;
}
public void inOrder(TreeNode root){
//注意这里是先遍历右子树
if(root.right != null && n > 0) inOrder(root.right);
n--; //递减
if(n == 0) { //找到倒数第k大的值
res = root.val;
return;
}
if(root.left != null && n > 0) inOrder(root.left);
}
}
剑指 Offer 45. 把数组排成最小的数
解析:本质是排序问题,判断 a+b 与 b+a 的大小。
class Solution {
public String minNumber(int[] nums) {
String[] strs = new String[nums.length];
//整数数组转为字符串数组
for(int i=0; i<strs.length; i++){
strs[i] = String.valueOf(nums[i]);
}
//内置排序,定义排序规则 (x,y) -> (x+y).compareTo(y+x)
Arrays.sort(strs, (x,y) -> (x+y).compareTo(y+x));
//拼接字符串
StringBuilder res = new StringBuilder();
for(String s: strs){
res.append(s);
}
return res.toString();
}
}
剑指 Offer 61. 扑克牌中的顺子
解析:先进行排序,遍历时统计0的个数,如果有重复数字,提前返回false,遍历完成后如果 最大牌-最小牌<5 则可构成顺子。
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums);//排序
int n = 0;
for(int i=0; i<4; i++){
if(nums[i] == 0) n++; //统计0的个数
else if(nums[i] == nums[i+1] ) return false; //如果有重复数字,提前返回false
}
return nums[4] - nums[n] < 5; //最大牌-最小牌<5 则可构成顺子
}
}
剑指 Offer 40. 最小的k个数
解法一:冒泡排序后,返回前k个值。
解法二:内置排序
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
//内置排序
Arrays.sort(arr);
//结果赋值
int[] res = new int[k];
for(int i=0; i<k; i++){
res[i] = arr[i];
}
return res;
}
}
解法三:快速排序
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;
}
public void quickSort(int[] arr, int left, int right){
int l = left; //左下标
int r = right; //右下标
int mid = arr[(l + r) / 2]; //中间值
while(l < r){
//从左边开始找到比中间值大或相等的
while(arr[l] < mid) l++;
//从右边开始找到比中间值小或相等的
while(arr[r] > mid) r--;
//如果重合了,就退出
if(l >= r) break;
//交换
int temp = arr[r]; arr[r] = arr[l]; arr[l] = temp;
// 如果交换完后,发现arr[l]==pivot,此时应将r左移一位
if(arr[l] == mid) r-=1;
// 如果交换完后,发现arr[r]==pivot,此时应将l右移一位
if(arr[r] == mid) l+=1;
}
// 如果l==r,要把这两个下标错开,否则会出现无限递归,导致栈溢出的情况
if(l == r){
l += 1;
r -= 1;
}
if(left < r) quickSort(arr,left,r); //向左递归
if(right > l) quickSort(arr,l,right); //向右递归
}
}
剑指 Offer 41. 数据流中的中位数
解法一:创建list集合,调用添加函数时直接添加元素,再排序;调用查询元素时,分别对总数个数奇偶情况进行处理。
class MedianFinder {
List<Integer> list;
/** initialize your data structure here. */
public MedianFinder() {
this.list = new ArrayList<>();
}
public void addNum(int num) {
list.add(num); //添加元素
Collections.sort(list); //排序
}
public double findMedian() {
int len = list.size();
if(len <= 0) return (double)0;
if(len == 1) return (double) list.get(0);
int midIndex = (len-1) / 2; //中位数下标
if(len % 2 == 0){ //如果总数个数是偶数,就返回排序后中间两个数的平均值
return (double)((list.get(midIndex) + list.get(midIndex+1)) / 2.0);
}else{ //如果总数个数是奇数,就直接返回中位数
return (double)(list.get(midIndex));
}
}
}
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder obj = new MedianFinder();
* obj.addNum(num);
* double param_2 = obj.findMedian();
*/
解法二:
参考:数据流中的中位数(Java | 大小顶堆)
class MedianFinder {
// 大顶堆存储较小一半的值
PriorityQueue<Integer> maxHeap;
// 小顶堆存储较大一般的值
PriorityQueue<Integer> minHeap;
public MedianFinder() {
maxHeap = new PriorityQueue<Integer>((x, y) -> (y - x)); //定义排序规则
minHeap = new PriorityQueue<Integer>();
}
public void addNum(int num) {
// 长度为奇数时先放入小顶堆,重新排序后在插入到大顶堆
if(maxHeap.size() != minHeap.size()) {
minHeap.add(num);
maxHeap.add(minHeap.poll());
} else {
// 长度为偶数时先放入大顶堆,重新排序后在插入到小顶堆
maxHeap.add(num);
minHeap.add(maxHeap.poll());
}
}
public double findMedian() {
if(maxHeap.size() != minHeap.size()) { //奇数
return minHeap.peek();
} else { //偶数
return (maxHeap.peek() + minHeap.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 55 - I. 二叉树的深度
解析:层序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int n = 0; //树的深度
while(!que.isEmpty()){
n++;
int len = que.size();
while(len > 0){
TreeNode node = que.poll();
if(node.left != null) que.offer(node.left);
if(node.right != null) que.offer(node.right);
len--;
}
}
return n;
}
}
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right)) + 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) {
return dfs(root) >= 0;
}
public int dfs(TreeNode root){
if(root == null) return 0;
int leftHeight = dfs(root.left); //左子树高度
int rightHeight = dfs(root.right); //右子树高度
if(leftHeight == -1 || rightHeight == -1 || Math.abs(leftHeight - rightHeight) > 1){ //如果左右子树高度为-1,或者两者的差值大于1,就返回-1
return -1;
}else{
return Math.max(leftHeight, rightHeight) + 1; //树的深度
}
}
}
剑指 Offer 64. 求1+2+…+n
解析:递归,短路&&
class Solution {
public int sumNums(int n) {
//如果n小于等于0,就不会执行&&后面的语句
boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
return n;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
解析:
/**
* 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(p.val < root.val && q.val < root.val){ //往左搜索
return lowestCommonAncestor(root.left, p, q);
}
if(p.val > root.val && q.val > root.val){ //往右搜索
return lowestCommonAncestor(root.right, p, q);
}
return root; //如果出现p,q在一左一右,就直接返回当前的根节点
}
}
解析:
三种情况
1、p q 一个在左子树 一个在右子树,那么当前节点即是最近公共祖先。
2、p q 都在左子树
3、p q 都在右子树
/**
* 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;
//如果当前节点等于p或q,返回当前节点
if(root == p || root == q) return root;
//在左子树找
TreeNode left = lowestCommonAncestor(root.left, p, q);
//在右子树找
TreeNode right = lowestCommonAncestor(root.right, p, q);
// p q 一个在左,一个在右
if(left != null && right != null) return root;
// p q 都在左子树
if(left != null) return left;
// p q 都在右子树
if(right != null) return right;
return null;
}
}
解析:
参考:重建二叉树(分治算法,清晰图解)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();//标记中序遍历
int[] preorder;//保留的先序遍历,方便递归时依据索引查看先序遍历的值
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
//将中序遍历的值及索引放在map中,方便递归时获取左子树与右子树的数量及其根的索引
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
//三个索引分别为
//当前根的的索引
//递归树的左边界,即数组左边界
//递归树的右边界,即数组右边界
return recur(0,0,inorder.length-1);
}
TreeNode recur(int pre_root, int in_left, int in_right){
if(in_left > in_right) return null;// 相等的话就是自己
TreeNode root = new TreeNode(preorder[pre_root]);//获取root节点
int idx = map.get(preorder[pre_root]);//获取在中序遍历中根节点所在索引,以方便获取左子树的数量
//左子树的根的索引为先序中的根节点+1
//递归左子树的左边界为原来的中序in_left
//递归左子树的右边界为中序中的根节点索引-1
root.left = recur(pre_root+1, in_left, idx-1);
//右子树的根的索引为先序中的 当前根位置 + 左子树的数量 + 1
//递归右子树的左边界为中序中当前根节点+1
//递归右子树的右边界为中序中原来右子树的边界
root.right = recur(pre_root + (idx - in_left) + 1, idx+1, in_right);
return root;
}
}
解析:
参考:快速幂
class Solution {
public double myPow(double x, int n) {
if(n == 0) return 1;
if(x == 0) return 0;
long b = n;
double res = 1.0;
if(b < 0){
x = 1 / x;
b = -b;
}
while(b > 0){
//最后一位为1,需要乘上改位上的权重
if((b & 1) == 1) res *= x; // 即 n % 2 == 1
x *= x; // 即 x = x ^ 2;
b >>= 1; // 即 n //= 2;
}
return res;
}
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
解析:
参考:二叉搜索树的后序遍历序列(递归分治 / 单调栈,清晰图解)
class Solution {
public boolean verifyPostorder(int[] postorder) {
return recur(postorder, 0, postorder.length-1);
}
public boolean recur(int[] postorder, int left, int right){
//说明此子树节点数量≤1 ,无需判别正确性,因此直接返回 truetrue
if(left >= right) return true;
int temp = left;
while(postorder[temp] < postorder[right]) temp++; //找到第一个比根节点大的节点
int m = temp;
while(postorder[temp] > postorder[right]) temp++;
//temp = right : 判断 此树 是否正确。
//recur(postorder, left, m-1): 判断 此树的左子树 是否正确。
//recur(postorder, m, right-1) : 判断 此树的右子树 是否正确。
return temp == right && recur(postorder, left, m-1) && recur(postorder, m, right-1);
}
}
剑指 Offer 15. 二进制中1的个数
解析:
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res = 0;
while(n != 0){
if((n & 1) == 1) res++; //统计1的次数
n >>>= 1; //无符号右移
}
return res;
}
}
解法二:
public class Solution {
// you need to treat n as an unsigned value
public int hammingWeight(int n) {
int res = 0;
while(n != 0){
res++; //次数+1
n &= (n-1); //消去数字 n 最右边的 1。
}
return res;
}
}
解析:
参考:不用加减乘除做加法(位运算,清晰图解)
class Solution {
public int add(int a, int b) {
while(b != 0){ //当进位为0时跳出
int c = (a & b) << 1; //c=进位
a ^= b; // a = 非进位和
b = c; // b = 进位
}
return a;
}
}
剑指 Offer 56 - I. 数组中数字出现的次数
解析:
参考:数组中数字出现的次数(位运算,清晰图解)
注释精简版:
class Solution {
public int[] singleNumbers(int[] nums) {
int x = 0, y = 0, n = 0, m = 1;
for(int num : nums) // 1. 遍历异或
n ^= num;
while((n & m) == 0) // 2. 循环左移,计算 m
m <<= 1;
for(int num: nums) { // 3. 遍历 nums 分组
if((num & m) != 0) x ^= num; // 4. 当 num & m != 0
else y ^= num; // 4. 当 num & m == 0
}
return new int[] {x, y}; // 5. 返回出现一次的数字
}
}
注释详细版:
class Solution {
public int[] singleNumbers(int[] nums) {
//因为相同的数字异或为0,任何数字与0异或结果是其本身。
//所以遍历异或整个数组最后得到的结果就是两个只出现一次的数字异或的结果:即 z = x ^ y
int z = 0;
for(int i : nums) z ^= i;
//我们根据异或的性质可以知道:z中至少有一位是1,否则x与y就是相等的。
//我们通过一个辅助变量m来保存z中哪一位为1.(可能有多个位都为1,我们找到最低位的1即可)。
//举个例子:z = 10 ^ 2 = 1010 ^ 0010 = 1000,第四位为1.
//我们将m初始化为1,如果(z & m)的结果等于0说明z的最低为是0
//我们每次将m左移一位然后跟z做与操作,直到结果不为0.
//此时m应该等于1000,同z一样,第四位为1.
int m = 1;
while((z & m) == 0) m <<= 1;
//我们遍历数组,将每个数跟m进行与操作,结果为0的作为一组,结果不为0的作为一组
//例如对于数组:[1,2,10,4,1,4,3,3],我们把每个数字跟1000做与操作,可以分为下面两组:
//nums1存放结果为0的: [1, 2, 4, 1, 4, 3, 3]
//nums2存放结果不为0的: [10] (碰巧nums2中只有一个10,如果原数组中的数字再大一些就不会这样了)
//此时我们发现问题已经退化为数组中有一个数字只出现了一次
//分别对nums1和nums2遍历异或就能得到我们预期的x和y
int x = 0, y = 0;
for(int i : nums) {
//这里我们是通过if...else将nums分为了两组,一边遍历一遍异或。
//跟我们创建俩数组nums1和nums2原理是一样的。
if((i & m) == 0) x ^= i;
else y ^= i;
}
return new int[]{x, y};
}
}
剑指 Offer 56 - II. 数组中数字出现的次数 II
解法一:用map存数字和布尔值, 存过的数字 布尔值都是true, 只有那个出现一次的数的布尔值是false,找出来就行了
class Solution {
public int singleNumber(int[] nums) {
Map<Integer, Boolean> map = new HashMap<>();
for(Integer num : nums){
//如果已经存在,就存true,否则为false
map.put(num, map.containsKey(num));
}
for(Integer num : nums){ //找出为false的就为出现1次的数
if(!map.get(num)) return num;
}
return -1;
}
}
解法二:位运算 - 有限状态自动机
class Solution {
public int singleNumber(int[] nums) {
int ones = 0, twos = 0;
for(int num : nums){
ones = ones ^ num & ~twos;
twos = twos ^ num & ~ones;
}
return ones;
}
}
解法一:哈希map,把元素为键,出现的次数为值存入map中,找到出现的次数超过数组长度的一半的数字返回。
class Solution {
public int majorityElement(int[] nums) {
int len = nums.length;
HashMap<Integer, Integer> map = new HashMap<>();
for(Integer num : nums){
//把元素为键,出现的次数为值存入map中;
//getOrDefault(num, 0):如果map不存在该key,则返回默认值;若存在key,则返回key所对应的的value。
map.put(num, map.getOrDefault(num, 0) + 1);
//如果找到出现的次数超过数组长度的一半的数字就直接返回
if(map.get(num) > len/2) return num;
}
return -1;
}
}
解法二:
class Solution {
public int majorityElement(int[] nums) {
int len = nums.length;
int x = 0, sum = 0, count = 0;
for(int num: nums){
if(sum == 0) x = num; //如果当前的和为0,num为众数
if(num != x) sum -= 1; //如果当前数不等于众数,和-1
else sum += 1; //否则当前数等于众数,和+1
}
//验证 x 是否为众数
for(int num : nums){
if(num == x) count++;
}
return count > len/2 ? x : 0; //当无众数时返回0
}
}
剑指 Offer 66. 构建乘积数组
解法一:写一个计算除了下标i以外的元素的乘积的函数,然后遍历调用,遍历时当i>=1时,如果当前元素与前一个元素相同,则直接将前一个值赋值给当前值。
class Solution {
public int[] constructArr(int[] a) {
int[] res = new int[a.length];
if(a == null || a.length <= 0) return res;
for(int i=0; i<a.length; i++){
if(i == 0){ // i为0的情况
res[0] = otherSum(a,0);
}else{ // 当i>=1时,如果当前元素与前一个元素相同,则直接将前一个值赋值给当前值
if(a[i-1] == a[i]) res[i] = res[i-1];
else res[i] = otherSum(a,i);
}
}
return res;
}
//计算除了下标i以外的元素的乘积
public int otherSum(int[] arr, int cur){
int sum = 1;
for(int i=0; i<arr.length; i++){
if(cur == i) continue;
if(arr[i] != 1){
sum *= arr[i];
}
}
return sum;
}
}
解法二:
class Solution {
public int[] constructArr(int[] a) {
int[] ans = new int[a.length];
//计算i前
for (int i = 0, p = 1; i < a.length; i++) {
ans[i] = p;
p *= a[i];
}
//计算i后
for (int i = a.length - 1, p = 1; i >= 0; i--) {
ans[i] *= p;
p *= a[i];
}
return ans;
}
}
剑指 Offer 14- I. 剪绳子
解析:
首先使用动态规划解决问题,最重要的就是理解dp数组的含义。 此处的dp[i] 表示长度为i的绳子剪成m段后的最大乘积。初始化dp[2] = 1 表示如果一个绳子长度为2必然只能分成两段长度为1的绳子,两者的乘积为1。
class Solution {
public int cuttingRope(int n) {
// dp[i]表示长度为i的绳子被剪成m段,每段乘机的最大值
int[] dp = new int[n+1];
// 因为m>1,所以dp[2] = 1而不是2
dp[2] = 1;
// i表示绳子长度
for (int i=3;i<=n;++i){
// i-j >= 2。j代表第一次剪掉的长度,剪掉1没用,所以从2开始,剩余的长度i-j怎么减直接取dp[i-j]
for (int j=1;j<=i-2;++j){
// 这里因为m>1,所以dp[2] = 1而不是2,dp[3]不能是dp[2] * 1,这样答案是1,错误.
// 因此下面要添加Math.max(dp[i-j], i-j)。
dp[i] = Math.max(Math.max(dp[i-j], i-j) * j, dp[i]);
}
}
return dp[n];
}
}
剑指 Offer 57 - II. 和为s的连续正数序列
解析:滑动窗口,双指针
参考:和为 s 的连续正数序列(求和公式 / 滑动窗口,清晰图解)
class Solution {
public int[][] findContinuousSequence(int target) {
int i=1, j=2, sum=3;
List<int[]> list = new ArrayList<>();
while(i < j){
if(sum == target){
//新建数组保存
int[] ans = new int[j-i+1];
for(int k=i; k<=j; k++){
ans[k-i] = k;
}
//添加进集合
list.add(ans);
}
if(sum >= target){
sum -= i;
i++; //移动左指针
}else{
j++; //移动右指针
sum += j;
}
}
return list.toArray(new int[0][]);
}
}
解析:
class Solution {
public int lastRemaining(int n, int m) {
int res = 0;
for(int i=2; i<=n; i++){
res = (res + m) % i;
}
return res;
}
}
剑指 Offer 29. 顺时针打印矩阵
解析:设置上下左右边界,模拟行走。
class Solution {
public int[] spiralOrder(int[][] matrix) {
//空值处理
if(matrix == null || matrix.length == 0) return new int[0];
int left = 0; // 左边界
int right = matrix[0].length - 1; //右边界
int top = 0; //上边界
int bottom = matrix.length - 1; //下边界
int[] res = new int[(right+1) * (bottom+1)]; //保存结果
int index = 0; //数组下标
while(true){
//从左到右
for(int i=left; i<=right; i++){
res[index++] = matrix[top][i];
}
//上边界下移,如果超过下边界则退出
if(++top > bottom) break;
//从上到下
for(int i=top; i<=bottom; i++){
res[index++] = matrix[i][right];
}
//右边界左移,如果超过左边界则退出
if(--right < left) break;
//从右到左
for(int i=right; i>=left; i--){
res[index++] = matrix[bottom][i];
}
//下边界上移,如果超过上边界则退出
if(--bottom < top) break;
//从下到上
for(int i=bottom; i>=top; i--){
res[index++] = matrix[i][left];
}
//左边界右移,如果超过右边界则退出
if(++left > right) break;
}
return res;
}
}
剑指 Offer 31. 栈的压入、弹出序列
解析:判断合不合法,用个栈试一试:把压栈的元素按顺序压入,当栈顶元素和出栈的第一个元素相同,则将该元素弹出,出栈列表指针后移并继续判断。最后判断出栈列表指针是否指向出栈列表的末尾即可。
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Deque<Integer> stack = new ArrayDeque();
int index = 0; //记录下标
for(int item : pushed){
stack.push(item); //入栈,把压栈的元素按顺序压入
while(index < popped.length && !stack.isEmpty() && stack.peek() == popped[index]){
stack.pop(); //出栈,当栈顶元素和出栈的第一个元素相同,则将该元素弹出
index++; //出栈列表popped的指针后移并继续判断
}
}
//如果index等于出栈数组的长度,说明该出栈顺序合法,否则返回false
return index == popped.length;
}
}
剑指 Offer 20. 表示数值的字符串
解析:
参考:表示数值的字符串(有限状态自动机,清晰图解)
//小数表示可省去0,-0.4 = -.4,0.4 = .4;2.、3. = 2、3,小数点前有数,后面可以不跟数代表原数
//注意e8即10的8次幂(8次方),也可以是e-7,但题目要求必须跟整数
//题目规定是数值前后可有空格,中间不能有,这个情况要考虑清楚。s:符号、d:数字
class Solution {
public boolean isNumber(String s) {
Map[] states = {
//0:规定0是初值,字符串表示数值,有4种起始状态,开头空格、符号、数字、前面没有数的小数点
//其中 开头空格 还是指向states[0],上一位是 开头空格,下一位可以是 空格、符号、数字、前面没有数的小数点
new HashMap<>() {{ put(' ', 0); put('s', 1); put('d', 2); put('.', 4); }},
//1:上一位是符号,符号位后面可以是 数字、前面没有数的小数点
new HashMap<>() {{ put('d', 2); put('.', 4); }},
//2:上一位是数字,数字的下一位可以是 数字、前面有数的小数点、e、结尾空格
new HashMap<>() {{ put('d', 2); put('.', 3); put('e', 5); put(' ', 8); }},
//3:上一位是前面有数的小数点,下一位可以是 数字、e(8.e2 = 8e2,和2的情况一样)、结尾空格
new HashMap<>() {{ put('d', 3); put('e', 5); put(' ', 8); }},
//4:上一位是前面没有数的小数点,下一位只能是 数字(符号肯定不行,e得前面有数才行)
new HashMap<>() {{ put('d', 3); }},
//5:上一位是e,下一位可以是 符号、数字
new HashMap<>() {{ put('s', 6); put('d', 7); }},
//6::上一位是e后面的符号,下一位只能是 数字
new HashMap<>() {{ put('d', 7); }},
//7:上一位是e后面的数字,下一位可以是 数字、结尾空格
new HashMap<>() {{ put('d', 7); put(' ', 8); }},
//8:上一位是结尾空格,下一位只能是 结尾空格
new HashMap<>() {{ put(' ', 8); }}
};
int p = 0;
char t;
//遍历字符串,每个字符匹配对应属性并用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 = '?';
//当前字符标记和任何一种当前规定格式都不匹配,直接返回false
if(!states[p].containsKey(t)) return false;
//更新当前字符的规定格式,进入下一个规定的Map数组
p = (int)states[p].get(t);
}
//2(正、负整数)、3(正、负小数)、7(科学计数法)、8(前三种形式的结尾加上空格)
//只有这四种才是正确的结尾
return p == 2 || p == 3 || p == 7 || p == 8;
}
}
剑指 Offer 67. 把字符串转换成整数
解析:
class Solution {
public int strToInt(String str) {
//去前后空格
char[] chars = str.trim().toCharArray();
if (chars.length == 0) return 0;
//记录第一个符合是否为负数
int sign = 1;
//开始遍历的位置
int i = 1;
//如果首个非空格字符为负号,那么从位置1开始遍历字符串,并且结果需要变成负数
if (chars[0] == '-') {
sign = -1;
} else if (chars[0] != '+') { //如果首个非空格字符不是负号也不是加号,那么从第一个元素开始遍历
i = 0;
}
int number = Integer.MAX_VALUE / 10;
//结果
int res = 0;
for (int j = i; j < chars.length; j++) {
//遇到非数字直接退出
if (chars[j] > '9' || chars[j] < '0') break;
/*
这里这个条件的意思为,因为题目要求不能超过int范围,所以需要判断结果是否越界
因为res每次都会 * 10 ,所以外面定义了一个int最大值除以10的数字
此时只需要保证本次循环的res * 10 + chars[j] 不超过 int 即可保证不越界
res > number 意思是,此时res已经大于number了,他 * 10 一定越界
res == number && chars[j] > '7' 的意思是,当res == number时,即:214748364
此时res * 10 变成 2147483640 此时没越界,但是还需要 + chars[j],
而int最大值为 2147483647,所以当chars[j] > 7 时会越界
*/
if (res > number || (res == number && chars[j] > '7')) {
//根据字符串首负号判断返回最大值还是最小值
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
//字符获取数字需要 - '0' 的位移
res = res * 10 + (chars[j] - '0');
}
//返回结果,需要判断正负
return res * sign;
}
}
剑指 Offer 59 - I. 滑动窗口的最大值1
解法一:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0) return nums;
int left = 0, right = left+k-1; //滑动窗口左右指针
List<Integer> list = new ArrayList<>();
while(right <= nums.length-1){
int max = Integer.MIN_VALUE; //记录最大值
for(int i=left; i<=right; i++){
if(nums[i] > max) max = nums[i]; //找到当前窗口的最大值
}
list.add(max); //添加进集合
left++; //移动左指针
right++; //移动右指针
}
//集合赋值给数组返回
int res[] = new int[list.size()];
for(int i=0; i<list.size(); i++){
res[i] = list.get(i);
}
return res;
}
}
解法二:
参考:滑动窗口的最大值(单调队列,清晰图解)
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
//单调队列
//下面是要注意的点:
//队列按从大到小放入
//如果首位值(即最大值)不在窗口区间,删除首位
//如果新增的值小于队列尾部值,加到队列尾部
//如果新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中
//如果新增值大于队列中所有值,删除所有,然后把新增值放到队列首位,保证队列一直是从大到小
if (nums.length == 0) return nums;
Deque<Integer> deque = new LinkedList<>();
int[] arr = new int[nums.length - k + 1];
int index = 0; //arr数组的下标
//未形成窗口区间
for (int i = 0; i < k; i++) {
//队列不为空时,当前值与队列尾部值比较,如果大于,删除队列尾部值
//一直循环删除到队列中的值都大于当前值,或者删到队列为空
while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();
//执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中
deque.addLast(nums[i]);
}
//窗口区间刚形成后,把队列首位值添加到队列中
//因为窗口形成后,就需要把队列首位添加到数组中,而下面的循环是直接跳过这一步的,所以需要我们直接添加
arr[index++] = deque.peekFirst();
//窗口区间形成
for (int i = k; i < nums.length; i++) {
//i-k是已经在区间外了,如果首位等于nums[i-k],那么说明此时首位值已经不再区间内了,需要删除
if (deque.peekFirst() == nums[i - k]) deque.removeFirst();
//删除队列中比当前值大的值
while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();
//把当前值添加到队列中
deque.addLast(nums[i]);
//把队列的首位值添加到arr数组中
arr[index++] = deque.peekFirst();
}
return arr;
}
}
剑指 Offer 59 - II. 队列的最大值
解法一:集合
class MaxQueue {
List<Integer> list;
public MaxQueue() {
list = new ArrayList<>();
}
public int max_value() {
if(list.size() == 0) return -1;
int max = Integer.MIN_VALUE;
for(int i=0; i<list.size(); i++){
if(list.get(i) > max) max = list.get(i); //找出最大值
}
return max;
}
public void push_back(int value) {
list.add(value);
}
public int pop_front() {
if(list.size() == 0) return -1;
int res = list.get(0);
list.remove(0); //删除首元素
return res;
}
}
/**
* 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();
*/
解法二:队列与双向队列
class MaxQueue {
Queue<Integer> queue;
Deque<Integer> deque;
public MaxQueue() {
queue = new LinkedList<>();
deque = new LinkedList<>();
}
public int max_value() {
//如果双向队列不为空,就返回双向队列首元素
return deque.isEmpty() ? -1 : deque.peekFirst();
}
public void push_back(int value) {
queue.offer(value); //队列正常入队
while(!deque.isEmpty() && deque.peekLast() < value){
deque.pollLast(); //如果双向队列的尾元素小于要添加的值,就移除该尾元素
}
deque.offerLast(value); //添加进尾部
}
public int pop_front() {
if(queue.isEmpty()) return -1;
if(queue.peek().equals(deque.peekFirst())){
deque.pollFirst(); //如果两个队列的首元素相等,则双向队列也要弹出首元素
}
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();
*/
剑指 Offer 37. 序列化二叉树
解析:
参考:序列化二叉树(层序遍历 BFS ,清晰图解)
/**
* 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) {
if(root == null) return "[]";
StringBuilder res = new StringBuilder("["); //左大括号
Queue<TreeNode> que = new LinkedList<>();
que.add(root);
while(!que.isEmpty()){
TreeNode node = que.poll();
if(node != null){ //如果节点不为空
res.append(node.val + ","); //添加给节点值
que.add(node.left); //添加左孩子
que.add(node.right); //添加右孩子
}else{
res.append("null,"); //节点为空,添加空值
}
}
res.deleteCharAt(res.length()-1); //删除逗号
res.append("]"); //右大括号
return res.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> que = new LinkedList<>();
que.add(root);
int i=1; //下标
while(!que.isEmpty()){
TreeNode node = que.poll();
if(!vals[i].equals("null")){
node.left = new TreeNode(Integer.parseInt(vals[i])); //构建左孩子
que.add(node.left); //添加左孩子
}
i++; //移动
if(!vals[i].equals("null")){
node.right = new TreeNode(Integer.parseInt(vals[i])); //构建右孩子
que.add(node.right); //添加右孩子
}
i++; //移动
}
return root;
}
}
// Your Codec object will be instantiated and called as such:
// Codec codec = new Codec();
// codec.deserialize(codec.serialize(root));
剑指 Offer 38. 字符串的排列
解析:回溯
class Solution {
List<String> res;
StringBuilder path;
boolean[] visited;
public String[] permutation(String s) {
//空值处理
if(s.equals("")) return new String[]{};
//初始化
this.res = new ArrayList<>();
this.path = new StringBuilder();
this.visited = new boolean[s.length()];
//字符串转为字符数组
char[] ch = s.toCharArray();
//排序,方便处理重复的字符
Arrays.sort(ch);
backtrack(ch,0);
return res.toArray(new String[0]);
}
public void backtrack(char[] ch, int depth){
if(depth == ch.length){
res.add(path.toString());
return;
}
//遍历字符数组
for(int i=0; i<ch.length; i++){
if(visited[i]) continue; //跳过
if(i > 0 && ch[i-1] == ch[i] && !visited[i-1]) continue; //重复字串的处理
path.append(ch[i]); //添加字符
visited[i] = true; //标记已访问
backtrack(ch, depth+1); //回溯,深度+1
visited[i] = false; //重置访问
path.deleteCharAt(path.length()-1); //删除刚添加的字符,重新尝试
}
}
}
剑指 Offer 19 正则表达式匹配
解析:
参考:正则表达式匹配(动态规划,清晰图解)
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length() + 1, n = p.length() + 1;
boolean[][] dp = new boolean[m][n];
dp[0][0] = true;
// 初始化首行
for(int j = 2; j < n; j += 2)
dp[0][j] = dp[0][j - 2] && p.charAt(j - 1) == '*';
// 状态转移
for(int i = 1; i < m; i++) {
for(int j = 1; j < n; j++) {
if(p.charAt(j - 1) == '*') {
if(dp[i][j - 2]) dp[i][j] = true; // 1.
else if(dp[i - 1][j] && s.charAt(i - 1) == p.charAt(j - 2)) dp[i][j] = true; // 2.
else if(dp[i - 1][j] && p.charAt(j - 2) == '.') dp[i][j] = true; // 3.
} else {
if(dp[i - 1][j - 1] && s.charAt(i - 1) == p.charAt(j - 1)) dp[i][j] = true; // 1.
else if(dp[i - 1][j - 1] && p.charAt(j - 1) == '.') dp[i][j] = true; // 2.
}
}
}
return dp[m - 1][n - 1];
}
}
剑指 Offer 49. 丑数
解析:
参考:丑数(动态规划,清晰图解)
class Solution {
public int nthUglyNumber(int n) {
if(n == 1) return 1;
int a = 0, b = 0, c = 0;
int[] dp = new int[n]; //dp[i]代表第 i + 1 个丑数;
dp[0] = 1;
for(int i=1; i<n; i++){
int n2 = dp[a] * 2;
int n3 = dp[b] * 3;
int n5 = dp[c] * 5;
dp[i] = Math.min(Math.min(n2, n3), n5); //取最小值
if(dp[i] == n2) a++; //更新索引
if(dp[i] == n3) b++;
if(dp[i] == n5) c++;
}
return dp[n-1];
}
}
解析:
参考: n 个骰子的点数(动态规划,清晰图解)
class Solution {
public double[] dicesProbability(int n) {
//因为最后的结果只与前一个动态转移数组有关,所以这里只需要设置一个一维的动态转移数组
//原本dp[i][j]表示的是前i个骰子的点数之和为j的概率,现在只需要最后的状态的数组,所以就只用一个一维数组dp[j]表示n个骰子下每个结果的概率。
//初始是1个骰子情况下的点数之和情况,就只有6个结果,所以用dp的初始化的size是6个
double[] dp = new double[6];
//只有一个数组
Arrays.fill(dp,1.0/6.0);
//从第2个骰子开始,这里n表示n个骰子,先从第二个的情况算起,然后再逐步求3个、4个···n个的情况
//i表示当总共i个骰子时的结果
for(int i=2;i<=n;i++){
//每次的点数之和范围会有点变化,点数之和的值最大是i*6,最小是i*1,i之前的结果值是不会出现的;
//比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6*i-(i-1),化简:5*i+1
//当有i个骰子时的点数之和的值数组先假定是temp
double[] temp = new double[5*i+1];
//从i-1个骰子的点数之和的值数组入手,计算i个骰子的点数之和数组的值
//先拿i-1个骰子的点数之和数组的第j个值,它所影响的是i个骰子时的temp[j+k]的值
for(int j=0;j<dp.length;j++){
//比如只有1个骰子时,dp[1]是代表当骰子点数之和为2时的概率,它会对当有2个骰子时的点数之和为3、4、5、6、7、8产生影响,因为当有一个骰子的值为2时,另一个骰子的值可以为1~6,产生的点数之和相应的就是3~8;比如dp[2]代表点数之和为3,它会对有2个骰子时的点数之和为4、5、6、7、8、9产生影响;所以k在这里就是对应着第i个骰子出现时可能出现六种情况,这里可能画一个K神那样的动态规划逆推的图就好理解很多
for(int k=0;k<6;k++){
//这里记得是加上dp数组值与1/6的乘积,1/6是第i个骰子投出某个值的概率
temp[j+k]+=dp[j]*(1.0/6.0);
}
}
//i个骰子的点数之和全都算出来后,要将temp数组移交给dp数组,dp数组就会代表i个骰子时的可能出现的点数之和的概率;用于计算i+1个骰子时的点数之和的概率
dp = temp;
}
return dp;
}
}
剑指 Offer 17. 打印从1到最大的n位数
解法一:先计算出n位的最大数,再遍历添加到数组返回
class Solution {
public int[] printNumbers(int n) {
if(n == 0) return new int[]{};
int num = 0;
num = (int)Math.pow(10,n) - 1; //计算n位的最大数
int[] res = new int[num];
for(int i=0; i<num; i++){
res[i] = i+1; //赋值
}
return res;
}
}
解法二:考虑大数情况
参考:打印从 1 到最大的 n 位数(分治算法 / 全排列,清晰图解)
class Solution {
int[] res;
int nine = 0, count = 0, start, n;
char[] num, loop = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
public int[] printNumbers(int n) {
this.n = n;
res = new int[(int)Math.pow(10, n) - 1];
num = new char[n];
start = n - 1;
dfs(0);
return res;
}
void dfs(int x) {
if(x == n) {
String s = String.valueOf(num).substring(start);
if(!s.equals("0")) res[count++] = Integer.parseInt(s);
if(n - start == nine) start--;
return;
}
for(char i : loop) {
if(i == '9') nine++;
num[x] = i;
dfs(x + 1);
}
nine--;
}
}
剑指 Offer 51. 数组中的逆序对
解析:归并排序,在合并时统计逆序对的数量。
class Solution {
//利用归并排序解答,在合并的时候,当左边的大于右边,就计算逆序数。
//计算公式; mid-left+1
//定义一个全局的计数器变量
int count = 0;
public int reversePairs(int[] nums) {
this.count = 0;
mergeSort(nums, 0, nums.length-1);
return count;
}
//归并排序
public void mergeSort(int[] nums,int left,int right){
//当只有一个节点的时候,直接返回,退出递归
if(left >= right){
return;
}
int mid = (left+right)/2;
//左拆分
mergeSort(nums,left,mid);
//右拆分
mergeSort(nums,mid+1,right);
//合并
merge(nums,left,mid,right);
}
//合并
public void merge(int[] nums,int left,int mid,int right){
//定义一个临时数组
int[] temp = new int[right-left+1];
//定义一个指针,指向第一个数组的第一个元素
int i = left;
//定义一个指针,指向第二个数组的第一个元素
int j = mid+1;
//定义一个指针,指向临时数组的第一个元素
int t = 0;
//当两个数组都有元素的时候,遍历比较每个元素大小
while(i <= mid && j <= right){
//比较两个数组的元素,取较小的元素加入到,临时数组中
//并将两个指针指向下一个元素
if(nums[i] <= nums[j]){
temp[t++] = nums[i++];
}else{
//当左边数组的大与右边数组的元素时,就对当前元素以及后面的元素的个数进行统计,
//此时这个数就是,逆序数
//定义一个计数器,记下每次合并中存在的逆序数。
count += mid-i+1;
temp[t++] = nums[j++];
}
}
//当左边的数组没有遍历完成后,直接将剩余元素加入到临时数组中
while(i <= mid){
temp[t++] = nums[i++];
}
//当右边的数组没有遍历完成后,直接将剩余元素加入到临时数组中
while(j <= right){
temp[t++] =nums[j++];
}
//将新数组中的元素,覆盖nums旧数组中的元素。
//此时数组的元素已经是有序的
for(int k =0; k< temp.length;k++){
nums[left+k] = temp[k];
}
}
}
剑指 Offer 14- II. 剪绳子 II
解析:贪心算法,循环求余
class Solution {
public int cuttingRope(int n) {
if(n <= 3) return n-1;
long res = 1L; //最大乘积
int p = (int)1e9+7; //int最大值
//贪心算法,优先切三
while(n > 4){
res = res * 3 % p; //计算当前乘积
n -= 3; //减去切掉的长度
}
//此时出循环只有三种情况,n=2,3,4;将它与前面的结果res相乘即可。
return (int)(res * n % p);
}
}
剑指 Offer 43. 1~n 整数中 1 出现的次数
解析:
参考:1~n 整数中 1 出现的次数
class Solution {
public int countDigitOne(int n) {
// mulk 表示 10^k
// 在下面的代码中,可以发现 k 并没有被直接使用到(都是使用 10^k)
// 但为了让代码看起来更加直观,这里保留了 k
long mulk = 1;
int ans = 0;
for (int k = 0; n >= mulk; ++k) {
ans += (n / (mulk * 10)) * mulk + Math.min(Math.max(n % (mulk * 10) - mulk + 1, 0), mulk);
mulk *= 10;
}
return ans;
}
}
剑指 Offer 44. 数字序列中某一位的数字
解析:
参考:数字序列中某一位的数字(迭代 + 求整 / 求余,清晰图解)
class Solution {
public int findNthDigit(int n) {
if(n==0) return 0;
//由于是n=0时对应开始的0,这里不需要进行减操作n--;,但是如果n=1对应开始的0则需要减操作
//排除n=0后,后面n从1开始。
int digit = 1;
int start = 1;
long count = 9; //count的值有可能会超出int的范围,所以变量类型取为long
while(n>count){//不能带=号,此时n从1开始,n=count时,属于上一个循环的最后一个数字
n=(int)(n-count);//这里(int)不能省略
digit++;
start = start*10;
count = (long)start*9*digit;
//这里的long不能省略,否则,会先按照int类型进行计算,然后赋值给long型的count,超过int大小限制时,会出现负数
}
int num = start + (n-1)/digit;
int index = (n-1)%digit;//index最大取digit-1,即此时num坐标从左到右为0,1,...,digit-1,共digit位
while(index<(digit-1)){
//最后的结果是num中的第index个数字,index从左到右从0开始递增,考虑到踢出右侧末尾的数字比较简单,我们从右侧开始依次踢出数字
num = num/10;
digit--;
}
return num%10;//此时num的右侧末尾数字即为结果
}
}