leetcode java_剑指offer刷题(下)--Java版(LeetCode)

剑指offer41 数据流中的中位数

题目

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。

思路

建立两个堆,一个大顶堆A,一个小顶堆B,大顶堆用来保存较小的一半数m,小顶堆用来保存较大的另一半数n,总数为N并规定:

A保存较小的一部分,m=N/2,N为偶数 或m=(N+1)/2 N奇数;

B保存较大的一部分,n=N/2,N为偶数 或n=(N-1)/2 N为奇数;

添加元素时,通过判断A和B的大小是否相同,如果相同,(需要将较小的元素加入A堆中)将元素入B堆,将B的堆顶元素(得到较小的元素)出堆加入到A堆中;如果A和B大小不同,(需要将较大的元素加入到B堆中),将元素入A堆,将A的堆顶元素(得到较大的元素)出堆加入到B堆中

这样一来,当N为奇数时,中位数即为大顶堆A中的堆顶元素;N为偶数时,中位数为两个堆顶的平均数

public class MedianFinder {

Queue bigQueue,smallQueue;

/** initialize your data structure here. */

public MedianFinder() {

//建立大顶堆,保存较小的一半数,总数大于等于小顶堆 bigQueue=new PriorityQueue<>((o1,o2)->(o2-o1));

//建立小顶堆,保存较大的一半数 smallQueue=new PriorityQueue<>();

}

public void addNum(int num) {

//数量相等,将较小的数保存到大顶堆 if (bigQueue.size()==smallQueue.size()){

//先将数入小顶堆,得到堆顶的数即为较小的数 smallQueue.add(num);

//将堆顶元素出堆加入到大顶堆中 bigQueue.add(smallQueue.poll());

}else{//数量不相等时需要将较大的数保存在小顶堆,保持两边数量平衡 //先将数入大顶堆,得到堆顶的数即为较大的数 bigQueue.add(num);

//将堆顶元素出堆加入到小顶堆中 smallQueue.add(bigQueue.poll());

}

}

public double findMedian() {

if (bigQueue.size()==smallQueue.size()){//两个堆中元素个数相同,总数为偶数,取两个堆的堆顶元素平均 return (bigQueue.peek()+smallQueue.peek())/2.0;

}

return 1.0*bigQueue.peek();//不相等说明总数为奇数,直接返回大顶堆中的堆顶元素 }

}

时间复杂度: 查找中位数 O(1) : 获取堆顶元素使用 O(1) 时间; 添加数字 O(logN) : 堆的插入和弹出操作使用 O(logN) 时间。 空间复杂度 O(N) : 其中 N为数据流中的元素数量,小顶堆 A和大顶堆 B 最多同时保存 N个元素。

剑指offer42 连续数组的最大和

题目

输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。

要求时间复杂度为O(n)。

思路

利用动态规划可满足题目要求,

状态定义:设动态规划列表 dp,dp[i] 代表以元素 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]结尾的连续子数组最大和为 nums[0]。

返回值: 返回 dp 列表中的最大值,代表全局最大值。

class Solution {

public int maxSubArray(int[] nums) {

int max = nums[0];//记录最大值 int former = 0;//用于记录dp[i-1]的值,对于dp[0]而言,其前面的dp[-1]=0 int cur = nums[0];//用于记录dp[i]的值 for(int num:nums){

cur = num;

if(former>0) cur +=former;

if(cur>max) max = cur;

former=cur;

}

return max;

}

}

剑指offer43 1~n中整数n出现的次数

题目

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

思路

可以求出整数每一位的1的个数,然后将每个位数出现的1的个数相加,就可以得出总共出现的1的个数。如何求每个位数的1的个数

举个例子,如求34X47在百位出现1的个数,这里百位先用X代替,因为出现的数字不同,得到结果不一样,由于是求1出现的个数,可以1为界进行分类讨论,分为等于1,等于0,大于1

(1)X=1

现将这个整数的高位固定为33,则数字变为33147,百位出现1的个数由高位和低位出现的排列决定,高位可有033共34种排列,低位有099共100种排列,所以此时1的排列为34*100中排列,而当高位固定为34时,数字变为34147,此时排列由低位决定,故共有0~47共48种排列,总共排列为:34*100+48种排列

(2)X=0

同理.将高位固定为33,则数字变为33147,也有34*100种排列;但是当高位固定为34时,数字变为34047,此时百位不能出现1,故总次数为:34*100

(2)X>1

例如将X=2;同上,如将高位固定为33,则有34*100种排列;将高位固定为34时,将百位固定为1,数字变为34147,此时由低位决定,但是低位可以取0~99也不会超出数字,所以由100种,综上有34*100+100

推广,设数字为n,设当前数字为cur,高位的数字为high,低位的数字为low,低位位数的排列为digit(低位有i位数,digit=10^i),则当前位数cur出现1的个数棵分为三种情况:cur==1,res=highdigit+low+1;

cur==0,res=high*digit;

cur>1,res=high*digit+digit

class Solution {

public int countDigitOne(int n) {

//设置初始值,digit为1代表求个位,此时低位low为0 int res=0,high=n/10,cur=n%10,digit=1,low=0;

while(high!=0||cur!=0){

if (cur==0){

res+=high*digit;

}else if(cur==1){

res+=high*digit+low+1;

}else if(cur>1){

res+=high*digit+digit;

}

low+=digit*cur;

cur=high%10;

high=high/10;

digit=10*digit;

}

return res;

}

}

剑指offer44 数字序列中某一位的数字

题目

数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。

请写一个函数,求任意第n位对应的数字。

思路分析

找数学规律

将 101112⋯ 中的每一位称为数位 ,记为 n ; 将 10, 11, 12,⋯ 称为 数字 ,记为 num ; 数字 10是一个两位数,称此数字的 位数 为 2 ,记为 digit ; 每 digit位数的起始数字(即:1, 10, 100, ⋯),记为 start。

根据以上分析,可将求解分为三步:

1.确定 n 所在 数字 的 位数 ,记为 digit;

循环执行 n 减去 一位数、两位数、... 的数位数量 count,直至 n≤count 时跳出。由于 n 已经减去了一位数、两位数、...、(digit-1)位数的数位数量 count,因而此时的 n 是从起始数字 start开始计数的。

2.确定 n 所在的 数字 ,记为 num ;

num = start + (n - 1) // digit start从0开始算起,所以应该是n-1

3.确定 n 是 num中的哪一数位,并返回结果。

所求数位为数字 num的第 (n - 1) % digit位( 数字的首个数位为第 0 位)。

s = str(num) # 转化为 string res = int(s[(n - 1) % digit]) # 获得 num 的 第 (n - 1) % digit 个数位,并转化为 int

class Solution {

public int findNthDigit(int n) {

int digit = 1;//初始位数为第1位 long start = 1;//初始数值从1开始 long count = 9;//初始的数位数量为9 while (n > count) {

n -= count;//不断化简n的大小 digit += 1;//位数加1 start *= 10;//开始数值*10 count = digit * start * 9;

}

long num = start + (n - 1) / digit;

return Long.toString(num).charAt((n - 1) % digit) - '0';

}

}

时间复杂度 O(logn): 所求数位 n对应数字 num的位数 digit 最大为 O(logn) ;第一步最多循环 O(logn) 次;第三步中将 num 转化为字符串使用 O(logn) 时间;因此总体为O(logn) 。 空间复杂度 O(logn): 将数字 num 转化为字符串 str(num) ,占用 O(logn) 的额外空间。

剑指offer45 把数组排成最小的数

题目

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

思路

此题求拼接起来的 “最小数字” ,本质上是一个排序问题。 排序判断规则: 设 nums任意两数字的字符串格式 x 和 y,则 若拼接字符串 x + y > y + x,则 m > n; 反之,若 x + y < y + x,则 n < m; 根据以上规则,套用任何排序方法对 nums执行排序即可。

初始化: 字符串列表 strs,保存各数字的字符串格式; 列表排序: 应用以上 “排序判断规则” ,对 strs执行排序; 返回值: 拼接 strs 中的所有字符串,并返回。

利用快排

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]);

fastSort(strs, 0, strs.length - 1);

StringBuilder res = new StringBuilder();

for(String s : strs)

res.append(s);

return res.toString();

}

void fastSort(String[] strs, int l, int r) {

if(l >= r) return;

int i = l, j = r;

String tmp = strs[i];

while(i < j) {

while((strs[j] + strs[l]).compareTo(strs[l] + strs[j]) >= 0 && i < j) j--;

while((strs[i] + strs[l]).compareTo(strs[l] + strs[i]) <= 0 && i < j) i++;

tmp = strs[i];

strs[i] = strs[j];

strs[j] = tmp;

}

strs[i] = strs[l];

strs[l] = tmp;

fastSort(strs, l, i - 1);

fastSort(strs, i + 1, r);

}

}

//内置排序 class Solution {

public String minNumber(int[] nums) {

List list = new ArrayList<>();

for (int num : nums) {

list.add(String.valueOf(num));

}

list.sort((o1, o2) -> (o1 + o2).compareTo(o2 + o1));//lambda表达式 return String.join("", list);

}

}

剑指offer46 把数组翻译成字符串

题目

给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。

思路

此题属于斐波那契数列的衍生题,属于动态规划的题目,和青蛙跳台阶非常相似,最后两个数字两种选择`

状态定义: 设动态规划列表 dp ,dp[i] 代表以xi为结尾的数字的翻译方案数量。

转移方程:可被翻译的两位数区间:组成的两位数是无法被翻译的(例如 00, 01, 02, ⋯ ),因此区间为 [ 10, 25 ]。

初始状态:dp[0]=dp[1]=1 ,即 “无数字” 和 “第 1位数字” 的翻译方法数量均为 1

返回值: dp[n] ,即此数字的翻译方案数量。

法一:动态规划遍历法使用字符串的截取,每次截取两个字符串,将这个字符串与'10'和'25'比较大小,根据上述转移方程来保存dp数组的数值,最后返回dp[n].

class Solution {

public int translateNum(int num) {

String str = String.valueOf(num);

int[] dp = new int[str.length()+1];

dp[0] = 1;//设置初始值 dp[1] = 1;

for (int i=2;i<=str.length();i++){//从左至右截取两个字符串 String substr = str.substring(i - 2, i);

if (substr.compareTo("10")>=0 && substr.compareTo("25")<=0){

dp[i] = dp[i-1] + dp[i-2];

}else{

dp[i] = dp[i-1];

}

}

return dp[str.length()];

}

}

时间复杂度 O(N): N 为字符串 s的长度(即数字 num的位数 log(num) ),其决定了循环次数。 空间复杂度 O(N): 字符串 s使用 O(N)大小的额外空间。

法二:递归

class Solution {

public int translateNum(int num) {

if (num<=9) {return 1;}

//获取输入数字的余数,然后递归的计算翻译方法,这里是获取两位数字 int ba = num%100;

//如果小于等于9或者大于等于26的时候,余数不能按照2位数字组合,比如56,只能拆分为5和6;反例25,可以拆分为2和5,也可以作为25一个整体进行翻译。 if (ba<=9||ba>=26) {return translateNum(num/10);}

// ba=[10, 25]时,既可以当做一个字母,也可以当做两个字母 else {return translateNum(num/10)+translateNum(num/100);}

}

}

剑指offer47 礼物的最大价值

题目

在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

思路

这道题为基础的动态规划,将dp[i][j]当作从左上角走到棋盘i,j出的礼物最大值,由于只能向右或者向下走,那么它的最大值为dp[i] [j] =max{dp[i-1] [j],dp[i] [j-1]} +grid[i] [j],前一步两种方式的较大值加上目前的礼物价值即为当前的最大值,动态规划考虑的最大值都是建立在前n-1个上面,有多种方式,取其中较优的一种,在动态规划的题目中,一般将dp的大小定义比给定的数组大一个单位,这样既可以保证代码的可理解性,又可以保证简洁性,同时还不容易出界.

代码

class Solution {

public int maxValue(int[][] grid) {

int row = grid.length;

int column = grid[0].length;

int[][] dp = new int[row+1][column+1];

for (int i=0;i<=row;i++){

for (int j=0;j<=column;j++){

dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j])+grid[i-1][j-1];

}

}

return dp[row][column];

}

}

优化

然后可以用滚动数组的技巧把空间复杂度优化成O(N),由于当前状态只与左边的状态和上边的状态有关,故可以只用一维数组表示dp,一层一层的扫下来,其实相当于复用这个一维数组

class Solution {

public int maxValue(int[][] grid) {

int m = grid.length, n = grid[0].length;

int[] dp = new int[n + 1];

for (int i = 1; i <= m; i++) {

for (int j = 1; j <= n; j++) {

//dp[j]表示上一层的累计最大值,dp[j - 1]表示左边一个的累计最大值 dp[j] = Math.max(dp[j], dp[j - 1]) + grid[i - 1][j - 1];

}

}

return dp[n];

}

}

剑指offer48 最长不含重复字符的子字符串

题目

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

思路

利用滑动窗口,可以利用双端队列或者hashset维护不重复的窗口,不重复就加入数据,重复删除重复数据之前的所有数据(包括重复数据),不断更迭最大的数值

法一:双端队列模拟窗口

class Solution {

public int lengthOfLongestSubstring(String s) {

Deque deque = new ArrayDeque<>();

int max = 0;

for (int i=0;i

if (!deque.contains(s.charAt(i))){

deque.addLast(s.charAt(i));//不重复从尾部加入数据 }else{

max = Math.max(max,deque.size());//更新最大的长度 while(deque.peekFirst()!=s.charAt(i)){//重复了删除队首元素直到队首是重复的元素 deque.removeFirst();

}

deque.removeFirst();//删除重复元素 deque.addLast(s.charAt(i));//加入该元素 }

}

max = Math.max(max,deque.size());//更新最大长度 return max;

}

}

法二:Hashset模拟滑动窗口

public class Solution {//时间复杂度O(2n) //滑动窗口算法 public int lengthOfLongestSubstring(String s) {

int n = s.length();

Set set = new HashSet<>();

int ans = 0, i = 0, j = 0;

while (i < n && j < n) {//窗口的左边是i,右边是j,下列算法将窗口的左右移动,截取出其中一段 // try to extend the range [i, j] if (!set.contains(s.charAt(j))){//如果set中不存在该字母,就将j+1,相当于窗口右边向右移动一格,左边不动 set.add(s.charAt(j++));

ans = Math.max(ans, j - i);//记录目前存在过的最大的子字符长度 }

else {//如果set中存在该字母,则将窗口左边向右移动一格,右边不动,直到该窗口中不存在重复的字符 set.remove(s.charAt(i++));

}

}

return ans;

}

}

剑指offer49 丑数

题目

我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。

思路

丑数的递推性质: 丑数只包含因子 2, 3, 5,因此有 “丑数 == 某较小丑数 × 某因子” (例如:10=5×2)

取三个指针a,b,c指着丑数序列(初始均指向0),分别让三个指针指向的当前丑数乘以2,3,5得到三个丑数n1,n2,n3,此时取其中的最小值得到当前的丑数d[i],若d[i]= =n1,表明由2产生了一个丑数,让指针a指向下一个丑数;若d[i]= =n2,表明由3产生了一个丑数,让指针b指向下一个丑数;若d[i]= =n3,表明由3产生了一个丑数,让指针c指向下一个丑数,这样循环就可以产生n个丑数,返回第n个丑数d[n-1]。

代码

class Solution {

public int nthUglyNumber(int n) {

int[] dp = new int[n];//建立丑数序列数组 int a=0,b=0,c=0;//初始化三个指针,刚开始都指向丑数数组第一个数 dp[0] = 1;//第一个丑数为1,已知 for(int i=1;i

}

if (dp[i]==n2){//说明该丑数由3生成,指针指向下一个丑数 b++;

}

if (dp[i]==n3){//说明该丑数由5生成,指针指向下一个丑数,注意这三个if必须并列,因为可能出现一个丑数同时由这三个因子产生,指针都需要后移 c++;

}

}

return dp[n-1];

}

}

时间和空间复杂度

时间复杂度 O(N) : 其中 N = n,动态规划需遍历计算 dp列表

空间复杂度 O(N): 长度为 N 的 dp 列表使用 O(N)的额外空间。

剑指offer50 第一个只出现一次的字符

题目

在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。

思路

法一:巧妙利用数组

class Solution{

public char firstUniqChar(String s) {

int[] arr = new int[26];//建立数组,保存每个字符出现的个数 char[] chars = s.toCharArray();

for (char ch : chars){//遍历数组计算每个字符出现的个数 arr[ch -'a'] ++;

}

for (char c:chars){//遍历数组,寻找只出现一次的字符 if (arr[c-'a'] == 1){

return c;

}

}

return ' ';//数组为空,返回单空格 }

}

法二:利用HashMap建立字典

class Solution {

public char firstUniqChar(String s) {

HashMap dic = new HashMap<>();//利用HashMap建立字典,第一个存放字符,代表索引,,第二个存放是否是一个字符的布尔值 char[] sc = s.toCharArray();

for(char c : sc)//遍历数组,将字符放入字典中,布尔值为包含布尔值的相反值,因为我们指定true为只包含一个值 dic.put(c, !dic.containsKey(c));

for(char c : sc)//遍历数组,得到第一个为true的字符 if(dic.get(c)) return c;

return ' ';

}

}

剑指offer51 数组中的逆序对

题目

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

思路

如果一个数组是从小到大有序的,那么它的逆序对为0,所以就容易猜想到逆序对其实就是在交换过程中进行得到的,就是递归排序多了代码

class Solution {

public int reversePairs(int[] nums) {

return mergeSortPlus(nums, 0, nums.length - 1);

}

int mergeSortPlus(int[] arr, int arrStart, int arrEnd) {

if (arrStart >= arrEnd) return 0;//递归结束条件

int arrMid=arrStart+((arrEnd-arrStart)>>1);//左右数组分裂的中间界点位置

//左右分别进行递归并统计逆序对数 int count = mergeSortPlus(arr, arrStart, arrMid) + mergeSortPlus(arr, arrMid + 1, arrEnd);

int[] tempArr = new int[arrEnd - arrStart + 1];//新建一个该层次的临时数组用于左右数组排序后的合并 int i = arrStart;//左边数组的移动指针 int j = arrMid + 1;//右边数组的移动指针 int k = 0;//临时数组tempArr的移动指针 ​

while (i <= arrMid && j <= arrEnd) {//左右两数组都还剩有数字未排序时 if(arr[i]>arr[j]){ //如果左边大于右边,构成逆序对 /*核心代码*/

//左数组i位置及其后的数值均比右数值j位置大,故累加上i位置及其后的数值的长度 count+=arrMid-i+1;

/*核心代码*/

tempArr[k++]=arr[j++]; //右数组移动到tempArr中 }else{ //如果左小于等于右,不构成逆序对 tempArr[k++]=arr[i++]; //左数组移动到tempArr中 }

}

//左右两数组有一边已经移动完毕,剩下另一边可进行快速移动 while (i <= arrMid) //右边数组已全部被对比过且移动完成,将剩下的左边数组快速移入tempArr中 tempArr[k++] = arr[i++];

while (j <= arrEnd) //左边数组已全部被对比过且移动完成,将剩下的右边数组快速移入tempArr中 tempArr[k++] = arr[j++];

//将tempArr中的数还原回原arr中,需加上arrStart确保在原arr上位置正确 for(int a = 0;a < tempArr.length;a++)

arr[a+arrStart]=tempArr[a];

return count;

}

}

归并排序

方式一

void merge(int[] arr, int start, int end) {

if (start == end) return;

int mid = (start + end) / 2;

merge(arr, start, mid);//向左递归拆解 merge(arr, mid + 1, end);//向右递归拆解

//进行合并操作 int[] temp = new int[end - start + 1];

int i = start, j = mid + 1, k = 0;

while(i <= mid && j <= end)

temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];

while(i <= mid)

temp[k++] = arr[i++];

while(j <= end)

temp[k++] = arr[j++];

System.arraycopy(temp, 0, arr, start, end);

}

方式二

/*** @ClassName MergeSort* @Description 归并排序* @Author Eric* @Date 2020/10/22 22:34* @Version 1.0*/

public class MergeSort {

/*** @Description 分+合的方法* @param arr* @param left* @param right* @param temp* @Return void* @Author Eric* @Date 2020/10/22 22:57*/

public static void mergeSort(int[] arr,int left,int right,int[] temp){

if (left

int mid = (left+right)/2;//中间的索引 //向左递归进行分解 mergeSort(arr,left,mid,temp);

//向右递归进行分解 mergeSort(arr,mid+1,right,temp);

merge(arr,left,mid,right,temp);

}

}

/*** @Description 合并的方法* @param arr 待排序的数组* @param left 左边有序序列的初始索引* @param mid 中间索引* @param right 右边有序序列的初始索引* @param temp 中转数组* @Return void* @Author Eric* @Date 2020/10/22 22:40*/

public static void merge(int[] arr,int left,int mid,int right,int[] temp){

int i = left;//初始化i,左边有序序列的初始索引 int j = mid+1;//初始化j,右边有序序列的初始索引 int t=0;//指向中转数组(temp)的索引 ​

//(一) //先把左右两边(有序)的数据按照规则填充到temp数组 //直到左右两边的有序序列,有一遍处理完毕为止 while (i<=mid&&j<=right){//继续 //左边的当前元素小于右边的当前元素 if (arr[i]<=arr[j]){

//将左边的当前元素填入数组 temp[t] = arr[i];

t++;//中转指针右移 i++;//左边的指针右移 }else{//反之,将右边的当前元素填入temp数组 temp[t] = arr[j];

t++;

j++;

}

}

//(二) //把有剩余数据的一遍的数据依次全部填充到temp while (i<=mid){//左边的有序序列还有剩余的元素,就全部填充到temp数组 temp[t] = arr[i];

t++;

i++;

}

while (j<=right){//右边的有序序列还有剩余的元素,就全部填充到temp数组 temp[t] = arr[j];

t++;

j++;

}

//(三) //将temp数组的元素拷贝到arr //注意不是每次都是拷贝所有,根据每一层的治的个数来 t=0;

int tempLeft = left;

while (tempLeft<=right){

arr[tempLeft] = temp[t];

t++;

tempLeft++;

}

}

}

剑指offer52 两个链表的第一个公共节点

题目

输入两个链表,找出它们的第一个公共节点。

思路

设第一个链表L1长度为a+c,第二个链表长度为b+c,其中c为共同部分,利用两个指针h1和h2,分别遍历两个链表,当h1遍历完了L1就去遍历L2链表,当h2遍历完了L2就去遍历第L1链表,这样当他们就走了相同的路程a+b+c,由于遍历速度也一样,所以它们回同时在第一个公共点相遇

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {

ListNode h1 = headA, h2 = headB;

while (h1 != h2) {//相等的时候会相遇 h1 = h1 == null ? headB : h1.next;//如果为null,说明已经遍历完了链表,进而开始遍历第二个链表 h2 = h2 == null ? headA : h2.next;//如果为null,说明已经遍历完了链表,进而开始遍历第一个链表 }

return h1;

}

剑指offer53-1 在排序数组中查找数字1

题目

统计一个数字在排序数组中出现的次数。

思路

由于数组是已经排好序的,所以采用二分法可以提高效率

法一:先利用二分法找到目标值,然后往目标值的左右两边延伸查找,这样就可以找到所有的目标值

class Solution {

public int search(int[] nums, int target) {

int left=0,right=nums.length-1,mid,count=0;

while(count==0&&left<=right){//注意count==0用于判断是否已经执行了查找目标值的算法里面,可以减少不必要的循环操作 mid=(left+right)/2;

if (target>nums[mid]){

left=mid+1;

}else if(target

right=mid-1;

}else{

count++;//已经找到第一个目标值 for (int i=mid-1;i>=0;i--){//查找左边的目标值 if (target!=nums[i]){

break;

}else{

count++;

}

}

for (int i=mid+1;i<=nums.length-1;i++){//查找右边的目标值 count++;

if (target!=nums[i]){

break;

}else{

count++;

}

}

}

}

return count;

}

}

法二:分别查找目标值的左右边界由于数组 nums中元素都为整数,因此可以分别二分查找 target和 target - 1的右边界,将两结果相减并返回即可

class Solution {

public int search(int[] nums, int target) {

return helper(nums, target) - helper(nums, target - 1);

}

int helper(int[] nums, int tar) {//查找右边界 int i = 0, j = nums.length - 1;

while(i <= j) {

int m = (i + j) / 2;

if(nums[m] <= tar) i = m + 1;//每次都插到大于目标值的前面 else j = m - 1;

}

return i;

}

}

剑指offer53-2 0-n-1中缺失的数字

题目

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。

思路

由于数组是有序的,所以应该采用二分法解决,由于是向下取整,所以中位数右边的错位不会影响中位数的数值大小,有序的数组本应该是nums[i]=i,但是如果num[i]!=i说明前面的数组发生了错位

代码

class Solution {

public int missingNumber(int[] nums) {

int i=0,j=nums.length-1;

while (i<=j){//最终i==j的时候,i与j会同时指向缺失数字的前一位,最后一次进来会进入nums[m]==m语句,此时i会加1,则得到即为最终缺失数字 int m = (i+j)/2;

if (nums[m]==m){//相等说明中位数前面的数字没有发生错位,继续前进一位 i=m+1;

}else{//不相等,说明中位数前面的数字发生了错位,后移一位 j=m-1;

}

}

return i;//最后返回的即为缺失数字 }

}时间复杂度 O(log N): 二分法为对数级别复杂度。

空间复杂度 O(1): 几个变量使用常数大小的额外空间。

剑指offer54 二叉搜索树中的第k大节点

题目

给定一棵二叉搜索树,请找出其中第k大的节点

思路

这里显然需要用到树的深度优先遍历算法,由于二叉搜索树的前序遍历为从小到大,为第k小,所以这里需要将它反过来,即分别遍历右子树,根,左子树,这样得到的就是第k大

class Solution {

private int res = 0;

private int count = 0;

public int kthLargest(TreeNode root, int k) {

if (root != null) {

kthLargest(root.right, k);//遍历右子树 if (++count == k) return (res = root.val);//注意这里只能使用全局变量,不能使用k--,因为k只是一个局部变量,每一个递归函数都有一个自己的k,所以不能共用 kthLargest(root.left, k);

}

return res;

}

}

剑指offer55-1 二叉树的深度

题目

输入一棵二叉树的根节点,求该树的深度。从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,最长路径的长度为树的深度。

思路

很明显,二叉树的深度为左子树深度和右子树深度的最大值+1,采用递归做即可

class Solution {

int leftDepth=0,rightDepth=0;

public int maxDepth(TreeNode root) {

if (root==null){//树为空,返回深度0 return 0;

}else{//树非空,分别计算左右子树的深度 return Math.max(maxDepth(root.left),maxDepth(root.right))+1;

}

}

}

时间复杂度 O(N) : N为树的节点数量,计算树的深度需要遍历所有节点。 空间复杂度 O(N):最差情况下(当树退化为链表时),递归深度可达到 N 。

剑指offer55-2 平衡二叉树

题目

输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。

思路

此题是对上一题计算树深度的拓展,因此采用计算树深度+前序遍历的方法,先单独写一个方法用来计算树的深度,然后判断当前树是否是平衡二叉树,再判断左子树和右子树是否是平衡二叉树

class Solution {

public boolean isBalanced(TreeNode root) {

if (root==null){

return true;

}

if (Math.abs(depth(root.left)-depth(root.right))<=1){//先判断当前树 return isBalanced(root.left)&&isBalanced(root.right);//再判断左右子树 }

return false;

}

//计算树的深度 public int depth(TreeNode root){

if (root==null){

return 0;

}

return Math.max(depth(root.left), depth(root.right));

}

}

剑指offer56-1 数组中数字出现的次数

利用异或相关性质

题目

一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。

思路

本题目用到了一种新的方法,即异或的性质相同为0:a^a=0 a^0=a

符合结合律和交换律:a ^ b ^ c=(a ^ c) ^ b

a&(-a)=最低位为1的二进制(从右到左)

自反性:a ^ b ^ b=a

将数组中所有的数字都进行异或,将相同的数字两两结合得到0,最终得到的结果即为两个不同的数字a和b进行异或,再根据上述性质求解两个数字其中一个不同的位flag,根据flag进行分组,根据自反性即可得到其中一个数字,再根据自反性即可得到另一个数字具体见代码

class Solution {

public int[] singleNumbers(int[] nums) {

//异或知识 //a^a=0 a^0=a a^b^c=a^c^b a&(-a)=最低位为1的二进制(从又到左) 自反性a^b^b=a //将所有的数进行异或,将相同的数两两结合得到0^a^b=a^b // 最终得到的结果为两个不同的数a,b异或的结果,记录a和b异或的结果 int ab=0;

for (int num : nums) {

ab^=num;

}

//得到最低一位的1,即得到a和b不同的位置 int flag = ab&(-ab);

//用于记录其中一个结果 int res=0;

//分组 for (int num : nums) {

//根据标志位,根据自反性得到其中一个数 if ((num&flag)!=0){

res^=num;

}

}

//利用自反性,进行异或即可得到另一个数 return new int[]{res,ab^res};

}

}

剑指offer56-2 数组中数字出现的次数

题目

在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。

思路

法一:利用Hashmap

空间复杂度为O(n),时间复杂度为O(n),不推荐该方法

法二:利用数电相关知识(推荐使用)

统计每一位出现的1的个数,然后对3求余,那么所有出现三次的数字都会变为0,而出现1次的数字会被保留下来,对3取余只存在3种情况,即余数为0,1,2,用两位分别为 two和one来描述这三个状态,根据数电中的知识,根据输入n(n只能取0,1,这里的n指的数字中的某一位)画出状态转移表,根据表格得到two和one的转移方程(取次态为1的最小项加起来,化简),得到one = one ^ n & ~two, two = two ^ n & ~one (~表示非),注意以上分析,只针对其中一位,对于其他31位同上,遍完所有数字后,所有的状态为00和01,因为只存在3个数字和一个数字,对3取余为0或者1,由于所有的two位均为0,所以利用one来表示留下的数字,出现三次的数字余数全部为0,剩下的即为只出现一次的数字,(最后的two和one都是32位的二进制数字)

class Solution {

public int singleNumber(int[] nums) {

int ones = 0, twos = 0;

for(int num : nums){

ones = ones ^ num & ~twos;

twos = twos ^ num & ~ones;

}

return ones;

}

}

时间复杂度 O(N) : 其中 N 位数组 nums的长度;遍历数组占用 O(N),每轮中的常数个位运算操作占用O(32×3×2)=O(1) 。 空间复杂度 O(1) : 变量 ones , twos 使用常数大小的额外空间。

法三:遍历统计

使用与运算,可以获取二进制数字num的最右一位n1,n1=num&i,配合无符号右移,可以得到每一位的值num = num>>>1,建立一个长度为 32 的数组 counts ,通过以上方法可记录所有数字的各二进制位的 1的出现次数。将 counts各元素对 3 求余,则结果为 “只出现一次的数字” 的各二进制位。利用 左移操作 和 或运算 ,可将 counts数组中各二进位的值恢复到数字 res上(循环区间是i∈[0,31] )。

class Solution {

public int singleNumber(int[] nums) {

//统计每一位的1的个数,并将其保存在数组中 int[] counts = new int[32];

for(int num : nums) {

for(int j = 0; j < 32; j++) {

counts[j] += num & 1;//得到最右边位的数值 num >>>= 1;//无符号右移 }

}

int res = 0, m = 3;

for(int i = 0; i < 32; i++) {

res <<= 1;//左移 res |= counts[31 - i] % m;//将数组中的结果恢复到res中 }

return res;

}

}

时间复杂度 O(N): 其中 N位数组 nums的长度;遍历数组占用 O(N),每轮中的常数个位运算操作占用 O(1)。 空间复杂度 O(1): 数组 counts长度恒为 32 ,占用常数大小的额外空间。

剑指offer57-1 和为s的两个数字

题目

输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得它们的和正好是s。如果有多对数字的和等于s,则输出任意一对即可。

思路

法一:利用hashset或者hashmap(本题不推荐)

时间和空间复杂度均为 O(N)

public int[] twoSum(int[] nums, int target) {

Set set = new HashSet<>();

for (int num : nums) {

if (!set.contains(target - num))

set.add(num);

else

return new int[]{num, target - num};

}

return new int[]{};

}

法二:利用双指针

由于数组有序,利用双指针可以将空间复杂度降低至O(1)

class Solution {

public int[] twoSum(int[] nums, int target) {

int left=0,right= nums.length-1;

while(left

int cur = nums[left]+nums[right];

if (cur==target){

return new int[]{nums[left],nums[right]};

}else if(cur

left++;

}else{

right--;

}

}

return new int[]{};

}

}

剑指offer57-2 和为s的两个数字

题目

输入一个正整数 target ,输出所有和为 target 的连续正整数序列(至少含有两个数)。

序列内的数字由小到大排列,不同序列按照首个数字从小到大排列。

思路

利用滑动窗口(双指针)

class Solution {

public int[][] findContinuousSequence(int target) {

List arr = new ArrayList<>();

for (int l=1,r=2;l

int sum = (l+r)*(r-l+1)/2;

if (sum==target){

int[] temp = new int[r-l+1];

for (int i=0;i

temp[i] = l+i;

}

arr.add(temp);

l++;

}else if(sum

r++;

}else if(sum>target){

l++;

}

}

return arr.toArray(new int[arr.size()][]);

}

}

剑指offer59-1 滑动窗口的最大值

单调队列

题目

给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。

思路

利用一种新方法,单调队列,其使用 单调栈 实现了随意入栈、出栈情况下的 O(1)时间获取 “栈内最小值”。

class Solution {

public int[] maxSlidingWindow(int[] nums, int k) {

//单调队列 //下面是要注意的点: //队列按从大到小放入 //如果首位值(即最大值)不在窗口区间,删除首位 //如果新增的值小于队列尾部值,加到队列尾部 //如果新增值大于队列尾部值,删除队列中比新增值小的值,如果在把新增值加入到队列中 //如果新增值大于队列中所有值,删除所有,然后把新增值放到队列首位,保证队列一直是从大到小 if (nums.length == 0) return nums;

Deque deque = new LinkedList<>();

int[] arr = new int[nums.length - k + 1];

int index = 0; //arr数组的下标 //未形成窗口区间 for (int i = 0; i < k; i++) {

//队列不为空时,当前值与队列尾部值比较,如果大于,删除队列尾部值 //一直循环删除到队列中的值都大于当前值,或者删到队列为空 while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();

//执行完上面的循环后,队列中要么为空,要么值都比当前值大,然后就把当前值添加到队列中 deque.addLast(nums[i]);

}

//窗口区间刚形成后,把队列首位值添加到队列中 //因为窗口形成后,就需要把队列首位添加到数组中,而下面的循环是直接跳过这一步的,所以需要我们直接添加 arr[index++] = deque.peekFirst();

//窗口区间形成 for (int i = k; i < nums.length; i++) {

//i-k是已经在区间外了,如果首位等于nums[i-k],那么说明此时首位值已经不再区间内了,需要删除 if (deque.peekFirst() == nums[i - k]) deque.removeFirst();

//删除队列中比当前值小的值 while (!deque.isEmpty() && nums[i] > deque.peekLast()) deque.removeLast();

//把当前值添加到队列中 deque.addLast(nums[i]);

//把队列的首位值添加到arr数组中 arr[index++] = deque.peekFirst();

}

return arr;

}

}

时间复杂度 O(n) : 其中 n 为数组 nums长度;线性遍历 nums 占用 O(N) ;每个元素最多仅入队和出队一次,因此单调队列 deque 占用 O(2N) 。 空间复杂度 O(k) : 双端队列 deque中最多同时存储 k个元素(即窗口大小)。

法二

获取第一个窗口的最大值及最大值索引,右移一位,如果该值大于前一个窗口最大值,则该值一定是这个窗口的最大值;如果该值小于前一个窗口最大值,且该最大值在当前窗口中,则该最大值也是当前窗口最大值;否则,只能堆当前窗口三个值进行排序,寻找最大值.

class Solution {

public int[] maxSlidingWindow(int[]nums, int k) {

int[] temp=new int[0];

if(nums.length==0)

return temp;

int[] result=new int[nums.length-k+1];

int index=0;

int max=Integer.MIN_VALUE;

int maxIndex=-1;

for (int i = 0; i < k; i++) {

if(nums[i]>max)

{

max=nums[i];

maxIndex=i;

}

}

result[index++]=max;

for (int i = k; i < nums.length; i++) {

//当前值大于前一个窗口最大值,当前值为当前窗口最大值 if(nums[i]>=max)

{

max=nums[i];

maxIndex=i;

result[index++]=max;

}

//当前值小于前一个窗口最大值,且最大值在当前窗口 else if(inWindow(maxIndex,i-k+1,i))

{

result[index++]=max;

}

else //对当前窗口三个值进行排序 {

max=Integer.MIN_VALUE;

for (int j = i-k+1; j < i+1; j++) {

if(nums[j]>max)

{

max=nums[j];

maxIndex=j;

}

}

result[index++]=max;

}

}

return result;

}

//判断最大值索引是否在当前窗口中 boolean inWindow(int maxIndex,int from,int end)

{

if(maxIndex>=from&&maxIndex<=end)

return true;

else

return false;

}

}

剑指offer59-2 队列的最大值

题目

请定义一个队列并实现函数 max_value 得到队列里的最大值,要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。

若队列为空,pop_front 和 max_value 需要返回 -1

思路

很明显使用单调队列,维护一个双端队列的单调性,另一个队列执行正常的入队和出队,从队列尾部插入元素时,我们可以提前取出队列中所有比这个元素小的元素,使得队列中只保留对结果有影响的数字。这样的方法等价于要求维持队列单调递减,即要保证每个元素的前面都没有比它小的元素。

Integer在进行比较时,超过128不能使用==进行比较,只能使用equals()

class MaxQueue {

Deque dq1;

Deque dq2;

public MaxQueue() {

dq1 = new ArrayDeque<>();

dq2 = new ArrayDeque<>();

}

public int max_value() {

if (dq1.isEmpty()){

return -1;

}else{

return dq2.peekFirst();

}

}

public void push_back(int value) {

dq1.addLast(value);

while(!dq2.isEmpty()&&value>dq2.peekLast()){

dq2.pollLast();

}

dq2.addLast(value);

}

public int pop_front() {

if (dq1.isEmpty()){

return -1;

}else{

if (dq1.peekFirst().equals(dq2.peekFirst())){

dq2.pollFirst();

}

return dq1.pollFirst();

}

}

}

剑指offer60 n个骰子的点数

题目

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

思路

本题属于动态规划的范畴,先将题目进行细化,已知前面n-1的值,求n的值,我们可以将n分为n-1和1,若得到投掷第n-1个骰子后的概率数组dp[n-1],那么投掷最后一个骰子如何得到dp[n]呢?只需要将dp[n-1]和dp[1]的数组概率值两两(设两个数值索引分别为x,y)相乘,然后将得到的值保存到dp[n]中,且其索引值为x+y,因为索引值其实与实际值是一一对应的,一个索引对应一个值.

然后不断复用这个n-1,从小到大开始解决,如n=1时概率分布是已知的,为dp[1] = [1/6d,1/6d,1/6d,1/6d,1/6d,1/6d,],那么可以将n=2分为1和1,就可以得到dp[2],同理3分为2和1,就可以得到dp[3],以此类推,可以得到dp[n].

class Solution {

//使得n-1点数概率数组和1点数概率数组元素两两相乘,并将乘积结果加到n点数概率数组上。 //运算完成后就得到了最终的n点数概率数组。 //比如n为4,1和1=>2,2和1=>3,3和1=>4 最终得出4中所有可能出现的和的概率 public double[] twoSum(int n) {

//初始化n-1(需要复用)数组,投掷1个骰子的情况 double pre[]={1/6d,1/6d,1/6d,1/6d,1/6d,1/6d};

for(int i=2;i<=n;i++){//求投掷i个骰子的概率数组,从小到大求 double tmp[]=new double[5*i+1];//给第i个骰子后得到概率值开辟空间(6*i-i+1) for(int j=0;j

tmp[j+x]+=pre[j]/6;//将得到的值保存到第i个概率数组中 }

}

pre=tmp;//复用概率数组,当作下一个概率数组的i-1的部分 }

return pre;

}

}

剑指offer61 扑克牌中的顺子

题目

从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。

思路

法一:数组+遍历

根据题意,此 5张牌是顺子的 充分条件 如下:

除大小王外,所有牌 无重复 ; 设此 5 张牌中最大的牌为 max,最小的牌为 min(大小王除外),则需满足:max - min < 5

class Solution {

public boolean isStraight(int[] nums) {

int[] bucket = new int[14];

int min = 14,max = -1;

for (int i=0;i

if (nums[i]==0){

continue;

}

if (bucket[nums[i]]==1){

return false;

}

bucket[nums[i]]++;

min = Math.min(min,nums[i]);

max = Math.max(max,nums[i]);

}

return (max-min)<=4;

}

}

剑指offer62 圆圈中最后剩下的数字

题目

0,1,,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

思路

法一:数学+递归

当1个人,最后剩下的人下标为0

当2个人,最后剩下的人下标为(0+3)%2=1

当3个人,最后剩下的人下标为(1+3)%3=1

.....

当n个人,最后剩下的人下标为(f(n-1,m)+m)%3

可以发现n个人,需要建立在n-1个人最后剩下索引的基础之上

所以我们需要知道前面n-1个人最后剩下那个人的下标,由于我们知道n=1的答案,由此递归可以知道n=2,...n-1

class Solution {

public int lastRemaining(int n, int m) {

return f(n, m);

}

public int f(int n, int m) {

if (n == 1) {

return 0;

}

int x = f(n - 1, m);//递归计算前面n-1个人最后剩下的索引,这里会一直递归到f(1,m),然后触底反弹,递归计算到 f(n - 1, m) return (m + x) % n;//根据这个索引计算n个人最后剩下的索引 }

}

时间复杂度:O(n),需要求解的函数值有 n个。

空间复杂度:O(n),函数的递归深度为 n,需要使用 O(n)的栈空间。

法二:数学方法

将上面写法写成非递归,从1计算2,2计算3...以此类推

class Solution {

public int lastRemaining(int n, int m) {

int ans = 0;

// 最后一轮剩下2个人,所以从2开始反推 for (int i = 2; i <= n; i++) {

ans = (ans + m) % i;

}

return ans;

}

}

剑指offer63 股票的最大利润

题目

假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

思路

这题属于动态规划的范畴

状态定义: 设动态规划列表 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[i]=max(dp[i−1],prices[i]−min(prices[0:i]))

利用min来保存这个最小值,利用res来更新最大的利润,可以降低空间复杂度

class Solution {

public int maxProfit(int[] prices) {

int min = prices[0];

int res = 0;

if(prices==null||prices.length==0){

return 0;

}

for (int i=0;i

if (prices[i]<=min){

min = prices[i];

}else{

res = Math.max(res,prices[i]-min);

}

}

return res;

}

}

时间复杂度 O(N) : 其中 N 为 prices列表长度,动态规划需遍历 prices。 空间复杂度 O(1) : 变量 cost和 profit使用常数大小的额外空间。

剑指offer64 求1+2+3+...n

题目

求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

思路

逻辑运算符的短路效应常见的逻辑运算符有三种,即 “与 && ”,“或 || ”,“非 ! ” ;而其有重要的短路效应,如下所示:

if(A && B) // 若 A 为 false ,则 B 的判断不会执行(即短路),直接判定 A && B 为 false ​

if(A || B) // 若 A 为 true ,则 B 的判断不会执行(即短路),直接判定 A || B 为 true

本题需要实现 “当 n = 1时终止递归” 的需求,可通过短路效应实现。

n > 1 && sumNums(n - 1) // 当 n = 1 时 n > 1 不成立 ,此时 “短路” ,终止后续递归

时间复杂度 O(n) : 计算 n + (n-1) + ... + 2 + 1需要开启 n 个递归函数。 空间复杂度 O(n): 递归深度达到 n,系统使用 O(n)大小的额外空间。

Java 中,为构成语句,需加一个辅助布尔量 x ,否则会报错; Java 中,开启递归函数需改写为 sumNums(n - 1) > 0 ,此整体作为一个布尔量输出,否则会报错; 初始化变量 res记录结果。( Java 可使用第二栏的简洁写法,不用借助变量 res)。

class Solution {

int res = 0;

public int sumNums(int n) {

boolean x = n > 1 && sumNums(n - 1) > 0;

res += n;

return res;

}

}

class Solution {

public int sumNums(int n) {

boolean x = n > 1 && (n += sumNums(n - 1)) > 0;

return n;

}

}

剑指offer65 不用加减乘除做加法

题目

写一个函数,求两个整数之和,要求在函数体内不得使用 “+”、“-”、“*”、“/” 四则运算符号。

思路

使用位运算实现加法,无进位和 与 异或运算 规律相同,进位 和 与运算 规律相同(并需左移一位)。因此,无进位和 n 与进位 c 的计算公式如下;

递归法

class Solution {

public int add(int a, int b) {

if (b == 0) {

return a;

}

// 转换成非进位和 + 进位 return add(a ^ b, (a & b) << 1);

}

由于不能用加法,因此要一直转换直到第二个加数变成0。遍历法

class Solution {

public int add(int a, int b) {

while(b != 0) { // 当进位为 0 时跳出 int c = (a & b) << 1; // c = 进位 a ^= b; // a = 非进位和 b = c; // b = 进位 }

return a;

}

}

剑指offer66 构建乘积数组

题目

给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积, 即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。

思路

可以从左边往右计算除当前数值的前一部分,从右边往左计算除当前数值的后一部分,再将两部分进行相乘即可得到最终结果

class Solution {

public int[] constructArr(int[] a) {

int n = a.length;

int[] B = new int[n];

for (int i = 0, product = 1; i < n; product *= a[i], i++) /* 从左往右累乘 */

B[i] = product;

for (int i = n - 1, product = 1; i >= 0; product *= a[i], i--) /* 从右往左累乘 */

B[i] *= product;

return B;

}

}

时间复杂度 O(N) : 其中 N为数组长度,两轮遍历数组 a ,使用 O(N)时间。 空间复杂度 O(1) : 变量 tmp使用常数大小额外空间(数组 b 作为返回值,不计入复杂度考虑)。

剑指offer67 把字符串转换为整数

题目

写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。

首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。

当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。

该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。

注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。

在任何情况下,若函数不能进行有效的转换时,请返回 0。

假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。

思路

首先需要去掉前面空格,用一个标志表示负数,将索引定位到数字位,遍历后面的数字位,如果为符号位,返回为0,如果为数字位,首先判断是否溢出边界,未溢出就往上一位位加上去判断超出边界有两种: 边界数字为2147483647,不能直接利用数字判断是否大于边界,这样会超出题目规定的数字位,应该除以10为214748364,将这个作为边界数字判断

一种是在拼接前大于214748364(比如214748365),拼接后(2147483650)肯定大于边界;

一种是拼接前等于边界214748364,但个位数大于7,拼接后也会大于边界

class Solution {

public int strToInt(String str) {

int sign = 1;

int res = 0;

int i = 0;

int boundary = Integer.MAX_VALUE/10;

if (str.length()==0){

return 0;

}

while(str.charAt(i)==' '){//定位到空格处 i++;

if (i==str.length()){

return 0;

}

}

if (str.charAt(i)=='-'){//等于负号就将标志为置负 sign = -1;

}

if (str.charAt(i)=='-'||str.charAt(i)=='+'){//将数字置位到数字位 i++;

}

for (int j=i;j'9'){

break;

}

//判断是否有超出边界 if (res>boundary||res==boundary&&str.charAt(j)>'7'){

return sign==1?Integer.MAX_VALUE:Integer.MIN_VALUE;

}

//前面一部分表示高位,后面一位表示个位 res += res*10 + str.charAt(j) - '0';

}

return sign*res;

}

}

剑指offer68-1 二叉搜索树的最近公共祖先

题目

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

思路

法一:迭代

循环搜索: 当节点 root为空时跳出; 当 p, q都在 root的 右子树 中,则遍历至 root.right; 否则,当 p, q都在 root的 左子树 中,则遍历至 root.left; 否则,说明找到了 最近公共祖先 ,跳出。 返回值: 最近公共祖先 root

class Solution {

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

while(root != null) {

if(root.val < p.val && root.val < q.val) // p,q 都在 root 的右子树中 root = root.right; // 遍历至右子节点 else if(root.val > p.val && root.val > q.val) // p,q 都在 root 的左子树中 root = root.left; // 遍历至左子节点 else break;

}

return root;

}

}

时间复杂度 O(N) : 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为logN (满二叉树),最大为 N(退化为链表)。 空间复杂度 O(1): 使用常数大小的额外空间。

法二:递归

class Solution {

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { //都在右子树,往右子树递归,在右子树中寻找公共祖先 if(root.val < p.val && root.val < q.val)

return lowestCommonAncestor(root.right, p, q);

//都在左子树,往左子树递归,在左子树中寻找公共祖先 if(root.val > p.val && root.val > q.val)

return lowestCommonAncestor(root.left, p, q);

//其他情况,分布在两侧,则根节点为root return root;

}

}

时间复杂度 O(N): 其中 N 为二叉树节点数;每循环一轮排除一层,二叉搜索树的层数最小为 logN (满二叉树),最大为 N (退化为链表)。 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到树的层数 N

剑指offer68-2 二叉树的最近公共祖先

题目

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

思路

采用前序遍历的方法,根左右的办法进行递归遍历,首先判断根节点(当前节点)是否为最近公共祖先,然后在左子树寻找最近公共祖先,在右子树寻找最近公共祖先,然后对这两个祖先进行判断

class Solution {

public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

//判断当前(树)节点是否为最近公共祖先 if(root==null||p==root||q==root){

return root;

}

//在左子树中寻找最近公共节点 TreeNode left = lowestCommonAncestor(root.left,p,q);

//在右子树中寻找最近公共节点 TreeNode right = lowestCommonAncestor(root.right,p,q);

//左右子树都找到了公共节点,只能是根节点(因为左右子树交集只有根节点) if (left!=null&&right!=null){

return root;

}else if (left!=null){//在左子树找到了公共节点,返回公共节点 return left;

}else if (right!=null){//在右子树找到了公共节点,返回公共节点 return right;

}else{//左右两边都没找到公共节点,说明没有公共祖先 return null;

}

}

}

时间复杂度 O(N) : 其中 N 为二叉树节点数;最差情况下,需要递归遍历树的所有节点。 空间复杂度 O(N): 最差情况下,递归深度达到 N ,系统使用 O(N)大小的额外空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值