本文为刷【剑指】备战校招的过程,持续更新
备战虽晚但卷,共勉!
目录,按刷题时间排序
斐波那契数列
写一个函数,输入 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。
一刷 2021.7.13
-
无脑平推法(渣渣能快速想到的就是这一种)
public class Solution { public int Fibonacci(int n) { int x = 0; int y = 1; if(n<1 || n>39){ return 0; } for(int i = 1;i<n;i++){ y = x+y; x = y-x; } return y; } }
-
递归实现
这个解法是百度发到了斐波那契数列的公式得出的,下面que出斐波那契数列的公式
通过以上公式,就能得出以下的算法啦public class Solution { public int Fibonacci(int n) { if(n==0) return 0; if(n==1) return 1; return Fibonacci(n-1) +Fibonacci(n-2); } }
-
数组实现
用一个数组将每个值存起来,需要的时候直接取,不用重复计算
//第一种
public static int Fibonacci2(int n) {
ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
if(n <= 1){
return list.get(n);
}else{
for(int i = 2; i <= n; i++){
list.add(list.get(i-1)+list.get(i-2));
}
return list.get(n);
}
}
//第二种
class Solution {
public int fib(int n) {
if (n<2) return n;
long[] result = new long[n+1];
result[0] = 0;
result[1] = 1;
for(int i = 2; i<=n;i++){
result[i] = result[i-1]+result[i-2];
result[i] %= (Math.pow(10,9) + 7);
}
return (int)result[n];
}
}
用两个栈实现队列
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
一刷 2021.8.2
ps:渣渣第一眼看完题目,这是什么
过一会联想到上午看《大话数据结构》时书上说到的可以用两个栈怼一起,方便入栈和出栈,但好像跟这道题没关系,不过我联想到的,我觉得至少算学习了,普通且自信
但是还是没有任何解题思路,所以我看了题解视频,网址如下
题解链接
二刷 2021.8.10
游客前一次的经验,这次上手就熟悉了很多,比较顺利
-
思路1
要实现队头删除和队尾插入,首先我们需要知道元素入栈是放在栈底的,且在此思路中,栈1和栈2不会同时存在元素,即若队列不为空,则元素要么全在栈1中,要么全在栈2中队尾插入
若元素都在栈2,栈2的栈顶元素为队列的队头,要实现队尾插入,就要栈2的元素全部入栈1,此时栈1的栈顶元素为队尾,插入即可
若元素都在栈1,此时栈顶元素即为队尾,直接在栈顶插入即可队头删除
若元素起初都入了栈1,要删除栈1的栈底元素,就把栈1的元素全部入栈2.此时栈1的栈底元素就变为了栈2的栈顶元素,只需要将其pop出即可
若元素都在栈2,直接pop出栈2的栈顶元素即可
但要注意校验队列是否为空代码实现
class CQueue { //初始化两个栈 private Stack<Integer> stack1; private Stack<Integer> stack2; //队列 public CQueue() { this.stack1 = new Stack<>(); this.stack2 = new Stack<>(); } //队尾插入 public void appendTail(int value) { //若栈1 while(!stack2.isEmpty()){ stack1.push(stack2.pop()); } stack1.push(value); } //删除头部 public int deleteHead() { while(!stack1.isEmpty()){ stack2.push(stack1.pop()); } if(stack2.isEmpty()){ return -1; } return stack2.pop(); } }
-
思路2
要实现队头删除和队尾插入,首先我们需要知道元素入栈是放在栈底的,在此思路中,栈1和栈2可能同时存在元素队尾插入
在思路1的基础上,不论栈2是否为空,我们将元素直接插入到栈1都是正确的,所以我们在插入时,直接将元素入栈1,此时插入的时间复杂度为O(1)队头删除
由思路1,栈2的栈顶元素永远是队列的头部,所以队头删除只需要将栈2中的栈顶元素pop出来即可
但是删除元素需要对栈2做非空校验
若栈2为空,将栈1中的元素pop出来,放到栈2中,此时栈2的栈顶元素还是是队列的头部,栈2中的栈顶元素pop出来即可
若此时仍为空,即队列为空,return -1即可代码实现
class CQueue { //初始化两个栈 private Stack<Integer> stack1; private Stack<Integer> stack2; //队列 public CQueue() { this.stack1 = new Stack<>(); this.stack2 = new Stack<>(); } //队尾插入 public void appendTail(int value) { stack1.push(value); } //删除头部 public int deleteHead() { if(stack2.isEmpty()){ while(!stack1.isEmpty()){ stack2.push(stack1.pop()); } } if(stack2.isEmpty()){ return -1; }else{ return stack2.pop(); } } } ``
二维数组中的查找
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
一刷2021.8.4
- 思路一 暴力破解
无脑但好使
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;
for(int i = 0;i<rows;i ++){
for(int j = 0; j < columns; j++ ){
if(matrix[i][j] == target){
return true;
}
}
}
return false;
}
}
- 思路二 线性查找
由题可知:每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序
从右上角开始找
如果当前元素大于目标值,说明当前元素的下边的所有元素都一定大于目标值,因此往下查找不可能找到目标值,往左查找可能找到目标值。如果当前元素小于目标值,说明当前元素的左边的所有元素都一定小于目标值,因此往左查找不可能找到目标值,往下查找可能找到目标值。
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;
int 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;
}
}
包含min函数的栈
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
一刷 2021.8.10
看到的时候是比较蒙的,所以只能看题解
- new 两个栈,数据栈和辅助栈
- 数据栈用来存储入栈的元素,辅助栈用来动态存储栈中的最小元素,且栈中的最小元素恒为辅助栈的栈顶元素
- 当往栈中添加元素的时候,在入数据栈之前先与辅助栈的栈顶元素比较,
- 若要添加元素小于辅助栈的栈顶元素,即此时栈中的最小元素为添加元素,将该元素分别入辅助栈和数据栈即可
- 若要添加元素大于或等于辅助栈的栈顶元素,即此时栈中的最小元素仍为辅助栈的栈顶元素,故将辅助栈的栈顶元素再次入栈,然后将元素入数据栈
- 当删除栈顶元素的时候,由于栈中的最小元素恒为辅助栈的栈顶元素,只需将数据栈和辅助栈的栈顶元素分别出栈即可
- 返回栈顶元素即返回数据栈的栈顶元素
- 返回栈中的最小元素即返回辅助栈的栈顶元素
class MinStack {
//数据栈
private Stack<Integer> dataStack;
//辅助栈,每当有元素进出数据栈,更新辅助栈的最小值
private Stack<Integer> minStack;
public MinStack() {
//初始化数据栈和辅助栈
dataStack = new Stack<>();
minStack = new Stack<>();
}
public void push(int x) {
//如果辅助栈为空或者x小于辅助栈的栈顶元素
//即此时栈的最小元素应为x,则将x入辅助栈
if(minStack.isEmpty() || x<minStack.peek()){
minStack.push(x);
}else{//如果x大于等于辅助栈的栈顶元素,则将栈顶元素再次入栈
minStack.push(minStack.peek());
}
//将x入数据栈
dataStack.push(x);
}
//删除元素只需将数据栈和辅助栈的栈顶元素分别出栈即可
public void pop() {
dataStack.pop();
minStack.pop();
}
//返回数据栈栈顶元素
public int top() {
return dataStack.peek();
}
//返回栈中的最小元素即返回辅助栈的栈顶元素
public int min() {
return minStack.peek();
}
}
从尾到头打印链表
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
一刷 2021.8.10
- 思路一(辅助栈)
- 有了刚刚包含min函数的栈的经验,看到这道题我想到了辅助栈的做法,即先将链表元素入栈,再将栈顶元素依次入数组即可
class Solution {
public int[] reversePrint(ListNode head) {
Stack<Integer> stack = new Stack<Integer>();
while(head != null){
stack.push(head.val);
head = head.next;
}
int[] res = new int[stack.size()];
for(int i = 0; i< res.length; i++){
res[i] = stack.pop();
}
return res;
}
}
- 思路二(递归)
- 这个方法是看题解看的,思想简单,先遍历数组到head指向null,即遍历到链表的末尾,再将元素倒序填入到数组中即可
- 但是下边这个实现方法着实将递归思想运用到了极致,先递归到链表的末尾,然后再回到每次递归中间继续给数组赋值的操作,言语组织的不太好,请见谅,这个代码本菜鸡昨晚研究了老半天才看懂,懂了之后真的是服,太妙了,也许这就是代码的魅力把!
class Solution {
//new 数组和两个辅助值
int[] res;
int i = 0,j = 0;
public int[] reversePrint(ListNode head) {
solve(head);
return res;
}
public void solve(ListNode head){
//当指针指向null的时候,递归结束,给数组规定大小并结束递归
if(head == null){
res = new int[i];
return;
}
i++;
solve(head.next);
//给数组赋值
res[j] = head.val;
j++;
}
}
反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
一刷 2021.8.11
- 思路一(三指针迭代)
- 我认为自己的思路语言总结远没有图来的清晰,特放上题解
- 下边是我的理解
- 首先定义两个指针,一个cur指向链表的头部,一个pre指向null,一个tmp置空
- 开始迭代,将tmp指向cur的后继,将cur指向pre,即为null,此时原链表的第一个元素与后续失去连接,且后继为null,可视为反转链表的尾部
- 随后将pre指向cur,即反转链表的尾部,将cur指向tmp,即原链表的新头部
- 继续迭代,将tmp指向cur的后继,将cur的后继指向pre,则又一个元素与原链表失去连接,指向反转链表,后续继续迭代即可
。。。。。。。。。。。。。。。。。。。。。。。
class Solution {
public ListNode reverseList(ListNode head) {
//三指针迭代
ListNode cur = head,pre = null;
while(cur!=null){
ListNode tmp = cur.next;
cur.next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
}
- 思路二(递归)
- 递归的思路比较好理解,定义两个节点cur,pre分别指向链表的头部和null
- 开始递归,先将pre指向cur,再将cur指向后继节点
- 一直递归到cur指向null,递归结束
- 保存头节点
- 再回到每一次的递归中,将cur的后继指向pre,即反转链表的过程
class Solution {
public ListNode reverseList(ListNode head) {
//递归
return recur(head,null);
}
public ListNode recur(ListNode cur,ListNode pre){
if(cur == null) return pre; //终止的条件
ListNode res = recur(cur.next,cur); //递归后继节点
cur.next = pre;
return res; //返回反转链表的头结点
}
}
复杂链表的复制
请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
一刷 2021.8.11
不瞒各位大佬说,这是本菜鸡刷的第一道中等难度题,看到题,感觉难点在random,但是不理解,索性直接看题解,一刷我几乎都是照着题解敲的,等待二刷巩固,收工,下班!
- 思路一(哈希表)
哈希表实现比较直观,先只复制数据域,再分别复制其next和random节点的指向
class Solution {
public Node copyRandomList(Node head) {
//若链表为空,返回
if(head == null) return null;
//定义一个指针指向链表的头部
Node cur = head;
//new一个hashmap用来存储复制的链表
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);
}
}
- 思路二(拼接+拆分)
思路我觉得比较简单,主要是实现时候的兜兜绕绕比较头疼
先复制原链表中的每个节点(此时不考虑random),拼接到原链表中,即本来是123,新链表是112233
再构建新链表的random指向,注意新元素的random要指向新元素
最后拆分拼接的链表,代码如下
class Solution {
public Node copyRandomList(Node head) {
//若链表为空,返回
if(head == null) return null;
//定义一个指针指向链表的头部
Node cur = head;
//复制各个节点,并构建拼接链表
while(cur!=null){
Node tmp = new Node(cur.val);
tmp.next = cur.next;
cur.next = tmp;
cur = tmp.next;
}
cur = head;
//构建新节点的random
while(cur!=null){
if(cur.random!=null){
//将新节点的random指向原节点的random的后继节点即原节点的新节点
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
//拆分两个链表
cur = head.next;
Node pre =head,res = head.next;
while(cur.next!=null){
pre.next = pre.next.next;
cur.next = cur.next.next;
pre = pre.next;
cur = cur.next;
}
pre.next = null;
return res;
}
}
替换空格
请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
一刷 2021.8.12
- 思路一(StringBuilder)
通过StringBuilder实现动态的写string,当遍历到空格,就写%20,否则就写值,最后toString即可
class Solution {
public String replaceSpace(String s) {
StringBuilder string = new StringBuilder();
for(int i = 0;i<s.length();i++){
char c = s.charAt(i);
if(c == ' '){
string.append("%20");
}else{
string.append(c);
}
}
return string.toString();
}
}
左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
一刷 2021.8.12
- 思路一
先通过string自带的分割字符串函数substring分割成两个string,再通过StringBuilder进行拼接
class Solution {
public static String reverseLeftWords(String s, int n) {
String s1 = s.substring(n,s.length());
String s2 = s.substring(0,n);
StringBuilder s3 = new StringBuilder();
s3.append(s1);
s3.append(s2);
return s3.toString();
}
}
数组中重复的数字
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
一刷 2021.8.12
- 思路一(哈希表)
使用哈希表依次存储数组的数字,找到重复数字的时候直接返回
class Solution {
public int findRepeatNumber(int[] nums) {
Set<Integer> dic = new HashSet<>();
for(int num:nums){
if(dic.contains(num)){
return num;
}
dic.add(num);
}
return -1;
}
}
- 思路二 原地交换
class Solution {
public int findRepeatNumber(int[] nums) {
int i = 0;
while(i<nums.length){
if(nums[i] == i){
i++;
continue;
}
//结束条件:若有重复值,返回
if(nums[nums[i]] == nums[i]){
return nums[i];
}
int tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp]=tmp;
}
return -1;
}
}
在排序数组中查找数字 I
统计一个数字在排序数组中出现的次数。
一刷 2021.8.13
- 思路一
- 分别通过二分查找target和target-1的右边界,再将减即可得出一个数组重复的次数
class Solution {
public int search(int[] nums, int target) {
return helper(nums,target)-helper(nums,target-1);
}
int helper(int[] nums,int tar){
int i = 0,j=nums.length-1;
while(i<=j){
int m = (i+j)/2;
if (nums[m]<=tar){
i=m+1;
}else
j=m-1;
}
return i;
}
}
0~n-1中缺失的数字
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
一刷 2021.8.13
排序数组中的搜索问题,首先想到 二分法 解决。
- 思路一
这道题的思路其实与上一题有异曲同工之妙
class Solution {
public int missingNumber(int[] nums) {
int i = 0,j=nums.length-1;
while(i<=j){
int m = (i+j)/2;
if(nums[m]==m){
i= m+1;
}else
j = m-1;
}
return i;
}
}
旋转数组的最小数字
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
一刷 2021.8.16
- 思路一(二分法)
- 依旧是上一题的思路,排序数组中的搜索问题,首先想到 二分法 解决
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];
}
}
当出现 nums[m] = nums[j]nums[m]=nums[j] 时,一定有区间 [i, m][i,m] 内所有元素相等 或 区间 [m, j][m,j] 内所有元素相等(或两者皆满足)。对于寻找此类数组的最小值问题,可直接放弃二分查找,而使用线性查找替代。
优化如下:
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{
int x = i;
for(int k = i+1;k<j;k++){
if(numbers[k]<numbers[x]){
x = k;
}
}
return numbers[x];
}
}
return numbers[i];
}
}
第一个只出现一次的字符
在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
一刷 2021.8.16
- 思路一(哈希表)
class Solution {
public char firstUniqChar(String s) {
HashMap<Character,Boolean> dic = new HashMap<>();
char[] sc = s.toCharArray();
for(char c : sc){
dic.put(c, !dic.containsKey(c));
}
for(char c : sc){
if(dic.get(c)){
return c;
}
}
return ' ';
}
}
- 思路二(数组)
要求找出第一个只出现一次的字符,就像哟啊技术每个字符出现了几次,一般都是用哈希表,但是字母最多也就是26个,使用int[] count代替哈希表,key为值,value为出现的次数
先遍历一遍字符统计次数,再遍历一遍返回第一个值为一即出现一次的字符
class Solution {
public char firstUniqChar(String s) {
int[] count = new int[26];
char[] sc = s.toCharArray();
for(char c : sc){
count[c]++;
}
for(char c : sc){
if(count[c] == 1){
return c;
}
}
return ' ';
}
}
从上到下打印二叉树I
从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
一刷 2021.8.17
- 思路一(广度优先搜索BFS)
class Solution {
public int[] levelOrder(TreeNode root) {
//如果二叉树为空,直接返回空数组
if(root == null){
return new int[0];
};
//如果二叉树不为空,则初始化包含根节点的队列
Queue<TreeNode> queye = new LinkedList<>(){{add(root);}};
ArrayList<Integer> ans = new ArrayList<>();
while(!queye.isEmpty()){
//若节点不为空,poll一个节点,再将其入队列
TreeNode node = queye.poll();
ans.add(node.val);
//若左子节点不为空,将其入队列
if(node.left!=null){
queye.add(node.left);
}
//若右子节点不为空,将其入队列
if(node.right!=null){
queye.add(node.right);
}
}
int[] res = new int[ans.size()];
for(int i = 0;i<ans.size();i++){
res[i] = ans.get(i);
}
return res;
}
}
从上到下打印二叉树 II
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
一刷 2021.8.17
- 思路一(BFS)
这道题和上一题只是有微小的差别,这一题需要把每层打印到一行
每层打印到一行: 将本层全部节点打印到一行,并将下一层全部节点加入队列,以此类推,即可分为多行打印。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//初始化
Queue<TreeNode> queue = new LinkedList<>();
//初始化存储结果队列的队列
List<List<Integer>> res = new ArrayList<>();
//若二叉树非空,则将根节点加入到queue中
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> tmp= new ArrayList<>();
//将每一层的节点分别加入队列tmp存储到队列res中
for(int i = queue.size();i>0;i--){
TreeNode node = queue.poll();
tmp.add(node.val);
if(node.left!=null){
queue.add(node.left);
}
if(node.right!=null){
queue.add(node.right);
}
}
res.add(tmp);
}
return res;
}
}
从上到下打印二叉树 III
请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
一刷 2021.8.17
这三道题简直了,三胞胎无疑
- 思路一(BFS)
只需在上一题的思路基础上,对当前层添加一个判断
若 res 的长度为 奇数 ,说明当前是偶数层,则对 tmp 执行 倒序 操作。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//初始化
Queue<TreeNode> queue = new LinkedList<>();
//初始化存储结果队列的队列
List<List<Integer>> res = new ArrayList<>();
//若二叉树非空,则将根节点加入到queue中
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> tmp= new ArrayList<>();
//将每一层的节点分别加入队列tmp存储到队列res中
for(int i = queue.size();i>0;i--){
TreeNode node = queue.poll();
tmp.add(node.val);
if(node.left!=null){
queue.add(node.left);
}
if(node.right!=null){
queue.add(node.right);
}
}
//判断当前层是技术层还是偶数层,若为奇数层,反转
if(res.size()% 2 == 1){
Collections.reverse(tmp);
}
res.add(tmp);
}
return res;
}
}
青蛙跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
一刷 2021.8.18
- 思路一(动态规划)
上第一阶台阶有1种走法,上第二节台阶有2种走法(可以直接上两步,或者上两次一步),上第三阶台阶的走法为上第一阶台阶的走法和上第二阶台阶的走法之和(因为上第三阶台阶可以相当于在上第一阶台阶的基础上,直接再加最后一步上两个台阶走完,也可以当作是上到第二阶台阶后再加最后一步上一个台阶走完。所以就得到了个递推公式:
f[n] = f[n-1] + f[n-2]; (n>=3) 。然后用变量迭代,可以节约开n个空间,把空间复杂度将为O(1).
class Solution {
final static int MOD = 1000000007;
public int numWays(int n) {
int a = 1,b = 2,sum;
for(int i = 1;i<n;i++){
sum = (a+b)%MOD;
a=b;
b=sum;
}
return a;
}
}
股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
一刷 2021.8.18
-
思路一(动态规划)
初始化两个变量花费cost和profit利润,遍历每日价格
cost每日更新最低价格
profit存储当前最大利润,并每日取当前最大利润和昨日最大利润的最大值 -
我的语言形容无法超越下列题解
题解链接
class Solution {
public int maxProfit(int[] prices) {
//初始化花费cost和profit利润,每日更新
int cost = Integer.MAX_VALUE,profit = 0;
for (int price:prices){
cost = Math.min(cost,price);
profit = Math.max(profit,price-cost);
}
return profit;
}
}
树的子结构
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
一刷 2021.8.19
- 思路一(递归)
若树B为树A的子树,则B的根节点可能为树A的任意一个节点,因此,判断B是否为A的子树,要走以下两步
- 先序遍历A中的每一个节点N
- 判断A中以N为根节点的子树是否包含树B
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
//若A树或B树为空,则直接返回false
//否则分别递归A树的所有子树是否包含B树
return (A != null && B != null) && (recur(A,B) || isSubStructure(A.left,B) || isSubStructure(A.right,B));
}
boolean recur(TreeNode A, TreeNode B){
//若B树遍历完,直接返回true
if(B == null) return true;
//若A树遍历完或者值不同,则返回false
if(A == null || A.val != B.val) return false;
//递归对比A和B树的各个节点
return recur(A.left,B.left) && recur(A.right,B.right);
}
}
二叉树的镜像
请完成一个函数,输入一个二叉树,该函数输出它的镜像。
一刷 2021.8.19
- 思路一(递归)
自顶向下递归,到最底,交换左右节点
class Solution {
public TreeNode mirrorTree(TreeNode root) {
//若二叉树为空,直接return
if(root == null){
return null;
}
TreeNode leftRoot = mirrorTree(root.right);
TreeNode rightRoot = mirrorTree(root.left);
root.left = leftRoot;
root.right = rightRoot;
return root;
}
}
- 思路二(辅助栈)
辅助栈是自上而下遍历再交换
class Solution {
public TreeNode mirrorTree(TreeNode root) {
//若二叉树为空,返回null
if(root == null) return null;
//若二叉树不为空,初始化一个栈并将根节点加入到栈中
Stack<TreeNode> stack = new Stack<>(){{add(root);}};
while(!stack.isEmpty()){
TreeNode node = stack.pop();
if(node.left!=null) stack.add(node.left);
if(node.right!=null) stack.add(node.right);
TreeNode tmp = node.left;
node.left = node.right;
node.right = tmp;
}
return root;
}
}
对称的二叉树
请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
一刷 2021.8.19
- 思路一(递归)
跟上一题的递归有异曲同工之妙,自顶向下递归,比较
class Solution {
public boolean isSymmetric(TreeNode root) {
return root == null?true:recur(root.left,root.right);
}
boolean recur(TreeNode L,TreeNode R){
if(L == null && R == null) return true;
if(L == null || R == null || L.val!=R.val) return false;
return recur(L.left,R.right) && recur(L.right,R.left);
}
}
连续子数组的最大和
输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
一刷 2021.8.20
- 思路一(动态规划)
- 初始化res用来存储最大值
- 遍历数组,
若前一位置的值小于0,存储当前值
若前一位置的值大于0,则存储当前值与前一值的和 - 将当前值与res的值做对比,存储较大值
- 返回res
class Solution {
public 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;
}
}
礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
一刷 2021.8.20
- 思路一(动态规划)
- 题解
class Solution {
public int maxValue(int[][] grid) {
int m = grid.length,n = grid[0].length;
for(int i = 0;i < m;i++){
for(int j = 0;j < n;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[m-1][n-1];
}
}
把数字翻译成字符串
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
一刷 2021.8.23
- 思路一(动态规划–>翻译字符串)
把这串数组翻译为字符串,通过遍历字符串来实现动态规划
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++){
//截取字符串,不包含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;
}
}
- 思路二(动态规划–>数字求余)
使用数组秋雨的方式,省去字符串s占用的空间,减小空间复杂度
class Solution {
public int translateNum(int num) {
int a =1,b=1;
int x, y = num%10;
while(num!=0){
num /= 10;
x = num%10;
int tmp = x*10+y;
int c = (tmp >=10 && tmp <=25?a+b:a);
b=a;
a=c;
y = x;
}
return a;
}
}
最长不含重复字符的子字符串
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
一刷 2021.8.23
- 思路一(动态规划+哈希表)
通过哈希表的charAt获取元素重复出现的两个位置,然后遍历字符串进行动态规划
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++){
//获取元素j的最近出现位置
int i = dic.getOrDefault(s.charAt(j),-1);
//更新哈希表中每个元素出现的位置
dic.put(s.charAt(j),j);
//与j两次出现的位置区间长度比较,若小于,即未出现重复元素,tmp+i否则,最大长度则为j元素两次出现的位置区间长度
tmp = tmp < j - i ? tmp + 1: j - i;
res = Math.max(res,tmp);
}
return res;
}
}
- 思路二(动态规划+线性遍历)
与上一题不同的仅是对于左边界的查找,使用此种方法,时间复杂度相比于上一种更大,为O(N2)
class Solution {
public int lengthOfLongestSubstring(String s) {
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = j - 1;
while(i >= 0 && s.charAt(i) != s.charAt(j)) i--; // 线性查找 i
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}
这种解法的思路和思路一一致,但是双指针不断更新左右边界,比思路一更直观
class Solution {
public int lengthOfLongestSubstring(String s) {
//初始化哈希表用来存储各元素上一次出现的位置
Map<Character,Integer> dic = new HashMap<>();
int i = -1,res = 0;
for(int j = 0;j < s.length();j++){
if(dic.containsKey(s.charAt(j))){
i = Math.max(i,dic.get(s.charAt(j)));
}
dic.put(s.charAt(j),j);
res = Math.max(res,j-i);
}
return res;
}
}
删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
一刷 2021.8.24
- 思路一(双指针)
初始化两个指针,pre指向head,cur 指向head的后续节点,然后遍历
class Solution {
public ListNode deleteNode(ListNode head, int val) {
//若头部节点为要删除的值,直接返回头节点的后续节点
if(head.val == val) return head.next;
//初始化指针pre指向head,cur 指向head的后续节点
ListNode pre = head,cur = head.next;
//当未遍历到链表的尾部时,继续向后遍历
while(cur!=null && cur.val != val){
pre = cur;
cur = cur.next;
}
//若便利到要删除的值,删除该值
if(cur!=null){
pre.next = cur.next;
}
return head;
}
}
链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
一刷 2021.8。24
- 思路一(双指针)
初始化两个指针,一个快的,一个慢的,快的先跑k,慢的开始跑,等快的跑完,慢的正好到倒数第k个节点
要考虑,如果链表没有k长,那么直接返回null
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode fast = head,slow = head;
for(int i = 0;i<k;i++){
if(fast == null) return null;
fast = fast.next;
}
while(fast!=null){
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
合并两个排序的链表
输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
一刷 2021.8.24
初始化一个伪头节点dum作为新链表的头部
比较两链表的头部,若l1的头部大于等于l2的头部,把l2的头节点依次放在新链表的尾部,把l2的新头节点改为原来的后继节点,若l1的头部小于l2的头部,把l1的头节点放在新链表的尾部,把l1的新头节点改为原来的后继节点,遍历即可
要考虑l1和l2为空的情况
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dum = new ListNode(0),cur = dum;
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 dum.next;
}
}
两个链表的第一个公共节点
输入两个链表,找出它们的第一个公共节点。
一刷2021.8.25
- 思路一(拼接链表)
我们无法保证两个链表同样长,索性将两个链表拼接起来
链表1后接链表2
链表2后接链表1
然后遍历
当值一致了,即为第一个公共节点,return
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA,p2 = headB;
while(p1 != p2){
p1 = p1 == null?headB:p1.next;
p2 = p2 == null?headA:p2.next;
}
return p1;
}
}
调整数组顺序使奇数位于偶数前面
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
一刷 2021.8.26
- 思路一(双指针)
看到题目就想到的思路,一个头指针,一个尾指针,
头指针向后遍历,尾指针向前遍历
头指针碰到偶数停止,尾指针碰到奇数停止,交换,再继续遍历
直到两者重合
class Solution {
public int[] exchange(int[] nums) {
int i = 0,j = nums.length - 1,tmp;
while(i<j){
while(i<j && (nums[i] % 2) == 1) i++;
while(i<j && (nums[j] % 2) == 0) j--;
tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
return nums;
}
}
和为s的两个数字
输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。
一刷 2021.8.26 今天写双指针的题感觉好顺
- 思路一(双指针)
定义两个指针,一个指向数组的头部,一个指向数组的尾部
头尾相加,
若大于目标值,尾指针后移
若小于目标值头指针前移
若等于,返回此时头指针和尾指针值的数组即可
class Solution {
public int[] twoSum(int[] nums, int target) {
int i = 0,j = nums.length-1;
while(i<j){
int add = nums[i] + nums[j];
if(add < target){
i ++;
}else if(add > target){
j --;
}else{
return new int[]{nums[i],nums[j]};
}
}
return new int[0];
}
}
翻转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如输入字符串"I am a student. “,则输出"student. a am I”。
一刷 2021.8.26
- 思路一(双指针)
定义两个指针,并初始化Java的StringBuilder
从末尾开始遍历,先去除头尾空格,
随后倒序遍历完最后一个单词,用双指针将其切片加入java的StringBuilder中
往前继续遍历
跳过空格,遍历单词,然后加入,一直到结束
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;
}
return res.toString().trim();
}
}