目录
剑指offer
一、数组
1. 数组中重复的数字
我的简单思路:先排序,再输出答案,但是效率不高。
import java.util.Arrays;
class Solution {
public int findRepeatNumber(int[] nums) {
Arrays.sort(nums);
int a=0;
for(int i=0;i<nums.length-1;i++){
if(nums[i]==nums[i+1]){
a=nums[i];
}
}
return a;
}
}
下面是最好的思路:原地交换。数组元素的 索引 和 值 是 一对多 的关系。遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = inums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
时间复杂度 O(N) : 遍历数组使用 O(N)O(N) ,每轮遍历的判断和交换操作使用 O(1)O(1) 。
空间复杂度 O(1) : 使用常数复杂度的额外空间。
代码:遍历数组
1.所遍历的数字=所在索引值,就遍历下一个;
2.所遍历的数字≠所在索引值,就和它该在的索引的数字进行交换;
3.所遍历的数字=它该在的索引的数,则得出结果。
class Solution {
public int findRepeatNumber(int[] nums) {
for(int i=0;i<nums.length;i++){
if(nums[i]==i){
continue;
}
if(nums[i]==nums[nums[i]]){
return nums[i];
}
int temp;
temp=nums[i];
nums[i]=nums[temp];
nums[temp]=temp;
}
return -1;
}
}
2.二维数组中的查找
思路:从右上角开始。对于每个元素,其左分支元素更小、右分支元素更大。
复杂度分析:
时间复杂度 O(M+N)O(M+N) :其中,NN 和 MM 分别为矩阵行数和列数,此算法最多循环 M+NM+N 次。
空间复杂度 O(1)O(1) : i, j 指针使用常数大小额外空间。
多个if和if,elseif语句的区别
①if无论是否满足条件都会向下执行,知道程序结束,else if 满足一个条件就会停止执行。
②由于if都会执行一遍,则可能会同一个需要判断的事件,会进入2个if语句中,出现错误,而else if就不会发生这样的事情。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
int i = matrix.length - 1, j = 0;
while(i >= 0 && j < matrix[0].length)
{
if(matrix[i][j] == target){
return true;
}
if(matrix[i][j] > target) i--;
else if(matrix[i][j] < target) j++;//换成if就报错
}
return false;
}
}
3.旋转数组的最小数字
思路:普通的太简单了,使用二分查找。注意性质(下图一目了然)。
循环二分: 设 m = (i + j) / 2m=(i+j)/2 为每次二分的中点( “/” 代表向下取整除法,因此恒有 i≤m<j ),可分为以下三种情况:
①当 nums[m] > nums[j]时: m一定在 左半,最小值一定在 [m+1,j] 闭区间内,因此执行 i = m + 1;
②当 nums[m] < nums[j] 时: m 一定在 右半,最小值 一定在[i, m]闭区间内,因此执行 j = m;
③当 nums[m] = nums[j] 时: 无法判断 m 在哪个排序数组中,即无法判断旋转点 x 在 [i, m还是 [m + 1, j]区间中。解决方案: 执行 j = j - 1缩小判断范围
class Solution {
public int minArray(int[] numbers) {
int i = 0, j = numbers.length - 1;
while (i < j) {
int mid = (i + j) / 2;
if (numbers[mid] > numbers[j]) {
i = mid + 1;
}
else if (numbers[mid] < numbers[j]){
j = mid;
}
else j--;
}
return numbers[i];
}
}
补充思考: 为什么本题二分法不用 nums[m]和 nums[i]作比较?
二分目的是判断 m 在哪个排序数组中,从而缩小区间。而在 nums[m] > nums[i]情况下,无法判断 m 在哪个排序数组中。本质上是由于 j 初始值肯定在右排序数组中; i 初始值无法确定在哪个排序数组中。举例如下:
对于以下两示例,当 i = 0, j = 4, m = 2时,有 nums[m] > nums[i] ,而结果不同。
[1, 2, 3, 4 ,5]旋转点 x = 0 : m 在右排序数组(此示例只有右排序数组);
[3, 4, 5, 1 ,2] 旋转点 x = 3 : m 在左排序数组。
4.构建乘积数组
思路:把图画出来就知道了。
class Solution {
public int[] constructArr(int[] a) {
int length=a.length;
if(length==0){
return a;
}
int[] b=new int[length];
int temp=1;
b[0]=1;
int i=0;
for(i=1;i<length;i++){
b[i]=b[i-1]*a[i-1];看图易知B2=B1*A1
}
for(i=length-2;i>=0;i--){
temp=temp*a[i+1];
b[i]=b[i]*temp;
}
return b;
}
}
5.把数组排成最小的数
Arrays.sort(Object[] oj1,new SortComparator()):这种方式能够对引用类型数组,按照Comparator中声明的compare方法对对象数组进行排序.
lamda表达式
相对于for(;;)而言 增强for循环有两个好处:
1.写起来简单
2.遍历集合、容器简单
//没弄明白
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]);//String.valueOf()把基本类型转换成string类型
Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));//自定义排序需要深入研究一下,还有lamda表达式
StringBuilder res = new StringBuilder();//StringBuilder非线程安全但是效率高
for(String s : strs)//增强for循环
res.append(s);
return res.toString();//转换成string类型
}
}
6.矩阵中的路径
class Solution {
public boolean exist(char[][] board, String word) {
char[] words = word.toCharArray();//String转换成char数组
// 遍历图
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;
}
boolean dfs(char[][] board, char[] word, int i, int j, int k) {
// 判断传入参数的可行性: i 与图行数row比较,j与图列数col比较,i,j初始都是0,都在图左上角
// k是传入字符串当前索引,一开始是0,如果当前字符串索引和图当前索引对应的值不相等,表示第一个数就不相等
// 所以继续找第一个相等的数。题目说第一个数位置不固定,即路径起点不固定(不一定是左上角为第一个数)
// 如果board[i][j] == word[k],则表明当前找到了对应的数,就继续执行(标记找过,继续dfs 上下右左)
//board[i][j] != word[k]要放里面否则会有越界报错
if(i >= board.length || i < 0 || j >= board[0].length || j < 0 || board[i][j] != word[k]){
return false;
}
// 表示每个字符都找到了
// 一开始k=0,而word.length肯定不是0,所以没找到,就执行dfs继续找。
if(k == word.length - 1) return true;
// 访问过的暂时标记空字符串:不标记会导致搜回去,
//“ ”是空格 '\0'是空字符串,不一样的!
board[i][j] = '\0';
// 顺序是 上下 右 左(这四个顺序顺便);上面找到了对应索引的值所以k+1
boolean res = dfs(board, word, i-1, j, k + 1) || dfs(board, word, i +1, j, k + 1) ||
dfs(board, word, i, j + 1, k + 1) || dfs(board, word, i , j - 1, k + 1);
// 还原找过的元素,因为之后可能还要访问它的其他路径
board[i][j] = word[k];
// 返回结果,如果false,则if(dfs(board, words, i, j, 0)) return true;不会执行,就会继续找
return res;
}
}
7.调整数组顺序使奇数位于偶数前面
考虑定义双指针 i , j 分列数组左右两端,循环执行:
指针 i 从左向右寻找偶数;
指针 j 从右向左寻找奇数;
将 偶数nums[i] 和 奇数 nums[j] 交换。
可始终保证: 指针 i 左边都是奇数,指针 j 右边都是偶数 。
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;
}
}
8.顺时针打印矩阵
思路:四个边界,注意最后一个元素可能会重复读取
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0) return new int[0];
int a = 0, d = matrix[0].length - 1, w = 0, s = matrix.length - 1, x = 0;
int i,j,temp=0;
int[] sum = new int[(d + 1) * (s + 1)];
while(true){
for(j=a;j<=d;j++){// a to d.
sum[temp++]=matrix[w][j];
}
w++;
if(temp >= sum.length) break;//每个for循环因为有等号,所以到了最后会重复加进去,所以要加这个
for(i=w;i<=s;i++){// w to s.
sum[temp++]=matrix[i][d];
}
d--;
if(temp >= sum.length) break;
for(j=d;j>=a;j--){// d to a.
sum[temp++]=matrix[s][j];
}
s--;
if(temp >= sum.length) break;
for(i=s;i>=w;i--){// s to w.
if(++a > d) break;
sum[temp++]=matrix[i][a];
}
a++;
if(temp >= sum.length) break;
}
return sum;
}
}
9.栈的压入、弹出序列
class Solution {
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int j=0;
for(int i=0;i<pushed.length;i++){
stack.push(pushed[i]);// 进栈
while(!stack.isEmpty()&&stack.peek()==popped[j]){// 栈顶元素和出栈的元素相等就出栈,不相等继续进栈
stack.pop();
j++;//出栈就往后遍历poped
}
}
return stack.isEmpty();
}
}
10.数组中出现次数超过一半的数字
核心就是对拼消耗。
假设有一个擂台,有一组人,每个人有编号,相同编号为一组,依次上场,没人时上去的便是擂主(x),若有人,编号相同则继续站着(人数+1),若不同,假设每个人战斗力相同,都同归于尽,则人数-1;那么到最后站着的肯定是人数占绝对优势的那一组啦~
class Solution {
public int majorityElement(int[] nums) {
int leizhu=nums[0];
int renshu=1;
for(int i=1;i<nums.length;i++){
if(renshu==0){
leizhu=nums[i];
}
if(nums[i]==leizhu){
renshu++;
}else{
renshu--;
}
}
return leizhu;
}
}
11.最小的k个数
思路:题目只要求返回最小的 k 个数,对这 k 个数的顺序并没有要求。因此,只需要将数组划分为 最小的 k 个数 和 其他数字 两部分即可,而快速排序的哨兵划分可完成此目标。
根据快速排序原理,如果某次哨兵划分后 基准数正好是第 k+1 小的数字 ,那么此时基准数左边的所有数字便是题目所求的 最小的 k 个数 。
根据此思路,考虑在每次哨兵划分后,判断基准数在数组中的索引是否等于 k ,若 true 则直接返回此时数组的前 k 个数字即可。
《啊哈!算法》 关于快速排序法为什么一定要哨兵j 先出动的原因?
假如i先动,如果后面没有大于基准的数就导致排序错误!!
class Solution {
public int[] getLeastNumbers(int[] arr, int k) {
if (k == arr.length) return arr;
return quickSort(arr, k, 0, arr.length - 1);
}
public int[] quickSort(int[] arr, int k, int l, int r) {
int i = l, j = r;//l:left,r:rigtht
while (i < j) {
while (i < j && arr[j] >= arr[l]) j--;//i<j控制越界,j先动,改成i就报错了
while (i < j && arr[i] <= arr[l]) i++;
swap(arr, i, j);//快排思路,后面小于基准的和前面大于基准的要交换
}
swap(arr, i, l);//交换完了一轮基准,把基准放到该放的位置
if (i > k) {//i > k时基准在k后面,所以在前一半找
return quickSort(arr, k, l, i - 1);
}
if (i < k){//i < k时基准在k前面,所以在后一半找
return quickSort(arr, k, i + 1, r);
}
return Arrays.copyOf(arr, k);//i==k时就可以返回前k个数
}
public void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
13. 在排序数组中查找数字 I
思路:二分查找找右边界,简单来说就是找到第一个比target大的数的位置。
假如tar-1不存在呢?比如{1,2,3,5,7,7,7,8},tar=7,而tar-1=6并不在数组nums里边。回看第二个问题if(nums[m] <= tar)合并了两个条件, 上边说了nums[m] = tar,这里说说nums[m] < tar。其实tar-1存在与否并不重要,按照开头的数组,设tar=6。
第一次i = 0,j = 7,m=3,nums[m]=5<tar;
第二次 i = 4,j = 7,m = 5,nums[m] = 7 > tar;
第三次i = 4,j = 4,m = 4,nums[m] = 7 >tar;
第四次 i = 4,j = 3,i>j 退出while,renturn 4,tar的右边界为下标4。
为什么tar不存在不影响?因为helper方法是为了求右边界(简单来说就是找到第一个比target大的数的位置),tar不存在时,不考虑nums[m] == tar的判断;nums[m] > tar,j = m-1;nums[m] < tar,i =m+1,最终返回的是tar的右边界(return i 保证是返回右边界,不可用return j)。所以tar的右边界一定存在,即使tar不存在。
class Solution {
public int search(int[] nums, int target) {
//helper(nums, target):找到target的右边界
//helper(nums, target - 1):找到target-1的右边界
return helper(nums, target) - helper(nums, target - 1);//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;
}
if(nums[m] > tar){
j = m - 1;//中间值大于目标值,就在左半找
}
}
return i;
}
}
14. 0~n-1中缺失的数字
思路:
返回值: 跳出时,变量 i 和 j 分别指向 “右子数组的首位元素” 和 “左子数组的末位元素” 。因此返回 i 即可。
class Solution {
public int missingNumber(int[] nums) {
int i = 0, j = nums.length - 1;
while(i <= j) {
int mid = (i + j) / 2;
if(nums[mid] == mid){//是正常数就找右半
i = mid + 1;
}
else{
j = mid - 1;//不是正常数就找左半
}
}
return i;
}
}
15.数组中数字出现的次数
思路:回归异或的本质,1^0 = 1, 1^1 = 0, 0^0 = 1。a^b的结果里,为1的位表明a与b在这一位上不相同。这个不相同很关键,不相同就意味着我们在结果里任选一位位1的位置i,所有数可以按照i位的取值(0,1)分成两组,那么a与b必然不在同一组里。再对两组分别累计异或。那么两个异或结果就是a、b。
一个例子:5,3,2,3,4,5
1.遍历异或:结果为2=0100异或4=0010,n=0110=6
2.使用&找出a与b在这一位上不相同,不相同就可以把a和b划分开。先0110&0001=0,然后0110&0010=1,m=2
3,划分成了[0010=2,0011=3],[0100=4,0101=5],两个子数组异或结果为2,5
class Solution {
public int[] singleNumbers(int[] nums) {
int x = 0, y = 0, n = 0, m = 1;
for(int num : nums) // 1. 遍历异或,初始化为0,因为0异或a=a;
n ^= num;
while((n & m) == 0) // 2. 循环左移,计算出第一个二进制异或结果为1的m值,也就是这两个值一个&m=0,另一个&m≠0,所以可以起到划分作用
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) {
int n=0,m=1,x=0,y=0;
for(int i=0;i<nums.length;i++){// 1. 遍历异或,初始化为0,因为0异或a=a;
n^=nums[i];
}
while((n&m)==0){//中间要加括号,因为判断符先计算,循环左移,计算出第一个二进制异或结果为1的m值,也就是这两个值一个&m=0,另一个&m≠0,所以可以起到划分作用
m=m<<1;
}
for(int i=0;i<nums.length;i++){
if((nums[i]&m)!=0){
x=x^nums[i];
}else{
y=y^nums[i];
}
}
return new int[]{x,y};
}
}
16. 和为s的两个数字
思路:就是简单的双指针
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];
}
}
17.扑克牌中的顺子
思路:为什么不直接写成==4,因为可能是0,0,3, 4 ,5
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++; // 统计大小王数量
else if(nums[i] == nums[i + 1]) return false; // 若有重复,提前返回 false
}
return nums[4] - nums[joker] < 5; // 最大牌 - 最小牌 < 5 则可构成顺子
}
}
18.数组中的逆序对
思路:归并排序中分治计算。
双指针,因为子数组已经排好了序,每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着
「左子数组当前元素 至 左子数组末尾元素」 与 「右子数组当前元素」 构成了几个 「逆序对」 。
class Solution {
int count;//全局变量计数器
public int reversePairs(int[] nums) {
count=0;//默认返回值
GB(nums,0,nums.length-1);
return count;
}
public void GB(int[] nums,int left,int right){
int mid=(left+right)/2;
if(left<right){//没有=,因为一个数字不需要排序
GB(nums,left,mid);//处理左半
GB(nums,mid+1,right);//处理右半
GBpaixu(nums,left,mid,right);//归并排序
}
}
public void GBpaixu(int[] nums,int left,int mid,int right){
int[] temparr=new int[right-left+1];//创建临时数组存储排序好的数
int index=0;//临时数组指针
//定义两个指针
int temp1=left;
int temp2=mid+1;
while(temp1<=mid&&temp2<=right){//两个指针要满足的条件
if(nums[temp1]<=nums[temp2]){//不是逆序
temparr[index++]=nums[temp1];
temp1++;//指针往后走
}else{//是逆序
count=count+(mid+1-temp1);//因为左边已经排好序,所以左边的后面全满足
temparr[index++]=nums[temp2];
temp2++;//指针往后走
}
}
//两个子数组可能剩下数字
while(temp1<=mid){
temparr[index++]=nums[temp1++];
}
while(temp2<=right){
temparr[index++]=nums[temp2++];
}
//把排好的临时数组赋值到原始数组nums
for(int i=0;i<temparr.length;i++){
nums[i+left]=temparr[i];
}
}
}
19.II. 数组中数字出现的次数 II
class Solution {
public int singleNumber(int[] nums) {
int[] counts = new int[32];//辅助记录所有数字的各二进制位的 1
for(int i = 0; i < nums.length; i++) {
for(int j = 0; j < 32; j++) {
counts[j]= counts[j]+(nums[i] & 1); //使用与运算,可获取二进制数字 num[i]的最右一位,更新第 j 位1的个数
nums[i] =nums[i]>>> 1; // 无符号右移,统计前一位
}
}
int res = 0, m = 3,temp=0;
for(int i = 0; i < 32; i++) {
res =res<< 1;// 左移 1 位,在couns没出现1之前res一直是0
temp=counts[31 - i] % m;//counts[31 - i]得到只出现一次的数字的第 (31 - i)位
res =res|temp; // 恢复第i位的值到 res
}
return res;
}
}
二、动态规划
1.连续子数组的最大和
思路:以某个数作为结尾,意思就是这个数一定会加上去,那么要看的就是这个数前面的部分要不要加上去。大于零就加,小于零就舍弃。
1.状态定义: 设动态规划列表 dp ,dp[i] 代表以元素 nums[i]为结尾的连续子数组最大和。
为何定义最大和 dp[i] 中必须包含元素 nums[i]:保证 dp[i]递推到 dp[i+1] 的正确性;如果不包含 nums[i],递推时则不满足题目的 连续子数组 要求。
转移方程: 若 dp[i−1]≤0 ,说明 dp[i−1] 对dp[i] 产生负贡献,即 dp[i-1] + nums[i] 还不如 nums[i] 本身大。
①当 dp[i - 1] > 0 时:执行 dp[i] = dp[i-1] + nums[i] ;
②当 dp[i−1]≤0 时:执行 dp[i]=nums[i] ;
初始状态: dp[0]=nums[0],即以 nums[0] 结尾的连续子数组最大和为 nnums[0] 。
返回值: 返回 dp 列表中的最大值,代表全局最大值。
class Solution {
public int maxSubArray(int[] nums) {
int max= nums[0];//最大值初始化为第一个数
for(int i = 1; i < nums.length; i++) {
if(nums[i-1]>0){//如果dp[i-1]>0,则dp[i]=dp[i-1]+nums[i] (代表以元素 nums[i]为结尾的连续子数组最大和。)
nums[i]=nums[i]+nums[i-1];
}
if(nums[i]>max){//更新最大值
max=nums[i];
}
}
return max;
}
}
2.股票的最大利润
思路:
①状态定义: 设动态规划列表 dp,dp[i]代表以 prices[i]为结尾的子数组的最大利润(以下简称为 前 i 日的最大利润 )。
②转移方程: 由于题目限定 “买卖该股票一次” ,因此前 i 日最大利润 dp[i] 等于前 i - 1 日最大利润 dp[i-1]和第 i 日卖出的最大利润中的最大值。
前 i 日最大利润 = max(前 (i-1) 日最大利润, 第 i 日价格 - 前 i 日最低价格)
dp[i]=max(dp[i−1],prices[i]−min(prices[0:i]))
③初始状态: dp[0] = 0,即首日利润为 0 ;
④返回值: dp[n - 1],其中 n 为 dp 列表长度。
class Solution {
public int maxProfit(int[] prices) {
int cost = Integer.MAX_VALUE, profit = 0;
for(int i=0;i<prices.length;i++) {
cost = Math.min(cost, prices[i]);//成本
profit = Math.max(profit, prices[i] - cost);//利润
}
return profit;
}
}
3. I. 斐波那契数列
class Solution {
public int fib(int n) {
if(n == 0) return 0;
int[] dp = new int[n + 1];//使用辅助dp数组保存下来防止数字爆炸
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i-2];
dp[i] %= 1000000007;//每次求模防止数字错误
}
return dp[n];
}
}
4. 机器人的运动范围
思路:实际上就是深搜或者广搜;
class Solution {
//深度搜
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
return dfs(visited, m, n, k, 0, 0);//0,0初始化移动指针
}
private int dfs(boolean[][] visited, int m, int n, int k, int i, int j) {//i,j是移动指针,
if(i >= m || j >= n || visited[i][j] || bitSum(i ,j) > k){//越界或者不满足条件或者访问过
return 0;
}
visited[i][j] = true;
return 1 + dfs(visited, m, n, k, i + 1, j) + dfs(visited, m, n, k, i, j + 1) ;
}
// //得出数位之和
public int bitSum(int m,int n){
int sum=0;
while(m!=0){
sum=sum+m%10;
m=m/10;
}
while(n!=0){
sum=sum+n%10;
n=n/10;
}
return sum;
}
}
5. I. 剪绳子
思路:数学结论
class Solution {
public int cuttingRope(int n) {
if(n <= 3) return n - 1;//题目n≥2,m>1
int a = n / 3, b = n % 3;
if(b == 0) return (int)Math.pow(3, a);//pow() 方法的返回类型为double。
if(b == 1) return (int)Math.pow(3, a - 1) * 4;
return (int)Math.pow(3, a) * 2;
}
}
5. II. 剪绳子
关键在于大数 循环求余
// // 求 (x^a) % p —— 循环求余法。固定搭配建议背诵
// public long remainder(int x,int a,int p){ //x为底数,a为幂,p为要取的模
// long rem = 1 ;
// for (int i = 0; i < a; i++) {
// rem = (rem * x) % p ;
// }
// return rem;
// }
class Solution {
public int cuttingRope(int n) {
if(n <= 3){
return n - 1;
}
int b = n % 3;//求余数
int p = 1000000007;//定值
//a统计含有3的个数
long rem = 1,a = n / 3;
//直接套循环求余公式
for(int i = 0; i < ((b == 1)?a-1:a); i++) { //b == 1代表余数为1的时候,需要单独取出一个3出来凑成2*2达到最大值效果
rem = (rem * 3) % p;//循环求余防止越界
}
if(b == 0) return (int)(rem % p);//这行不加%p也可以因为已经求余过
if(b == 1) return (int)(rem * 4 % p);
return (int)(rem * 2 % p);
}
}
6.圆圈中最后剩下的数字
根据状态转移方程的递推特性,无需建立状态列表 dp ,而使用一个变量 x 执行状态转移即可。
class Solution {
public int lastRemaining(int n, int m) {
int x = 0;
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x;
}
}
7.丑数
思路:
答案链接
在已有的丑数序列上每一个数都必须乘2, 乘3, 乘5, 这样才不会漏掉某些丑数。
假设已有的丑数序列为[1, 2, 3, …, n1, n2], 如果单纯的让每个丑数乘2, 乘3, 乘5顺序排列的话肯定会有问题,
比如如果按照这样的顺序排列下去肯定有问题[12, 13, 15, 22, 23, 25, 32, 33, 3*5, … , n1 2, n1 * 3, n1 * 5, n2 * 2, n3 3, n2 * 5],因为后面乘2的数据可能会比前面乘3乘5的数据要小,那这个乘2的数应该排在他们的前面, 后面乘3的数据也可能比前面乘5的数据要小,那这个乘3的数应该排在他们的前面。
那怎么办呢,每个数都必须乘2, 乘3, 乘5这样才能保证求出所有的丑数,而且还要保证丑数的顺序,这个改如何同时实现呢?
通过观察网上的各个题解,终于找到了办法,那就是记录每个丑数是否已经被乘2, 乘3, 乘5了, 具体的做法是
设置3个索引a, b, c,分别记录前几个数已经被乘2, 乘3, 乘5了,比如a表示前(a-1)个数都已经乘过一次2了,下次应该乘2的是第a个数;b表示前(b-1)个数都已经乘过一次3了,下次应该乘3的是第b个数;c表示前(c-1)个数都已经乘过一次5了,下次应该乘5的是第c个数;
对于某个状态下的丑数序列,我们知道此时第a个数还没有乘2(有没有乘3或者乘5不知道), 第b个数还没有乘3(有没有乘2或者乘5不知道),第c个数还没有乘5(有没有乘2或者乘3不知道), 下一个丑数一定是从第a丑数乘2, 第b个数乘3, 第c个数乘5中获得,他们三者最小的那个就是下个丑数。
求得下个丑数后就得判断这个丑数是谁,是某个数通过乘2得到的,还是某个数乘3得到的,又或是说某个数通过乘5得到的。我们可以比较一下这个新的丑数等于究竟是等于第a个丑数乘2, 还是第b个数乘3, 还是第c个数乘5, 通过比较我们肯定可以知道这个新的丑数到底是哪个数通过乘哪个数得到的。假设这个新的丑数是通过第a个数乘2得到的,说明此时第a个数已经通过乘2得到了一个新的丑数,那下个通过乘2得到一个新的丑数的数应该是第(a+1)个数,此时我们可以说前 a 个数都已经乘过一次2了,下次应该乘2的是第 (a+1) 个数, 所以a++;如果新的丑数是通过第b个数乘3得到的, 说明此时第 b个数已经通过乘3得到了一个新的丑数,那下个需要通过乘3得到一个新的丑数的数应该是第(b+1)个数,此时我们可以说前 b 个数都已经乘过一次3了,下次应该乘3的是第 (b+1) 个数, 所以 b++;同理,如果这个这个新的丑数是通过第c个数乘5得到的, 那么c++;
但是注意,如果第a个数乘2后等于第b个数乘3,或者等于第c个数乘5, 说明这个新的丑数是有两种或者三种方式可以得到,这时应该给得到这个新丑数的组合对应的索引都加一,比如新丑数是第a个数乘2后和第b个数乘3得到的,那么 a 和 b都应该加一, 因为此时第a个数已经通过乘2得到了一个新的丑数,第b个数已经通过乘3得到了一个新的丑数, 只不过这两个数相等而已。所以我们给计数器加一的时候不能使用 if else else if, 而应该使用if, if, if, 这样才不会把应该加一的计数器漏掉。
经过n次循环,就能得到第n 个丑数了。
class Solution {
public int nthUglyNumber(int n) {
int[] dp = new int[n]; // 使用dp数组来存储丑数序列
dp[0] = 1; // dp[0]已知为1
int a = 0, b = 0, c = 0; // 下个应该通过乘2来获得新丑数的数据是第a个, 同理b, c
for(int i = 1; i < n; i++){
// 第a丑数个数需要通过乘2来得到下个丑数,第b丑数个数需要通过乘2来得到下个丑数,同理第c个数
int n2 = dp[a] * 2, n3 = dp[b] * 3, n5 = dp[c] * 5;
dp[i] = Math.min(Math.min(n2, n3), n5);
if(dp[i] == n2){
a++; // 第a个数已经通过乘2得到了一个新的丑数,那下个需要通过乘2得到一个新的丑数的数应该是第(a+1)个数
}
if(dp[i] == n3){
b++; // 第 b个数已经通过乘3得到了一个新的丑数,那下个需要通过乘3得到一个新的丑数的数应该是第(b+1)个数
}
if(dp[i] == n5){
c++; // 第 c个数已经通过乘5得到了一个新的丑数,那下个需要通过乘5得到一个新的丑数的数应该是第(c+1)个数
}
}
return dp[n-1];
}
}
8.n个骰子的点数
因为最后的结果只与前一个动态转移数组有关,所以这里只需要设置一个一维的动态转移数组。
原本dp[i][j]表示的是前i个骰子的点数之和为j的概率,现在只需要最后的状态的数组,所以就只用一个一维数组dp[j]表示n个骰子下每个结果的概率。
初始是1个骰子情况下的点数之和情况,就只有6个结果,所以用dp的初始化的size是6个。
从第2个骰子开始,这里n表示n个骰子,先从第二个的情况算起,然后再逐步求3个、4个···n个的情况。
i表示当总共i个骰子时的结果,每次的点数之和范围会有点变化,点数之和的值最大是i6,最小是i1,i之前的结果值是不会出现的;比如i=3个骰子时,最小就是3了,不可能是2和1,所以点数之和的值的个数是6i-(i-1),化简:5i+1。
当有i个骰子时的点数之和的值数组先假定是temp,从i-1个骰子的点数之和的值数组入手,计算i个骰子的点数之和数组的值。先拿i-1个骰子的点数之和数组的第j个值,它所影响的是i个骰子时的temp[j+k]的值。
比如只有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神那样的动态规划逆推的图就好理解很多。
这里记得是加上dp数组值与1/6的乘积,1/6是第i个骰子投出某个值的概率。
i个骰子的点数之和全都算出来后,要将temp数组移交给dp数组,dp数组就会代表i个骰子时的可能出现的点数之和的概率;用于计算i+1个骰子时的点数之和的概率。
class Solution {
public double[] dicesProbability(int n) {
//结果只与前一个动态转移数组有关,所以只需设置一个一维的动态转移数组,概率数组初始化为6个位置。
double[] dp = new double[6];
Arrays.fill(dp,1.0/6.0);//初始是1个骰子情况,概率初始化
for(int i=2;i<=n;i++){//2到n个骰子
double[] temp = new double[5*i+1];//(5*n+1)种点数之和情况,i个骰子时的点数之和的值数组先假定是temp
for(int j=0;j<dp.length;j++){//遍历前一个动态转移数组dp:少一个骰子的结果。
for(int k=0;k<6;k++){//增加的6个骰子,拿i-1个骰子的点数之和数组的第j个值,它影响的是i个骰子时的temp[j+k]的值。
//不断的覆盖,题解图比较形象。1/6是第i个骰子投出某个值的概率,原始概率乘以1*6变成新概率,
//temp初始化全为0,概率会叠加是因为j+k=同一个值造成的叠加。从j+k=5的整数倍也可以看出来就是结果的长度
temp[j+k]=temp[j+k]+dp[j]*(1.0/6.0);
}
}
dp = temp;
}
return dp;
}
}
9. 把数字翻译成字符串
其实就是有条件的青蛙跳格子。
compareTo返回参与比较的前后两个字符串的ASCII码的差值(前面减后面),如果两个字符串首字母不同,则该方法返回首字母的ASCII码的差值。compareTo方法
class Solution {
public int translateNum(int num) {
String s = String.valueOf(num);//转换成字符串
int[] dp = new int[s.length()+1];//因为增加了无数字,所以加1
// “无数字” 和 “1位数字” 的翻译方法数量均为 1 ;
dp[0] = 1;
dp[1] = 1;
for(int i = 2; i <= s.length(); i ++){//数字的个数
String temp = s.substring(i-2, i);//遍历取两个数字
//如果最后组成的两位数字可以被翻译
if(temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0)
dp[i] = dp[i-1] + dp[i-2];
//如果最后组成的两位数字不能被翻译
else
dp[i] = dp[i-1];
}
return dp[s.length()];
}
}
10.1~n 整数中 1 出现的次数
思路:详细思路
求百位是1的个数,相当于固定了百位
以2593为例,例子挺经典,k为5
从最后一位开始,cur表示当前位的值依次为3,9,5,2,high,low分别表示cur的前后,比如:
cur为9,high=25,low=3
下面开始分析:
①cur = 3,high=259,low=0,[1,3]中不可能出现5,所以前缀只能取0,1,2,3…258,取不到259,所以5在个位出现了259次.
②cur = 9,high = 25,low=3,[1-93]中可以出现5x,所以前缀可以0,1,2,3…25,后缀可以取0,1,…,9。所以5在10位出现了26x10=260次.
③cur = 5,high = 2,low=93,[1-593]中可以出现5xx,所以前缀可以取0,1,2。后缀可以取0,1,…,93。当前缀取0,1,后缀可取0,1,…99。当前缀取2,后缀可取0,1,…93。5在百位出现了2x100+93+1次,(为啥加1呢,0-93是94次,所以加1)。
④cur = 2,high = 0,low=593,[1-2593]中不能出现5xxx,因此5在千位0次.
总结规律:
base = 10^(i-1)
cur >k,结果为 (high+1)*digit
cur ==k,结果为 (high)digit+ low + 1
cur < k,结果为 highdigit
class Solution {
public int countDigitOne(int n) {
int digit = 1, res = 0;
int high = n / 10, cur = n % 10, low = 0;//初始化
while(high != 0 || cur != 0) {//当 high 和 cur 同时为 0 时,说明已经越过最高位,因此跳出
if(cur == 0){
res =res+ high * digit;
}
else if(cur == 1){
res =res+ high * digit + (low + 1);
}
else{
res =res+ (high + 1) * digit;//实际上也等于high * digit + (low + 1),刚好(low + 1)=digit
}
low=low+ cur * digit;//将 cur 加入 low ,组成下轮 low
cur = high % 10;//下轮 cur 是本轮 high 的最低位
high /= 10; //将本轮 high 最低位删除,得到下轮 high
digit *= 10;// 位因子每轮 × 10
}
return res;
}
}
三、链表
1.反转链表
题目:
答案:
class Solution {
ListNode pre=null,temp=null;
public ListNode reverseList(ListNode head) {
if(head==null){
return null;
}
while(head!=null){
temp=head.next;
head.next=pre;
pre=head;
head=temp;
}
return pre;
}
}
2.从尾到头打印链表(用递归)
class Solution {
ArrayList<Integer> array=new ArrayList<>();
public int[] reversePrint(ListNode head) {
recur(head);
//复制到数组
int[] res=new int[array.size()];
for(int i=0;i<res.length;i++){
res[i]=array.get(i);
}
return res;
}
//递归倒序进列表
public void recur(ListNode head){
if(head==null){
return;
}
recur(head.next);
array.add(head.val);
}
}
3.链表中倒数第k个节点(用非递归法)
双指针:第一个指针先走k步,然后两个指针同时走。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode former = head, latter = head;
for(int i = 0; i < k; i++)
former = former.next;
while(former != null) {
former = former.next;
latter = latter.next;
}
return latter;
}
}
4.合并两个排序的链表
最初写的代码不够简洁:巧妙的new ListNode(0)和p.next就不用先找头结点.
class Solution {
//
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1==null){
return l2;
}
if(l2==null){
return l1;
}
if(l1==null||l2==null){
return null;
}
ListNode res=new ListNode(0);//创建了一个0结点,d多了一个结点,不过p.next解决了这个,就不需要先找头结点了
ListNode p=res;
while(l1!=null&&l2!=null){
if(l1.val>=l2.val){
res.next=l2;
l2=l2.next;
}else{
res.next=l1;
l1=l1.next;
}
res=res.next;
}
if(l1!=null){
res.next=l1;
}
if(l2!=null){
res.next=l2;
}
return p.next;
}
}
5.两个链表的第一个公共节点
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode a=headA;
ListNode b=headB;
while(a!=b){
if(a!=null){//假如两个链表没有公共节点,就把NULL当作它们的公共节点,所以两个判断是X!=NULL而不是X.next!=NULL
a=a.next;
}else{
a=headB;
}
if(b!=null){
b=b.next;
}else{
b=headA;
}
}
return a;
}
}
6.复杂链表的复制
整体思路:
1. 首先可能会想到双指针边遍历边复制这种方式, 但是此方式只适合普通链表的复制
由于本题中存在 random 属性,其指向的结点是随机的,因此双指针这种方式不行(因为链表查找元素只能一个一个遍历查找)
2. 那么问题在于,怎么使其变成可以支持快速访问呢? --> 使用哈希表, 存储每个结点复制后的新结点;
3. 在得到新结点与旧结点的映射关系后,怎么进行复制呢?
4. 问题转化为 通过某种方式 将新结点按照旧结点的关系进行连线
由于要参照旧结点的关系 —> 一定需要遍历原始链表
5. 连线方式:遍历到原始链表的当前结点cur, 从 map 中取出 cur 结点映射的结点 newCur,
我们需要做的就是 将newCur 按照 cur 在原始链表中的关系连线,
因为是要对 新结点 进行连线, 因此所有的结点都必须 从 map 中取对应的新结点
newCur.next 对应的 next 在map中的映射为 map.get(cur.next);
newCur.random 对应的 random 在 map中映射为 map.get(cur.random);
6. 返回头结点, 新链表的头结点也存储在 map中, 因此需要返回 map.get(head);!!!!
不能改成下面的代码:因为最后进行连接的是map中存放的新结点,这样属于一个新的,一个旧的(旧结点还有原来的连线),所以连出来的不对 整体思路是创建一份新的没有连接关系的结点,然后按照原来的关系进行连接,因为都要从map中取新结点。
map.get(cur).next = cur.next;
map.get(cur).random = cur.random;
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
Node cur = head;//定义遍历指针cur
HashMap<Node, Node> map = new HashMap();
// 存储每个结点的新结点,此时新节点(map.get(x)代表新节点)之间没有关系
while(cur != null){
map.put(cur, new Node(cur.val));//Node(cur.val)使用了构造函数
cur = cur.next;
}
cur = head; // 将 cur 指针重置为 head 头结点进行遍历
// 问题转化为 通过某种方式 将新结点按照旧结点的关系进行连线
// 由于要参照旧结点的关系 ---> 推出一定需要遍历原始链表
while(cur != null){
map.get(cur).next = map.get(cur.next);//map.get(x)代表新节点,map.get(cur.next)就代表cur.next所在的新节点
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
// 返回头结点, 新链表的头结点也存储在 map中, 因此需要返回 map.get(head);!!!!
return map.get(head);
}
}
四、二叉树
1.二叉树的深度
思路:dfs和bfs;
//DFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
}
//BFS
/**
* 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> queue = new LinkedList<>();//队列
queue.add(root);//头结点先进队列
int res = 0;//初始化结果
while (!queue.isEmpty()) {//队列非空
res++;
int n = queue.size();//队列的大小
for (int i = 0; i < n; i++) {//上层出队,下层进队,
TreeNode node = queue.poll();//出队并复制
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
}
}
return res;
}
}
2.二叉树的最近公共祖先
思路:一层层回溯return。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 如果树为空,直接返回null
if(root == null) return null;
// 如果 p和q中有等于 root的,那么它们的最近公共祖先即为root(一个节点也可以是它自己的祖先)
if(root == p || root == q) return root;
// 递归遍历左子树,只要在左子树中找到了p或q,则先找到谁就返回谁
TreeNode left = lowestCommonAncestor(root.left, p, q);
// 递归遍历右子树,只要在右子树中找到了p或q,则先找到谁就返回谁
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果在左子树中 p和 q都找不到,则 p和 q一定都在右子树中,右子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
if(left == null) return right;
else if(right == null) return left; // 如果 left不为空,在左子树中有找到节点(p或q),这时候要再判断一下右子树中的情况,如果在右子树中,p和q都找不到,则 p和q一定都在左子树中,左子树中先遍历到的那个就是最近公共祖先(一个节点也可以是它自己的祖先)
else return root; //当 left和 right均不为空时,说明 p、q节点分别在 root异侧, 最近公共祖先即为 root
}
}
3.二叉搜索树的最近公共祖先
思路:一层层回溯return。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
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;
}
3.二叉搜索树的第k大节点
题目:给定一棵二叉搜索树,请找出其中第k大的节点。
答案:求 “二叉搜索树第 k 大的节点” 可转化为求 “此树的中序遍历倒序的第 k 个节点”。
关键还是用类变量来维护k和res。类变量(也叫静态变量)是类中独立于方法之外的变量,用static 修饰。
class Solution {
int res, k;
public int kthLargest(TreeNode root, int k) {
this.k = k;//因为下面的函数要用到k,而这个函数不好递归
dfs(root);
return res;
}
void dfs(TreeNode root) {//中序遍历倒序的第 kk 个节点
if(root == null) return;
dfs(root.right);
if(k == 0){
return;
}
if(--k == 0) {
res = root.val;
}
dfs(root.left);
}
}
错误做法:
public static int n;
public int kthLargest(TreeNode root, int k) {
if(root==null){
return 0;
}
kthLargest(root.right,k);
n=--k;//这样做n的值一直不变,所以k要作为类变量
if(n==0){
res=root.val;
}
kthLargest(root.left,k);
return res;
}
4.二叉树的镜像
题目:请完成一个函数,输入一个二叉树,该函数输出它的镜像。
答案:
递归解析:
- 终止条件: 当节点 root 为空时(即越过叶节点),则返回 null;
- 递推工作:
初始化节点 tmp ,用于暂存 root 的左子节点;
开启递归 右子节点 mirrorTree(root.right) ,并将返回值作为 root 的 左子节点 。
开启递归 左子节点 mirrorTree(tmp),并将返回值作为 root 的 右子节点 。 - 返回值: 返回当前节点 root ;
当结点是叶子结点是就会return它自己。
复杂度分析:
时间复杂度 O(N): 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用O(N) 时间。
空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N)大小的栈空间。
代码:
class Solution {
public TreeNode mirrorTree(TreeNode root) {
if(root == null) return null;
TreeNode tmp = root.left;//先暂存左右结点都一样
root.left = mirrorTree(root.right);
root.right = mirrorTree(tmp);
return root;
}
}
5.对称的二叉树
思路:
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);
}
}
6.平衡二叉树
错误做法
class Solution {
public boolean isBalanced(TreeNode root) {
if(root==null){
return true;
}
if(Math.abs(gaodu(root.left)-gaodu(root.right))>1){
return false;
}
return true;
}
public int gaodu(TreeNode t) {
int lgao=0,rgao=0;
if(t==null){
return 0;
}
if(t.left!=null){
lgao=1+gaodu(t.left);
}
if(t.right!=null){
rgao=1+gaodu(t.right);
}
return lgao>rgao?lgao:rgao;
}
}
上面错误原因:
1.没有考虑到每个子树都需要是平衡二叉树。
2.T不为null高度至少为1。
class Solution {
public boolean isBalanced(TreeNode root) {
if (root == null) return true;
return Math.abs(depth(root.left) - depth(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
}
private int depth(TreeNode root) {
if (root == null) return 0;
return Math.max(depth(root.left), depth(root.right)) + 1;
}
}
7.从上到下打印二叉树 II
思路:层序遍历。在求二叉树的深度的代码基础上改进即可。
常用函数:
1.倒转newl列表:Collections.reverse(newl)
2.复制int[] b=Arrays.copyOfRange(res, 0, s),取得res数组前s个数给数组b。
/**
* 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>> l=new LinkedList<>();
Queue<TreeNode> queue = new LinkedList<>();//创建队列(树节点类型)
if(root==null){
return l;
}
queue.add(root);//头结点先进队列
while(!queue.isEmpty()){//判断队列是否为空
List<Integer> newl=new LinkedList<>();//辅助一维列表
int n = queue.size();//得出队列的大小
for (int i = 0; i < n; i++) {//上层出队,下层进队,
TreeNode node = queue.poll();//出队并复制
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
newl.add(node.val);
}
l.add(newl);//一维列表进二维列表
}
return l;
}
}
8.重建二叉树
思路:关键在于理解三个指针,使用简单例子更容易理解。
先序遍历:中左右。
中序遍历:左中右。
HashMap泛型的类型参数T 不能使用原始类型,应该是用原始类型的包装类。
/**
* 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);
}
//三个坐标:
//1.先序遍历根节点的坐标
//2.中序遍历的左边界
//3.中序遍历的右边界
return recur(0,0,inorder.length-1);
}
//主要操作在于中序:包括三个指针
TreeNode recur(int pre_root, int leftindex, int rightindex){
if(leftindex> rightindex) return null;// 越界说明为空,就返回null,相等的话就是自己,自己为root
TreeNode root = new TreeNode(preorder[pre_root]);//获取root节点
int index= map.get(preorder[pre_root]);//获取在中序遍历中根节点所在坐标,以方便获取左子树的数量
//如果有左子树,左子树的根的坐标为[先]序中的根节点+1 ,没有就返回null
//递归左子树的左边界:不变,为原来的中序in_left
//递归左子树的右边界为原来的中序中的根节点索引-1
root.left = recur(pre_root+1, leftindex, index-1);//(1,0,0)
//如果有右子树,右子树的根的坐标为[先]序中的 当前根位置pre_root + 左子树的数量(idx - in_left) + 1 ,没有就返回null
//递归右子树的左边界为中序中当前根节点+1
//递归右子树的右边界:不变,中序中原来右子树的边界
root.right = recur(pre_root + (idx - leftindex) + 1, index+1, rightindex);//(2,2,4)
return root;
}
}
9.树的子结构
思路:
注意下图情况:两个不直接连接的节点,遍历第一个节点就返回了true,但实际为false。
class Solution {
public boolean isSubStructure(TreeNode A, TreeNode B) {
//对这棵树进行前序遍历(也就是遍历了A树的所有节点):即处理根节点,再递归左子节点,再递归处理右子节点
//特殊情况是:当A或B是空树的时候 返回false
//用||关系可以达到 不同顺序遍历的作用
if(A==null||B==null){
return false;
}
boolean inleft=isSubStructure(A.left,B);
boolean inright=isSubStructure(A.right,B);
// isSubStructure(A.left,B);//这样写虽然遍历了所有节点,但是返回的是叶子节点是否存在B
// isSubStructure(A.right,B);
//遍历A中每个节点,A树中任一节点包含B就能返回true
return recur(A,B)||inleft||inright;
}
//此函数的作用是从上个函数得到的根节点开始递归比较 是否是子树
boolean recur(TreeNode A, TreeNode B){
//当最后一层B已经为空的,证明则B中节点全是A中节点
if(B==null){
return true;
}
//这里因为有上一个条件,则说明 A已经为空了,B却不为空,则一定不是子树
if(A==null){
return false;
}
//处理本次递归,即处理当前节点
if(A.val!=B.val){//不使用==return true;因为第一个节点相同不代表就是子树,需要遍历完B树
return false;
}
//递归,同时递归左右两个子节点
return recur(A.left,B.left)&&recur(A.right,B.right);
}
}
10.二叉搜索树的后序遍历序列
思路:答案区讲的很清楚,后序遍历最右边是根节点。
class Solution {
public boolean verifyPostorder(int[] postorder) {
//定义两个指针一个指向第一个,第二个指向根节点(在最右端)
return recur(postorder, 0, postorder.length - 1);
}
boolean recur(int[] postorder, int i, int j) {
//说明此子树节点数量≤1 ,无需判别正确性,因此直接返回true ;
if(i >= j) return true;
int p = i;//遍历指针,每次从头开始遍历
while(postorder[p] < postorder[j]) p++;
int m = p;//第一个大于根节点 的节点,索引记为 m,划分左右子树
while(postorder[p] > postorder[j]) p++;
//p=j 判断是否为二叉搜索树,另外两个继续判断子树
return p == j && recur(postorder, i, m - 1) && recur(postorder, m, j - 1);
}
}
11. 二叉树中和为某一值的路径
思路:
因为递归时传入的是 path 的引用(地址),因此回溯前也需要将 path 恢复。 removeLast() 比较方便,不用传入数组索引。
res.add(path)将 path 对象加入了 res ;后续 path 改变时, res 中的 path 对象也会随之改变。正确做法:res.add(new LinkedList(path))相当于复制了一个 path 并加入到 res 。
另外这里没有用ArrayList,而用的LinkedList,也是为了能够方便调用其专有的removeLast()方法。
class Solution {
LinkedList<List<Integer>> res = new LinkedList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int target) {
recur(root, target);
return res;
}
void recur(TreeNode root, int tar) {
if(root==null){
return;
}
path.add(root.val);
tar=tar-root.val;
if(root.left==null&&root.right==null&&tar==0){
// res.add(path);
// path=new LinkedList<>();
res.add(new LinkedList(path));
}
recur(root.left,tar);
recur(root.right,tar);
path.removeLast();
}
}
12. 二叉搜索树与双向链表
题目较长:原题
class Solution {
Node pre,head;//定义指向前驱节点和头节点的指针
public Node treeToDoublyList(Node root) {
if(root==null) return null;
cur(root);
pre.right = head;//pre最后指向的是最后一个节点
head.left =pre;
return head;
}
//c节点指向当前节点
public void cur(Node c){
if(c==null){
return;
}
//中序遍历
cur(c.left);
if(pre==null){
head=c;//固定好头结点
}else{
pre.right=c;
}
c.left=pre;
pre=c;
cur(c.right);
}
}
13. 序列化二叉树
public class Codec {
public String serialize(TreeNode root) {
if(root == null){
return "[]";
}
StringBuilder res = new StringBuilder("[");
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()) {
TreeNode node = queue.poll();//队头
if(node != null) {//非null才加孩子
res.append(node.val + ",");//直接append int类型不需要炸转换成string
queue.add(node.left);//进队
queue.add(node.right);
}
else{
res.append("null,");
}
}
res.deleteCharAt(res.length() - 1);//删除最后一个逗号
res.append("]");
return res.toString();
}
public TreeNode deserialize(String data) {
if(data.equals("[]")){
return null;
}
String[] vals = data.substring(1, data.length() - 1).split(",");//取得所有节点的val,substring不包含结尾
TreeNode root = new TreeNode(Integer.parseInt(vals[0]));//第一个数字是根节点
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int i = 1;
while(!queue.isEmpty()) {
TreeNode node = queue.poll();
if(!vals[i].equals("null")) {
node.left = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.left);
}
i++;
if(!vals[i].equals("null")) {
node.right = new TreeNode(Integer.parseInt(vals[i]));
queue.add(node.right);
}
i++;
}
return root;
}
}
五、字符串
1.替换空格
会做但是这个效率高。
思路:直接取字符串的字符进行判断
class Solution {
public String replaceSpace(String s) {
StringBuilder res = new StringBuilder();
for(int i = 0 ; i < s.length(); i++){
char c = s.charAt(i);
if(c == ' ') res.append("%20");
else res.append(c);
}
return res.toString();
}
}
2. I.翻转单词顺序
String中用“==”比较的是地址,用equals比较的是内容。
注意空单词的例子。
class Solution {
public String reverseWords(String s) {
String[] strs = s.trim().split(" "); // 删除首尾空格,分割字符串
StringBuilder res = new StringBuilder();
for(int i = strs.length - 1; i >= 0; i--) { // 倒序遍历单词列表
if(strs[i].equals("")) continue; // 遇到空单词则跳过
res.append(strs[i] + " "); // 将单词拼接至 StringBuilder
}
return res.toString().trim(); // 转化为字符串,删除尾部空格,并返回
}
}
3.II. 左旋转字符串(主要是忘了函数)
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n) + s.substring(0, n);
}
}
4.字符串的排列
动图有助于理解:题解
返回时交换回来,这样保证到达第1层的时候,一直都是abc。这里捋顺一下,开始一直都是abc,那么第一位置总共就3个交换
分别是
①a与a交换,这个就相当于 x = 0, i = 0;
②a与b交换 x = 0, i = 1;
③a与c交换 x = 0, i = 2;
就相当于上图中开始的三条路径,第一个元素固定后,每个引出两条路径,
①b与b交换 x = 1, i = 1;
②b与c交换 x = 1, i = 2;
所以,结合上图,在每条路径上标注上i的值,就会非常容易好理解了.
1.hashset:哈希集合,不需要键值对,所以不用hashmap。
2.将字符数组转换为字符串:String.valueOf()
3.ArrayList和LinkedList
4.把list转换成数组:res.toArray(new String[res.size()])
class Solution {
List<String> res = new LinkedList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return res.toArray(new String[res.size()]);//将字符串数组ArrayList转化为String类型数组
}
void dfs(int x) {//深搜
if(x == c.length - 1) {//到了最后一个字母
res.add(String.valueOf(c)); // 将字符数组转换为字符串,添加排列方案
return;
}
//为了防止同一层递归出现重复元素,使用hash
HashSet<Character> set = new HashSet<>();//哈希集合,只要一个数字不需要map
for(int i = x; i < c.length; i++) {//主要循环体
if(set.contains(c[i])){
continue; // 字母重复,因此剪枝
}
set.add(c[i]); //字母不重复就加入
swap(i, x); // 交换,将 c[i] 固定在第 x 位
dfs(x + 1); //进入下一层递归
swap(i, x); // 恢复,返回时交换回来,这样保证到达第1层的时候,一直都是abc
}
}
//交换函数
void swap(int a, int b) {
char tmp = c[a];
c[a] = c[b];
c[b] = tmp;
}
}
5. 把字符串转换成整数
思路:
class Solution {
public int strToInt(String str) {
char[] chars = str.trim().toCharArray(); //去前后空格并转换为char数组
if (chars.length == 0){//由题意如果内容长度为0,直接返回0,
return 0;
}
//记录第一个符合是否为负数
int sign = 1;
//默认开始遍历的位置,也就是默认为正数
int i = 1;
if (chars[0] == '-') {//如果首个非空格字符为负号,那么从位置1开始遍历字符串,并且结果需要变成负数
sign = -1;
} else if (chars[0] != '+') { //如果首个非空格字符不是负号也不是加号,那么从第一个元素开始遍历
i = 0;
}
int number = Integer.MAX_VALUE / 10;// 因为不能直接赋值为Integer.MAX_VALUE,需要绕着实现
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;
}
}
6. 表示数值的字符串
思路:举出所有情况
三种特殊情况
1.‘e’或’E’前面不能不是整数,前面不能出现‘e’或’E’。
2.小数点之前不能出现小数点、且不能出现‘e’、‘E’。
3.正负号不在第一个位置,或者不在‘e’或’E’的后面一个位置。
需要else return false;
比如". 1"
补充:e前面可以为点。
class Solution {
public boolean isNumber(String s) {
char[] str = s.trim().toCharArray(); // 删除字符串头尾的空格,转为字符数组,方便遍历判断每个字符
if (str.length == 0){//由题意如果内容长度为0,直接返回0,
return false;
}
boolean isNum = false, isDot = false, ise_or_E = false; // 标记是否为数字、小数点、‘e’或'E'
for(int i=0; i<str.length; i++) {
if(str[i] >= '0' && str[i] <= '9'){// 判断当前字符是否为 0~9 的数字
isNum = true;
}
else if(str[i] == 'e' || str[i] == 'E') { // 1.‘e’或'E'前面不是整数,且前面重复出现‘e’或'E'
if(!isNum || ise_or_E){
return false;
}
ise_or_E = true; // 标记已经遇到‘e’或'E'
isNum = false; // 重置isNum,因为‘e’或'E'之后也必须接上整数,防止出现 123e或者123e+的非法情况
}
else if(str[i] == '.') { // 遇到小数点
if(isDot || ise_or_E){// 2.小数点之前重复出现小数点、或出现‘e’、'E'
return false;
}
isDot = true; // 标记已经遇到小数点
}
else if(str[i] == '-' ||str[i] == '+') { // 3.正负号不出现在第一个位置,或者不出现在‘e’或'E'的后面一个位置,
if(i!=0 && str[i-1] != 'e' && str[i-1] != 'E'){
return false;
}
}
else{// 其它情况均为不合法字符
return false;
}
}
return isNum;
}
}
六、栈,队列
1.用两个栈实现队列
我的思路:进栈就把所有元素先拿出来放到另一个栈,效率太差了。
更好的思路:栈1有数字就放入栈2(但要保证元素是对的),取栈顶直接取栈2的顶部。
Stack 已经被 Java 官方说明不推荐使用了,因为本题是要用两个栈实现队列,因此要把 LinkedList 看作栈来使用。
class CQueue {
LinkedList<Integer> A, B;
public CQueue() {
A = new LinkedList<Integer>();
B = new LinkedList<Integer>();
}
public void appendTail(int value) {
A.addLast(value);
}
public int deleteHead() {
if(!B.isEmpty()) return B.removeLast();//保证栈顶元素是对的
if(A.isEmpty()) return -1;
while(!A.isEmpty())
B.addLast(A.removeLast());
return B.removeLast();
}
}
2.包含min函数的栈
1.如果用==将会无法通过 Integer的equals重写过,比较的是内部value的值, ==如果在[-128,127]会被cache缓存,超过这个范围则比较的是对象是否相同。
2.B.peek() >= x 避免了重复最小值被弹出。
class MinStack {
Stack<Integer> A, B;
public MinStack() {
A = new Stack<>();
B = new Stack<>();
}
public void push(int x) {
A.add(x);
if(B.empty() || B.peek() >= x)
B.add(x);
}
public void pop() {
if(A.pop().equals(B.peek()))//有A.pop()就代表出栈了
B.pop();
}
public int top() {//需要取顶元素所以适合用Stack而不是LinkeList
return A.peek();
}
public int min() {
return B.peek();
}
}
3.II. 队列的最大值
Deque:双向队列。
peekFirst():队头元素值。
pollFirst():队头出队。
offerFirst():进队头(这里没用到)
peekLast():队尾元素值。
offerLast(value):进队尾。
pollLast():队尾出队。
class MaxQueue {
Queue<Integer> queue= new LinkedList<>();
Deque<Integer> deque= new LinkedList<>();//双向队列
public MaxQueue() {
}
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();//出队并返回,因为有返回值要求
}
}
4. I. 滑动窗口的最大值
双端单调递减队列:详细思路
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0){
return new int[0];
}
//单调队列:
//队列按从大到小放入
//如果首位值(即最大值)不在窗口区间,删除首位
//如果新增的值小于队列尾部值,加到队列尾部
//如果新增值大于队列尾部值,删除队列中比新增值小的值,再把新增值加入到队列中
//如果新增值大于队列中所有值,删除所有,然后把新增值放到队列首位,保证队列一直是从大到小
Deque<Integer> deque = new LinkedList<>(); //双端单调递减队列
int[] res = new int[nums.length - k + 1];//结果数组
// 未形成窗口
for(int i = 0; i < k; i++) {
//队列不为空时,当前值与队列尾部值比较,如果大于,删除队列尾部值
//一直循环删除到队列中的值都大于当前值,或者删到队列为空
while(!deque.isEmpty() && deque.peekLast() < nums[i]){
deque.removeLast();
}
//执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中
deque.addLast(nums[i]);
}
//窗口区间刚形成后,把队列首位值添加到队列中
//因为窗口形成后,就需要把队列首位添加到数组中,而下面的循环是直接跳过这一步的,所以需要直接添加
int index=0;
res[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() && deque.peekLast() < nums[i]){
deque.removeLast();
}
//把当前值添加到队列中
deque.addLast(nums[i]);
//把队列的首位值添加到arr数组中
res[index++] = deque.peekFirst();
}
return res;
}
}
七、位运算
1.不用加减乘除做加法
思路:
(a&b)<<1为相加的进位。
a^b为没有进位的和。
class Solution {
public int add(int a, int b) {
if (b == 0) {
return a;
}
// 转换成非进位和 + 进位
return add(a ^ b, (a & b) << 1);
}
}
2.求1+2+…+n
思路:
class Solution {
public int sumNums(int n) {
//左边为false,就不执行右边。
//左边为true,就执行右边。
//右边的>0,0任意设置都可以,和x一样是工具人
boolean x = (n > 1) && ((n =n+ sumNums(n - 1)) > 0);
return n;
}
}
八、哈希表
1.第一个只出现一次的字符
class Solution {
public char firstUniqChar(String s) {
HashMap<Character, Boolean> map = new HashMap<>();
char[] sc = s.toCharArray();//转换成char数组
for(char c : sc)//遍历char数组
map.put(c, !map.containsKey(c));//containsKey()包含,已经包含就更改成false,最后只剩下一个true
for(char c : sc)//遍历char数组 只剩下一个true
if(map.get(c)) return c;
return ' ';
}
}
九、滑动窗口
1. II. 和为s的连续正数序列
class Solution {
public int[][] findContinuousSequence(int target) {
int i = 1, j = 2, s = 3;
List<int[]> res = new ArrayList<>();
while(i < j) {//窗口左边界i,右边界j
if(s == target) {//s统计和,满足条件
int[] ans = new int[j - i + 1];//创建窗口大小的答案数组
for(int k = i; k <= j; k++)//复制到答案数组
ans[k - i] = k;
res.add(ans);//加入结果
}
if(s >= target) {//i前进一格,找到了也要前进一格
s=s-i;
i++;//前进之前减去
}else{//j前进一格
j++;
s=s+j;//前进之后再加上
}
}
return res.toArray(new int[res.size()][]);//int[0][] 就是多维数组第一维的数目吧,也就是res中ans的数目
}
}
2. 最长不含重复字符的子字符串
长度length、length()、size()
下面是我自己的做法,效率较低。
class Solution {
public int lengthOfLongestSubstring(String s) {
// 记录每个字母出现的最后位置
HashMap<Character, Integer> hashMap = new HashMap<>();
int left = 0;
int right = 0;
int res = 0;
while (right < s.length()) {//遍历整个字符串
char ch = s.charAt(right);
if (hashMap.containsKey(ch)) {
// 当前值已经出现过了,更新左边界
left = Math.max(left, hashMap.get(ch) + 1);
}
//更新当前字母最后出现的下标
hashMap.put(ch, right);
// 统计不含重复字符的子字符串的长度
res = Math.max(res, right - left + 1);
right++;
}
return res;
}
}
答案快一个数量级。
思路:双指针,右指针遍历,左指针随时更新,从而统计每个不含重复字符的子字符串长度。
class Solution {
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> map=new HashMap<>();
//定义两个指针和最大值
int left=0;
int right=0;
int max=0;
while(right<s.length()){//右指针遍历字符串
char ch=s.charAt(right);//取右边指针字母
if(map.containsKey(ch)){
//更新左边界
left=Math.max(left,map.get(ch)+1);
}
//更新每个字母的最后坐标
map.put(ch,right);
//right-left+1统计每个不含重复字符的子字符串长度
//并更新到max
max=Math.max(max,right-left+1);
right++;//移动右指针
}
return max;
}
}
十、数学
1.数字序列中某一位的数字
思路:
int类型和long类型计算,会先把int类型转换成long类型。
class Solution {
public int findNthDigit(int n) {
if(n==0){
return 0;
}
//排除n=0后,后面n从1开始。
int digit=1;//从1位数开始
long count=9;//count的值有可能会超出int的范围,所以变量类型取为long,存储当前位数(大段)的总坐标数
int start=1;//存储每次位数的起点,1~9,10~99
//1.找到在哪个大段以及所在位数的起点
while(n>count){//n≤count结束循环
n=(int)(n-count);//n减去当前位数的总坐标数,这里的int不能省略
digit++;//进入下一个位数
start=start*10;//更新位数起点
count=(long)start*9*digit;//更新count,一个数组加long,都会变成long
//这里的long不能省略,否则,会先按照int类型进行计算,然后赋值给long型的count,超过int大小限制时,会出现负数
}
//2.找到在哪个数字
int sum=start+(n-1)/digit;//比如10的1和0。(1-1)/2=0,(2-1)/2=0
//3.找到需要的那个
int index=(n-1)%digit;//因为是0到digit-1位,不是n-1就是最后是0
//下面的代码去一个简单的例子就容易知道,比如获取4567中的5
while(index<(digit-1)){
sum=sum/10;
index++;
}
return sum%10;//此时num的右侧末尾数字即为结果,比如想得到120中的2,先除以10得到12,再12%10
}
}
2.数值的整数次方
思路:快速幂:详细思路
class Solution {
public double myPow(double x, int n) {
//将正数n和负数n都给转换为正数n进行计算
//注意:Java 代码中 int32 变量n∈[−2147483648,2147483647]
//因此当 n = -2147483648 时执行 n = -n 会因越界而赋值出错
//我们此处一开始就把 n 用 long 存储
long b = n;
if (n < 0) {//幂小于0转换成分数形式
b = -b;
x = 1 / x;
}
return culc(x, b);
}
//快速幂模版
public double culc(double base, long power) {
double res = 1.0;//存储结果
while (power > 0) {
//两个作用
//1.幂次若为奇数,提前多乘一次x
//2.当幂次除到1,把x赋值给res
if ((power & 1) == 1) {//power % 2 == 1 变为 (power & 1) == 1判断奇数
res *= base;
}
//幂次除以2
power = power >> 1;//power = power / 2 变为 power = power >> 1
//底数平方
base = base * base;
}
return res;
}
}
十一、堆
1.数据流中的中位数
思路:大小顶堆。详细思路
class MedianFinder {
Queue<Integer> A, B;
public MedianFinder() {
A = new PriorityQueue<>(); // 优先队列小顶堆,保存较大的一半
B = new PriorityQueue<>((x, y) -> (y - x)); // lambda表达式定义大顶堆,保存较小的一半
}
public void addNum(int num) {//这样就可以始终保持 A 保存较大一半、
if(A.size() != B.size()) {//不相等加到B(先入A再弹出A堆顶入B),因为新加入的都会重新排序成顶堆
A.add(num);
B.add(A.poll());
} else { //相等加到A,(先入B再弹出B堆顶入A)
B.add(num);
A.add(B.poll());
}
}
public double findMedian() {
if(A.size() != B.size()){//不相等结果就在A顶 比如 A[6,5,4] B[3,2]
return A.peek();
}
return (A.peek() + B.peek()) / 2.0; //相等就取两顶平均 比如 A[5,4] B[3,2]
}
}
腾讯精选练习 50 题
一、数组
1.螺旋矩阵(和剑指offer第8题差不多)
差别在于返回的是列表
class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res=new ArrayList<>();
if(matrix.length == 0) return res;
int a = 0, d = matrix[0].length - 1, w = 0, s = matrix.length - 1, x = 0;
int gehsu=(d+1)*(s+1);//总个数
int i,j;
while(true){
for(j=a;j<=d;j++){// a to d.
res.add(matrix[w][j]);
}
w++;
if(res.size()==gehsu) break;
for(i=w;i<=s;i++){// w to s.
res.add(matrix[i][d]);
}
d--;
if(res.size()== gehsu) break;
for(j=d;j>=a;j--){// d to a.
res.add(matrix[s][j]);
}
s--;
if(res.size()== gehsu) break;
for(i=s;i>=w;i--){// s to w,这里和剑指offer有点不一样
res.add(matrix[i][a]);
}
a++;
if(res.size()== gehsu) break;
}
return res;
}
}
2.盛最多水的容器
思路:
定义两个指针,一个指向最左,一个指向最右。
面积取决于短板。①因此即使长板往内移动时遇到更长的板,矩形的面积也不会改变;遇到更短的板时,面积会变小。②因此想要面积变大,只能让短板往内移动(因为移动方向固定了),当然也有可能让面积变得更小,但只有这样才存在让面积变大的可能性…
class Solution {
public int maxArea(int[] height) {
int a=0;
int b=height.length-1;
int area=0;
while(a<b){
if(height[a]<=height[b]){
area=Math.max(area,(b-a)*Math.min(height[a],height[b]));
a++;
}else{
area=Math.max(area,(b-a)*Math.min(height[a],height[b]));
b--;
}
}
return area;
}
}
3.最接近的三数之和
class Solution {
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);//排序
int res = nums[0] + nums[1] + nums[2];//初始化结果
for(int i=0;i<nums.length;i++) {//遍历数组
int start = i+1, end = nums.length - 1;
while(start < end) {
int sum = nums[start] + nums[end] + nums[i];//更新当前和
if(Math.abs(target - sum) < Math.abs(target - res))//距离更近则更新结果
res = sum;
if(sum==target){//距离为0直接返回结果
return res;
}
if(sum > target)//当前和比targe大
end--;
else if(sum < target)//当前和比targe小
start++;
}
}
return res;
}
}
4.三数之和
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 排序
Arrays.sort(nums);
List<List<Integer>> result = new LinkedList<>();
// k、i、j分别为第1、2、3位的数
for (int k = 0; k < nums.length - 2; k++) {
if (nums[k] > 0) {//若nums[k]大于0,则后面的数字也是大于零(排序后是递增的)
return result;
}
if (k > 0 && nums[k] == nums[k - 1]) {//nums[k]值重复了,去重,k>0因为有k-1
continue;
}
int i = k + 1, j = nums.length - 1;
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if ( sum==0) {
List<Integer> temp = new LinkedList<>();
temp.add(nums[k]);
temp.add(nums[i]);
temp.add( nums[j]);
result.add(temp);
while (i < j && nums[i] == nums[i + 1]) {//左指针去重
i++;
}
while (i < j && nums[j] == nums[j - 1]) {//右指针去重
j--;
}
i++;//左指针前进
j--;//右指针后退
} else if (sum<0) { //和<0左指针前进
i++;
} else {//和>0右指针后退
j--;
}
}
}
return result;
}
}
5.搜索旋转排序数组
思路:这道题和平常二分法查找的不同就在于,把一个有序递增的数组分成了,两个递增的数组,我们需要做的就是判断这个数在哪一个递增的数组中,然后再去用常规的二分法去解决
class Solution {
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1, mid = 0;
while (low <= high) {
mid = low + (high - low) / 2;
if (nums[mid] == target) {
return mid;
}
// 先判断 mid 是在左段还是右段 再判断 target 是在 mid 的左边还是右边,
if (nums[mid] >= nums[low]) {//mid在左段
if (target >= nums[low] && target < nums[mid]) {//target在左边
high = mid - 1;
} else {//在右边
low = mid + 1;
}
} else {//在mid右段
if (target > nums[mid] && target <= nums[high]) {//target在右边
low = mid + 1;
} else {//在左边
high = mid - 1;
}
}
}
return -1;
}
}
6.寻找两个正序数组的中位数
思路:解法三的二分法很详细
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
//因为数组是从索引0开始的,因此我们在这里必须+1,即索引(k+1)的数,才是第k个数。
int left = (n + m + 1) / 2;
int right = (n + m + 2) / 2;
//将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k
return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
}
//找第k个小的数
private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
//len代表当前数组(也是经过递归排除后的数组)的长度
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
//让 len1 的长度小于 len2,这样就能保证如果有数组空了(不一定空),一定是 len1
//就是如果len1长度小于len2,把getKth()中参数互换位置,即原来的len2就变成了len1,即len1,永远比len2小
if (len1 > len2){
return getKth(nums2, start2, end2, nums1, start1, end1, k);
}
//如果一个数组中没有了元素,那么从剩余数组nums2的其实start2开始加k再-1.
//因为k代表个数,而不是索引,那么从nums2后再找k个数,那个就是start2 + k-1索引处就行了。因为还包含nums2[start2]也是一个数。因为它在上次迭代时并没有被排除
if (len1 == 0){
return nums2[start2 + k - 1];
}
//如果k=1表明最接近中位数了,即两个数组中start索引处,谁的值小,中位数就是谁(start索引之前表示经过迭代已经被排出的不合格的元素,即数组没被抛弃的逻辑上的范围是nums[start]--->nums[end])。
if (k == 1){
return Math.min(nums1[start1], nums2[start2]);
}
//防止数组长度小于 k/2,每次比较都会从当前数组所在长度和k/2作比较,取其中的小的(如果取大的,数组就会越界)
//然后数组如果len1小于k / 2,表示数组经过下一次遍历就会到末尾,然后后面就会在那个剩余的数组中寻找中位数
int i = start1 + Math.min(len1, k / 2) - 1;//索引-1,个数+1
int j = start2 + Math.min(len2, k / 2) - 1;
//如果nums1[i] > nums2[j],表示nums2数组中包含j索引之前的元素,逻辑上全部淘汰,即下次从j+1开始。
//而k则变为k - (j - start2 + 1),即减去逻辑上排出的元素的个数(要加1,因为索引相减,相对于实际排除的时要少一个的)
//≤同理
if (nums1[i] > nums2[j]) {
return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
}
else {
return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
}
}
二、动态规划
1.子集
思路:
class Solution {
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> res=new ArrayList<>();//定义结果集
List<Integer> list=new ArrayList<>();//定义空集
res.add(list);//把空集加入结果集
for(int i=0;i<nums.length;i++){//遍历nums
int size = res.size(); // 当前结果集的大小,不要放在for循环,因为res.size会更新
for(int j=0;j<size;j++){//遍历已有的子集,给所有子集添加num
List<Integer> newlist=new ArrayList<>(res.get(j));//初始化新子集并拷贝当前的子集
newlist.add(nums[i]);//给新子集添加num
res.add(newlist);//添加到结果集
}
}
return res;
}
}
2.买卖股票的最佳时机 II
思路:等价于每天都买卖
class Solution {
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1; i < prices.length; i++) {
int tmp = prices[i] - prices[i - 1];
if (tmp > 0){
profit =profit+tmp;
}
}
return profit;
}
}
3.不同路径
思路:类似于跳台阶。
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
//第0行和第0列都是只有一条路径
for(int i=0;i<m;i++){
dp[i][0]=1;
}
for(int i=0;i<n;i++){
dp[0][i]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
//状态转移方程
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
4.最长回文子串
思路:关键在于状态转移方程
class Solution {
public String longestPalindrome(String s) {
int len = s.length();
if (len == 1){// 特判
return s;
}
int maxLen = 1;//初始化最大长度
int begin = 0;//初始化起点
// 状态初始化
boolean[][] dp = new boolean[len][len];//dp[i][j] 表示s[i..j] 是否为回文串
for (int i = 0; i < len; i++) {//单个字母本身就是回文
dp[i][i] = true;
}
char[] chars = s.toCharArray();
// 状态转移
// 注意:先填左下角
// 填表规则:先一列一列的填写,再一行一行的填,保证左下方的单元格先进行计算
for (int j = 1;j < len;j++){//由大到小,先(0,3),(1,3),(2,3)
for (int i = 0; i < j; i++) {
if (chars[i] != chars[j]){// 头尾字符不相等,不是回文串
dp[i][j] = false;
}else {// 头尾字符相等
// 考虑头尾去掉以后没有字符剩余,或者剩下一个字符的时候,
if (j-i+1 <=3){//如果头尾字符相等并且长度<=3那就肯定是回文串
dp[i][j] = true;
}else {
dp[i][j] = dp[i + 1][j - 1];// 状态转移:头尾相等,只需要判断去掉头尾的
}
}
// s[i...j] 是回文串并且长度比maxLen大就记录下起点以及长度
if (dp[i][j] && j - i + 1 > maxLen){
maxLen = j - i + 1;
begin = i;
}
}
}
return s.substring(begin,begin + maxLen);
}
}
三、链表
1. 删除链表中的节点
思路:理解透彻链表结构。先把下个结点的值复制给node,再node指向下下个结点。
我的思路由于没理解透彻链表,导致写复杂了。
class Solution {
//覆盖 遍历到下一个就交换值
public void deleteNode(ListNode node) {//最少两个节点
ListNode a=null;
while(node.next!=null){//下个结点部位空
int temp=node.val;
node.val=node.next.val;
node.next.val=temp;
if(node.next.next==null){
a=node;
}
node=node.next;
}
a.next=null;
}
}
答案:
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;//直接覆盖,同时覆盖结点,
node.next = node.next.next;
}
}
2. 环形链表
思路:快慢指针
我的思路:效率很低并且代码复杂,使用HashSet
public class Solution {
public boolean hasCycle(ListNode head) {
if(head==null){
return false;
}
if(head.next==null){
return false;
}
HashSet<ListNode> map=new HashSet<>();
while(!map.contains(head)&&head.next!=null){
map.add(head);
head=head.next;
}
return head.next==null?false:true;
}
}
答案的HashSet解法,效率低,但代码简单。
HashSet.add()添加元素有返回值:如果该元素不存在于HashSet中,则该函数返回True;否则,如果该元素已经存在于HashSet中,则返回False。
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> seen = new HashSet<ListNode>();
while (head != null) {
if (!seen.add(head)) {
return true;
}
head = head.next;
}
return false;
}
}
答案最好的解法:快慢指针
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
//如果将两个指针初始都置于 head,那么while 循环就不会执行。
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {//判断快慢指针是否重合,不重合继续循环
if (fast == null || fast.next == null) {//快指针到了终点就返回
return false;
}
slow = slow.next;//快指针移动一步
fast = fast.next.next;//快指针移动两步
}
return true;
}
}
3. 排序链表
使用自底向上的方法实现归并排序,则可以达到 O(1) 的空间复杂度。
1个结点的排,2个结点的排,4个结点的排
/**
* 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 sortList(ListNode head) {
if(head==null){
return null;
}
int length = 0;
ListNode node = head;
while(node != null){// 统计链表长度
length++;
node = node.next;
}
if(length==1){
return head;
}
//初始化res结点
ListNode res=new ListNode(0);//不能为null,因为null没有next,加了0更快
res.next=head;
for(int sublen=1;sublen<length;sublen<<=1){// subLen每次左移一位(即sublen = sublen*2) PS:位运算对CPU来说效率更高
ListNode rescur=res;//res的遍历指针
ListNode cur=res.next;//遍历指针,cur每轮都要重新指向res的头结点
while(cur!=null){// 这一轮归并没有排完
ListNode head1=cur;//head1的头结点
for(int i=1;i<sublen&&cur!=null&&cur.next!=null;i++){//找到head1的尾巴
cur=cur.next;
}
ListNode head2=null;//head2的头结点
if(cur.next!=null){//cur下个结点不为空
head2=cur.next;//head2的头结点
cur.next=null;//断开尾巴
cur=head2;
}
for(int i=1;i<sublen&&cur!=null&&cur.next!=null;i++){//找到head2的尾巴
cur=cur.next;
}
ListNode next=null;//next保存尾巴 new一个结点不行,会多一个结点
if(cur!=null){//cur不为空,cur.next可以是null也就是尾巴可以是null
next=cur.next;
cur.next=null;//断开尾巴
cur=next;//记下尾巴
}
//归并排序
ListNode sort=mergeSort(head1,head2);
rescur.next=sort;//排好序放到结果里面
while(rescur.next!=null){//rescur指向res尾巴
rescur=rescur.next;
}
}
}
return res.next;
}
//归并排序
public ListNode mergeSort(ListNode l1,ListNode l2) {
ListNode a=new ListNode(0);
ListNode acur=a;//遍历指针
while(l1!=null&&l2!=null){
if(l1.val<=l2.val){
acur.next=l1;
l1=l1.next;
}else{
acur.next=l2;
l2=l2.next;
}
acur=acur.next;
}
if(l1!=null){
acur.next=l1;
}
if(l2!=null){
acur.next=l2;
}
return a.next;
}
}
4. 环形链表 II
设入口结点前a个结点,环有b个结点
快指针fast走2步,慢指针slow走1步。
第一次相遇,fast 比 slow多走了 n 个环的长度:
①fast=2*slow
②fast=slow+nb
由①②推出fast=2nb,slow=nb.即fast和slow 指针分别走了2n,n个环的周长。
而需要走到入口,需要走a+nb,也就是第一次相遇的点再走a步,但a是未知数。把快指针指向头结点,两个指针以相同的速度走,而slow已经走了nb,两个都走了a步,相遇了,就是入环口。
简单来说第一次相遇后fast指向head,slow还在相遇点,fast速度改成一步,再次相遇就是入口。
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head, slow = head;
while (true) {
if (fast == null || fast.next == null) return null;//fast == null要写前面
fast = fast.next.next;
slow = slow.next;
if (fast == slow) break;
}
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
}
5. LRU 缓存
思路:答案思路讲的很好
LinkedHashMap:基本用法&简单的LRU缓存
LinkedHashMap的一些函数:
containsKey(key):用于检查特定键是否已映射到LinkedHashMap中。它使用key元素作为参数,如果该元素在映射中映射,则返回True。
size():获取长度
remove(key):删除key
put(key, val):放入数据
get(key)获取数据
Map中的 keySet() 方法 与 Iterator 迭代的遍历,LinkedHashMap与HashMap区别
获取linkedhashmap的第一个元素:cache.keySet().iterator().next();
如果有一个Map对象,可以使用 map.keySet() 方法获取所有的key值。
Iterator it_link = linkedhashmap.keySet().iterator(); //新建一个迭代器
迭代器和数据结构中的链表一样,有个header指针,header->next()就是链表中第一个元素
class LRUCache {
int cap;
LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();
public LRUCache(int capacity) {
this.cap = capacity;
}
public int get(int key) {
if (!cache.containsKey(key)) {
return -1;
}
// 将 key 变为最近使用
makeRecently(key);
return cache.get(key);
}
public void put(int key, int val) {
if (cache.containsKey(key)) {//如果包含,直接换
// 修改 key 的值
cache.put(key, val);
// 将 key 变为最近使用
makeRecently(key);
return;
}
if (cache.size() >= cap) {//如果不包含并且满了
// 删除队头
int oldestKey = cache.keySet().iterator().next();
cache.remove(oldestKey);
}
// 将新的 key 添加链表尾部
cache.put(key, val);//如果不包含并且没满
}
private void makeRecently(int key) {
int val = cache.get(key);
// 删除 key,重新插入到队尾
cache.remove(key);
cache.put(key, val);
}
}
6. 两数相加
下面是我的答案,但是超时了
class Solution {//先算出来再存进链表
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
int res=resnum(l1,l2);
ListNode resnode=new ListNode(0);
ListNode p=resnode;
if(res==0){
return resnode;
}
while(res!=0){
int a=res%10;
ListNode temp=new ListNode(0);//新结点
temp.val=a;
p.next=temp;
p=p.next;
res=res/10;
}
return resnode.next;
}
public int resnum(ListNode l1, ListNode l2){
if(l1==null&&l2==null){
return 0;
}
if(l1==null){
return l2.val+10*resnum(l2.next,null);
}
if(l2==null){
return l1.val+10*resnum(l1.next,null);
}
return l1.val+l2.val+10*resnum(l1.next,l2.next);
}
}
答案思路:直接顺着算,因为本身就是从个位开始算。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode respre=new ListNode(0);//结果的前驱节点
ListNode p=respre;//遍历指针
int jinwei=0;//统计进位
while(l1!=null||l2!=null){//两个链表都没遍历完
int x=(l1==null?0:l1.val);//x,y存储当前结点的值
int y=(l2==null?0:l2.val);
int sum=x+y+jinwei;//当前的和
jinwei=sum/10;//更新进位
sum=sum%10;//当前结点的值
p.next=new ListNode(sum);//创建当前结点
p=p.next;
if(l1!=null){//l1不为空就往后走,如果为空,就没有next。l2同理
l1=l1.next;
}
if(l2!=null){
l2=l2.next;
}
}
//由于最后也可能进位
//两数相加最多小于20,所以的的值最大只能时1
if(jinwei==1){
p.next=new ListNode(jinwei);
}
return respre.next;
}
}
7. 合并K个升序链表
我的做法:两两归并
class Solution {
public ListNode mergeKLists(ListNode[] lists) {//两两归并
if(lists.length==0){
return null;
}
ListNode preres=new ListNode(0);
ListNode pp=preres;
for(int i=0;i<lists.length-1;i++){
lists[i+1]=mergesort(lists[i],lists[i+1]);
}
return lists[lists.length-1];
}
public ListNode mergesort(ListNode l1,ListNode l2){
ListNode preres2=new ListNode(0);
ListNode p=preres2;
while(l1!=null&&l2!=null){
if(l1.val>=l2.val){
p.next=l2;
l2=l2.next;
}else{
p.next=l1;
l1=l1.next;
}
p=p.next;
}
if(l1!=null){
p.next=l1;
}
if(l2!=null){
p.next=l2;
}
return preres2.next;
}
}
答案:二分法归并,自顶向下。比如[0,4]=mergeTwoLists([0,2],(2,4)])
[0,2]=mergeTwoLists([0,1],(1,2)])
[2,4]=mergeTwoLists([2,3],(3,4)])
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
public ListNode merge(ListNode[] lists, int l, int r) {//自顶向下二分
if (l == r) {
return lists[l];
}
if (l > r) {
return null;
}
int mid = (l + r) /2;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
public ListNode mergeTwoLists(ListNode l1,ListNode l2){//归并
ListNode preres=new ListNode(0);
ListNode p=preres;
while(l1!=null&&l2!=null){
if(l1.val>=l2.val){
p.next=l2;
l2=l2.next;
}else{
p.next=l1;
l1=l1.next;
}
p=p.next;
}
if(l1!=null){
p.next=l1;
}
if(l2!=null){
p.next=l2;
}
return preres.next;
}
}
四、二叉树
1. 二叉树中的最大路径和
思路:很详细的思路
class Solution {
int result = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return result;
}
private int dfs(TreeNode root) {
if(root == null){
return 0;
}
//左右中,后序遍历,所以优先计算的子树的result
int left = dfs(root.left);
int right = dfs(root.right);
result = Math.max(result, root.val + left + right);//子树中的内部路径要包含根节点
int max = Math.max(root.val + left, root.val + right);//当前子树的最大收益
return max < 0 ? 0 : max;//如果某个子树 dfs 结果为负,走入它,收益不增反减,该子树就没用,需杜绝走入,像对待 null 一样让它返回 0(
}
}
五、字符串
1. 有效的括号
string 也有判空函数isEmpty()
class Solution {
public boolean isValid(String s) {
if(s.isEmpty()){
return true;
}
Stack<Character> stack=new Stack<>();
for(char c:s.toCharArray()){//遍历字符串中的字符,遇到了左边把对应右边的进栈,
if(c=='(')
stack.push(')');
else if(c=='{')
stack.push('}');
else if(c=='[')
stack.push(']');
else if(stack.isEmpty()||c!=stack.pop())// 栈为空说明没有左边的字符进栈而还没有遍历完,所以直接return false,或者,当当前字符不等于栈顶字符(相等就出栈),必须先判空因为右边有出栈
return false;
}
return stack.isEmpty();
}
}
2. 最长公共前缀
思路:别想着全部同时对比
class Solution {
public String longestCommonPrefix(String[] strs) {
if (strs.length == 0) return "";
// 初始值为首位元素
String res = strs[0];
for (int i = 1; i < strs.length; i++) {
int j = 0;
// 挨着对比
while (j < res.length() && j < strs[i].length() && res.charAt(j) == strs[i].charAt(j)) {
j++;
}
// substring 是左闭右开的
res = res.substring(0, j);
}
return res;
}
}
3. 字符串相乘
思路:
class Solution {
public String multiply(String num1, String num2) {
if (num1.equals("0") || num2.equals("0")) {//不要用==,因为==比较的是地址
return "0";
}
int[] res = new int[num1.length() + num2.length()];//根据规律不会超过长度之和
for (int i = num1.length() - 1; i >= 0; i--) {//从高位开始遍历num1也就是从个位开始
int n1 = num1.charAt(i) - '0';//取num1中的数字
for (int j = num2.length() - 1; j >= 0; j--) {//从高位开始遍历num2也就是从个位开始
int n2 = num2.charAt(j) - '0';//取num2中的数字
int sum = (res[i + j + 1] + n1 * n2);
res[i + j + 1] = sum % 10;//更新第二位
res[i + j] =res[i + j] + sum / 10;//更新第一位
}
}
StringBuilder result = new StringBuilder();
for (int i = 0; i < res.length; i++) {
if (i == 0 && res[i] == 0){//从前往后遍历的,第一个是0越过
continue;
}
result.append(res[i]);
}
return result.toString();
}
}
六、位运算
1. 只出现一次的数字
思路:位运算
class Solution {
public int singleNumber(int[] nums) {
int single = 0;
for (int num : nums) {
single = single^num;
}
return single;
}
}
2. 2 的幂
我的做法:很low
class Solution {
public boolean isPowerOfTwo(int n) {
if(n==1){
return true;
}
while(n!=0&&n%2==0){
if(n==2){
return true;
}
n=n>>1;
}
return false;
}
}
答案思路:
class Solution {
public boolean isPowerOfTwo(int n) {
return n > 0 && (n & (n - 1)) == 0;
}
}
七、深搜
1.全排列
没理解透彻.
class Solution {
private List<List<Integer>> res = new ArrayList<>();//res存储结果
public List<List<Integer>> permute(int[] nums) {
int n = nums.length;//数组的长度
List<Integer> list = new ArrayList<>();//初始化列表
boolean[] used = new boolean[n];//标记数组
int count = 0;//nums从0开始
backtrace(list, used, count, nums);//深搜
return res;
}
public void backtrace(List<Integer> list, boolean[] used, int count, int[] nums) {
if (count == nums.length) {//遍历到底层就加进来并向上走
res.add(new ArrayList(list));
return;
}
for (int i = 0; i < used.length; i++) {//遍历标记数组
if (!used[i]) {//如果当前数字没有加过
list.add(nums[i]);
used[i] = true;//加进了就复制为true
backtrace(list, used, count + 1, nums);//往下层走
//恢复数字和标记数组,以供下次使用
list.remove(list.size() - 1);
used[i] = false;
}
}
}
}
// 键在于res存放的是list引用。 那么回溯过程中,将数字使用状态重置撤销的时候,会将list的元素移除掉,也会影响到res里面的list情况。因为它们是同一个引用。 全部为空,是因为回溯结束的同时,会将全部数字重置撤销,这样list里面的元素就会为空了,同样的,也会影响到res的存放情况。
八、数学
1.Nim 游戏
思路:
class Solution {
public boolean canWinNim(int n) {
return n % 4 != 0;
}
}
2.回文数
我的解法,字符串,效率相对低点
class Solution {
public boolean isPalindrome(int x) {
if(x<0){
return false;
}
String s=String.valueOf(x);
char[] ch=s.toCharArray();
int i=0,j=ch.length-1;
while(i<j){
if(ch[i]!=ch[j]){
return false;
}
i++;
j--;
}
return true;
}
}
答案直接对数字操作,求出反转后的数字再判断。
class Solution {
public boolean isPalindrome(int x) {
if(x < 0)
return false;
//求出反转后的数字
int res=0;
int temp=x;//直接使用x,会影响最后的res==x的结果
while(temp!=0){
res=temp%10+res*10;
temp=temp/10;
}
return res==x;
}
}
3.格雷编码
思路:简单来说就是:n每次增加,对已有结果倒序遍历,然后在加上1的头
class Solution {
public List<Integer> grayCode(int n) {
ArrayList<Integer> res = new ArrayList<Integer>();//添加元素ArrayList快
res.add(0);//初始化
int head = 1;
for (int i = 0; i < n; i++) {//n实际上代表有几位
for (int j = res.size() - 1; j >= 0; j--){//倒序遍历
res.add(head + res.get(j));
}
head=head<<1;//head就是需要添加的1
}
return res;
}
}
4.整数反转
思路:
Integer.MAX_VALUE表示int数据类型的最大取值数:2 147 483 647
Integer.MIN_VALUE表示int数据类型的最小取值数:-2 147 483 648
①int存储不了比MIN小和比MAX大的,所以这样做。
②类似于剑指offer《把字符串转换成整数》
③负数不需要单独考虑因为二进制有符号位。
class Solution {
public int reverse(int x) {
int res = 0;
while (x != 0) {
int yushu = x % 10;
//下面*10必然会超过,比如标准是28,3*10=30,2*10=20+8=28
if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && yushu > 7))
return 0;
if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && yushu < -8))
return 0;
res = res * 10 + yushu;
x /= 10;
}
return res;
}
}