剑指offer
当数组作为 函数的参数进行传递时,数组就自动退化为 同类型的指针。
03. 数组中重复的数字
- 解法一:哈希表
- 解法二:原地置换
03.02 不修改数组找出重复的数字
采用类似于二分法的方式,拆成两个区间进行数字数目的判断。
int getDuplication(const int* numbers, int length)
{
if(numbers == nullptr || length <=0) return -1;
int start = 1;
int end = length - 1;
while(end >= start) {
int middle = ((end - start) >> 1) + start; //运算符>>代表二进制右移一位,效果就是“除以2”
int count = countRange(numbers, length, start, middle);
if (end == start) {
if (count > 1)
return start;
else
break;
}
if (count > (middle - start + 1))
//如果[start, middle]区间的实际数字数目count大于区间编号数middle-start+1,说明必然有重复数字。
//因此缩小区间。
end = middle;
else
//区间[middle, end]必然有重复数字
//注意:此时区间[start, middle]其实也可能有重复数字(按下不表)
start = middle +1;
}
return -1;
}
int countRange(const int* numbers, int length, int start, int end) {
if(numbers == nullptr)
return 0;
int count = 0;
for(int i = 0; i < length; i++) {
if (numbers[i] >= start && numbers[i] <= end)
++count;
}
return count;
}
04. 二维数组查找
选择左下角(右上角)开始,当不满足条件时可以排除一列or一行,以此迭代。
05. 替换空格
若是从前往后,则字符串靠后的字符都要依次向后移动两格,这样的时间复杂度是O(n^2)。
因此先遍历一次字符串检查空格数量并resize字符串,然后用两个指针分别标记原字符串的尾部和新字符串尾部,然后开始从尾部往前遍历,非空格的字符都赋值到新字符串中,空格则被替换为‘%’‘2’‘0’,调整指针位置,继续遍历,依此类推。
06. 从尾到头打印链表
- 解法一:反转法
- 解法二:栈
- 解法三:递归
- 解法四:改变链表结构
07. 重建二叉树
通过前序遍历找到“根结点”,再通过“根结点”将中序遍历分成“左子树”和“右子树”,递归此流程。
class Solution {
private:
vector<int> preorder;
unordered_map<int, int>dic; //哈希表,键值为数据,数值为中序的位置
TreeNode* recur(int root, int left, int right) {
if (left > right) //当左子树边界超过右子树边界
return nullptr;
TreeNode* node = new TreeNode(preorder[root]);
int i = dic[preorder[root]]; //找出某前序节点在中序的位置
node->left = recur(root + 1, left, i - 1); //前序继续推进,通过中序确定左子树区间
node->right = recur(root + i - left + 1, i + 1, right); //其中i-left+1是利用中序中确定右子树的相对位置,于是再加在root找到前序中的右子树位置。
return node;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
this->preorder = preorder;
for (int i = 0; i < inorder.size(); i++)
dic[inorder[i]] = i;
return recur(0, 0, inorder.size() - 1); //递归式子
}
};
08. 二叉树的下一个节点
分为三种情况:
- 如过一个节点有右子树,那么它的下一个节点就是它的右子树中最左子节点。
- 如果一个节点没有右子树,……
- 它是父节点的左节点,那么下一个节点就是父节点。
- 它是父节点的右节点,那么要沿着父节点一直向上遍历,直到找到一个节点(该节点是它父节点的左节点),那么这个节点的父节点就是我们下一个节点。
BinaryTreeNode* GetNext(BinaryTreeNode* pNode)
{
if (pNode == nullptr)
return nullptr;
BinaryTreeNode* pNext = nullptr;
if (pNode->m_pRight != nullptr) {
BinaryTreeNode* pRight = pNode->m_pRight;
while (pRight->m_pLeft != nullptr)
pRight = pRight->m_pLeft;
pNext = pRight;
}
else if (pNode->m_pParent != nullptr)
{
BinaryTreeNode* pCurrent = pNode;
BinaryTreeNode* pParent = pNode->m_pParent;
while (pParent != nullptr && pCurrent == pParent->m_pRight)
{
pCurrent = pParent;
pParent = pParent->m_pParent;
}
pNext = pParent;
}
return pNext;
}
09. 用两个栈实现队列
栈1用于push数据,当队列输出时,将栈1的数据push进栈2,然后在将栈2出栈,从而实现先进先出的规则。
10. 斐波那契数列
- 青蛙跳台阶问题同理
- 解法一:递归法
将f(n)问题的计算拆分为f(n-1)和f(n-2)两个子问题计算。但是缺点是会大量重复的递归计算,例如f(n)和f(n-1)两者向下递归需要各自计算f(n-2)的值。
- 如果用记忆化递归,在递归时存储f(0)至f(n)的数字值,重复遇到某数字则直接从数组取用,避免了重复的计算。
- 解法二:动态规划
由于第i项只与第i-1项、第i-2项有关,因此只需初始化三个整型变量sum
,a
,b
,利用辅助变量使得a
,b
两数字交替前进即可。这样就不需要用一个数组来存放前面项的值。
class Solution {
public:
int fib(int n) {
int f_n = 0, f_n1 = 1, f_n2 = 0;
if (n < 2)
return f_n = n;
for (int i = 2; i <= n; i++) {
f_n = (f_n1 + f_n2)%1000000007;
f_n2 = f_n1;
f_n1 = f_n;
}
return f_n;
}
};
11. 旋转数组的最小数字
将区间分为两个子(递增)区间,因此利用二分法算出index_m
,进而将num[index_m]
的值和num[index1]
、num[index2]
进行对比,从而将区间进一步缩小。
class Solution {
public:
int minArray(vector<int>& numbers) {
int index1 = 0, index2 = numbers.size() - 1;
while (index1 < index2) {
int index_m = (index1 + index2) / 2;
if (numbers[index_m] > numbers[index2])
index1 = index_m + 1;
else if (numbers[index_m] < numbers[index2])
index2 = index_m;
else {
int x = index1;
for(int k = index1 + 1; k < index2; k++) {
if(numbers[k] < numbers[x])
x = k;
}
return numbers[x];
}
}
return numbers[index1];
}
};
12. 矩阵中的路径
回溯算法可以看作暴力解法的升级版
- 它从解决问题每一步的所有可能选项里系统地选出一个可行的解决方案。
- 回溯法非常适合解决由多个步骤组成的问题,而且每个步骤都有多个选项。
- 回溯法一般用递归来实现。
class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
if (word.empty())
return false;
for(int i = 0; i < board.size(); i++) {
for(int j = 0; j < board[0].size(); j++) {
// 使用回溯法解题
if(dfs(board, word, i, j, 0))
return true;
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string& word, int i, int j, int k) {
if (i>=board.size() || i<0 || j>=board[0].size()
|| j<0 || board[i][j] != word[k]) // 如果索引越界或者值不匹配,返回false
return false;
if(k == word.size() - 1)
return true;
char temp = board[i][j];
board[i][j] = '\0'; // 将当前元素标记为'\0',即一个不可能出现在word里的元素,表明当前元素不可再参与比较
if (dfs(board, word, i+1, j, k+1) || dfs(board, word, i-1, j, k+1) ||
dfs(board, word, i, j+1, k+1) || dfs(board, word, i , j-1, k+1))
return true; // 当前元素的上下左右,如果有匹配到的,返回true
board[i][j] = temp; // 将当前元素恢复回其本身值
return false;
}
};
13. 机器人的运动范围
-
解法一:回溯算法
先判断机器人是否能进入(i,j)格;
如果能够进入(i,j)格,则再判断它能否进入4个相邻的格子;
即(i-1,j),(i+1,j),(i,j-1),(i,j+1) -
解法二:深度优先遍历DFS
一个方向(向下or向右)走到底再回溯
-
解法三:广度优先遍历BFS
“平推”方向向前搜索
其中由于每次机器人只能移动一格,因此一个思路是计算相邻两格的数位和增量。
【回溯算法】
class Solution {
public:
int movingCount(int rows, int cols, int threshold) {
if (threshold < 0 || rows <= 0 || cols <= 0)
return 0;
bool* visited = new bool[rows*cols];
for (int i = 0; i < rows*cols; ++i)
visited[i] = false;
int count = movingCountCore(threshold, rows, cols, 0, 0, visited);
delete[] visited;
return count;
}
int movingCountCore(int threshold, int rows, int cols, int row, int col, bool* visited) {
int count = 0;
if (check(threshold, rows, cols, row, col, visited)) {
visited[row*cols + col] = true;// 定位到即将进入的格子
//开始对格子四方的格子进行判断
count = 1 + movingCountCore(threshold, rows, cols, row-1, col, visited)
+ movingCountCore(threshold, rows, cols, row+1, col, visited)
+ movingCountCore(threshold, rows, cols, row, col-1, visited)
+ movingCountCore(threshold, rows, cols, row, col+1, visited);
}
return count;
}
//check函数:检查机器人能否进入下一个格子
bool check(int threshold, int rows, int cols, int row, int col, bool* visited) {
if (row >= 0 && row < rows && col >= 0 && col < cols
&& getDigitSum(row)+getDigitSum(col) <= threshold
&& !visited[row*cols + col])
return true;
return false;
}
//getDigitSum函数:记录一个数字的数位之和
int getDigitSum(int num) {
int sum = 0;
while (num > 0) {
sum += num % 10;
num /= 10;
}
return sum;
}
};
【DFS算法】
class Solution {
public:
int movingCount(int m, int n, int k) {
vector<vector<bool>> visited(m, vector<bool>(n, 0));
return dfs(0, 0, 0, 0, visited, m, n, k);
}
private:
int dfs(int i, int j, int si, int sj, vector<vector<bool>> &visited, int m, int n, int k) {
if(i >= m || j >= n || k < si + sj || visited[i][j]) return 0;
visited[i][j] = true;
return 1 + dfs(i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj, visited, m, n, k) +
dfs(i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8, visited, m, n, k);
}
};
【BFS算法】
class Solution {
public:
int movingCount(int m, int n, int k) {
vector<vector<bool>> visited(m, vector<bool>(n, 0));
int res = 0;
queue<vector<int>> que;
que.push({ 0, 0, 0, 0 });
while(que.size() > 0) {
vector<int> x = que.front();
que.pop();
int i = x[0], j = x[1], si = x[2], sj = x[3];
if(i >= m || j >= n || k < si + sj || visited[i][j]) continue;
visited[i][j] = true;
res++;
que.push({ i + 1, j, (i + 1) % 10 != 0 ? si + 1 : si - 8, sj });
que.push({ i, j + 1, si, (j + 1) % 10 != 0 ? sj + 1 : sj - 8 });
}
return res;
}
};
14. 剪绳子
- 解法一:动态规划
每一节绳子剪成两截之后,都是两个子问题。因此可以提前存储好短截绳子的数据,以避免之后的重复计算。 - 解法二:贪心算法
通过数学推导我们可以知道,3是最优长度,2是次优长度。
【1.动态规划】
class Solution {
public:
int cuttingRope(int n) {
if (n < 2)
return 0;
if (n == 2)
return 1;
if (n == 3)
return 2;
int* products = new int[n + 1];// 下面用于记录初始长度,便于后面计算
products[0] = 0;
products[1] = 1;
products[2] = 2;
products[3] = 3;
int max = 0;// 记录最大乘积
for (int i = 4; i <= n; ++i) {
max = 0;
for (int j = 1; j <= i/2; ++j) {
int product = products[j] * products[i-j];
if (max < product)
max = product;
products[i] = max;
}
}
max = products[n];
delete[] products;
return max;
}
};
【2.贪心算法】
class Solution {
public:
int cuttingRope(int n) {
if (n <= 3)
return n-1;
int TimesOf3 = n/3, left = n%3;
if (left == 0)
return (int)pow(3, TimesOf3);
if (left == 1)
return 4 * (int)pow(3, TimesOf3 - 1);
return 2 * (int)pow(3, TimesOf3);
}
};
15. 二进制中1的个数
-
解法一:将输入的二进制向右移位,逐位比较。
缺点:可能会引起死循环,当输入的二进制数时负数时,右移会使得移位后的最高位置为1;如果一直做右移运算,那么最终这个数字就会变成0xFFFFFFFF而陷入死循环。 -
解法二:输入的二进制不动,将数字1进行左移(即对00000001进行左移)
-
解法三:利用n & (n-1)
【解法一】
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
while (n) {
res += n & 1;
n >>= 1;
}
return res;
}
};
【解法二】
class Solution {
public:
int hammingWeight(uint32_t n) {
int count = 0;
unsigned int flag = 1;
while(flag) {
if (n & flag)
count++;
flag = flag << 1;
}
return count;
}
};
【解法三】
class Solution {
public:
int hammingWeight(uint32_t n) {
int res = 0;
while (n != 0) {
res++;
n &= n - 1;
}
return res;
}
};
25. 合并两个排序的链表
- 解法一:迭代法
- 1.设置伪头结点
dum
- 2.不断循环迭代至尾节点再做判断
- 解法二:递归法
- 1.先判断终止条件
- 2.递归部分
【1.迭代解法】
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode* dum = new ListNode(0);
ListNode* cur = dum;
while (l1 != nullptr && l2 != nullptr) { //直至一个链到了尾部
if (l1->val < l2->val) {
cur->next = l1;
l1 = l1->next;
}
else {
cur->next = l2;
l2 = l2->next;
}
cur = cur->next;
}
if (l1 != nullptr) cur->next = l1;
else cur->next = l2;
return dum->next;
}
};
【2.递归解法】
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
//递归解法
//1. 先判断终止条件
if (!l1 || !l2) //如果有一个链表到了尾节点
return !l1? l2 : l1; //如果是l1到底,则返回l2;否则是l2到底,返回l1
//2. 递归部分
if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
};
26. 树的子结构
解法:递归
建议:
- 感觉树的很多递归题,想清楚如何分解为子问题就好做了,函数内调用递归函数不要太纠结细节,清楚函数的功能就行
- 有时间自己还是到递归底层去看看,然后总结出递归规律方便下次用。比如这道题我的收获是:布尔类型返回值里面是递归的话,如果是或逻辑,则只要有一条路成功每层都会返回true;要想最终返回false只有所有层返回的都是false。这样可以方便以后做递归题快速构思
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSubStructure(TreeNode* A, TreeNode* B) {
//函数:进行先序遍历
return (A != NULL && B != NULL) &&
(recur(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B));
}
//函数:用于判断是否为子结构
bool recur(TreeNode* A, TreeNode* B) {
//终止条件
if (B == NULL) return true;
if (A == NULL || A->val != B->val) return false;
//递归部分
//A存在且A和B的值相等,则看子节点
return recur(A->left, B->left) && recur(A->right, B->right);
}
};
27. 二叉树的镜像
- 解法一:递归
- 自己的收获:1.递归终止条件 2.抽象出每个子问题的共同点,即做相同操作。
- 解法二:辅助栈
- 利用栈or队列遍历树的所有节点node,并交换每个node的左右子节点
- 1.特列处理:直接返回null
- 2.初始化:栈(或队列),加入根节点root
- 3.循环交换:当栈为空时跳出
- 出栈:记为node
- 添加子节点:将node左右子节点入栈
- 交换:交换node左右子节点
【1.递归】
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
//termination condition
if(!root) return nullptr;
//recursion
TreeNode* temp = root->left;
root->left = mirrorTree(root->right);
root->right = mirrorTree(temp);
return root;
}
};
【2.辅助栈】
class Solution {
public:
TreeNode* mirrorTree(TreeNode* root) {
if(!root) return nullptr; //如果root为空
stack<TreeNode*> stack;
stack.push(root); //节点入栈
while (!stack.empty()) {
TreeNode* node = stack.top(); //出栈节点记为node,
stack.pop();
//将node的左右节点入栈
if (node->left != nullptr) stack.push(node->left);
if (node->right != nullptr) stack.push(node->right);
//交换左右节点
TreeNode* temp = node->right;
node->right = node->left;
node->left = temp;
}
return root;
}
};
28. 对称的二叉树
- 解法一:递归
- 特殊处理:若根结点
root
为空,则直接返回true
- 返回值:
recur(root->left, root->right)
递归函数recur(L, R)
- 终止条件:
- 当L,R同时越过叶节点:此树从顶至底的节点都对称,返回true;
- 当L,R只有一个越过叶节点:此树不对称,返回false;
- 当L->val ≠ R->val:此树不对称,返回false;
- 递归工作:
class Solution {
public:
bool isSymmetric(TreeNode* root) {
return root == nullptr? true : recur(root->left, root->right);
}
//function to compare
bool recur(TreeNode* L, TreeNode* R) {
if (L == nullptr && R == nullptr) return true;
if (L == nullptr || R == nullptr || L->val != R->val) return false;
return recur(L->left, R->right) && recur(L->right, R->left);
}
};
29. 顺时针打印矩阵
- 空值处理:返回空列表
{}
- 初始化:设置上下左右四个边界,和用于打印结果的列表
res
- 循环打印:“从左向右,从上到下,从右到左,从下到上”四个方向循环。每个方向做下面三件事情
- 根据边界打印,将元素按顺序添加到列表
res
的尾部 - 边界向内收缩1(代表已被打印)
- 判断是否打印完毕(边界是否相遇),若打印完毕则跳出
- 返回
res
即可
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
if (matrix.empty()) return {};
vector<int> res;
int Left = 0; //左边界
int Right = matrix[0].size() - 1; //右边界
int Top = 0; //上边界
int Bottom = matrix.size() - 1; //下边界
while (true) {
//Left->Right
for (int i = Left; i <= Right; ++i)
res.push_back(matrix[Top][i]);
if (++Top > Bottom) break;
//Top->Bottom
for (int i = Top; i <= Bottom; ++i)
res.push_back(matrix[i][Right]);
if (--Right < Left) break;
//Right->Left
for (int i = Right; i >= Left; --i)
res.push_back(matrix[Bottom][i]);
if (--Bottom < Top) break;
//Bottom->Top
for (int i = Bottom; i >= Top; --i)
res.push_back(matrix[i][Left]);
if (++Left > Right) break;
}
return res;
}
};
30. 包含min函数的栈
借用辅助栈
- 数据栈:用于存储所有元素,保证函数
push
pop
top
的正常逻辑 - 辅助栈:存储数据栈中所有非严格降序的元素,数据栈中的最小元素始终对应辅助栈中的栈顶元素,即
min()
函数只需返回辅助栈的栈顶元素即可
class MinStack {
public:
stack<int> A, B;
void push(int x) {
A.push(x);
if (B.empty() || x <= B.top()) //避免有重复的最小值
B.push(x);
}
void pop() {
if (B.top() == A.top())
B.pop();
A.pop();
}
int top() {
return A.top();
}
int min() {
return B.top();
}
};
31. 栈的压入/弹出序列
- 将[压入序列]不断和[弹出队列]比较,遇到相同的则[压入序列]的元素出栈;不等则继续执行[压入序列]的进栈操作
- 简言之,就是循环拿[弹出队列]去遍历[压入序列]的栈顶元素。
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {
stack<int> mystack;
int i = 0;
for (int num:pushed) {
mystack.push(num); // num入栈
while (!mystack.empty() && mystack.top() == popped[i]) {
// 循环跟出栈序列比较,如果对上了出栈序列的数则进入如判断
mystack.pop();// 对上了则入栈序列的该数可以剔除
i++;// 然后和出栈序列比较下一个数字
}
}
return mystack.empty();// 全部对上了则栈会为空,否则说明没有对上。
}
};
32. 从上到下打印二叉树
【1.从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印】
class Solution {
public:
vector<int> levelOrder(TreeNode* root) {
// 实现层序遍历
vector<int> ans;
if (!root) return ans;
queue<TreeNode*> myqueue;
myqueue.push(root);
while (myqueue.size()) {
TreeNode* node = myqueue.front();
myqueue.pop();
ans.push_back(node->val);
if(node->left) myqueue.push(node->left);// 如果有左子节点,则加入队列
if(node->right) myqueue.push(node->right);// 如果有右子节点,则加入队列
}
return ans;
}
};
【2.每一层打印到一行】
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
if (!root) return ans;
queue<TreeNode*> Myq;
Myq.push(root);
while (!Myq.empty()) {
vector<int> temp;
for (int i = Myq.size(); i>0; i--) {
// i初始化为队列的长度(而不是0),避免循环中队列的长度发生变化
TreeNode* node = Myq.front();
Myq.pop();
temp.push_back(node->val);
if (node->left) Myq.push(node->left);// 添加左右子节点时,此时队列可能发生变化
if (node->right) Myq.push(node->right);
}
ans.push_back(temp);
}
return ans;
}
};
【3.打印顺序交替变化】
方法:层序遍历+双端队列(奇偶层逻辑分离)
- 奇数层,则添加至队列尾部
- 偶数层,则添加至队列头部
时间复杂度 O(N):N为二叉树的节点数量,即BFS需要循环N次,占用O(N);双端队列的队首和队尾的添加和删除操作的时间复杂度均为O(1);
空间复杂度 O(N):最差的情况下树是满二叉树时,最多有N/2个树节点同时在deque中,使用O(N)大小的额外空间。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> ans;
if (!root) return ans;
deque<TreeNode*> Myq;
Myq.push_front(root);
while (!Myq.empty()) {
vector<int> temp;
for (int i = Myq.size(); i>0 ;i--) {
TreeNode* node = Myq.front();
Myq.pop_front();
temp.push_back(node->val);
if (node->left) Myq.push_back(node->left);
if (node->right) Myq.push_back(node->right);
}
ans.push_back(temp);
if (Myq.empty()) break;// 如果此时已经空了,就提前跳出
temp.clear();
for (int i = Myq.size(); i>0 ;i--) {
TreeNode* node = Myq.back();
Myq.pop_back();
temp.push_back(node->val);
if (node->right) Myq.push_front(node->right);
if (node->left) Myq.push_front(node->left);
}
ans.push_back(temp);
}
return ans;
}
};
33. 二叉搜索树的后序遍历序列
后序遍历:左右根
二叉搜索树:左子树所有节点<根结点;右子树所有节点>根结点
- 解法一:递归分治
判断所有子树的正确性
- 终止条件:当遍历起点i超过终点j,说明此子树节点数量≤1,无需判断正确性,因此直接true;
- 递推工作:
- 划分左右子树:遍历后序遍历的[i ,j]区间元素,寻找第一个大于根节点标记为
mid
。可划分出左子树区间[i, mid-1]、右子树区间[mid, j-1]、根节点索引j。 - 判断是否为二叉搜索树:
- 左子树区间[i, mid-1]内的所有节点都 <
postorder[j]
。这一步骤在第一步划分左右子树时已经保证了左子树区间的正确性,因此只需要判断右子树区间的正确性就行。 - 右子树区间[mid, j-1]内的所有节点都 >
postorder[j]
。遇到≤postorder[j]
的节点则跳出;然后通过p=j
是否为二叉搜索树(如果此时哨兵p不是根结点j,返回false)
- 左子树区间[i, mid-1]内的所有节点都 <
- 划分左右子树:遍历后序遍历的[i ,j]区间元素,寻找第一个大于根节点标记为
- 返回值:所有字数都需正确才可判定正确,因此使用与逻辑符&&连接。
- p=j 判断此树是否正确
- recur(i ,m-1) 判断此树的左子树
- recur(m ,j-1) 判断此树的右子树
时间复杂度O(N2):每次调用recur(i,j)减去一个根结点,因此递归占用O(N);最差情况下,树退化成链表,每轮递归都需要遍历树的所有节点,占用O(N)
空间复杂度O(N):最差情况下,树退化成链表,递归深度达到N
- 解法二:辅助单调栈
后序遍历倒序,即:根->右->左
见题解
时间复杂度O(N):遍历postorder所有节点,各节点均入栈/出栈一次,使用O(N)时间。
空间复杂度O(N):最差情况下,树退化成链表,单调栈stack存储所有节点,使用*O(N)*额外空间。
【1.递归】
class Solution {
public:
bool verifyPostorder(vector<int>& postorder) {
return recur(postorder, 0, postorder.size() - 1);
}
bool recur(vector<int>& postorder, int start, int end) {
if (start >= end) return true;// 当起始点i超过终点j,意味着遍历结束
int pointer = start;// 初始化哨兵pointer
while (postorder[pointer] < postorder[end]) pointer++;
int mid = pointer;
while (postorder[pointer] > postorder[end]) pointer++;
return pointer == end && recur(postorder, start, mid - 1) && recur(postorder, mid, end -1);
}
};
// 由于是二叉搜索树的后序遍历,因此根结点一定是序列中的最后一位,即postorder[j]
【2.辅助栈】
class Solution {
public:
bool verifyPostorder(vector<int>& postorder) {
stack<int> MyStack;
int root = INT_MAX;// 初试根结点为正无穷大,把树的根看成是这个初试结点的左孩子
for (int i = postorder.size() - 1; i >= 0; i--) {// 后序遍历的倒序:根->右->左
if (postorder[i] > root) return false;
while(!MyStack.empty() && MyStack.top() > postorder[i]) {
// 找一个大于postorder[i]但同时差距最小的,就是postorder[i]的父结点
root = MyStack.top();
MyStack.pop();
}
MyStack.push(postorder[i]);
}
return true;
}
};
34. 二叉树中和为某一值的路径
- 先序遍历:由于是从根结点出发,所以选定先序遍历(根->左->右)
- 路径记录:在前序遍历中,记录从根节点到当前节点的路径。如果满足下面两点则将此路径加入到结果列表
- 根节点到叶节点形成的路径
- 各节点值的和等于目标值
- 函数
path(root, sum)
- 初始化:结果列表
res
,路径列表path
- 返回值:
res
- 函数
recur(root, tar)
- 递推参数:当前节点
root
,当前目标值tar
- 终止条件:若节点
root
为空,则直接返回 - 递推工作:
- 路径更新:将当前节点值
root->val
加入到路径path
- 目标值更新:
tar
=tar
-root->val
(即目标值从sum减到0) - 路径记录:当
root
为叶节点且路径和等于目标值,此path
加入res
- 先序遍历:递归左/右子节点
- 路径恢复:向上回溯前,需要将当前节点从路径
path
中删除,即执行path.pop()
- 路径更新:将当前节点值
时间复杂度O(N):N为二叉树的节点数,先序遍历需要遍历所有节点
空间复杂度O(N):最差情况下,树退化成链表,path
存储所有节点,使用*O(N)*额外空间。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> pathSum(TreeNode* root, int target) {
recur(root, target);
return res;
}
void recur(TreeNode* root, int tar) {
if (!root) return;
path.push_back(root->val);
tar -= root->val;
if (tar==0 && !root->left && !root->right)
res.push_back(path);
recur(root->left, tar);
recur(root->right, tar);
path.pop_back();
}
};
35.