剑指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)大小的额外空间。