说明:比较简单的题解,在这里就不总结了。
第 1 天 栈与队列(简单)
剑指 Offer 09. 用两个栈实现队列
思路:
用栈实现队列,首先搞懂队列和栈的特点,队列是先进先出,栈是先进后出。
两个先进后出的栈如何能做到先进先出呢?
这里我们的核心思路如下:
1.添加数据:全部压进1栈;
2.删除数据:若是2栈为空,则把1栈的数据弹出压入2栈中
如果2栈仍然为空,则返回-1,否则弹出一个元素返回。
JAVA实现:
class CQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;
public CQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void appendTail(int value) {
stack1.addFirst(value);
}
public int deleteHead() {
if (stack2.isEmpty()) {
while (!stack1.isEmpty()) {
stack2.addFirst(stack1.removeFirst());
}
}
if (stack2.isEmpty()) {
return -1;
} else {
return stack2.removeFirst();
}
}
}
GO实现:
这里列举了两种方法:
第一个调list包,双链表结构
type CQueue struct {
// 借助list包实现,主要借助PushBack, Remove, Back, Len实现
stack1, stack2 *list.List
}
func Constructor() CQueue {
return CQueue{
stack1: list.New(),
stack2: list.New(),
}
}
func (this *CQueue) AppendTail(val int) {
// 添加队尾-入队
this.stack1.PushBack(val)
}
func (this *CQueue) DeleteHead() int {
// 删除队头-出队
if this.stack2.Len() == 0 {
for this.stack1.Len() > 0 {
this.stack2.PushBack(this.stack1.Remove(this.stack1.Back()))
}
}
if this.stack2.Len() != 0 {
e := this.stack2.Back()
this.stack2.Remove(e)
return e.Value.(int)
}
return -1
}
第二种:切片做
type CQueue struct {
inStack []int
outStack []int
}
func Constructor() CQueue {
return CQueue{}
}
func (this *CQueue) AppendTail(value int) {
this.inStack = append(this.inStack, value)
}
func (this *CQueue) DeleteHead() int {
if len(this.outStack) == 0 {
for len(this.inStack) > 0 {
this.outStack = append(this.outStack, this.inStack[len(this.inStack)-1])
this.inStack = this.inStack[:len(this.inStack)-1]
}
}
if len(this.outStack) == 0 {
return -1
} else {
value := this.outStack[len(this.outStack)-1]
this.outStack = this.outStack[:len(this.outStack)-1]
return value
}
}
剑指 Offer 30. 包含min函数的栈
思路:
本题比较难的我感觉是pop栈中元素的时候,还能保证不干扰到min方法:因为这里要求方法时间复杂度为O(1),所以可以用空间换时间的思路。
核心思路如下:
1.用两个栈实现
2.栈A添加元素时,同时考虑栈B:若新增的元素小于或等于栈B顶部元素,则也压入此元素到栈B;栈B就是起到维护一张顺序表的作用
3.弹出元素时思路相同,栈A要弹出的元素若与栈B顶部元素相同,栈B顶部元素同样弹出
JAVA实现:
class MinStack {
Deque<Integer> stack1, stack2;
public MinStack() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}
public void push(int x) {
stack1.addFirst(x);
if (stack2.isEmpty() || stack2.peekFirst() >= x) {
stack2.addFirst(x);
}
}
public void pop() {
if (stack1.removeFirst().equals(stack2.peekFirst())) {
stack2.removeFirst();
}
}
public int top() {
return stack1.peekFirst();
}
public int min() {
return stack2.peekFirst();
}
}
go实现:
list包实现
type MinStack struct {
stackA *list.List
stackB *list.List
}
/** initialize your data structure here. */
func Constructor() MinStack {
return MinStack{
stackA: list.New(),
stackB: list.New(),
}
}
func (this *MinStack) Push(x int) {
this.stackA.PushBack(x)
if this.stackB.Len() == 0 || this.stackB.Back().Value.(int) >= x {
this.stackB.PushBack(x)
}
}
func (this *MinStack) Pop() {
if this.stackA.Back().Value.(int) == this.stackB.Back().Value.(int) {
this.stackB.Remove(this.stackB.Back())
}
this.stackA.Remove(this.stackA.Back())
}
func (this *MinStack) Top() int {
return this.stackA.Back().Value.(int)
}
func (this *MinStack) Min() int {
return this.stackB.Back().Value.(int)
}
切片实现
type MinStack struct {
stack1 []int
stack2 []int
}
func Constructor() MinStack {
return MinStack{}
}
func (this *MinStack) Push(x int) {
this.stack1 = append(this.stack1, x)
if len(this.stack2) == 0 || this.stack2[len(this.stack2)-1] >= x {
this.stack2 = append(this.stack2, x)
}
}
func (this *MinStack) Pop() {
popValue := this.stack1[len(this.stack1)-1]
this.stack1 = this.stack1[:len(this.stack1)-1]
if popValue == this.stack2[len(this.stack2)-1] {
this.stack2 = this.stack2[:len(this.stack2)-1]
}
}
func (this *MinStack) Top() int {
return this.stack1[len(this.stack1)-1]
}
func (this *MinStack) Min() int {
return this.stack2[len(this.stack2)-1]
}
第 2 天 链表(简单)
剑指 Offer 06. 从尾到头打印链表
思路:
这个其实是对链表的翻转,翻转链表相关题目非常多,但是对于此题比较容易实现的方法是使用栈先进后出的特点:
把链表每个节点压进栈,然后再弹出
JAVA实现:
class Solution {
public int[] reversePrint(ListNode head) {
Deque<ListNode> stack = new LinkedList<>();
ListNode temp = head;
while (temp != null) {
stack.addFirst(temp);
temp = temp.next;
}
int size = stack.size();
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = stack.removeFirst().val;
}
return result;
}
}
GO实现
go到没有使用栈的方法,本人对go也是初学,只能运用最简单的办法,遍历两次链表,第一次为了统计元素个数,初始化数组后,第二次遍历倒着填充数组即可
func reversePrint(head *ListNode) []int {
index := -1
p := head
for p != nil {
index++
p = p.Next
}
res := make([]int, index+1)
p = head
// 倒序填充
for p != nil {
res[index] = p.Val
index--
p = p.Next
}
return res
}
好一点做法,翻转链表
func reversePrint(head *ListNode) []int {
var tmp = head
res := make([]int, 0)
if tmp == nil {
return nil
}
tmp = flip(tmp)
for tmp != nil {
res = append(res, tmp.Val)
tmp = tmp.Next
}
return res
}
func flip(tmp *ListNode) *ListNode {
if tmp.Next == nil {
return tmp
}
lastNode := flip(tmp.Next)
tmp.Next.Next = tmp
tmp.Next = nil
return lastNode
}
剑指 Offer 24. 反转链表
比较简单,此题是所有翻转链表题目最简单最核心的思想。
java实现:
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode last = reverseList(head.next);
head.next.next = head;
head.next = null;
return last;
}
}
go实现
func reverseList(head *ListNode) *ListNode {
if head == nil || head.Next == nil {
return head
}
last := reverseList(head.Next)
head.Next.Next = head
head.Next = nil
return last
}
剑指 Offer 35. 复杂链表的复制
思路:
复制普通链表比较容易实现,就是创建只有值的节点,通过next一个个串起来。
对于复杂链表,由于随机节点指向混乱,我们无法像能将next指向的节点创建出并且一个个串起来那样去处理random节点。
核心理念:
next是个单向线性结构对于遍历很友好,random想要复制需要借助next,将每个节点作为一个整体结构看待:利用哈希表的查询特点,构建原链表节点 和 新链表对应节点的键值对映射关系,再遍历构建新链表各节点的 next 和 random 引用指向即可
java实现:
class Solution {
public Node copyRandomList(Node head) {
if(head == null)
{
return null;
}
Node cur = head;
Map<Node, Node> map = new HashMap<>();
// 复制各节点,建立 “原节点 -> 新节点” 的 Map 映射
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);
}
}
go实现
func copyRandomList(head *Node) *Node {
if head == nil {
return nil
}
var nodeMap map[*Node]*Node
nodeMap = make(map[*Node]*Node, 1)
cur := head
for cur != nil {
tmpNode := &Node{Val: cur.Val}
nodeMap[cur] = tmpNode
cur = cur.Next
}
cur = head
for cur != nil {
nodeMap[cur].Next = nodeMap[cur.Next]
nodeMap[cur].Random = nodeMap[cur.Random]
cur = cur.Next
}
return nodeMap[head]
}
第 5 天 查找算法(中等)
剑指 Offer 04. 二维数组中的查找
思路:
这道题大家上来第一时间肯定觉得太水了,就是将二维数组遍历一下呗,但是题目说要求高效,肯定还有更快的解法。这里暴力求解就不说了,只分析更快解法的思路:
首先,这道题描述有个特点,从上到下,从左到右,元素都是由小到大排列。那么解题的思路就是显而易见了,将输入的数字与每行的最大元素(即最右边的作对比),大于最右边的值的话,那就直接跳过这一行,再去下一行比较(比暴力求解省了很多比较步骤),反之则减少行数,同样是缩小了范围。
实现:
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. 旋转数组的最小数字
思路:
查找最小数字,根据题目特点,使用二分法来解比较合适
实现:
class Solution {
public int minArray(int[] numbers) {
int i = 0;
int j = numbers.length - 1;
while (i < j) {
int m = (i + j) / 2;
if (numbers[m] > numbers[j]) {
i = m + 1;
} else if (numbers[m] < numbers[j]) {
j=m;
}else {
j--;
}
}
return numbers[i];
}
}
剑指 Offer 50. 第一个只出现一次的字符
思路:
这里应用了hashMap的特点:键值对,键存储字符,值存储字符出现的次数
遍历字符串,将对应字符出现的频数保存好,再遍历一次字符串,将第一个频数为1的字符返回。
实现:
class Solution {
public char firstUniqChar(String s) {
Map<Character, Integer> integerMap = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
integerMap.put(c, integerMap.getOrDefault(c, 0) + 1);
}
for (int i = 0; i < s.length(); i++) {
if (integerMap.get(s.charAt(i)) == 1) {
return s.charAt(i);
}
}
return ' ';
}
}
第 6 天 搜索与回溯算法(简单)
剑指 Offer 32 - I. 从上到下打印二叉树
思路:
感觉这道题对于还没咋做过二叉树题的同学来说,非常适合用来作为二叉树练习的第一题,因该不算是中等难度题,思路简单,关键在于实现方法不出纰漏。
首先考虑用一个队列结构存储二叉树头节点;
然后再创建一个list作为存储整数的容器;
其次最核心的就是循环遍历,先序遍历;
循环完了再遍历list,放进数组返回;
最后别忘记,特殊情况,root为空,要直接返回[]。
实现:
class Solution {
public int[] levelOrder(TreeNode root) {
if (root == null) {
return new int[0];
}
Deque<TreeNode> nodes = new LinkedList<>();
List<Integer> list = new ArrayList<>();
nodes.addFirst(root);
while (!nodes.isEmpty()) {
TreeNode node = nodes.removeLast();
list.add(node.val);
if (node.left != null) {
nodes.addFirst(node.left);
}
if (node.right != null) {
nodes.addFirst(node.right);
}
}
return list.stream().mapToInt(Integer::intValue).toArray();
}
}
剑指 Offer 32 - II. 从上到下打印二叉树 II
思路:
与上题有类似之处,基于上题思路,最关键的地方在于将list分层存储,这里就需要考虑每层list中nodes的数量了,所以需要用for循环,通过每层nodes的数量来控制单独一个list中存储的元素个数。
实现:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> nodes = new LinkedList<>();
List<List<Integer>> lists = new ArrayList<>();
if (root != null) {
nodes.offer(root);
}
while (!nodes.isEmpty()) {
List<Integer> list = new ArrayList<>();
for (int i = nodes.size(); i > 0; i--) {
TreeNode node = nodes.poll();
list.add(node.val);
if(node.left!=null){
nodes.offer(node.left);
}
if(node.right!=null){
nodes.offer(node.right);
}
}
lists.add(list);
}
return lists;
}
}
剑指 Offer 32 - III. 从上到下打印二叉树 III
思路:
这道题核心在判断层数,单层和双层,数据遍历顺序相反,最开始按照层数取余来判断单双层;放置元素的结构使用LinkedList,好处是可以根据单双层来应用不同的添加方法。单层使用addList方法添加元素,双层使用addFirst添加元素。
实现:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> nodes = new LinkedList<>();
List<List<Integer>> lists = new ArrayList<>();
if (root != null) {
nodes.add(root);
}
while (!nodes.isEmpty()) {
LinkedList<Integer> tmp = new LinkedList<>();
for (int i = nodes.size(); i > 0; i--) {
TreeNode node = nodes.poll();
if (lists.size() % 2 == 0) {
tmp.addLast(node.val);
} else {
tmp.addFirst(node.val);
}
if (node.left != null) {
nodes.add(node.left);
}
if (node.right != null) {
nodes.add(node.right);
}
}
lists.add(tmp);
}
return lists;
}
}
第 7 天 搜索与回溯算法(简单)
剑指 Offer 26. 树的子结构
思路:
这个题目就是判断两个二叉树A,B,是否A包含B。
下意识想到需要用到递归,具体解法还是比较巧妙,没有一定的二叉树刷题经验可能不好想,
主要包含两个函数:
第一个函数:B结构是否是A的一部分
条件是,B为空直接返回true;A不为空并且A和B的值相同,便递归下一节点继续比较,直到B为空为止,若是能递归到这最后一步,说明B是A的一部分。
第二个函数:B结构是否是A的左子树或者右子树的一部分
因为,B未必是A的一部分,B也有可能是A子树的一部分,所以需要用到这一函数:
条件是B是A左子树的一部分,或者B是A右子树的一部分,不断递归直到A和B都不为null的情况下,执行第一个函数判断B是否包含于A,得到最终的结果;
实现:
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
return (A!=null&&B!=null)&&(contain(A,B)||isSubStructure(A.left,B)||isSubStructure(A.right,B));
}
boolean contain(TreeNode A, TreeNode B) {
if (B == null) {
return true;
}
if (A == null || A.val != B.val) {
return false;
}
return contain(A.left,B.left)&&contain(A.right,B.right);
}
}
剑指 Offer 27. 二叉树的镜像
思路:
最基本的递归运用,递归方法内容是:交换传入根节点的两个子节点;终止条件是传入的根节点为空;
实现:
class Solution {
public TreeNode mirrorTree(TreeNode root) {
//终结递归的条件
if(root == null){
return null;
}
//递归的核心处理逻辑
TreeNode emp = root.left;
root.left = root.right;
root.right = emp;
//递归
mirrorTree(root.left);
mirrorTree(root.right);
//返回递归终止条件与上面对应
return root;
}
}
剑指 Offer 28. 对称的二叉树
思路:
这个题目需要自己动手画一下图了,在我看来题目中给了一个三层的树,不太好看,自己再加一层,找一下递归的规律:
其实可以分解为:
这就不难发现,其实是递归方法就是,判断两个子节点值要相同,并且子节点的结构对称。
实现:
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null || compare(root.left, root.right);
}
boolean compare(TreeNode L, TreeNode R) {
if (L == null && R == null) {
return true;
}
if (L == null || R == null || L.val != R.val) {
return false;
}
return compare(L.left, R.right) && compare(L.right, R.left);
}
}
第 8 天 动态规划(简单)
剑指 Offer 10- I. 斐波那契数列
思路:
这道题如果仅仅只考虑递归的话,那是十分简单的递归入门题,可是一提交发现事情没那么简单:
超时了(尴尬,不是简单题么。。。);
问题就是,有大量的重复计算,解决办法想到了两种:
第一个:利用循环,自下而上,直接将每一步的结果存储,为下一步做准备:
class Solution {
public int fib(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
int[] tmp = new int[n+1];
tmp[0]=0;
tmp[1]=1;
for (int i = 2; i < (n+1); i++) {
tmp[i]=(tmp[i-2]+tmp[i-1])%1000000007;
}
return tmp[n];
}
}
第二个:用数组记忆每一次递归的结果概率递归方法,这样就避免了大量的重复计算;
class Solution {
public int fib(int n) {
int[] tmp = new int[n + 1];
return fib2(n, tmp);
}
public int fib2(int n, int[] tmp) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
}
if (tmp[n] > 0) {
return tmp[n];
}
tmp[n] = (fib2(n - 2, tmp) + fib2(n - 1, tmp))%1000000007;
return tmp[n];
}
}
动态规划:
class Solution {
public int fib(int n) {
int a = 0, b = 1, sum;
for(int i = 0; i < n; i++){
sum = (a + b) % 1000000007;
a = b;
b = sum;
}
return a;
}
}
剑指 Offer 10- II. 青蛙跳台阶问题
思路:
与上题思路相似,我习惯用数组解决;当然题解推荐的动态规划也非常简练看着,需要多多研究以下如下推荐解法:
class Solution {
public int numWays(int n) {
int a = 1, b = 1, sum;
for (int i = 0; i < n; i++) {
sum = (a+b)%1000000007;
a=b;
b=sum;
}
return a;
}
}
我的实现:
class Solution {
public int numWays(int n) {
int[] tmp = new int[n+1];
return jump(n, tmp);
}
public int jump(int n, int[] tmp) {
if (n==0||n==1) {
return 1;
}
if(tmp[n]>0){
return tmp[n];
}
tmp[n]=(jump(n-2,tmp)+jump(n-1,tmp))%1000000007;
return tmp[n];
}
}
剑指 Offer 63. 股票的最大利润
思路:
刷题到这,我才渐渐感受到动态规划的巧妙,里面的递归思想说不出的巧妙,这种题关键还是要将庞大的循环以及判断简化;暴力求解时间复杂度O(N2)
用优秀题解的思路讲就是股票从最后一天的价格来看,当前最大的利润是:A(最后一天前最大的利润)和B(最后一天的价格减去之前最低的价格,即当天卖的话最大的利润)做对比;
如此往复循环,每天都是做同样的事情,将今天最大利润与之前对比,直到遍历到最后一天,能得到结果;时间复杂度变成O(N)
实现:
class Solution {
public int maxProfit(int[] prices) {
int cost = Integer.MAX_VALUE, profit = 0;
for(int price : prices) {
cost = Math.min(cost, price);
profit = Math.max(profit, price - cost);
}
return profit;
}
}
第 9 天 动态规划(中等)
剑指 Offer 42. 连续子数组的最大和
思路:
这个思路太难写了(捂脸),能够意会,讲不出来好难受。。。直接看leecode题解吧。
实现:
class Solution {
public static void main(String[] args) {
int[] nums = {-2, 1, -3, 4, -1};
System.out.println(maxSubArray(nums));
}
public static int maxSubArray(int[] nums) {
int res = nums[0];
for (int i = 1; i < nums.length; i++) {
nums[i] += Math.max(nums[i - 1], 0);
res = Math.max(res, nums[i]);
}
return res;
}
}
剑指 Offer 47. 礼物的最大价值
思路:
感觉类似上台阶问题的变种,思路是:
假设以及走完了,其中路径上的每一格,要判断具体是自上而来还是自左而来(比较两者大小,取其大者);
要注意特殊情况,在起始点(0,0),是没有来源的;
在边界上时(最上或最左),只有一个可能来源;
实现:
class Solution {
public int maxValue(int[][] grid) {
int x = grid.length, y = grid[0].length;
for (int i = 0; i < x; i++) {
for (int j = 0; j < y; j++) {
if (i == 0 && j == 0) {
continue;
}
if (i == 0) {
grid[i][j] += grid[i][j - 1];
} else if (j == 0) {
grid[i][j] += grid[i - 1][j];
}else {
grid[i][j]+=Math.max(grid[i][j-1],grid[i-1][j]);
}
}
}
return grid[x-1][y-1];
}
}
第 10 天 动态规划(中等)
剑指 Offer 46. 把数字翻译成字符串
思路:
核心是把数字转化为字符串,遍历中两个两个截取,判断是否是在能够转化为字母的范围内(10到25);
比如遍历到第i个字符Xi:
符合条件(10=<X(i-1)Xi<=25)的话意味着能够直接添加在字符X(i-1)后面,设X(i-1)的翻译数为f(i-1);还着能够X(i-1)Xi作为一个整体,此时翻译数为f(i-2);最终总翻译数为f(i)=f(i-1)+f(i-2)。
第一个例子:
1225
遍历到5的时候,25在10到25范围中;
所以不仅可以直接作为一个字符加到122所有排列组合的后面,此时翻译数为3,是122的翻译数
1,2,2,5
12,2,5
1,22,5
还能够25作为一个整体,排在12的后面,此时翻译数是2,是12的翻译数
1,2,25
12,25
最终的翻译数2+3=5个。
第二个例子:
12258
遍历到8的时候,58不在10到25范围中;
所以只可以作为一个字符加到1225所有排列组合的后面,此时翻译数为5,是1225的翻译数
1,2,2,5,8
12,2,5,8
1,22,5,8
1,2,25,8
12,25,8
最终的翻译数5个。
实现:
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);
int a = 1, b = 1;
for(int i = 2; i <= s.length(); i++) {
String tmp = s.substring(i - 2, i);
int c = tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0 ? a + b : a;
b = a;
a = c;
}
return a;
}
}
剑指 Offer 48. 最长不含重复字符的子字符串
思路:
这种题技巧性非常强,发现动态规划的题多做几道其中的重点是在于理解题目,找到最核心的点,不能用生活中的正常逻辑去理解题目,脑子不是计算机,重复多了就乱。
比如此题,正常顺着题目意思理解是将所有字串找出来比较长度大小,但这种暴力求解的方法,可想而知时间复制度是太高了点。
动态规划的方法:
我们应该重点关注返回的结果,就是最大字串数量,在遍历的过程中,动态变化这个数字,直到遍历完,返回。
利用hashMap存储字符,记录每个字符的索引,出现相同字符时将索引与上一次出现此字符时的索引相减,得到的是这一段不重复的字串的数量。
所有字符第一次出现时,结果直接加1.
实现:
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);
dic.put(s.charAt(j), j);
tmp = tmp < j - i ? tmp + 1 : j - i;
res = Math.max(res, tmp);
}
return res;
}
}
第 11 天 双指针(简单)
剑指 Offer 18. 删除链表的节点
思路:
用链表做删除非常高效了,核心就一个:
假如要删除的节点为a;用指向a的节点重新指向a下一个节点就完了。
只不过要注意一些小细节,实现代码中注释有写
实现:
class Solution {
public ListNode deleteNode(ListNode head, int val) {
//要删的是头节点的话直接返回次节点
if (head.val == val) {
return head.next;
}
ListNode pre = head, cur = head.next;
//遍历链表,直到找到值为val的节点或者遍历完为止
while (cur != null && cur.val != val) {
pre=cur;
cur=cur.next;
}
//cur!=null意味着找到了要删除的节点,直接改变pre的指向即可
if(cur!=null){
pre.next=cur.next;
}
return head;
}
}
剑指 Offer 22. 链表中倒数第k个节点
思路:
这个题用两个循环就能解决。假设链表有N个节点,找出倒数第k个节点,感觉有点别扭,但是正过来想就是找出正数第N-k个节点。
第一个循环用来计数,第二个循环找出结果;
实现:
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode a = head, b = head;
for (int i = 0; i < k; i++) {
a = a.next;
}
while (a != null) {
a = a.next;
b = b.next;
}
return b;
}
}
第 12 天 双指针(简单)
剑指 Offer 25. 合并两个排序的链表
思路:
比较重要的是要想到用一个新的“容器”来装结果:
只要想到能新创建一个伪头节点,这道题就豁然开朗了,之后就是单纯的遍历比大小排序,最后注意将没遍历完的链表“尾巴”接上去。
实现:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode res = new ListNode(0);
ListNode cur = res;
while (l1 != null && l2 != null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur =cur.next;
}
cur.next = l1 != null ? l1 : l2;
return res.next;
}
}
剑指 Offer 52. 两个链表的第一个公共节点
思路:
这道题我就想写一个双指针的解法,题目思路是不停的遍历两个链表,其中一个完了之后,再换到另一个链表的头部继续遍历,相互追逐肯定会走到共同的一个节点上。仔细品,若是没有共同的节点,那一定会走到同时为null的地步,不用担心无限循环下去。
实现:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA==null||headB==null) {
return null;
}
ListNode runA = headA, runB= headB;
while (runA != runB) {
runA = runA != null ? runA.next : headB;
runB = runB != null ? runB.next : headA;
}
return runA;
}
}
第 13 天 双指针(简单)
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
思路:
这道题用双指针来做,从两头同时往中间走,左边碰到偶数就停,右边碰到奇数就停,然后将这两个数交换位置,外面套一层循环,直到双指针相遇为止。
实现:
class Solution {
public int[] exchange(int[] nums) {
int i = 0, j = nums.length - 1, tmp;
while (i < j) {
while (i < j && (nums[i] & 1) == 1) {
i++;
}
while (i < j && (nums[j] & 1) == 0) {
j--;
}
tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
return nums;
}
}
剑指 Offer 57. 和为s的两个数字
思路:
使用双指针来做,依旧是对撞双指针,两头开始相加,和大于target的话,右指针左移一位;
和小于target的话,左指针右移一位。
实现:
class Solution {
public int[] twoSum(int[] nums, int target) {
int i = 0, j = nums.length - 1;
while (i < j) {
int s = nums[i] + nums[j];
if (s < target) {
i++;
} else if (s > target) {
j--;
} else {
return new int[]{nums[i], nums[j]};
}
}
return new int[0];
}
}
剑指 Offer 58 - I. 翻转单词顺序
思路:
这个算是用到双指针了吧感觉,就是用两个变量来确定截取的单词,其中涉及空格的处理。
还是挺巧妙的。
实现:
class Solution {
public String reverseWords(String s) {
//先去除空格
s = s.trim();
int i = s.length() - 1, j = i;
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移动至下一处单词结尾字符处
j = i;
}
return res.toString().trim();
}
}
第 14 天 搜索与回溯算法(中等)
剑指 Offer 12. 矩阵中的路径
思路:
这道题使用到DFS思想,首先两次循环遍历二维数组,其次再写dfs方法:
方法中重要的点是理解方法递归调用的边界条件,递归调用超过数组下标就返回false,若是一直这样,直到双重循环遍历结束,还没有触发返回true就说明没找到这样的单词;
若是每个单词字母都能够在上下左右方向中寻找到符合的下个字母,最终会返回true。
注意:终止条件是递归遍历完单词的每个字母;注意先暂时清除数组中此位置字符,因为怕下一次递归再返回原路寻找;
实现:
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, words, i, j, 0)) {
return true;
}
}
}
return false;
}
public boolean dfs(char[][] board, char[] words, int i, int j, int k) {
if (i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != words[k]) {
return false;
}
if (k == words.length - 1) {
return true;
}
board[i][j] = '\0';
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];
return res;
}
}
剑指 Offer 13. 机器人的运动范围
思路:
这道题用的深度优先搜索DFS,说实话还是没能很好理解。。。
实现:
class Solution {
int m, n, k;
boolean[][] visited;
public int movingCount(int m, int n, int k) {
this.m = m;
this.n = n;
this.k = k;
this.visited = new boolean[m][n];
return dfs(0, 0, 0, 0);
}
public int dfs(int i, int j, int si, int sj) {
if (i >= m || j >= n || k < si + sj || visited[i][j]) {
return 0;
}
visited[i][j] = true;
return 1 + dfs(i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj) + dfs(i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8);
}
}
第 15 天 搜索与回溯算法(中等)
剑指 Offer 34. 二叉树中和为某一值的路径
实现及思路注释:
import java.util.LinkedList;
import java.util.List;
class Solution {
/**
* 创建一个容器用来存放结果,一个用来存储过程的链路
*/
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
trav(root,target);
return res;
}
void trav(TreeNode root,int tar){
//结束条件
if(root==null){
return;
}
//存储链路的值
path.add(root.val);
tar-=root.val;
//符合结果的条件:满足再无子节点,而且刚好tar减去节点的值等于零,说明这一条链路的值和相加等于target
if(tar==0&&root.left==null&&root.right==null){
//因为符合的链不止一条,所以存起来
res.add(new LinkedList<>(path));
}
trav(root.left,tar);
trav(root.right,tar);
//因为path要重复使用,所以用一次就要清空一次
path.removeLast();
}
}
剑指 Offer 36. 二叉搜索树与双向链表
思路:暂时没搞懂
实现:
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if(root == null){
return null;
}
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
void dfs(Node cur) {
if(cur == null){
return;
}
dfs(cur.left);
if(pre != null){
pre.right = cur;
}
else{
head = cur;
}
cur.left = pre;
pre = cur;
dfs(cur.right);
}
}
剑指 Offer 54. 二叉搜索树的第k大节点
思路:
就是遍历过程中数数,这个数用中序遍历,倒着数,同时k减一,当k为0时,说明找到这个节点
实现:
class Solution {
int res, k;
public int kthLargest(TreeNode root, int k) {
this.k = k;
dfs(root);
return res;
}
void dfs(TreeNode root) {
if(root == null) {
return;
}
dfs(root.right);
if(k == 0) {
return;
}
if(--k == 0) {
res = root.val;
}
dfs(root.left);
}
}
第 16 天 排序(简单)
剑指 Offer 45. 把数组排成最小的数
思路:
就是量大一点,思路并不复制,总体分三步,先将字符串装进数组,然后排序,最后拼接起来。
实现:
class Solution {
public String minNumber(int[] nums) {
//创建一个数组,并把每个字符串装进去
String[] strs = new String[nums.length];
for(int i = 0; i < nums.length; i++) {
strs[i] = String.valueOf(nums[i]);
}
//排序字符串
quickSort(strs, 0, strs.length - 1);
//拼接起来
StringBuilder res = new StringBuilder();
for(String s : strs) {
res.append(s);
}
return res.toString();
}
void quickSort(String[] strs, int l, int r) {
if(l >= r){
return;
}
int i = l, j = r;
String tmp = strs[i];
while(i < j) {
while((strs[j] + strs[l]).compareTo(strs[l] + strs[j]) >= 0 && i < j) {
j--;
}
while((strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0 && i < j) {
i++;
}
tmp = strs[i];
strs[i] = strs[j];
strs[j] = tmp;
}
strs[i] = strs[l];
strs[l] = tmp;
quickSort(strs, l, i - 1);
quickSort(strs, i + 1, r);
}
}
剑指 Offer 61. 扑克牌中的顺子
思路与实现:
class Solution {
public boolean isStraight(int[] nums) {
int joker = 0;
//排序
Arrays.sort(nums);
//数一下有几个王
for(int i = 0; i < 4; i++) {
if(nums[i] == 0) {
joker++;
}
//除了王以外有连续的,直接为false
else if(nums[i] == nums[i + 1]){
return false;
}
}
//自己动手试一下,挺巧妙的,用最大的数减去除了王以外最小值,小于五就能用王补上
return nums[4] - nums[joker] < 5;
}
}
第 17 天 排序(中等)
剑指 Offer 41. 数据流中的中位数
思路:
这道题下意识就是使用了list做,发现思路很简单,三个方法就是初始化,添加元素并排序,判断不同个数返回对应结果而已,但是这么做发现用时特别多,空间也特别大。
class MedianFinder {
List<Integer> list = null;
public MedianFinder() {
list = new ArrayList<>();
}
public void addNum(int num) {
list.add(num);
Collections.sort(list);
}
public double findMedian() {
if (list.size() % 2 == 0) {
return (double) (list.get(list.size() / 2) + list.get(list.size() / 2 - 1)) / 2.0;
} else {
return list.get((list.size() - 1) / 2);
}
}
}
想要优化的话,推荐使用有限队列来做,具体思路及实现 如下:
实现:
import java.util.PriorityQueue;
import java.util.Queue;
class MedianFinder {
Queue<Integer> a, b;
public MedianFinder() {
//创建两个优先队列,插入的时候可以自动排序,一个正序,一个倒序
a = new PriorityQueue<>();
b = new PriorityQueue<>((x, y) -> (y - x));
}
public void addNum(int num) {
//两个队列中数量相同时,插入b,并且弹出栈顶元素给a
//两个队列中数量不同时,插入a,并且弹出栈顶元素给b
if (a.size() == b.size()) {
a.add(num);
b.add(a.poll());
} else {
b.add(num);
a.add(b.poll());
}
}
public double findMedian() {
//最后根据两个队列中数的个数不同,返回不同的计算结果
return a.size() != b.size() ? a.peek() : (a.peek() + b.peek()) / 2.0;
}
}
第 18 天 搜索与回溯算法(中等)
剑指 Offer 55 - I. 二叉树的深度
思路:
没啥好说的,递归
实现:
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
return Math.max(maxDepth(root.right), maxDepth(root.left)) + 1;
}
}
剑指 Offer 55 - II. 平衡二叉树
思路:
后序遍历,当节点的左右深度差大于等于2的时候,直接返回-1,说明不是平衡二叉树;
小于2时,返回左右子节点最大深度+1;
实现:
class Solution {
public boolean isBalanced(TreeNode root) {
return recur(root) != -1;
}
private int recur(TreeNode root) {
if (root == null) {
return 0;
}
int left = recur(root.left);
if (left == -1) {
return -1;
}
int right = recur(root.right);
if (right == -1) {
return -1;
}
return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
}
}
第 19 天 搜索与回溯算法(中等)
剑指 Offer 64. 求1+2+…+n
思路:
看到题目会想到递归,可是问题在于题目不让使用循环以及条件判断语句,递归的终止条件怎么写是个问题。
一个巧妙的方法是利用逻辑运算符&&的短路效应来控制递归。
实现:
class Solution {
public int sumNums(int n) {
boolean a = n > 1 && (n += sumNums(n - 1)) > 0;
return n;
}
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
思路:
这道题关键在于二叉搜索树,节点值小于根节点的都在根节点的左边,大于根节点在右边。
若是p,q一个比根节点大,一个比根节点小,那么最近公共节点只能是root;
若是p,q都比根节点小,那么递归到左子节点接着判断;
若是p,q都比根节点大,那么递归到右子节点接着判断;
所以通过递归可以实现。
实现:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root.val > p.val && root.val > q.val) {
return lowestCommonAncestor(root.left, p, q);
}
if (root.val < p.val && root.val < q.val) {
return lowestCommonAncestor(root.right, p, q);
}
return root;
}
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
思路:
是上一题的进阶版,少了二叉搜索树的特性。递归解决的话,需要遍历二叉树,把条件都考虑清楚;
终止条件:root为null或者root等于q,p其中一个。直接返回root;
递归左右节点,返回值有以下几种情况:
1、左右节点都是null,则返回null,说明q,p不在这个节点下;
2、左节点为null,右节点不为null,返回right;
2、右节点为null,左节点不为null,返回left;
4、左右节点同时不为空,返回root;
//单节点不为空即有可能p,q其中一个在节点下面,也有可能都在下面,返回就是了,直到顶,能够得到最终的最近公共节点。
实现:
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root;
}
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;
}
}
第 20 天 分治算法(中等)
剑指 Offer 07. 重建二叉树
思路:
感觉比较难,不是很理解
实现:
class Solution {
private Map<Integer, Integer> indexMap;
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left,
int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return null;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = indexMap.get(preorder[preorder_root]);
// 先把根节点建立出来
TreeNode root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
indexMap = new HashMap<Integer, Integer>();
for (int i = 0; i < n; i++) {
indexMap.put(inorder[i], i);
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
}
剑指 Offer 16. 数值的整数次方
思路:
其实挺简单的,主要还是要把情况分析清楚
1、指数为负数,即底数倒数的次方
2、为减少循环次数,可以指数,底数同时处理:指数除以二 ->底数便可做一次平方
整体原理:
实现:
class Solution {
public double myPow(double x, int n) {
if (x == 0) {
return 0;
}
long b = n;
double res = 1.0;
if (b < 0) {
x = 1 / x;
b = -b;
}
while (b > 0) {
if (b % 2 == 1) {
res *= x;
}
b = b / 2;
x *= x;
}
return res;
}
}
剑指 Offer 33. 二叉搜索树的后序遍历序列
思路:
这道题看着绕,实际上用分治、递归捋一遍,找到规律还是挺容易理解的
1、递归内容
- 将数组分成左右两个区间
遍历数组[0,n],找到第一个大于根节点的节点位置,记为m,就可以把树分成左右两个子树。区间为[0,m-1]和[m,n-1];
- 判断是否是二叉搜索树
就保证左子区间范围所有节点都比根节点小,右子区间范围所有节点都比根节点大就行了。
2、终止条件
将节点遍历完了,再无子节点时,就返回true了
只要递归过程中出现不符合二叉搜索树判断条件的情况,出现一个false,整个调用最终返回都是false。
实现:
class Solution {
public boolean verifyPostorder(int[] postorder) {
return recur(postorder, 0, postorder.length - 1);
}
boolean recur(int[] postorder, int i, int j) {
if (i >= j) {
return true;
}
int p = i;
while (postorder[p] < postorder[j]) {
p++;
}
int m = p;
while (postorder[p] > postorder[j]) {
p++;
}
return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
}
}
第 21 天 位运算(简单)
剑指 Offer 15. 二进制中1的个数
思路:
通过位运算能很简易的写出这道题:
用&1来判断二进制数最右边第一位是否是1,
循环计数,整体通过>>>右移。
实现:
public class Solution {
public int hammingWeight(int n) {
int res = 0;
while(n != 0) {
res += n & 1;
n >>>= 1;
}
return res;
}
}
剑指 Offer 65. 不用加减乘除做加法
思路:
这道题题解看似不难,但是实际上根本不好想啊,第一次做的人谁能有思路发现两数的和等于非进位和加上进位这么一个规律。。。
实现:
class Solution {
public int add(int a, int b) {
while(b != 0) {
//计算进位
int c = (a & b) << 1;
//非进位和
a ^= b;
b = c;
}
return a;
}
}
第 22 天 位运算(中等)
剑指 Offer 56 - I. 数组中数字出现的次数
思路:
思路非常巧妙,用了位运算异或的方法,再加上分组。
异或的作用是把所有相同的数排除,分组是为了能够找到这两个数字。
具体步骤如下:
1.先对所有数字进行一次异或,得到两个出现一次的数字的异或值。在异或结果中找到任意为 1 的位。
2.根据这一位对所有的数字进行分组。
3.在每个组内进行异或操作,得到两个数字。
实现:
class Solution {
public int[] singleNumbers(int[] nums) {
int ret = 0;
for (int n : nums) {
ret ^= n;
}
int div = 1;
//在异或结果中找到第一个不为0的位置
while ((div & ret) == 0) {
div <<= 1;
}
int a = 0, b = 0;
//分组了,然后再异或就好了
for (int n : nums) {
if ((div & n) != 0) {
a ^= n;
} else {
b ^= n;
}
}
return new int[]{a, b};
}
}
剑指 Offer 56 - II. 数组中数字出现的次数 II
思路:
核心思想:是把所有数做异或操作,得到的结果每一位和3求余,最终得到的就是那只出现一个的数。
具体实现分三步:
1、异或操作
2、遍历取余
3、整理结果
实现:
class Solution {
public int singleNumber(int[] nums) {
int[] counts = new int[32];
//遍历数组
for(int num : nums) {
for(int j = 0; j < 32; j++) {
//记录所有数字对应位数1出现的次数
counts[j] += num & 1;
num >>>= 1;
}
}
int res = 0, m = 3;
//取余操作,再把结果用或运算合在一起返回
for(int i = 0; i < 32; i++) {
res <<= 1;
res |= counts[31 - i] % m;
}
return res;
}
}
第 23 天 数学(简单)
剑指 Offer 66. 构建乘积数组
思路:
本题理解倒是好理解,但是不让用除法,就得好好琢磨一下了。
思路是假如求B[i] 的值,那么先把A[0]到A[i-1]的乘积算出来,再与A[i+1]到A[n-1]的积算出来。
这样子就好实现,具体如下:
实现:
class Solution {
public int[] constructArr(int[] a) {
int len = a.length;
if (len == 0) {
return new int[0];
}
int[] b = new int[len];
b[0] = 1;
int tmp = 1;
//将前部分的积存在b容器中,此时数组b还只是半成品
for (int i = 1; i < len; i++) {
b[i] = b[i - 1] * a[i - 1];
}
//第二步,需要倒序来遍历数组,给每个b中值补充上后半部分乘积
for (int i = len - 2; i >= 0; i--) {
tmp *= a[i + 1];
b[i] *= tmp;
}
return b;
}
}
第 24 天 数学(中等)
剑指 Offer 14- I. 剪绳子
思路:
这道题主要是发现规律,不用想的太复杂,其实想要得到乘积最大,就将其尽量分成若干个3相乘就好了。自己动手测试一下,便能发现此技巧。
实现:
class Solution {
public int cuttingRope(int n) {
if (n <= 3) {
return n - 1;
}
int a = n / 3, b = n % 3;
if (b == 0) {
return (int) Math.pow(3, a);
}
if (b == 1) {
return (int) Math.pow(3, a - 1) * 4;
}
return (int) Math.pow(3, a) * 2;
}
}
第 25 天 模拟(中等)
剑指 Offer 29. 顺时针打印矩阵
思路:
四个for循环,依次打印四条边;
然后判断边界值,逐渐缩圈;
实现:
class Solution {
public int[] spiralOrder(int[][] matrix) {
if (matrix.length == 0) {
return new int[0];
}
int l = 0, r = matrix[0].length - 1, t = 0, b = matrix.length - 1, x = 0;
int[] res = new int[(r + 1) * (b + 1)];
while (true) {
for (int i = l; i <= r; i++) {
res[x++] = matrix[t][i];
}
if (++t > b) {
break;
}
for (int i = t; i <= b; i++) {
res[x++] = matrix[i][r];
}
if (l > --r) {
break;
}
for (int i = r; i >= l; i--) {
res[x++] = matrix[b][i];
}
if (t > --b) {
break;
}
for (int i = b; i >= t; i--) {
res[x++] = matrix[i][l];
}
if (++l > r) {
break;
}
}
return res;
}
}
剑指 Offer 31. 栈的压入、弹出序列
思路:
就是判断入栈和出栈能否按照顺序对应上:
对于每次入栈元素,都判断一下是否栈顶元素能与弹出序列的栈顶元素对上。对上了,就可以弹出栈顶元素,并且弹出序列元素遍历到下一位,直到压入序列遍历完,判断是否栈内还有元素。没有了就说明符合题目要求。
实现:
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int i = 0;
for(int num : pushed) {
stack.push(num); // num 入栈
while(!stack.isEmpty() && stack.peek() == popped[i]) { // 循环判断与出栈
stack.pop();
i++;
}
}
return stack.isEmpty();
}
}