树
1.数据结构介绍
作为(单)链表的升级版,我们通常接触的树都是二叉树(binary tree),即每个节点最多有两个子节点;且除非题目说明,默认树中不存在循环结构。LeetCode 默认的树表示方法如下。
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x):val(x),left(NULL),right(NULL){}
};
可以看出,其与链表的主要差别就是多了一个子节点的指针。
2.树的递归
对于一些简单的递归题,某些 LeetCode 达人喜欢写 one-line code,即用一行代码解决问题,把 if-else 判断语句压缩成问号冒号的形式。我们也会展示一些这样的代码,但是对于新手仍然建议您使用 if-else 判断语句。
在很多时候,树递归的写法与深度优先搜索的递归写法相同。
题目一:求一个二叉树的最大深度
利用递归,可以很方便的求出最大深度。
int maxDepth(TreeNode* root)
{
return root? 1+max(maxDepth(root->left),maxDepth(root->right)):0;
}
题目二:判断一个二叉树是否平衡。树平衡的定义是,对于树上的任意节点,其两侧节点的最大深度
的差值不得大于 1。
解法类似于求树的最大深度,但有两个不同的地方:一是我们需要先处理子树的深度再进行比较,二是如果我们在处理子树时发现其已经不平衡了,则可以返回一个-1,使得所有其长辈节点可以避免多余的判断(本题的判断比较简单,做差后取绝对值即可;但如果此处是一个开销较大的比较过程,则避免重复判断可以节省大量的计算时间)。
//主函数
bool isBalanced(TreeNode* root)
{
return helper(root) != -1;
}
//辅函数
int helper(TreeNode* root)
{
if(!root)
{
return 0;
}
int left = helper(root->left),right = helper(root->right);
if(left == -1 || right == -1 || abs(left - right) > 1)
{
return -1;
}
return 1 + max(left,right);
}
题目三:求一个二叉树的最长直径。直径的定义是二叉树上任意两节点之间的无向距离。
同样的,我们可以利用递归来处理树。解题时要注意,在我们处理某个子树时,我们更新的最长直径值和递归返回的值是不同的。这是因为待更新的最长直径值是经过该子树根节点的最长直径(即两侧长度);而函数返回值是以该子树根节点为端点的最长直径值(即一侧长度),使用这样的返回值才可以通过递归更新父节点的最长直径值)。
//主函数
int diameterOfBinaryTree(TreeNode* root)
{
int diameter = 0;
helper(root,diameter);
return diameter;
}
//辅函数
int helper(TreeNode* node,int& diameter)
{
if(!node)
{
return 0;
}
int l = helper(node->left,diameter), r = helper(node->right,diameter);
diameter = max(l+r,diameter);
return max(l,r)=1;
}
题目四:给定一个整数二叉树,求有多少条路径节点值的和等于给定值。
递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。因此一个方便的方法是我们创建一个辅函数,专门用来计算连续加入节点的路径。
//主函数
int pathSum(TreeNode* root,int sum)
{
return root? pathSumStartWithRoot(root,sum) +
pathSum(root->left,sum) + pathSum(root->right,sum): 0;
}
//辅函数
int pathSumStartWithRoot(TreeNode* root,int sum)
{
if(!root)
{
return 0;
}
int count = root->val == sum? 1:0;
count += pathSumStartWithRoot(root->left,sum - root->val);
count += pathSumStartWithRoot(root->right,sum - root->val);
return count;
}
题目五:判断一个二叉树是否对称
判断一个树是否对称等价于判断左右子树是否对称。一般习惯将判断两个子树是否相等或对称类型的题的解法叫做“四步法”:(1)如果两个子树都为空指针,则它们相等或对称(2)如果两个子树只有一个为空指针,则它们不相等或不对称(3)如果两个子树根节点的值不相等,则它们不相等或不对称(4)根据相等或对称要求,进行递归处理。
//主函数
bool isSymmetric(TreeNode *root)
{
return root? isSymmetric(root->left,root->right):true;
}
//辅函数
bool isSymmetric(treeNode* left,TreeNode* right)
{
if(!left && !right)
{
return true;
}
if(!left || !right)
{
return false;
}
if(left->val != right->val)
{
return false;
}
return isSymmetric(left->left,right->right) && isSymmetric(left->right,right->left);
}
题目六:给定一个整数二叉树和一些整数,求删掉这些整数对应的节点后,剩余的子树。
例如:输入是一个整数二叉树和一个一维整数数组,输出一个数组,每个位置存储一个子树(的根节点)
这道题最主要需要注意的细节是如果通过递归处理原树,以及需要在什么时候断开指针。同时,为了便于寻找待删除节点,可以建立一个哈希表方便查找。
//主函数
vector<TreeNode*>delNodes(TreeNode* root,vector<int>&to_delete)
{
vector<TreeNode*>forest;
unordered_set<int>dict(to_delete.begin(),to_delete.end());
root = helper(root,dict,forest);
if(root)
{
forest.push_back(root);
}
return forest;
}
//辅函数
TreeNode* helper(TreeNode* root,unordered_set<int>&dict,vector<TreeNode*>&forest)
{
if(!root)
{
retrun root;
}
root->left = helper(root->left,dict,forest);
root->right = helper(root->right,dict,forest);
if(dict.count(root->val))
{
if(root->left)
{
forest.push_back(root->left);
}
if(root->right)
{
forest.push_back(root->right);
}
root = NULL;
}
return root;
}
3.层次遍历
我们可以使用广度优先搜索进行层次遍历。注意,不需要使用两个队列来分别存储当前层的节点和下一层的节点,因为在开始遍历一层的节点时,当前队列中的节点数就是当前层的节点数,只要控制遍历这么多节点数,就能保证这次遍历的都是当前层的节点。
题目一:给定一个二叉树,求每一层的节点值的平均数
利用广度搜索可以求取每层的平均值
vector<double>averageOfLevels(TreeNode* root)
{
vector<double>ans;
if(!root)
{
return ans;
}
queue<TreeNode*>q;
q.push(root);
while(!q.empty())
{
int count=q.size();
double sum=0;
for(int i=0;i<count;i++)
{
TreeNode* node = q.front();
q.pop();
sum += node->val;
if(node->left)
{
q.push(node->left);
}
if(node->right)
{
q.push(node->right);
}
}
ans.push_back(sum/count);
}
return ans;
}
4.前中后序遍历
前序遍历先遍历父结点,再遍历左结点,最后遍历右节点,我们得到的遍历顺序是 [1 2 4 5 3 6]。
void inorder(TreeNode* root)
{
visit(root);
preorder(root->left);
preorder(root->right);
}
中序遍历先遍历左节点,再遍历父结点,最后遍历右节点,我们得到的遍历顺序是 [4 2 5 1 6]。
void inorder(TreeNode* root)
{
inorder(root->left);
visit(root);
inorder(root->right);
}
后序遍历先遍历左节点,再遍历右结点,最后遍历父节点,我们得到的遍历顺序是 [4 5 2 6 3 1]。
void postorder(TreeNode* root)
{
postorder(root->left);
postorder(root->right);
visit(root);
}
题目一:给定一个二叉树的前序遍历和中序遍历结果,尝试复原这个树。已知树里不存在重复值的节点。
前序遍历的第一个节点是 4,意味着 4 是根节点。我们在中序遍历结果里找到 4 这个节点,根据中序遍历的性质可以得出,4 在中序遍历数组位置的左子数组为左子树,节点数为 1,对应的是前序排列数组里 4 之后的 1 个数字(9);4 在中序遍历数组位置的右子数组为右子树,节点数为 3,对应的是前序排列数组里最后的 3 个数字。有了这些信息,我们就可以对左子树和右子树进行递归复原了。为了方便查找数字的位置,我们可以用哈希表预处理中序遍历的结果。
//主函数
TreeNode* buildTree(vector<int>&preorder,vector<int>&inorder)
{
if(preorder.empty())
{
return nullptr;
}
unordered_map<int,int>hash;
for(int i=0;i<preorder.size();++i)
{
hash[inorder[i]]=i;
}
return buildTreeHelper(hash,preorder,0,preorder.size()-1,0);
}
//辅函数
TreeNode* buildTreeHelper(unordered_map<int,int>&hash,vector<int>&preorder,int s0,int e0,int s1)
{
if(s0>e0)
{
return nullptr;
}
int mid=preorder[s1],index=hash[mid],leftLen=index-s0-1;
TreeNode* node = new TreeNode(mid);
node->left=buildTreeHelper(hash,preorder,s0,index-1,s1+1);
node->right=buildTreeHelper(hash,preorder,index+1,e0,s1+2+leftLen);
return node;
}
题目二:不使用递归,实现二叉树的前序遍历
因为递归的本质是栈调用,因此我们可以通过栈来实现前序遍历。注意入栈的顺序。
vector<int>preorderTraversal(TreeNode* root)
{
vector<int>ret;
if(!root)
{
return ret;
}
stack<TreeNode*>s;
s.push(root);
while(!s.empty())
{
TreeNode* node=s.top();
s.pop();
ret.push_back(node->val);
if(node->right)
{
s.push(node->right); //先右后左,保证左子树先遍历
}
if(node->left)
{
s.push(node->left);
}
}
return ret;
}
5.二叉查找树
二叉查找树(Binary Search Tree, BST)是一种特殊的二叉树:对于每个父节点,其左子节点的值小于等于父结点的值,其右子节点的值大于等于父结点的值。因此对于一个二叉查找树,我们可以在 O(n log n) 的时间内查找一个值是否存在:从根节点开始,若当前节点的值大于查找值则向左下走,若当前节点的值小于查找值则向右下走。同时因为二叉查找树是有序的,对其中序遍历的结果即为排好序的数组。
一个二叉查找树的实现如下。
template<class T>
class BST
{
struct Node{
T data;
Node* left;
Node* right;
}
Node* root;
Node* makeEmpty(Node* t)
{
if(t==NULL) return NULL:
makeEmpty(t->left);
makeEmpty(t->right);
delete t;
return NULL;
}
Node* insert(Node* t,T x)
{
if(t==NULL)
{
t=new Node;
t->data=x;
t->left=t->right=NULL;
}
else if(x<t->data){
t->left=insert(t-left,x);
}
else if(x>t->data){
t->right=insert(t->right,x);
}
return t;
}
Node* find(Node* t,T x)
{
if(t==NULL) return NULL;;
if(x<t->data) return find(t->left,x);
if(x>t-data) return find(t->right,x);
return t;
}
Node* findMin(Node* t)
{
if(t==NULL || t->left==NULL) return t;
return findMin(t->left);
}
Node* findMax(Node* t)
{
if(t==NULL || t->right==NULL) return t;
return findMax(t->right);
}
Node* remove(Node* t,T x)
{
Node* temp;
if(t==NULL) return NULL;
else if(x<t->data) t->left=remove(t->left,x);
else if(x>t->data) t-right=remove(t->right,x);
else if(t->left && t->right){
temp=findMin(t->right);
t->data=temp->data;
t->right=remove(t->right,t->data);
}
else{
temp=t;
if(t->left==NULL) t=t->right;
else if(t->right==NULL) t=t->left;
delete temp;
}
return t;
}
public:
BST():root(NULL){}
~BST(){
root=makeEmpty(root);
}
void insert(T x)
{
insert(root,x);
}
void remove(T x)
{
remove(root,x);
}
};
题目一:给定一个二叉查找树,已知有两个节点被不小心交换了,试复原此树。
输入是一个被误交换两个节点的二叉查找树,输出是改正后的二叉查找树。
我们可以使用中序遍历这个二叉查找树,同时设置一个 prev 指针,记录当前节点中序遍历时的前节点。如果当前节点大于 prev 节点的值,说明需要调整次序。有一个技巧是如果遍历整个序列过程中只出现了一次次序错误,说明就是这两个相邻节点需要被交换;如果出现了两次次序错误,那就需要交换这两个节点。
//主函数
void recoverTree(TreeNode* root)
{
TreeNode *mistake1 = nullptr, *mistake2 = nullptr, *prev = nullptr;
inorder(root,mistake1,mistake2,prev);
if(mistake1 && mistake2)
{
int temp = mistake1->val;
mistake1->val = mistake2->val;
mistake2->val = temp;
}
}
//辅函数
void inorder(TreeNode* root,TreeNode*& mistake1,TreeNode*&mistake2,TreeNode*&prev)
{
if(!root)
{
return;
}
if(root->left)
{
inorder(root->left,mistake1,mistake2,prev);
}
if(prev&&root->val < prev->val)
{
if(!mistake1)
{
mistake1=prev;
mistake2=root;
}
else{
mistake2=root;
}
cout<<mistake1->val;
cout<<mistake2->val;
}
prev=root;
if(root->right)
{
inorder(root->right,mistake1,mistake2,prev);
}
}
题目二:给定一个二叉查找树和两个整数 L 和 R,且 L < R,试修剪此二叉查找树,使得修剪后所有
节点的值都在 [L, R] 的范围内。
利用二叉查找树的大小关系,我们可以很容易地利用递归进行树的处理。
TreeNode* trimBST(TreeNode* root,int L,int R)
{
if(!root){
return root;
}
if(root->val > R)
{
return trimBST(root->left,L,R);
}
if(root->val < L)
{
return trimBST(root->right,L,R);
}
root->left = trimBST(root->left,L,R);
root->right=trimBST(root->right,L,R);
return root;
}
6.字典树
字典树(Trie)用于判断字符串是否存在或者是否具有某种字符串前缀。
假如我们有一个储存了近万个单词的字典,即使我们使用哈希,在其中搜索一个单词的实际开销也是非常大的,且无法轻易支持搜索单词前缀。然而由于一个英文单词的长度 n 通常在 10 以内,如果我们使用字典树,则可以在 O(n)——近似 O(1)的时间内完成搜索,且额外开销非常小。
题目一:尝试建立一个字典树,支持快速插入单词、查找单词、查找单词前缀的功能。
以下是字典树的典型实现方法。
class TrieNode
{
public:
TrieNode* childNode[26];
bool isVal;
TrieNode(): isVal(false){
for(int i=0;i,26;++i)
{
childNode[i]=nullptr;
}
}
};
class Trie{
TrieNode* root;
public:
Trie(): root(new TrieNode()){}
//向字典插入一个词
void insert(string word)
{
TrieNode* temp=root;
for(int i=0;i<word.size();++i)
{
if(!temp->childNode[word[i]-'a']
{
temp->childNode[word[i]-'a']=new TrieNode();
}
temp=temp->childNode[word[i]-'a'];
}
temp->isVal=true;
}
//判断字典是否有一个词
bool search(string word)
{
TrieNode* temp = root;
for(int i=0;i<word.size();++i)
{
if(!temp)
{
break;
}
temp=temp->childNode[word[i]-'a'];
}
return temp? temp->isVal:false;
}
//判断字典树是否有一个以词开始的前缀
bool startWith(string prefix)
{
TrieNode* temp=root;
for(int i=0;i<prefix.size();++i)
{
if(!temp)
{
break;
}
temp=temp->childNode[prefix]-'a'];
}
return temp;
}
}