校招|剑指offer刷题记录 (下)g

JZ55 二叉树的深度
第一种方法:dfs

树的遍历方式总体分为两类:深度优先搜索(DFS)、广度优先搜索(BFS);
常见的 DFS : 先序遍历、中序遍历、后序遍历;
常见的 BFS : 层序遍历(即按层遍历)。
求树的深度需要遍历树的所有节点,本文将介绍基于 后序遍历(DFS) 和 层序遍历(BFS) 的两种解法。
方法一:后序遍历(DFS)
树的后序遍历 / 深度优先搜索往往利用 递归 或 栈 实现,本文使用递归实现。
关键点: 此树的深度和其左(右)子树的深度之间的关系。显然,此树的深度 等于 左子树的深度 与 右子树的深度 中的 最大值 +1 。

class Solution { public: int maxDepth(TreeNode* root) { if(root==nullptr)return 0; return max(maxDepth(root->left),maxDepth(root->right))+1; } };



校招|剑指offer刷题记录 (下)g
文章图片





此处使用层序遍历可以实现,但是有几点需要改改,先来看看传统的层序遍历:
class Solution { public: vector PrintFromTopToBottom(TreeNode* root) { vector res; if(!root)return res; queue q; q.push(root); while(!q.empty()){ TreeNode* nowNode=q.front(); q.pop(); res.push_back(nowNode->val); if(nowNode->left)q.push((nowNode->left)); if(nowNode->right)q.push(nowNode->right); } return res; } };

注意到一点,普通的层序遍历每次只出一个结点。而我们为了计算层的个数,需要每次出一整层的结点,而不是一次出一个,这样就可以计算层了。
代码如下:
class Solution { public: int maxDepth(TreeNode* root) { if(!root)return 0; queue q; q.push(root); int count = 0; while(!q.empty()){ int qSize=q.size(); //注意在pop的过程中size会发生变化,所以需要一开始记住size的大小 for(int i=0; ileft)q.push(nowNode->left); if(nowNode->right)q.push(nowNode->right); }count++; } return count; } };

JZ55-2 平衡二叉树 二叉树的每个节点的左右子树的高度差的绝对值不超过 11,则二叉树是平衡二叉树。根据定义,一棵二叉树是平衡二叉树,当且仅当其所有子树也都是平衡二叉树,因此可以使用递归的方式判断二叉树是不是平衡二叉树,递归的顺序可以是自顶向下或者自底向上。

方法一:自顶向下的递归
定义函数 height,用于计算二叉树中的任意一个节点 p的高度:
校招|剑指offer刷题记录 (下)g
文章图片



有了计算节点高度的函数,即可判断二叉树是否平衡。具体做法类似于二叉树的前序遍历,即对于当前遍历到的节点,首先计算左右子树的高度,如果左右子树的高度差是否不超过 1,再分别递归地遍历左右子节点,并判断左子树和右子树是否平衡。这是一个自顶向下的递归的过程。

校招|剑指offer刷题记录 (下)g
文章图片

代码如下:
class Solution { public: bool isBalanced(TreeNode* root) { if(!root)return true; return (abs(getHeight(root->left)-getHeight(root->right))<=1)&&isBalanced(root->left)&&isBalanced(root->right); } int getHeight(TreeNode* root){ if(!root)return 0; return max(getHeight(root->left),getHeight(root->right))+1; } };



至于本题的时间复杂度不是那么好算和理解,此处介绍一下:
关键在于,每个子树都要求其高度
校招|剑指offer刷题记录 (下)g
文章图片


校招|剑指offer刷题记录 (下)g
文章图片


校招|剑指offer刷题记录 (下)g
文章图片




方法二 自底向上
方法一由于是自顶向下递归,因此对于同一个节点,函数height 会被重复调用,导致时间复杂度较高。
方法一的具体调用过程是计算左子树的高度计算右子树的高度,而且还要判断左子树自己是否平衡,右子树自己是否平衡。在计算高度时,先一步步递归到最底下,然后再往回走,最后才计算出高度。
在判断左子树是否平衡的时候又要计算左子树的两个子树的高度,这样就会导致重复计算。
如果使用自底向上的做法,则对于每个节点,函数height 只会被调用一次。


校招|剑指offer刷题记录 (下)g
文章图片


自底向上递归的做法核心思想就是只求一次高度。
我们首先求根结点的高度,要求根节点的高度就去调用左右子树的高度,然后在计算的过程当中,如果发现左右子树的高度中有不平衡的子树,那此时之后我们无需再多计算什么东西了,可以直接让整个程序返回false了。
因为存在一个不平衡的则整个不平衡!

这种做法其实就是在通过求根节点的高度时候需要去计算左右子树的高度,于是乎,在计算左右子树的高度时顺便判断是否平衡,如果不平衡直接返回-1,这样可以使得根节点的高度也是-1,即不平衡。
class Solution { public: bool isBalanced(TreeNode* root) { return getHeight(root)>=0; } int getHeight(TreeNode* root){ if(!root)return 0; int leftHeight=getHeight(root->left); int rightHeight=getHeight(root->right); if(abs(leftHeight-rightHeight)>1)return -1; if(leftHeight==-1||rightHeight==-1)return -1; return max(leftHeight,rightHeight)+1; } };


JZ56 数组中数字出现的次数 方法一哈希

方法二排序搜索法:
这个方法也是特别容易想到的,我们首先对数组进行排序,然后遍历数组,因为数组中其他数字都出现两次,只有目标值出现一次,所以则让我们的指针每次跳两步,当发现当前值和前一位不一样的情况时,返回前一位即可,当然我们需要考虑这种情况,当我们的目标值出现在数组最后一位的情况,所以当数组遍历结束后没有返回值,则我们需要返回数组最后一位,下面我们看一下动图解析。
校招|剑指offer刷题记录 (下)g
文章图片



方法三:set去重法
我们依次遍历元素并与 HashSet 内的元素进行比较,如果 HashSet 内没有该元素(说明该元素第一次出现)则存入,若是 HashSet 已经存在该元素(第二次出现),则将其从 HashSet 中去除,并继续遍历下一个元素。最后 HashSet 内剩下的则为我们的目标数
校招|剑指offer刷题记录 (下)g
文章图片


方法五 栈、队列
本题同样可以使用栈或队列,以栈举例:
校招|剑指offer刷题记录 (下)g
文章图片



方法六:位运算
先将本题简化一下,假设只有一个出现次数为1的数,其他数字出现次数全为2

这个方法主要是借助咱们的位运算符 ^ 按位异或,我们先来了解一下这个位运算符。
按位异或(XOR)运算符“^”是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。相同时为0。
任何数和0异或,仍为本身:a⊕0 = a
任何数和本身异或,为0:a⊕a = 0
异或运算满足交换律和结合律:a⊕b⊕a = (a⊕a)⊕b = 0⊕b = b
校招|剑指offer刷题记录 (下)g
文章图片



我们通过上面的例子了解了异或运算,对应位相异时得 1,相同时得 0,那么某个数跟本身异或时,因为对应位都相同所以结果为 0 , 然后异或又满足交换律和结合律。则
校招|剑指offer刷题记录 (下)g
文章图片


class Solution { public int singleNumber(int[] nums) { int num = 0; //异或 for (int x : nums) { num ^= x; } return num; } }



JZ57 和为s的两个数字
解题思路:
利用 HashMap 可以通过遍历数组找到数字组合,时间和空间复杂度均为 O(N)O(N) ;
注意本题的 numsnums 是 排序数组 ,因此可使用 双指针法 将空间复杂度降低至 O(1)O(1) 。
算法流程:
初始化: 双指针 i, j 分别指向数组 nums 的左右两端 (俗称对撞双指针)。
循环搜索: 当双指针相遇时跳出;
计算和 s = nums[i] + nums[j];
若 s > targets,则指针 j 向左移动,即执行 j = j - 1 ;
若 s < targets,则指针 i 向右移动,即执行 i = i + 1 ;
若 s = targets ,立即返回数组 [nums[i], nums[j]] ;
返回空数组,代表无和为 targettarget 的数字组合。
校招|剑指offer刷题记录 (下)g
文章图片


正确性的证明:
状态 S(i, j)S(i,j) 切换至 S(i + 1, j)S(i+1,j) ,则会消去一行元素,相当于 消去了状态集合 {S(i, i + 1), S(i, i + 2), ..., S(i, j - 2), S(i, j - 1), S(i, j)S(i,i+1),S(i,i+2),...,S(i,j?2),S(i,j?1),S(i,j) } 。(由于双指针都是向中间收缩,因此这些状态之后不可能再遇到)。
由于 numsnums 是排序数组,因此这些 消去的状态 都一定满足 S(i, j) < targetS(i,j) 结论: 以上分析已证明 “每次指针 i 的移动操作,都不会导致解的丢失” ,即指针 i 的移动操作是 安全的 ;同理,对于指针 j 可得出同样推论;因此,此双指针法是正确的。

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


JZ57 - II. 和为s的连续正数序列 方法一:枚举 + 暴力
枚举每个正整数为起点,判断以它为起点的序列和sum是否等于target即可,由于题目要求序列长度至少大于2,所以枚举的上界为target/2
class Solution { public: vector> findContinuousSequence(int target) { vector> vec; vector res; int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整 for (int i = 1; i <= limit; ++i) { for (int j = i; ; ++j) { sum += j; if (sum > target) { sum = 0; break; } else if (sum == target) { res.clear(); for (int k = i; k <= j; ++k) { res.emplace_back(k); } vec.emplace_back(res); sum = 0; break; } } } return vec; } };

emplace_back和push_back效果一样

校招|剑指offer刷题记录 (下)g
文章图片




class Solution { public: vector> findContinuousSequence(int target) { vector> vec; vector res; int sum = 0, limit = (target - 1) / 2; // (target - 1) / 2 等效于 target / 2 下取整 for (int x = 1; x <= limit; ++x) { long long delta = 1 - 4 * (x - 1ll * x * x - 2 * target); if (delta < 0) { continue; } int delta_sqrt = (int)sqrt(delta + 0.5); if (1ll * delta_sqrt * delta_sqrt == delta && (delta_sqrt - 1) % 2 == 0) { int y = (-1 + delta_sqrt) / 2; // 另一个解(-1-delta_sqrt)/2必然小于0,不用考虑 if (x < y) { res.clear(); for (int i = x; i <= y; ++i) { res.emplace_back(i); } vec.emplace_back(res); } } } return vec; } };



方法三:重点!!
双指针法,又叫滑动窗口法
滑动窗口,这里也叫双指针,因为题中要求的是正整数,连续的,并且至少含有两个数。所以我们使用两个指针,一个left指向1,一个right指向2,他们分别表示窗口的左边界和右边界。然后计算窗口内元素的和。
如果窗口内的值大于target,说明窗口大了,left往右移一步。
如果窗口内的值小于target,说明窗口小了,right往右移一步。
如果窗口内的值等于target,说明找到了一组满足条件的序列,把它加入到列表中

校招|剑指offer刷题记录 (下)g
文章图片


class Solution { public: vector> findContinuousSequence(int target) { vector> vec; vector res; int sum = 0, limit = (target ) / 2; int left=1,right=1; sum=1; //注意这里left是取等 while(left<=limit){ if(sumtarget){ sum-=left; left++; } else{ res.clear(); for(int k=left; k<=right; k++)res.push_back(k); vec.push_back(res); left++; right++; sum=0; for(int k=left; k<=right; k++)sum+=k; } } return vec; } };



JZ58 -I. 翻转单词顺序 最简单的方法拼接
方法二双指针:
算法解析:
倒序遍历字符串 ss ,记录单词左右索引边界 ii , jj ;
每确定一个单词的边界,则将其添加至单词列表 resres ;
最终,将单词列表拼接为字符串,并返回即可。
复杂度分析:
时间复杂度 O(N)O(N) : 其中 NN 为字符串 ss 的长度,线性遍历字符串。
空间复杂度 O(N)O(N) : 新建的 list(Python) 或 StringBuilder(Java) 中的字符串总长度 \leq N≤N ,占用 O(N)O(N) 大小的额外空间。
校招|剑指offer刷题记录 (下)g
文章图片



JZ58 - II. 左旋转字符串 方法一 字符串切片
方法二 列表遍历拼接
校招|剑指offer刷题记录 (下)g
文章图片



JZ59 - I. 滑动窗口的最大值
滑动窗口如图所示:

在上述滑动窗口形成及移动的过程中,我们注意到元素是从窗口的右侧进入的,然后由于窗口大小是固定的,因此多余的元素是从窗口左侧移除的。 一端进入,另一端移除,这不就是队列的性质吗?所以,该题目可以借助队列来求解。

题目要求是返回每个窗口中的最大值。那么这个如何解决呢?
我们以数组{5, 3, 4, 1},窗口大小k=3来进行说明。这里我们规定窗口右侧边界为right,左侧边界为left,其值为元素下标。
然后,开始遍历nums = {5, 3, 4, 1}。当right指向第一个元素5时,此时队列为空,将第一个元素5入队

△ 元素5入队
继续考察第二个元素3,此时3小于队列末尾的元素5,因此元素3入队,以便看其是否是下一个窗口的最大值。这时队列中元素从队首到队尾是递减的。


△ 元素3入队
接着考察{5, 3, 4, 1}中的第三个元素4,此时4大于队尾元素3,这表明元素3不可能是窗口「5 3 4」中的最大元素,因此需要将其从队列移除。但队首有元素5存在,因此不能将其从队首移除,那怎么办呢?我们可以将其从队尾移除。
对于这种一端既可以有元素入队,又有元素出队的队列,称之为双向队列。
队尾元素3出队之后,由于新的队尾元素5大于当前考察元素4,因此元素4入队,以便看其是否是下一个窗口的最大值。
当元素4入队之后,我们发现这时,队列中元素从队首到队尾依旧是递减的。我们把从队首到队尾单调递减或递增的队列称之为单调队列。

△ 元素3出队,元素4入队

接着,考察第四个元素1,此时元素1小于队尾元素4,因此元素1入队。


但这时,窗口内有4个元素,而我们这里规定窗口大小是3,因此,需要缩小窗口左边界left。
在缩小窗口左边界left后,意味着元素5已经不再窗口内了,因此,队首元素5需要出队。也就是说,当队首元素在原数组中的下标小于窗口左边界时,队首元素就需要移除队列。

△ 缩小左边界left,队首元素5出队
至此,该题目的求解思路就清晰了,具体如下:
遍历给定数组中的元素,如果队列不为空且当前考察元素大于等于队尾元素,则将队尾元素移除。直到,队列为空或当前考察元素小于新的队尾元素;
当队首元素的下标小于滑动窗口左侧边界left时,表示队首元素已经不再滑动窗口内,因此将其从队首移除。
由于数组下标从0开始,因此当窗口右边界right+1大于等于窗口大小k时,意味着窗口形成。此时,队首元素就是该窗口内的最大值。




JZ59 - II. 队列的最大值 普通暴力法
class MaxQueue { int q[20000]; int begin = 0, end = 0; public: MaxQueue() { }int max_value() { int ans = -1; for (int i = begin; i != end; ++i) ans = max(ans, q[i]); return ans; }void push_back(int value) { q[end++] = value; }int pop_front() { if (begin == end) return -1; return q[begin++]; } };




JZ60 n个骰子的点数和 校招|剑指offer刷题记录 (下)g
文章图片
校招|剑指offer刷题记录 (下)g
文章图片



校招|剑指offer刷题记录 (下)g
文章图片


解决方法:剑指 Offer 60. n 个骰子的点数(动态规划,清晰图解) - n个骰子的点数 - 力扣(LeetCode) (leetcode-cn.com)
校招|剑指offer刷题记录 (下)g
文章图片

代码如下
class Solution { public: vector dicesProbability(int n) { vector dp(6,1.0/6); for(int i=2; i<=n; i++){ vector tmp(5*i+1,0); for(int j=0; j


JZ61 扑克牌中的顺子 首先,牌不可能有重复,如果有重复,则不可能有顺子。
接下来,在没有大小王的情况下, 如果最大值减去最小值大于5,则不可能有顺子。
如果有大小王,事实上有无大小王不影响结果。
class Solution { public: bool IsContinuous(vector nums) { set numSet; int min=100,max=01; for(auto val : nums){ if(val==0)continue; if(numSet.find(val)==numSet.end()){ numSet.insert(val); } else return false; if(min>val)min=val; if(max


JZ63股票的最大利润 方法一:平均公式法
问题: 此计算必须使用 乘除法 ,因此本方法不可取,直接排除
public int sumNums(int n) { return (1 + n) * n / 2; }

方法二: 迭代
问题: 循环必须使用 whilewhile 或 forfor ,因此本方法不可取,直接排除。
public int sumNums(int n) { int res = 0; for(int i = 1; i <= n; i++) res += i; return res; }

方法三: 递归
问题: 终止条件需要使用 ifif ,因此本方法不可取。
思考: 除了 ifif 和 switchswitch 等判断语句外,是否有其他方法可用来终止递归?
class Solution { public: int sum=0; int sumNums(int n) { if(n==0)return 0; sum+=n; sumNums(n-1); return sum; } };


使用短路效应:
校招|剑指offer刷题记录 (下)g
文章图片



【校招|剑指offer刷题记录 (下)g】校招|剑指offer刷题记录 (下)g
文章图片

    推荐阅读