文章目录
- 笔者记
- 说明
- 总结
- 剑指 Offer 41. 数据流中的中位数(大顶堆+小顶堆 &)
- 剑指 Offer 42. 连续子数组的最大和
- 剑指 Offer 43. 1~n 整数中 1 出现的次数
- 剑指 Offer 44. 数字序列中某一位的数字
- 剑指 Offer 45. 把数组排成最小的数(&)
- 剑指 Offer 46. 把数字翻译成字符串(动态规划)
- 剑指 Offer 47. 礼物的最大价值(动态规划)
- 剑指 Offer 48. 最长不含重复字符的子字符串(动态规划+哈希表*)
- 剑指 Offer 49. 丑数(动态规划)
- 剑指 Offer 50. 第一个只出现一次的字符(哈希表/有序哈希表)
- 剑指 Offer 51. 数组中的逆序对(归并/分治*)
- 剑指 Offer 52. 两个链表的第一个公共节点
- 剑指 Offer 53 - I. 在排序数组中查找数字 I(二分查找)
- 剑指 Offer 53 - II. 0~n-1中缺失的数字(二分查找)
- 剑指 Offer 54. 二叉搜索树的第k大节点
- 剑指 Offer 55 - I. 二叉树的深度
- 剑指 Offer 55 - II. 平衡二叉树(#)
- 剑指 Offer 56 - I. 数组中数字出现的次数(位运算)
- 剑指 Offer 56 - II. 数组中数字出现的次数 II
- 剑指 Offer 57. 和为s的两个数字(双指针)
- 剑指 Offer 57 - II. 和为s的连续正数序列(双指针)
- 剑指 Offer 58 - I. 翻转单词顺序
- 剑指 Offer 58 - II. 左旋转字符串
- 剑指 Offer 59 - I. 滑动窗口的最大值(双端队列/单调队列*)
- 剑指 Offer 59 - II. 队列的最大值
- 剑指 Offer 60. n个骰子的点数(动态规划)
- 剑指 Offer 61. 扑克牌中的顺子
- 剑指 Offer 62. 圆圈中最后剩下的数字
- 剑指 Offer 63. 股票的最大利润
- 剑指 Offer 64. 求1+2+…+n
- 剑指 Offer 65. 不用加减乘除做加法(位运算)
- 剑指 Offer 66. 构建乘积数组(*)
- 剑指 Offer 67. 把字符串转换成整数
- 剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
- 剑指 Offer 68 - II. 二叉树的最近公共祖先
笔者记
笔者目前大四,虽然已经保研了,但研究生阶段是两年制专硕。找工作的压力也感到了些许,在学习科研的同时,也抽空准备工作方面的事情,如:算法基础、深度学习强化学习理论基础与面试常用题等。目前个人感觉科研方面出成果进大厂实验室较为困难,所以暂定目标为强化学习/深度学习的开发岗,与生产实践相结合,找工作的难度也相对容易。
本篇是剑指offer的刷题笔记下半部分,在此做一个记录,也供大家参考。笔记的上半部分可以点击链接:链接。
说明
题目旁边加*,表示该题较为有价值
题目旁边加#,表示该题有更好的解法,做完一遍之后必须回顾
题目旁边加&,表示该题还有知识点需要整理
总结
至此,剑指offer的题目已经过了一遍。我发现有以下几点需要整理:
1.java中lambda表达式如何使用
2.java中各种数据结构的特性需要整理,如:Stack、Queue、PriorityQueue、Deque、LinkedList、ArrayList等。
3.剑指offer共75道题目中,有价值的题目仍需回顾,部分题目的最优解法在下一轮复习中需要着重整理。
剑指 Offer 41. 数据流中的中位数(大顶堆+小顶堆 &)
解题思路:采用堆(优先队列)的数据结构进行解题,采用大顶堆存储数据中较小的半部分,采用小顶堆存储数据中较大的半部分。插入数据时保持有序性,并且从大顶堆开始插入。大顶堆记为maxHeap,小顶堆记为minHeap。最终时间复杂度为O(logN),空间复杂度为O(n)。本题还涉及java8中lambda表达式的使用,后期需要整理。
插入时(保证数据的有序性):
1.如果maxHeap.size() == minHeap.size(),则先将新的数num插入minHeap,并将minHeap的堆顶弹出插入到maxHeap中,该轮操作后maxHeap中的数据量+1;
2.如果不等,则表示maxHeap.size() > minHeap.size(),则将num插入maxHeap()中,再将maxHeap的堆顶插入到minHeap中,该轮操作后minHeap中的数据量+1。
寻找中位数时:
1.如果 maxHeap.size() == minHeap.size(),表示数组长度为偶数,返回(maxHeap.peek()+minHeap.peek())/2.0;
2.如果不等,则表示数组长度为奇数,返回maxHeap.peek()。
class MedianFinder {
private PriorityQueue<Integer> minHeap, maxHeap;//用小顶堆存储较大的部分,用大顶堆存储较小的部分
/** initialize your data structure here. */
public MedianFinder() {
minHeap = new PriorityQueue<Integer>();
maxHeap = new PriorityQueue<Integer>(
new Comparator<Integer>(){
//原有比较器返回负值表示i1<i2,正值表示i1>i2,按照递增进行排序
//重写之后返回负值表示i1>i2,正值表示i1<i2,按照递减进行排序
public int compare(Integer i1, Integer i2){
return i2 - i1;
}});
//也可以采用lamda表达式简化为:maxHeap = new PriorityQueue<>((x, y) -> (y - x));
}
public void addNum(int num) {
if(minHeap.size() == maxHeap.size()){
//如果maxHeap和minHeap相等,则在maxHeap中添加数据
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
}else{
//如果minHeap和maxHeap不相等,则表示minHeap<maxHeap,则在minHeap中添加数据
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}
}
public double findMedian() {
return minHeap.size() == maxHeap.size()? (maxHeap.peek()+minHeap.peek())/2.0 : maxHeap.peek();
}
}
剑指 Offer 42. 连续子数组的最大和
解题思路:
1.朴素解法:用sum记录求和值,每次叠加num。如果sum>max,则用max进行记录。如果sum<0,则表示当前num及其前驱对于之后的求和没有正向影响,则将sum置为0,重新开始记录。
2.动态规划,用nums[i]记录以nums[i]结尾的最大和,且nums[i] += Math.max(nums[i-1], 0)。用res记nums的最大值。
public int maxSubArray(int[] nums) {
//解法1:朴素解法,思路类似解法2
int max = Integer.MIN_VALUE;
int sum = 0;
for(int num : nums){
sum += num;
if(sum>max)max = sum;
if(sum<0)sum = 0;
}
return max;
//解法2:动态规划
int res = nums[0];
for(int i = 1; i< nums.length; i++){
nums[i] += Math.max(nums[i-1],0);
res = Math.max(res,nums[i]);
}
return res;
}
剑指 Offer 43. 1~n 整数中 1 出现的次数
解题思路:将数字分为最高位和剩余位两部分,先计算带有最高位部分数字中1出现的次数。如将21345分为:1346 ~ 21345和1345。
对于带有高位的部分:
1.先计算最高位为1的情况,如果最高位的数字 > 1,则有10^n(最高位的位数)个1,如(21345包含10000 ~ 19999);如果最高位数字等于1,则有(剩余位+1)个1,如(11345包含10000 ~ 11345)。
2.然后计算除最高位外剩余部分1的个数,可以进行依次取1计算,假设剩余部分有k位,则1的个数为:最高位数字* k *10^(k-1),k位数任意位置取1,其他位置有0~9共10种选择。如(1346 ~ 21345中可分为1346 ~11345和11346 ~ 21345进行剩余4位的计算,所以要乘以最高位数字,表示分为几段)。
对于剩余部分
进行递归计算,递归跳出的条件为 n为1位数时,n == 0 返回0,1<=n<=9,返回1。
public int countDigitOne(int n) {//以n=21345为例
if(n == 0)return 0;
if(n >= 1 && n <= 9)return 1;
String str = String.valueOf(n);
int head = str.charAt(0) - '0', res = 0;
if(head > 1){
//如果首位大于1,则首位为1有10^(str.length()-1)个数,10000~19999
res = (int)Math.pow(10,str.length()-1);
}else{
//如果首位为1,则首位为1有剩余部分数+1个数
res = Integer.parseInt(str.substring(1,str.length())) + 1;
}
//计算大于去除首位数的剩余部分时,其他位置为1的情况
res += head*(str.length()-1)*(int)Math.pow(10,str.length()-2);//1346~21345
//首位不为0时,1存在的情况并加上递归求解首位为0时,1存在的情况
return res+countDigitOne(Integer.parseInt(str.substring(1,str.length())));//1346~21345+0~1345
}
剑指 Offer 44. 数字序列中某一位的数字
解题思路:用n>1位数、2位数、3位数…的总数,则依次减掉。如果n不满足i位数个数的总数,则表示n对应的数字是一个i位数中的一位,则从i位数中的最小值开始加上n/i,n指向的是其中的n%i位。所以需要记录位数i(digit),i位数的开始值start,以及i位数的总数count。
在代码中,对n<10的情况做了单独的讨论。
class Solution {
public int findNthDigit(int n) {
if(n<10)return n;
int digit = 1;
long count = 10;
long start = 1;
while(n > count){
n -= count;
start *=10;//10,100
digit++;//1,2,3
count = 9*start*digit;//10,180,2700
}
start = start + n/digit;
n = n % digit;
return String.valueOf(start).charAt(n)-'0';
}
}
剑指 Offer 45. 把数组排成最小的数(&)
解题思路:自定义新的排序方法,然后将数组排序进行后,进行连接。在此,不得不提lambda表达式真的很好用,比我自己写Comparator中的compare方法要方便多了。
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]);
Arrays.sort(strs, (x, y) -> (x + y).compareTo(y + x));
StringBuilder res = new StringBuilder();
for(String s : strs)
res.append(s);
return res.toString();
}
剑指 Offer 46. 把数字翻译成字符串(动态规划)
解题思路:用f[i]记录以str[i-1]结尾的数字串有多少种翻译方法,如果str[i-2],str[i-1]表示的数范围为10~25,则表示f[i] = f[i-1] + f[i-2]。否则当前只有一种表示方法,即f[i] = f[i-1]。
public int translateNum(int num) {
String str = String.valueOf(num);
int len = str.length();
int[] f = new int[len+1];
f[0]=f[1] = 1;
for(int i=2; i <= len ; i++){
f[i] = f[i-1];
int number = (str.charAt(i-2)-'0')*10+(str.charAt(i-1)-'0');
if(number>25|| number<10)continue;
f[i] += f[i-2];
}
return f[len];
}
剑指 Offer 47. 礼物的最大价值(动态规划)
解题思路:用dp[i][j]记录位置(i,j)礼物的最大价值,则dp[i][j] = max(dp[i-1][j],dp[i][j-1])+grid[i][j],i>0,j>0。从i = 0,j = 0 ,依次按照行向右下角遍历。需要注意第一行和第一列的特殊处理。
public int maxValue(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
for(int j = 1; j < cols; j++){
grid[0][j] += grid[0][j-1];
}
for(int i = 1; i < rows; i++){
grid[i][0] += grid[i-1][0];
}
for(int i = 1; i < rows; i++){
for(int j = 1; j < cols; j++){
grid[i][j] += Math.max(grid[i-1][j],grid[i][j-1]);
}
}
return grid[rows-1][cols-1];
}
剑指 Offer 48. 最长不含重复字符的子字符串(动态规划+哈希表*)
解题思路: 采用动态规划思路,找到以当前字符s[j]结尾的最长字符串长度dp[j],然后找到dp中的最大值。需要记录s[j]左边距离最近的相同字符s[i],状态转移方程如下:
1.如果dp[j-1] < j - i,表示s[i]不包含在dp[j-1]所对应的字符串中,dp[j] = dp[j-1] + 1。
2.如果dp[j-1] >= j - i,表示s[i]包含在dp[j-1]所对应的字符串中,dp[j] = j - i。
public int lengthOfLongestSubstring(String s) {
HashMap<Character,Integer> map = new HashMap();
int tmp = 0, res = 0;
for(int j = 0; j < s.length(); j++){
int i = map.getOrDefault(s.charAt(j),-1);
map.put(s.charAt(j),j);
tmp = tmp < j-i ? tmp+1 : j-i;
res = Math.max(res,tmp);
}
return res;
}
剑指 Offer 49. 丑数(动态规划)
解题思路:因为丑数定义是只包含2、3、5质因子,所以大的丑数一定是小的丑数*2/3/5。所以依次将目前的最小值加入数组中,目前的最小值=min(nums[i2]*2, nums[i3]*3, nums[i5]*5)。
public int nthUglyNumber(int n) {
int nums[] = new int[n+1];
nums[1] = 1;
int i2 = 1, i3 = 1, i5 = 1;
for(int i = 2; i <= n; i++){
int n2 = nums[i2]*2, n3 = nums[i3]*3, n5 = nums[i5]*5;
int k = Math.min(Math.min(n2,n3),n5);
nums[i] = k;
if(k == n2)i2++;
if(k == n3)i3++;
if(k == n5)i5++;
}
return nums[n];
}
剑指 Offer 50. 第一个只出现一次的字符(哈希表/有序哈希表)
解题思路:
1.无序哈希表解法:遍历一次字符串,用哈希表记录每个字符出现的次数(多于1次,置为false)。然后再次遍历字符串,找到第一个出现次数为1的字符。
2.有序哈希表解法:遍历一次字符串,按序将字符加入哈希表中,并记录出现次数(多于1次,置为false)。然后依次遍历有序哈希表,找到第一个出现次数为1的字符。
public char firstUniqChar(String s) {
//java中用LinkedHashMap实现有序哈希表
Map<Character, Boolean> dic = new LinkedHashMap<>();
char[] sc = s.toCharArray();
for(char c : sc)
dic.put(c, !dic.containsKey(c));
for(Map.Entry<Character, Boolean> d : dic.entrySet()){
if(d.getValue()) return d.getKey();
}
return ' ';
// 无序哈希表,两次遍历
// HashMap<Character,Boolean> map = new HashMap();
// char[] sc = s.toCharArray();
// for(char c : sc){
// map.put(c,!map.containsKey(c));
// }
// for(char c : sc){
// if(map.get(c))return c;
// }
// return ' ';
// 原书思路,因为C++中char为长度为8的数据类型,范围是0-255。
// 但在java中,char是unicode编码,占两个字节,范围是0-65535,所以不一定合适
// int map[] = new int[256];
// for(int i = 0; i < s.length(); i++){
// map[s.charAt(i)]++;
// }
// for(int i = 0; i < s.length(); i++){
// if(map[s.charAt(i)] == 1)return s.charAt(i);
// }
// return ' ';
}
剑指 Offer 51. 数组中的逆序对(归并/分治*)
解题思路:简单来说就是采用归并排序的思路进行解题,首先将原数组一份为二,递归计算左半部分中的逆序对个数、右半部分中的逆序对个数、以及左右部分之间的逆序对个数。左右部分中逆序对的个数为原问题的子问题,分治解决即可。
在计算左右部分间的逆序对个数时,首先保证左右两个部分是有序的,然后采用双指针依次从最后一位开始比较:
1.如果左指针大于右指针,则表示左指针对应值大于右指针对应值,则有k(右指针前数据的个数)个逆序对。将左指针对应值加入到合并数组中,左指针前移。
2.如果左边指针小于右指针,则未形成逆序对,将右指针对应值加入到合并数组中,右指针前移。
因为是有序的,所以不管是左指针还是右指针值加入合并数组中,都是当前剩余序列的最大值,因此保证了合并序列的有序性。
class Solution {
public int reversePairs(int[] nums) {
if(nums == null || nums.length == 0)return 0;
int copy[] = new int[nums.length];
for(int i = 0; i < nums.length; i++){
copy[i] = nums[i];
}
return reversePairsCore(nums, 0, nums.length-1, copy);
}
private int reversePairsCore(int[] nums, int left, int right,int[] copy){
if(left == right){
return 0;
}
int mid = (right - left)/2;
int l_num = reversePairsCore(copy, left, left+mid, nums);
//copy和nums互换,保证递归后回传的数组是有序的nums
int r_num = reversePairsCore(copy, left+mid+1, right, nums);
int i = left+mid;
int j = right;
int count = 0;
int indexCopy = right;
while(i >= left && j >= left+mid+1){
if(nums[i] > nums[j]){//左指针大于右指针,共有j-left-mid个逆序对
count += j-left-mid;
copy[indexCopy--] = nums[i--];
}else{//左指针小于等于右指针,未形成逆序对
copy[indexCopy--] = nums[j--];
}
}
//将剩余部分加入到合并数组
for(;i >= left; i--){
copy[indexCopy--] = nums[i];
}
for(;j >= left+mid+1; j--){
copy[indexCopy--] = nums[j];
}
return l_num + r_num + count;//返回逆序对的总数
}
}
剑指 Offer 52. 两个链表的第一个公共节点
解题思路:两次遍历,第一次遍历分别计算两个链表的长度。因为两个单链表有公共部分,所有公共部分的长度相同。将第一次遍历计算出的长度较长的链表,从头开始依次后移,直至长度与较短的链表相等时,开始判断剩余部分是否有相同节点。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = 0, lenB = 0;
ListNode curA = headA, curB = headB;
//计算链表长度
while(curA != null){
lenA++;
curA = curA.next;
}
while(curB != null){
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
//将较长链表依次后移,直至当前位置所剩链表长度与较短链表相等
while(lenA != lenB){
if(lenA > lenB){
curA = curA.next;
lenA--;
}else{
curB = curB.next;
lenB--;
}
}
//寻找公共节点
while(curA != null && curB != null){
if(curA == curB)return curA;
curA = curA.next;
curB = curB.next;
}
return null;
}
剑指 Offer 53 - I. 在排序数组中查找数字 I(二分查找)
解题思路:采用二分查找,查找左右边界。需要注意最终边界值落在哪个变量上,对于临界值的判断需要仔细分析。
public int search(int[] nums, int target) {
int i = 0, j = nums.length - 1;
while(i <= j){
int mid = (i + j)/2;
if(nums[mid] > target) j = mid - 1;
else i = mid + 1;//寻找右边界
}
int right = i;
if(j > 0 && nums[j] != target) return 0;
i = 0;
while(i <= j){
int mid = (i + j)/2;
if(nums[mid] >= target) j = mid - 1;//寻找左边界
else i = mid + 1;
}
int left = j;
return right - left - 1;
}
剑指 Offer 53 - II. 0~n-1中缺失的数字(二分查找)
public int missingNumber(int[] nums) {
int left = 0, right = nums.length - 1;
while(left < right){
int mid = (left + right) / 2;
if(nums[mid] == mid){
left = mid + 1;
}else{
right = mid;
}
}
if(left == nums.length-1 && nums[nums.length-1] == nums.length-1)return nums.length;
return left;
}
剑指 Offer 54. 二叉搜索树的第k大节点
解题思路:采用“右子树-根-左子树”的顺序得到二叉搜索树节点的降序排列。
class Solution {
int times,res;
public int kthLargest(TreeNode root, int k) {
times = k;
inOrder(root);
return res;
}
private void inOrder(TreeNode node){
if(node == null || times <= 0)return;
inOrder(node.right);
times--;
if(times == 0)res = node.val;
inOrder(node.left);
}
}
剑指 Offer 55 - I. 二叉树的深度
解题思路:找到叶子节点,比较深度,并记录最大值。
class Solution {
int res = 0;
public int maxDepth(TreeNode root) {
preOrder(root, 1);
return res;
}
private void preOrder(TreeNode root, int depth){
if(root == null)return;
if(root.left == null && root.right == null){
if(depth > res)res = depth;
return;
}else{
preOrder(root.left, depth+1);
preOrder(root.right, depth+1);
}
}
}
剑指 Offer 55 - II. 平衡二叉树(#)
解题思路:先序遍历+深度判断。先判断根节点是否平衡,再递归判断左右子树是否平衡。本题中也给出了计算树的深度的另一种方法,详见代码中的depth()方法。
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 node){
if(node == null) return 0;
return Math.max(depth(node.left),depth(node.right)) + 1;
}
}
剑指 Offer 56 - I. 数组中数字出现的次数(位运算)
解题思路:只有两个数字只出现了1次,其他数字都出现了两次。如果问题是:只有一个数字只出现了1次,那么我们可以通过将所有的数字进行异或操作进行解题,因为出现2次的数字和本身进行异或的结果为0。
由此我们可以将原问题转化为:将原数组拆分为两个子数组,每个子数组中包含一个只出现一次的数字,其余数字出现两次。我们将所有数字进行异或操作,得到两个只出现1次的数字异或操作的结果,然后根据结果二进制最低的非零位进行子数组的分类。
public int[] singleNumbers(int[] nums) {
//采用异或操作进行解题
int res = 0;
//所有数字进行异或
for(int num : nums){
res ^= num;
}
//找到最低非零位
int div = 1;
while((res & div) == 0){
div <<= 1;
}
int a = 0, b = 0;
for(int num : nums){
if((num & div) == 0){//分类
a ^= num;//找到子数组中只出现一次的数
}else{
b ^= num;
}
}
return new int[]{a,b};
}
剑指 Offer 56 - II. 数组中数字出现的次数 II
解题思路:将所有数字的二进制树按位相加,最后结果按位%3,则可以消除出现3次的数字,得到只出现1次的数字。
public int singleNumber(int[] nums) {
int bitArr[] = new int[32];
for(int num : nums){
for(int j = 0; j < 32; j++){
if((num & 1) != 0)
bitArr[j] += 1;
num >>>= 1;
}
}
int result = 0,bitMask = 1;
for(int i = 31; i >= 0; i--){
result <<= 1;
result |= bitArr[i]%3;
}
return result;
}
剑指 Offer 57. 和为s的两个数字(双指针)
public int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left < right){
if(nums[left] + nums[right] == target)return new int[]{nums[left],nums[right]};
if(nums[left] + nums[right] > target){
right--;
}else{
left++;
}
}
return null;
}
剑指 Offer 57 - II. 和为s的连续正数序列(双指针)
public int[][] findContinuousSequence(int target) {
List<int []> res = new ArrayList<int[]>();
int small = 1, big = 2;
while(small < big){
int sum = (small + big)*(big - small + 1)/2;
if(sum == target){
int arr[] = new int[big - small +1];
for(int i = 0; i <= big - small; i++){
arr[i] = small+i;
}
res.add(arr);
small++;
}else if(sum > target){
small++;
}else{
big++;
}
}
return res.toArray(new int[res.size()][]);
}
剑指 Offer 58 - I. 翻转单词顺序
public String reverseWords(String s) {
String arr[] = s.split(" ");
StringBuilder res = new StringBuilder();
for(int i = arr.length-1; i >= 0; i--){
if(arr[i].equals(""))continue;
res.append(arr[i]);
res.append(" ");
}
return res.toString().trim();
}
剑指 Offer 58 - II. 左旋转字符串
public String reverseLeftWords(String s, int n) {
String a = s.substring(0,n);
String b = s.substring(n,s.length());
return b+a;
}
剑指 Offer 59 - I. 滑动窗口的最大值(双端队列/单调队列*)
解题思路:用双端队列记录当前窗口的最大值。在未形成窗口时,依次将值加入队列,在每个新的值加入队列前,需要把队列中小于当前值的元素依次弹出,以保证队头元素是最大值。形成窗口后,如果窗口最前面的元素是队列中的最大值(队头),则将队头弹出。然后采用与未形成窗口时相同的加入方式,将新的值插入到队列中。最后获得当前窗口的最大元素——队列头部元素。
public int[] maxSlidingWindow(int[] nums, int k) {
if(nums.length == 0 || k == 0)return new int[0];
int[] res = new int[nums.length - k + 1];
Deque<Integer> deque = new LinkedList<>();
//未形成窗口时
for(int i = 0; i < k; i++){
//插入新元素,需要将队列中所有小于该元素的值从队尾弹出队列
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
}
res[0] = deque.peekFirst();
//形成窗口后
for(int i = k; i < nums.length; i++){
//如果队头元素是窗口即将删除的元素,则将其从队头弹出队列
if(deque.peekFirst() == nums[i-k])
deque.removeFirst();
while(!deque.isEmpty() && deque.peekLast() < nums[i])
deque.removeLast();
deque.addLast(nums[i]);
res[i - k + 1] = deque.peekFirst();
}
return res;
}
剑指 Offer 59 - II. 队列的最大值
解题思路:同 59 - I
class MaxQueue {
Deque<Integer> deque;
Queue<Integer> queue;
public MaxQueue() {
deque = new LinkedList<>();
queue = new LinkedList<>();
}
public int max_value() {
if(!deque.isEmpty())return deque.peekFirst();
else return -1;
}
public void push_back(int value) {
queue.offer(value);
while(!deque.isEmpty() && deque.peekLast() < value){
deque.removeLast();
}
deque.addLast(value);
}
public int pop_front() {
if(!queue.isEmpty()){
if(queue.peek().equals(deque.peekFirst())){
deque.removeFirst();
}
return queue.poll();
}else{
return -1;
}
}
}
剑指 Offer 60. n个骰子的点数(动态规划)
解题思路:先计算1个骰子对应值出现的次数,然后是2、3、… 、n。递推式:dp(n)[k] = dp(n-1)[k-1]+dp(n-1)[k-2]+…+dp(n-1)[k-6]。
public double[] dicesProbability(int n) {
//取数的范围是n~6n,dp[k]表示总和为k的次数,最后返回的是dp[k]/6^n
//下一轮dp(i+1)[k] = dp(i)[k-1]+dp(i)[k-2]+...+dp(i)[k-6],从后向前更新
int dp[] = new int[6*n+1];
for(int i = 1; i <= 6; i++){
dp[i] = 1;
}
for(int i = 2; i <= n; i++){
for(int j = 6*n; j > 6; j--){
dp[j] = dp[j-1]+dp[j-2]+dp[j-3]+dp[j-4]+dp[j-5]+dp[j-6];
}
dp[6] = dp[5]+dp[4]+dp[3]+dp[2]+dp[1];
dp[5] = dp[4]+dp[3]+dp[2]+dp[1];
dp[4] = dp[3]+dp[2]+dp[1];
dp[3] = dp[2]+dp[1];
dp[2] = dp[1];
dp[1] = dp[0];
}
double res[] = new double[5*n+1];
double total = Math.pow(6,n);
for(int i = 0; i <= 5 * n; i++){
res[i] = dp[i+n]/total;
}
return res;
}
剑指 Offer 61. 扑克牌中的顺子
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int num_zero = 0;
for(int i = 0; i < nums.length-1; i++){
if(nums[i] == 0){
num_zero++;
continue;
}
int gap = nums[i+1] - nums[i];
if(gap == 0)return false;
if(gap != 1){
num_zero = num_zero - (gap - 1);
if(num_zero < 0)return false;
}
}
return true;
}
剑指 Offer 62. 圆圈中最后剩下的数字
解题思路:该题的方法十分巧妙,如果知道f(n-1,m)时最后所剩的数字为x,则f(n,m)时最后所剩数字为(x+m)%n。由此可以进行求解,具体理解过程官方题解给出了较为形象的解释。
public int lastRemaining(int n, int m) {
// if(n == 1)return 0;
// else return (lastRemaining(n-1,m)+m)%n;
int res = 0;
for(int i = 2; i <= n; i++){
res = (res + m) % i;
}
return res;
}
剑指 Offer 63. 股票的最大利润
public int maxProfit(int[] prices) {
int res = 0;
int sum = 0;
for(int i = 0; i < prices.length - 1; i++){
int gap = prices[i+1] - prices[i];
if(sum + gap > 0)sum += gap;
else sum = 0;
if(sum > res)res = sum;
}
return res;
}
剑指 Offer 64. 求1+2+…+n
解题思路:采用递归进行求解,采用逻辑运算进行判断
public int sumNums(int n) {
// return (int)n*(n+1)/2;
boolean flag = n > 0 && (n += sumNums(n - 1)) > 0;
return n;
}
剑指 Offer 65. 不用加减乘除做加法(位运算)
解题思路:采用异或操作进行按位加法计算,采用与操作表示进位,然后再将进位部分左移1位,并且与异或操作的结果进行相加。
public int add(int a, int b) {
// return a+b;
int sum, carry;
do{
sum = a^b;
carry = (a&b) << 1;
a = sum;
b = carry;
}while(b!=0);
return a;
}
剑指 Offer 66. 构建乘积数组(*)
解题思路:将数组a扩展为一个矩阵,并把对角线置为1,则最终结果为矩阵每行的乘积和。两次遍历:第一次遍历,先计算下三角形对应的值,进行累乘,并且res[i] = res[i-1] * a[i-1]。第二次遍历,计算上三角形对应的值,进行累乘( tmp = tmp * a[i+1]),并乘到结果中(res[i] *= tmp)。
public int[] constructArr(int[] a) {
if(a.length == 0)return a;
int res[] = new int[a.length];
res[0] = 1;
for(int i = 1; i < a.length; i++){
res[i] = res[i-1] * a[i-1];
}
int tmp = 1;
for(int i = a.length - 2; i >= 0; i --){
tmp = tmp * a[i+1];
res[i] *= tmp;
}
return res;
剑指 Offer 67. 把字符串转换成整数
解题思路:本题需要注意对于临界值的判断。
public int strToInt(String str) {
char[] c = str.trim().toCharArray();//去除首尾空格,转为char[]
if(c.length == 0) return 0;
int res = 0, bndry = Integer.MAX_VALUE / 10;
int i = 1, sign = 1;
if(c[0] == '-') sign = -1;//判断符号部分
else if(c[0] != '+') i = 0;
for(int j = i; j < c.length; j++) {
if(c[j] < '0' || c[j] > '9') break;//非数字,跳出循环
//对于越界进行讨论,int的最大值为21474836472147483647
if(res > bndry || res == bndry && c[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
res = res * 10 + (c[j] - '0');//数字部分进行拼接
}
return sign * res;
}
剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
解题思路:依据二叉搜索树的性质进行解题。
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root.val > p.val && root.val > q.val)return lowestCommonAncestor(root.left,p,q);
else if(root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right,p,q);
else return root;
}
剑指 Offer 68 - II. 二叉树的最近公共祖先
解题思路:从下到上,依次判断当前节点是否是最接近的公共祖先。最接近的公共祖先需要满足以下两个条件之一:
1.p和q分别在左右子树中,即flson && frson
2.root等于p或q,且剩余节点在左子树或右子树中,即 (root==p || root == q) && (flson || frson)
class Solution {
private TreeNode res;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.res;
}
public boolean dfs(TreeNode root, TreeNode p, TreeNode q){
if(root == null) return false;
boolean flson = dfs(root.left, p, q);
boolean frson = dfs(root.right, p, q);
if((flson && frson) || ((root == p || root == q) && (flson || frson)))
res = root;
return flson || frson || (root == p || root == q);
}
}