声明:以下算法实现参考代码随想录
数组
一维数组是连续的,二维数组不是。二维数组本质还是一位数组,其中每个元素存储的是一个一维数组的地址。
数组的元素是不能删的,只能覆盖。
数组一般考查双指针
二分查找的应用
1、搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0
class Solution {
/**
题意:在有序数组中,查找target,找到就返回它的索引,找不到就返回它会被插入的索引,时间复杂度O(logn)
思路:二分查找
// 分别处理如下四种情况
//1、 目标值等于数组中某一个元素 return middle
//2、 目标值在数组所有元素之前
//3、 目标值插入数组中的位置
//4、 目标值在数组所有元素之后的情况
*/
public int searchInsert(int[] nums, int target) {
int left=0,right=nums.length-1;
while(left<=right){//结束条件left=right+1
int mid=left+(right-left)/2;
if(nums[mid]==target){
//1、 目标值等于数组中某一个元素
return mid;
}else if(target<nums[mid]){
right=mid-1;
}else{
left=mid+1;
}
}
//2、目标值在数组所有元素之前 3、目标值插入数组中的位置 4、目标值在数组所有元素之后的情况
return left;
}
}
2、在排序数组中查找元素的第一个和最后一个位置
给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
class Solution {
/**
寻找target在数组里的左右边界,有如下三种情况:
情况1:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
情况2:target 在数组范围中,但数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
情况3:target 不在数组范围中,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
思路:先用二分查找查看能否找到target,如果找不到,则排除情况2和3,直接返回{-1,-1};如果找得到,则使用左右指针找到第一个不是target的元素
*/
public int[] searchRange(int[] nums, int target) {
int index=binarySearch(nums,target);
//target不在数组
if(index==-1) return new int[]{-1,-1};
//target在数组
int left=index;
int right=index;
while(left-1>=0&&nums[left-1]==target){
left--;
}
while(right+1<nums.length&&nums[right+1]==target){
right++;
}
return new int[]{left,right};
}
//二分查找
public int binarySearch(int[] nums,int target){
int left=0,right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
if(target==nums[mid]){
return mid;
}else if(target<nums[mid]){
right=mid-1;
}else{
left=mid+1;
}
}
return -1;
}
}
3、x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
示例 1:
输入:x = 4
输出:2
示例 2:
输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。
这其实是一个查找整数的问题,并且这个整数是有范围的,也就是我们把题目转换成判断这个整数是不是某个值的平方根,而不是直接去求平方根
如果这个整数的平方 恰好等于 输入整数,那么我们就找到了这个整数;比如2^2=4,那么2就是4的平方根
如果这个整数的平方 严格大于 输入整数,那么这个整数肯定不是我们要找的那个数;比如3^2>4,3不是4的平方根
如果这个整数的平方 严格小于 输入整数,那么这个整数 可能 是我们要找的那个数(重点理解这句话)。比如2^2<5,<9, 2虽然是5的平方根,但不是9的平方根
因此我们可以使用「二分查找」来查找这个整数,不断缩小范围去猜。
猜的数平方以后大于输入的值,就往小了猜;
猜的数平方以后恰恰好等于输入的数就找到了;
猜的数平方以后小了,可能猜的数就是,也可能不是。
class Solution {
//利用二分查找,在[0,x]中找到x的平方根
public int mySqrt(int x) {
int left=0,right=x,ans=-1;//x是可以取到的[0,x]
while(left<=right){
int mid=left+(right-left)/2;
//mid是当前猜的x的平方根,如果mid^2小于等于输入的x,则mid有可能是x的平方根
//注意mid的平方可能溢出,比如当x=Integer.MAX_VALUE
if((long)mid*mid<=x){
ans=mid;
left=mid+1;//猜的mid<=x,此时mid可能是我们要的。假设输入的是9,此时mid是2,继续搜索一个更大的看看;但是如果输入的是5,此时mid是2,那么mid就是结果
}else{
right=mid-1;//猜的mid太大了,mid不是我们要的。我们要往小的搜索
}
}
return ans;
}
}
移出数组元素
这种题就是left维持题目所要结果的边界,right去遍历,把要的元素复制到left处,不要的就跳过
1、移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。(其实数组的元素是不可删除的,只能覆盖,所以可能你返回的长度只是数组中你要的最后一个元素的索引)
不要使用额外的数组空间,你必须仅使用 $O(1)$ 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1: 给定 nums = [3,2,2,3], val = 3, 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 你不需要考虑数组中超出新长度后面的元素。
示例 2: 给定 nums = [0,1,2,2,3,0,4,2], val = 2, 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
class Solution {
//双指针,left表示前面的数字都已经放在正确的位置了,接下来不等于val的值都要插入到left的位置处,right则用于遍历
public int removeElement(int[] nums, int val) {
int left=0,right=0;
while(right<nums.length){
if(nums[right]!=val){
//把不等于val的值都复制到left那里去
nums[left]=nums[right];
left++;
}
//跳过等于val的值
right++;
}
return left;
}
public static void main(String[] args){
Solution solution=new Solution();
int reslut=solution.removeElement(new int[]{0,1,2,2,3,0,4,2},2);
System.out.println(reslut);
}
}
2、删除有序数组中的重复项
给你一个 升序排列 的数组 nums
,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。
示例 1:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。
示例 2:
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。
class Solution {
/**
思路:把不重复的元素放在数组前面,重复的元素放在数组后面
快慢指针:slow永远指向当前无重复序列的最后一个,因此当slow!=fast时,fast应该插到slow的下一位
由于需要保留重复元素的一个,所以我们让left指向第一个重复的元素,当right找到不是重复的元素时,复制到left+1的位置处覆盖原先的元素即可
*/
public int removeDuplicates(int[] nums) {
if(nums.length==0) return 0;
int left=0,right=1;
while(right<nums.length){
if(nums[left]!=nums[right]){
left++;//留下重复元素中的一个,也就是left处的那个,其余覆盖掉
nums[left]=nums[right];
}
right++;
}
return left+1;
}
}
3、移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
class Solution {
//左右指针,把不是0的元素复制到前面,当right到末尾时,将left..right的元素都补为0
//right把不是0的复制到left的位置处
public void moveZeroes(int[] nums) {
int left=0,right=0;
while(right<nums.length){
if(nums[right]!=0){
nums[left]=nums[right];//把不是0的数字都复制到left位置去
left++;
}
//等于0的时候只有right右移
right++;
}
while(left<nums.length){
nums[left]=0;
left++;
}
}
}
有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums
,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
按 非递减顺序 排序应该是说递增,但是可能会有重复元素的数组
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
class Solution {
/**
暴力解法:每个数平方之后,排个序,美滋滋
双指针法:数组是有序的,所以平方值最大的元素要么在数组最左边,要么是最右边
此时可以考虑双指针法了,i指向起始位置,j指向终止位置。看看i的平方大还是j的平方大,较大的元素先插入辅助数组的最后一个元素。
比如[-4,-1,0,3,10],i=0,j=4,100大,继续比较,i=0,j=3,16大,i=1,j=3,9大,i=1.j=2。。
*/
public int[] sortedSquares(int[] nums) {
int left=0,right=nums.length-1;
int[] result=new int[nums.length];
int index=result.length-1;
while(left<=right){
if(nums[left]*nums[left]>=nums[right]*nums[right]){
result[index]=nums[left]*nums[left];
left++;
}else{
result[index]=nums[right]*nums[right];
right--;
}
index--;
}
return result;
}
}
滑动窗口
长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 是该条件下的长度最小的子数组。
class Solution {
//两层for遍历所有可能的子数组,维护着长度最小的那个子数组;第一层i指向子数组的起始位置,j指向子数组的末位置
//子数组子串一般是用滑动窗口来解决,用left,right表示窗口的左右编辑,窗口[left,right]就是子数组。
//所谓滑动窗口,就是不断的调节子序列的起始位置left和终止位置right,从而得出我们要想的结果。
//当窗口内的元素之和小于target,不断滑动右指针;当窗口内的元素之和大于等于target,移动左指针,缩小窗口;当等于时,也要缩小窗口,看看有没有比较小的窗口满足条件>=target(寻更优)
public int minSubArrayLen(int target, int[] nums) {
int left=0,right=0;//滑动窗口边界的索引
int result=Integer.MAX_VALUE;
//sum是窗口内的值
int sum=0;
while(right<nums.length){
//不断扩大窗口
//把right值加入窗口,再移动指针
sum+=nums[right];
right++;
//当窗口内的值大于等于target,即满足条件时开始缩小窗口,不断寻更优(即>=target的最短数组)
while(sum>=target){
//缩小前更新result,因为缩小后可能就不满足条件了
result=Math.min(result,right-left);//此时[left,right)
//先把right排出窗口,再移动指针
sum-=nums[left];
//缩小窗口
left++;
}
}
return result==Integer.MAX_VALUE?0:result;
}
}
水果成篮
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。
示例 1:
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。
示例 2:
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
示例 3:
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
class Solution {
/**
只有两个篮子,也就是只能装2种水果,不过每个篮子可以装很多很多水果
可以从任何一棵树开始摘水果,一次只能在一棵树上摘一个水果
当某棵树的种类不是你两个篮子的种类,此时结束
这个的话,其实就是滑动窗口,找到连续满足的树区间
*/
//fruits = [1,2,1]表示第一棵和第三棵是种类1,第二棵是种类2
public int totalFruit(int[] fruits) {
//当发现这个树不是篮子能装的水果时,停止右边界,开始缩小窗口移动左边界,直到把其中一种类别踢出篮子,继续移动右边界
//ij分别代表窗口此时的左右边界,map用于统计窗口元素的一些东西
HashMap<Integer,Integer> countMap=new HashMap<>();//统计[i,j]窗口中各个水果的数量,key是水果种类,val是这个水果在窗口中出现的次数
int ans=0,i=0;
for(int j=0;j<fruits.length;j++){
countMap.put(fruits[j],countMap.getOrDefault(fruits[j],0)+1);//把j这个元素加入窗口
//这个水果加进去后会不会使得窗口中水果树超过2种
while(countMap.size()>=3){
//超过三种就缩小左边界,直到水果数量-1,,期间需要更新窗口值
//更新窗口
countMap.put(fruits[i],countMap.getOrDefault(fruits[i],0)-1);
if(countMap.get(fruits[i])==0){
countMap.remove(fruits[i]);
}
//缩小窗口
i++;
}
//原先的ans大,还是此时的窗口中数量大,因为一棵树只能摘一颗,所以区间有多少棵树就是篮子有多少棵水果
ans=Math.max(ans,j-i+1);
}
return ans;
}
}
数组遍历
螺旋矩阵
给你一个 m
行 n
列的矩阵 matrix
,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。
示例 1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] 输出:[1,2,3,6,9,8,7,4,5]
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
int m=matrix.length;
int n=matrix[0].length;
int mincol=0,maxcol=n-1;//左闭右闭
int minrow=0,maxrow=m-1;
List<Integer> list=new LinkedList<Integer>();
//当遍历到最后一个
while(list.size()<m*n){
//从左到右遍历第一行
if(minrow<=maxrow){
for(int j=mincol;j<=maxcol;j++){
list.add(matrix[minrow][j]);
}
//此时缩小需要遍历的范围
minrow++;
}
//从上到下遍历右侧第一列
if(mincol<=maxcol){
for(int i=minrow;i<=maxrow;i++){
list.add(matrix[i][maxcol]);
}
maxcol--;
}
//从右到左遍历下册第一行
if(minrow<=maxrow){
for(int j=maxcol;j>=mincol;j--){
list.add(matrix[maxrow][j]);
}
maxrow--;
}
//从下到上遍历左侧第一列
if(mincol<=maxcol){
for(int i=maxrow;i>=minrow;i--){
list.add(matrix[i][mincol]);
}
mincol++;
}
}
return list;
}
}
螺旋矩阵 II
给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。
示例 1:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
示例 2:
输入:n = 1
输出:[[1]]
模拟顺时针画矩阵的过程:
- 填充上行从左到右
- 填充右列从上到下
- 填充下行从右到左
- 填充左列从下到上
由外向内一圈一圈这么画下去。
这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。
那么我按照左闭右开的原则,来画一圈,大家看一下:
这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。
这也是坚持了每条边左闭右开的原则。下面的算法按照左闭右闭来实现。
class Solution {
public int[][] generateMatrix(int n) {
int[][] matrix=new int[n][n];
int low_row=0,high_row=n-1;//当前这一圈最小的行,最大的行[左闭右闭],是动态变化的,填充完一行就会+1或者-1
int low_col=0,high_col=n-1;//当前这一圈最小的列,最大的列[左闭右闭],是动态变化的,填充完一列就会+1或者-1
//low_row low_col其实就是这一圈的起点
//需要填入矩阵的数字(1-n^2)
int num=1;
while(num<=n*n){
//low_row这一行是否还需要填充?如果此时这圈最小的行大于最大的行,那就是圈没有任何行,自然不需要填充
if(low_row<=high_row){
//在这一圈的顶部从左向右遍历
for(int j=low_col;j<=high_col;j++){
matrix[low_row][j]=num++;//填充当前最小的行
}
//上边界下移
low_row++;
}
//这一列是否还需要填充
if(low_col<=high_col){
//在这一圈的右侧从上往下遍历
for(int i=low_row;i<=high_row;i++){
matrix[i][high_col]=num++;//填充当前最大的列
}
high_col--;
}
if(low_row<=high_row){
//在底部从右向左遍历
for(int j=high_col;j>=low_col;j--){
matrix[high_row][j]=num++;//填充当前最大的行
}
high_row--;
}
if(low_col<=high_col){
//在左侧从下往上遍历
for(int i=high_row;i>=low_row;i--){
matrix[i][low_col]=num++;//填充这一圈最小的列
}
low_col++;
}
}
return matrix;
}
}
链表
移除链表元素
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:
输入:head = [], val = 1
输出:[]
示例 3:
输入:head = [7,7,7,7], val = 7
输出:[]
需要考虑null问题,以及头结点就是val(包括链表全部是val的问题)
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head==null) return null;
//由于第一个元素可能要被删除,我们可以虚拟一个头结点,最后返回它的next结点就是新链表的头结点
ListNode newHead=new ListNode();
newHead.next=head;
ListNode p=newHead;
ListNode cur=head;
while(cur!=null){
if(cur.val==val){
p.next=cur.next;
}else{
p=cur;
}
cur=cur.next;
}
return newHead.next;
}
}
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head!=null&&head.val==val){
head=head.next;
}
if(head==null) return head;
ListNode pre=head;
ListNode cur=head.next;
//此时head的val不会是val的
while(cur!=null){
if(cur.val==val){
pre.next=cur.next;
}else{
pre=cur;
}
cur=cur.next;
}
return head;
}
}
设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
//单链表
class Node {
int val;
Node next;
public Node(){
}
public Node(int val){
this.val=val;
}
}
class MyLinkedList {
int size;//链表元素个数
//建立一个虚拟的头结点,操作会更简单,因为添加或删除都不是头结点,而是第一个实际结点
Node head;
public MyLinkedList() {
size=0;
head=new Node();
}
//获取链表索引为index这个结点的值
public int get(int index) {
if(index<0||index>=size) return -1;
Node p=head;
for(int i=0;i<=index;i++){
p=p.next;
}
return p.val;
}
//在链表头部添加一个值为val的元素
public void addAtHead(int val) {
addAtIndex(0,val);
}
//在链表尾部添加一个值为val的元素
public void addAtTail(int val) {
addAtIndex(size,val);
}
//在链表index位置添加一个值为val的元素
public void addAtIndex(int index, int val) {
if(index>size) return;
if(index<0){
index=0;
}
Node node = new Node(val);
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
node.next=p.next;
p.next=node;
size++;
}
//由于头结点是虚拟的,删除0的话,实际是删除第一个实际元素
public void deleteAtIndex(int index) {
if(index<0||index>=size) return;
Node p=head;
for(int i=0;i<index;i++){
p=p.next;
}
p.next=p.next.next;
size--;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例 2:
输入:head = [1,2]
输出:[2,1]
示例 3:
输入:head = []
输出:[]
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
思路:递归,当前元素的后面元素都反转好了,比如5->4->3->2<-1,此时把当前元素1拼接在2的后面,并把1的next指针置为null,否则后面部分是循环链表
*/
public ListNode reverseList(ListNode head) {
if(head==null) return null;
if(head.next==null) return head;
ListNode newHead=reverseList(head.next);//此时head是5
head.next.next=head;//让2指向1
head.next=null;//1的下一个结点置为null
return newHead;
}
}
class Solution {
/**
思路:双指针迭代,一个个反转
*/
public ListNode reverseList(ListNode head) {
if(head==null) return null;
ListNode cur=head;
ListNode pre=null;
ListNode tmp=null;
while(cur!=null){
tmp=cur.next;
cur.next=pre;
pre=cur;
cur=tmp;
}
return pre;
}
}
两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例 2:
输入:head = []
输出:[]
示例 3:
输入:head = [1]
输出:[1]
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
/**
迭代:
使用虚拟头结点,最后返回头结点的下一个即可
pre代表待交换的两个元素之前的那一个元素
head代表待交换的两个元素的第一个
tmp代表下次要交换的两个元素的第一个
*/
public ListNode swapPairs(ListNode head) {
ListNode newHead=new ListNode(0);
newHead.next=head;
ListNode pre=newHead;
ListNode tmp=null;
while(pre.next!=null&& pre.next.next!=null){//去掉空链表和只有一个结点的链表(双节点才能交换)
tmp=head.next.next;
pre.next=head.next;
head.next.next=head;
head.next=tmp;
pre=head;
head=tmp;
}
return newHead.next;
}
}
class Solution {
/**
递归:把后面的链表两两交换后,再把最前面的两个结点拼接上去
*/
public ListNode swapPairs(ListNode head) {
//递归结束条件
if(head==null||head.next==null) return head;
ListNode p=head.next;
ListNode newHead=swapPairs(p.next);
p.next=head;
head.next=newHead;
return p;
}
}
哈希表
一般哈希表都是用来快速判断一个元素是否出现集合里
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true【a:3,n:1,g:1,r:1,m:1】
示例 2:
输入: s = "rat", t = "car"
输出: false
class Solution {
//思路:由于a-z共26个字母,用一个长度26的数组填充s每个字符出现的次数,然后对t中每个字符在对应数组位置出现次数-1,如果最后数组每个元素都为0,则s与t互为异位词
public boolean isAnagram(String s, String t) {
//a存0,b存1,也就是每个字符-'a'得到其对应下标
int[] record=new int[26];
for(char c:s.toCharArray()){
record[c-'a']+=1;
}
for(char c:t.toCharArray()){
record[c-'a']-=1;
}
boolean flag=true;
for(int x:record){
if(x!=0){
flag=false;
}
}
return flag;
}
}
两个数组的交集
给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的(去重)。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
class Solution {
public int[] intersection(int[] nums1, int[] nums2) {
//
Set<Integer> set1=new HashSet<>();
Set<Integer> set2=new HashSet<>();
for(int x:nums1){
set1.add(x);//set会去重
}
for(int x:nums2){
if(set1.contains(x)){
set2.add(x);
}
}
//把set转为数组
int[] result=new int[set2.size()];
int index=0;
for(int x:set2){
result[index++]=x;
}
return result;
}
}
快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例 2:
输入:n = 2
输出:false
class Solution {
/**
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
*/
public boolean isHappy(int n) {
Set<Integer> set=new HashSet<>();
//判断在不断求和的过程中,是否有其中一次和与先前出现了重复?
while(n!=1&&!set.contains(n)){
set.add(n);
n=getNextSum(n);
}
//要么就是n==1,要么就是在求n的多次和中,set中已经有过这个结果
return n==1;
}
//取数值各个位上的单数操作,比如19,那么返回的是82
public int getNextSum(int x){
int sum=0;
while(x>0){
int tmp=x%10;
sum+=tmp*tmp;
x/=10;
}
return sum;
}
}
二叉树
理论
二叉树的种类
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
完全二叉树
什么是完全二叉树?
叶子结点只能出现在最下两层,且最下层叶子结点都靠着左边
二叉搜索树
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
- 左小右大
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树AVL
具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
链式存储如图:
顺序存储的方式如图:
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
- 深度优先遍历
- 前序遍历(递归法,迭代法):根左右
- 中序遍历(递归法,迭代法):左根右
- 后序遍历(递归法,迭代法):左右根
- 广度优先遍历
- 层次遍历(迭代法)
最后再说一说二叉树中深度优先和广度优先遍历实现方式
深度优先遍历(前中后序遍历)一般用递归,而栈就是递归的一种实现结构,所以我们可以利用栈使用非递归的方式来实现
广度优先遍历一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
二叉树的定义
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;
}
}
相关算法
递归遍历(前中后)
递归三要素:
-
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
-
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
-
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
前序遍历:1、遍历打印结点的值不需要返回值,参数的话就是每个结点的值 2、遍历到null 3、单层的逻辑就是取根节点的值
//中序遍历
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new LinkedList<>();
inOrder(root,ans);
return ans;
}
//将root为根的树进行中序遍历
public void inOrder(TreeNode root,List<Integer> ans){
//递归结束条件
if(root==null) return;
//左:以root.left为根的树全部中序遍历并添加进ans
inOrder(root.left,ans);
//do:把自己添加进list
ans.add(root.val);
//右
inOrder(root.right,ans);
}
}
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> ans = new LinkedList<>();
preOrder(root,ans);
return ans;
}
//将root为根的树进行前序遍历
public void preOrder(TreeNode root,List<Integer> ans){
if(root==null) return;
ans.add(root.val);
preOrder(root.left,ans);
preOrder(root.right,ans);
}
}
迭代遍历(前中后层次)
前中后--递归--栈
层次--队列
前序:根右左的顺序入栈,这样就是根入根出,然后分别把出栈的这个元素的右孩子左孩子入栈,再把它的左出,右出。
后序:根左右的顺序入栈,这样就是根入根出,然后分别把出栈的这个元素的左孩子右孩子入栈,再把它的右出,左出。最后的最后翻转一下集合。其实就是按照根右左的顺序遍历完翻转一下集合元素。
中序:不断把左孩子入栈直到空,此时开始出栈,然后把出栈的这个元素的右孩子入栈
//前序遍历
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root==null) return res;//返回的是[],而不是null
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode t=stack.pop();
//出栈的刚好就是我们要记录的(顺序一种)
res.add(t.val);
if(t.right!=null)
stack.push(t.right);
if(t.left!=null){
stack.push(t.left);
}
}
return res;
}
}
//后序
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new LinkedList<>();
if(root==null) return res;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode t = stack.pop();
res.add(t.val);
if(t.left!=null)
stack.push(t.left);
if(t.right!=null)
stack.push(t.right);
}
Collections.reverse(res);
return res;
}
}
//中序
class Solution {
/**
中序:左中右
思路:不断把左孩子入栈,直到null,此时开始出栈,每出栈一个就把自己的右孩子入栈
*/
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> ans = new LinkedList<>();
if(root==null) return ans;//返回[]
Stack<TreeNode> stack = new Stack<>();
TreeNode p=root;
while(p!=null||!stack.isEmpty()){
if(p!=null){
//不断把当前结点的左孩子入栈
stack.push(p);
p=p.left;
}else{
//没有左孩子了,开始出栈
p = stack.pop();
ans.add(p.val);
//以右节点开始不断把它的左孩子入栈
p=p.right;
}
}
return ans;
}
}
//层次
class Solution {
/**
思路:使用队列
每一个元素出队列,就把它的左右孩子入队列
添加offer,查看peek,删除poll
*/
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans=new ArrayList<List<Integer>>();
if(root==null) return ans;
Queue<TreeNode> queue = new LinkedList<>();//链表是队列的子类
queue.offer(root);
while(!queue.isEmpty()){
List<Integer> item = new ArrayList<Integer>();
//把一层添加到一个List,每次把这一层的元素出完之后要判断一下当前队列的元素个数,他就是下层的元素个数
int size=queue.size();
while(size>0){
//size是当前层还剩下几个
TreeNode t=queue.poll();
item.add(t.val);
if(t.left!=null)
queue.offer(t.left);
if(t.right!=null)
queue.offer(t.right);
size--;
}
//这一层出栈完,此时队列中的元素是下层的全部元素
ans.add(item);
//进行下一层
}
return ans;
}
}
翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
思路:前序遍历,1、do:交换当前结点的两个左右孩子 2、分别让左右孩子的树完成完全翻转
/*
递归三部曲:
- 确定递归函数的参数和返回值。参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。返回值的话其实也不需要,但是题目中给出的要返回root节点的指针。
- 确定终止条件。当前节点为空的时候,就返回头结点。
- 确定单层递归的逻辑。比如因为是前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
*/
class Solution {
/**
递归:在前序或者后序遍历的过程中交换每一个节点的左右孩子就可以达到整体翻转的效果
*/
public TreeNode invertTree(TreeNode root) {
//前序实现
//递归结束条件
if(root==null) return null;
//do 怎么样会翻转呢?只要让root的左右结点交换,然后它的左右结点所在树分别翻转就可以了
TreeNode tmp=root.left;
root.left=root.right;
root.right=tmp;
//左,按照递归的定义就是让以root.left为根的树翻转
invertTree(root.left);
invertTree(root.right);
return root;
}
}
对称二叉树
给你一个二叉树的根节点 root , 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
/**
递归三部曲:
1、确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
2、确定终止条件(所有可以直接给出结果的情况)
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
- 左节点为空,右节点不为空,不对称,return false
- 左不为空,右为空,不对称 return false
- 左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
- 左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了,剩下的全部是左右都不为空,且数值相同的。
3、确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
- 如果左右都对称就返回true ,有一侧不对称就返回false 。
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null) return false;
return compare(root.left,root.right);
}
//以left和right为根的两棵树是否是对称的
public boolean compare(TreeNode left,TreeNode right){
//1、递归结束条件
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;//左右节点不为空,而且值不相等
//到这下面说明是左右节点不为空,且值相等
//2、后序:左右根
//比较外侧是否相等:左结点的左孩子与右节点的右孩子是否是对称的
boolean one=compare(left.left,right.right);
//比较内侧是否相等,左结点的右子树与右结点的左子树是否是对称的
boolean two=compare(left.right,right.left);
//do 怎么样就是对称的???如果当前左右树的外侧树是对称的且内侧也是对称的,那么左右树是对称的
return one && two;
}
}
二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
//递归
class Solution {
/**
递归思想:子树的深度+1(自身是一个高度)
*/
public int maxDepth(TreeNode root) {
//递归结束条件
if(root==null) return 0;
return Math.max(maxDepth(root.left),maxDepth(root.right))+1;
}
}
//迭代
class Solution {
/**
迭代思想:层次遍历,每一层记录一次
*/
public int maxDepth(TreeNode root) {
if(root==null) return 0;
Queue<TreeNode> queue = new LinkedList<>();
int count=0;
queue.offer(root);
while(!queue.isEmpty()){
count+=1;
int size=queue.size();
//此时队列的元素个数就是这一层的个数,所以到下一层之前我们要把队列的元素都出掉,在出掉的同时还要把下一层的孩子都入栈
for(int i=0;i<size;i++){
TreeNode t = queue.poll();
if(t.left!=null)
queue.offer(t.left);
if(t.right!=null)
queue.offer(t.right);
}
//此时这层元素在队列的都出完了,下层的都入完了
}
return count;
}
}
N 叉树的最大深度
class Solution {
public int maxDepth(Node root) {
if(root==null) return 0;
int ans=0;
List<Node > childs=root.children;
//找出n个孩子子树最大的深度+1
for(int i=0;i<childs.size();i++){
ans=Math.max(ans,maxDepth(childs.get(i)));
}
return ans+1;
}
}
二叉树的最小深度
class Solution {
/**
思路:做这题有一个坑,就是当根节点只有右孩子时,此时很多人会认为最小深度就是1,也就是根节点所在层
实际上,根并不是叶子节点,叶子节点指的是左右子树都为null的结点。
*/
public int minDepth(TreeNode root) {
if(root==null) return 0;
//如果左节点右节点都为空,深度就是1
if(root.left==null&&root.right==null){
return 1;
}
int x1=minDepth(root.left);
int x2=minDepth(root.right);
//如果左为空,右节点不为空,那么最小深度就是右子树的最小深度+1;反之,最小深度是左子树的最小深度+1
if(root.left==null||root.right==null){
return x1+x2+1;//左节点为空的话,minDepth(root.left)会返回0
}
//左右子树都不为null
return Math.min(x1,x2)+1;
}
}
完全二叉树的节点个数
class Solution {
/**
完全二叉树:所有叶子结点只能在最后两层,且最后一层的结点都靠近左边
只有两种情况,
情况一:就是满二叉树,直接用 2^树深度 - 1 来计算
情况二:非完全二叉树,则分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
*/
public int countNodes(TreeNode root) {
//统计以root为根的树是否为完全二叉树
TreeNode leftNode=root,rightNode=root;
//记录左右子树的高度
int l=0,r=0;
while(leftNode!=null){
leftNode=leftNode.left;
l++;
}
while(rightNode!=null){
rightNode=rightNode.right;
r++;
}
//如果l==r,则root是一颗完全二叉树
if(l==r){
return (int)Math.pow(2,l)-1;
}
//不是完全二叉树,则暂时按照普通树的方式统计结点数,后面递归子树,子树仍可能是一颗完全二叉树的
return 1+countNodes(root.left)+countNodes(root.right);
}
}
动态规划DP
dp可解决最值或者序列,他们的特征就是需要穷举。
动态规划具有三要素:1、最优子结构--子问题之间相互独立,互不干扰 2、重叠子问题(备忘录或dp表优化)3、状态转移方程
dp表分为一维和二维: 一般涉及两个字符串或者数组用二维,dp[i][j]与arr[0..i]和arr[0..j]有关;不过涉及一个字符串或者数组时,也可能用到二维,比如最长回文子串--dp[i][j]:在子数组arr[i..j]中,最长回文子序列的长度
对于dp表,如果只涉及到可数个数的dp,那么可以进行状态压缩,如果是一维dp,则变为变量。如果是二维dp,则只留下j维度(映射法)
动态规划=回溯+剪枝(dp表或者递归+备忘录)
动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的
动态规划四步骤:
1、明确dp的定义:根据dp定义不同,结果所在的位置可能也不同,有的可能表的最后一个就是题目要的答案,有的需要遍历整个dp表得到最值。【对于dp的定义有些是以nums[i]为结尾,有些是表示在nums[i]之前..】
2、状态转移:要明确遍历的方向,能够从已知问题推出未知方向
3、选择:目前有n个选择呢?哪个与所求更符合就选哪个
4、base case:遍历方向最基本的已知问题
5、举个例子
斐波那契数
class Solution {
/***
1、dp[i]定义:i的斐波那契数值是dp[i]
2、状态转移:dp[i]=dp[i-1]+dp[i-2]
3、选择
4、base case :方向是从左到右;dp[0]=0,dp[1]=1;
5、举例子推导dp数组:0 1 1 2 3 5 8
*/
public int fib(int n) {
if(n<=1) return n;
int[] dp = new int[n+1];
//base case
dp[0]=0;
dp[1]=1;
//遍历填充
for(int i=2;i<dp.length;i++){
dp[i]=dp[i-1]+dp[i-2];
}
//返回结果,根据dp定义,就是dp[n]
return dp[n];
}
}
//状态压缩
class Solution {
/***
1、dp[i]定义:i的斐波那契数值是dp[i]
2、状态转移:dp[i]=dp[i-1]+dp[i-2]
3、选择
4、base case :方向是从左到右;dp[0]=0,dp[1]=1;
5、举例子推导dp数组:0 1 1 2 3 5 8
*/
public int fib(int n) {
if(n<=1) return n;
//base case
int pre2=0;
int pre1=1;
int cur=0;
for(int i=2;i<n+1;i++){
cur=pre1+pre2;
pre2=pre1;
pre1=cur;
}
//返回结果
return cur;
}
}
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
- 输入: 2
- 输出: 2
- 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
- 输入: 3
- 输出: 3
- 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
爬到第一层楼梯有一种方法【1】,爬到二层楼梯有两种方法【1+1、2】。
那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。【1+2】【1+1+1,2+1】=3种
所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。(也就是爬到第一层的方法+第二层的方法=爬到第三层的方法)
PS:如果每一步有三个选择,也就是爬1阶,爬2阶,爬3阶
那么爬到第四层可以由:爬到第一层后+3,爬到第二层后+2,爬到第三层后+1
class Solution {
/**
dp[i]定义:爬到第i层有多少种方法
状态转移:dp[i]=dp[i-1]+dp[i-2];
选择:不用选择,因为两种产生的方法都要
base case:dp[1]=1,dp[2]=2
*/
public int climbStairs(int n) {
if(n<=2) return n;
int[] dp = new int[n+1];
//base case
dp[1]=1;
dp[2]=2;
for(int i=3;i<dp.length;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
}
//状态压缩
class Solution {
/**
dp[i]定义:爬到第i层有多少种方法
状态转移:dp[i]=dp[i-1]+dp[i-2];
选择:不用选择,因为两种产生的方法都要
base case:dp[1]=1,dp[2]=2
*/
public int climbStairs(int n) {
if(n<=2) return n;
//base case
int pre2=1;
int pre1=2;
int cur=0;
for(int i=3;i<n+1;i++){
cur=pre1+pre2;
pre2=pre1;
pre1=cur;
}
return cur;
}
}
老板一共需要给某个员工发奖金n元,可以选择一次发1元,也可以选择一次发2元,也可以选择一次发3元。请问老板给这位员工发放完n元奖金共有多少种不同的方法?
数据范围:1 <= n <= 10
import java.util.*;
public class Solution {
/**
*
* @param num_money int整型 奖金的总数,单位为元
* @return int整型
*/
public int CalulateMethodCount (int num_money) {
// write code here
if(num_money<=2) return num_money;
int pre3=1;
int pre2=2;
int pre1=4;
int cur=0;
//红包为4=发红包为3的方法+发红包为2的方法+发红包为1的方法
for(int i=4;i<=num_money;i++){
cur=pre1+pre2+pre3;
pre3=pre2;
pre2=pre1;
pre1=cur;
}
return cur;
}
}
不同路径
//方法一:动态规划dp二维表
class Solution {
/**
机器人只能向右或者向下
dp[i,j]:机器人从(0,0)走到(i,j)的方法数
递推方程:由于机器人只可能从上面,或者左边过来,因此dp[i,j]=dp[i,j-1]+dp[i-1,j],就是到[i,j-1]以及dp[i-1,j]方法中加上最后一个步骤,数量仍然不变
选择:不需要选择,两个方向我都需要累计作为总共有多少种可能
base case:机器人在第一行或者第一列都只有一种方法,因为它只能向右或者向下直线走呗,dp[0,j]=1,dp[i,0]=0
*/
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
//base case
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int j=0;j<n;j++){
dp[0][j]=1;
}
//遍历方向,自上而下,从左至右
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i][j-1]+dp[i-1][j];
}
}
return dp[m-1][n-1];
}
}
//方法二:状态压缩
class Solution {
/**
机器人只能向右或者向下
dp[i,j]:机器人从(0,0)走到(i,j)的方法数
递推方程:由于机器人只可能从上面,或者左边过来,因此dp[i,j]=dp[i,j-1]+dp[i-1,j],就是到[i,j-1]以及dp[i-1,j]方法中加上最后一个步骤,数量仍然不变
选择:不需要选择,两个方向我都需要累计作为总共有多少种可能
base case:机器人在第一行或者第一列都只有一种方法,因为它只能向右或者向下直线走呗,dp[0,j]=1,dp[i,0]=0
*/
public int uniquePaths(int m, int n) {
int[]dp = new int[n];
//base case 有重复的投射不怕,因为他们都是1啊
for(int j=0;j<n;j++){
dp[j]=1;
}
//遍历方向,自上而下,从左至右
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[j]=dp[j-1]+dp[j];
}
}
return dp[n-1];
}
}
有障碍物的不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
//主要就是在状态转移和base的时候考虑一下出现障碍物的情况
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int n = obstacleGrid.length, m = obstacleGrid[0].length;
int[][] dp = new int[n][m];
//base case
for (int i = 0; i < m; i++) {
if (obstacleGrid[0][i] == 1) break; //一旦遇到障碍,后续都到不了,跟初始值一样保持0,也就是到达它的方法为0
dp[0][i] = 1;
}
for (int i = 0; i < n; i++) {
if (obstacleGrid[i][0] == 1) break; 一旦遇到障碍,后续都到不了
dp[i][0] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
if (obstacleGrid[i][j] == 1) continue;//如果(i,j)是障碍物,那么到达它的方法数保持默认,也就是0
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[n - 1][m - 1];
}
}
回溯
//存放结果集
result=[]
def backtrack():
if(满足某个条件时)
result.add(路径)
return
//所有的选择
for 选择 in 选择列表:
做选择
递归(下一层做选择)
撤销选择
重点:画出决策树
有的情况下,所有的结点都是结果,有的只有叶子结点是结果
一般是按序做选择,有些情况选过的不能选,我们要做一些限制
有的时候,父节点做的选择需要累计给子节点,需要通过参数递归传递给子节点
子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
class Solution {
List<List<Integer>> ans;
//回溯
public List<List<Integer>> subsets(int[] nums) {
ans=new LinkedList<>();
LinkedList<Integer> track=new LinkedList<>();
backtrack(nums,track,0);
return ans;
}
//start:如果父节点做了某了选择,子节点,只能选择它后面的
//track:父节点做的节点需要传递给子节点
public void backtrack(int[] nums,LinkedList<Integer> track,int start){
//不需要递归结束条件,因为选择是有限的,会自动结束
//所有结点都是一个结果,不需要条件判断
ans.add(new LinkedList(track));
//当前可以做的选择
for(int i=start;i<nums.length;i++){
//选择
track.add(nums[i]);
//递归,子节点做选择
backtrack(nums,track,i+1);
//撤销选择
track.removeLast();
}
}
}
全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
示例 2:
输入:nums = [0,1]
输出:[[0,1],[1,0]]
示例 3:
输入:nums = [1]
输出:[[1]]
class Solution {
List<List<Integer>> ans;
public List<List<Integer>> permute(int[] nums) {
ans=new LinkedList<>();
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums,track);
return ans;
}
public void backtrack(int[]nums,LinkedList track){
//当到达叶子结点,就是一个排列结果
if(track.size()==nums.length)
{
ans.add(new LinkedList(track));
return;
}
//当前可选的选择
for(int i=0;i<nums.length;i++){
//实际上这道题,只要是这个结点没被父节点选过的都可以,所以我们需要过滤掉已被选择的结点
if(track.contains(nums[i])) continue;
//选择
track.add(nums[i]);
//子节点做选择
backtrack(nums,track);
//撤回
track.removeLast();
}
}
}