面試複習-------演算法與資料結構------二叉樹
建立二叉樹
(1)二叉樹的反序列化
給定“6423####51##7##”(先序)這種序列,構造二叉樹(假設只出現0~9數字,如果要擴充套件可以加空格)
(2)前序+中序重建二叉樹(劍指offer6)TreeNode* preOrderToTree(string input,int& index) { if(input.length() == 0)return NULL; if(input.at(index) == '#') { index++; return NULL; } TreeNode* Node = new TreeNode(); Node->val = input.at(index) - '0'; index++; Node->left = preOrderToTree(input, index); Node->right = preOrderToTree(input, index); return Node; }
首先preOrder[begin]為根節點root;
每次在中序序列中找preOrder[begin],然後可以將中序分成兩個子序列,左邊一部分遞迴構造root->left;右邊一部分構造root->right;
TreeNode* TreeBuild(vector<int> preOrder, vector<int> middleOrder, int preBegin, int preEnd, int middleBegin,int middleEnd) { if(preOrder.size() == 0 || middleOrder.size() == 0 || middleOrder.size() != preOrder.size())return NULL; int rootValue = preOrder.at(preBegin); TreeNode* root = new TreeNode(); root->val = rootValue; root->left = root-> right = NULL; if(preBegin == preEnd){ if(middleBegin == middleEnd && preOrder.at(preBegin) == middleOrder.at(middleBegin)) return root; else throw std::exception("invalid input"); //return NULL; } //在中序遍歷中找rootValue int index = middleBegin; while(index <= middleEnd && middleOrder.at(index) != rootValue)++index; if(index == middleEnd && middleOrder.at(index) != rootValue) throw std::exception("invalid input"); //return NULL; int leftLength = index - middleBegin; int rightLength = middleEnd - index; if(leftLength > 0) root->left = TreeBuild(preOrder, middleOrder,preBegin+1, preBegin + leftLength, middleBegin, index-1); if(rightLength > 0) root->right = TreeBuild(preOrder, middleOrder,preBegin + leftLength + 1, preEnd, index + 1, middleEnd); return root; }
二叉樹的遍歷
要活用三種遍歷的方式:
當問題中需要根左右、左根右、左右根的順序對節點進行判斷的時候,要能夠立馬想到這三種遍歷方式。
(1)二叉樹三種遍歷的遞迴版本
(2)二叉樹的三種遍歷非遞迴實現void preOrder(TreeNode* Tree) { if(Tree == NULL)return; cout<<Tree->val; preOrder(Tree->left); preOrder(Tree->right); } void middleOrder(TreeNode* Tree) { if(Tree == NULL)return; middleOrder(Tree->left); cout << Tree->val; middleOrder(Tree->right); } void backOrder(TreeNode* Tree) { if(Tree == NULL) return ; backOrder(Tree->left); backOrder(Tree->right); cout << Tree->val; }
非遞迴主要是利用棧的特性來實現。
//迭代的先序遍歷的思想就是每次都向左下遍歷,當訪問到一個節點時,先
//將該節點的值列印,然後將該節點入棧,一直到為空
//下一步就是取出棧頂,該點是已經遍歷過了的,所以只需要取其右子節點
//繼續向左下遍歷即可
void preOrder(TreeNode* Tree)
{
if(Tree == NULL)return;
stack<TreeNode*> s;
TreeNode* pCur = Tree;
while(pCur != NULL || !s.empty()){
//首先遍歷到最左下節點
if(pCur != NULL){
cout << pCur->val;
s.push(pCur);
pCur = pCur->left;
}else{ //當左下為空的時候,回到上一個元素,繼續不停向左下遍歷
pCur = s.top()->right;
s.pop();
}
}
}
//迭代的中序遍歷與先序類似,不同的地方是,先入棧,出棧的時候再輸出
void middleOrder(TreeNode* Tree)
{
if(Tree == NULL)return;
stack<TreeNode*> s;
TreeNode* pCur = Tree;
while(pCur != NULL || !s.empty()){
if(pCur != NULL){
s.push(pCur);
pCur = pCur->left;
}else{
pCur = s.top();
cout << pCur->val;
s.pop();
pCur = pCur->right;
}
}
}
//迭代的後序遍歷與之前的稍有不同
//採用的技巧是將孩子節點入棧後,對節點執行左右置空的操作
//只有遇到空節點才輸出,此時保證了該節點的左右孩子節點已經被訪問到
void backOrder(TreeNode* Tree)
{
if(Tree == NULL)return;
stack<TreeNode*> s;
s.push(Tree);
TreeNode* pCur = NULL;
while(!s.empty()){
pCur = s.top();
if(pCur->left == NULL && pCur->right == NULL){
cout << pCur->val;
s.pop();
}else{
//先右後左入棧才能保障先左後右出棧
if(pCur->right != NULL){
s.push(pCur->right);
pCur->right = NULL;
}
if(pCur->left != NULL){
s.push(pCur->left);
pCur->left = NULL;
}
}
}
}
(3)二叉樹的層次遍歷
主要利用佇列的先入先出特性
void levelOrder(TreeNode* Tree)
{
if(Tree == NULL)return;
queue<TreeNode*> q;
q.push(Tree);
TreeNode* pCur;
while(!q.empty()){
pCur = q.front();
q.pop();
cout << pCur->val;
if(pCur->left != NULL)
q.push(pCur->left);
if(pCur->right != NULL)
q.push(pCur->right);
}
}
變形:按行列印二叉樹(劍指offer 60)
思路:維護兩個變數,pLast表示當前層最右邊的節點,pNextLast表示下一層最右邊的節點
void levelOrderByline(TreeNode* Tree)
{
if(Tree == NULL)return;
queue<TreeNode*> q;
q.push(Tree);
TreeNode* pLast = Tree;
TreeNode* pNextLast = NULL;
while(!q.empty()){
TreeNode* pCur = q.front();
q.pop();
cout << pCur->val;
if(pCur->left != NULL)
{
q.push(pCur->left);
pNextLast = pCur->left;
}
if(pCur->right != NULL)
{
q.push(pCur->right);
pNextLast = pCur->right;
}
if(pCur == pLast){
cout << endl;
pLast = pNextLast;
pNextLast = NULL;
}
}
}
其他的一些演算法題
(1)求二叉樹葉子節點個數
左子樹葉子節點個數+右子樹葉子節點個數;用遞迴實現
(2)求二叉樹高度(劍指offer39)
int getHeight(TreeNode* Tree)
{
if(Tree == NULL)return 0;
int left = getHeight(Tree->left);
int right = getHeight(Tree->right);
return 1 + max(left, right);
}
變形:判斷一棵樹是不是平衡二叉樹
可以仿照上述做法,求出左右子樹的高度然後進行判斷,但是這個時候一個節點可能會遍歷多次。
較好的做法:利用後序遍歷最後遍歷根節點的特性,記錄一個深度資訊
bool isBalance(TreeNode* Tree, int& depth)
{
if(Tree == NULL){
depth = 0;
return true;
}
int left,right; //用來記錄高度資訊
if(isBalance(Tree->left, left) && isBalance(Tree->right, right)){
if(abs(left - right) <= 1){
depth = 1 + max(left, right);
return true;
}
}
depth = 1 + max(left, right);
return false;
}
(3)二叉樹的映象(劍指offer 19)
解題思路:先序遍歷並且交換左右子樹,對左右子樹執行映象操作。
void mirror_tree(TreeNode* T)
{
if(T==NULL)
return;
if((T->left==NULL) && (T->right==NULL))
return;
TreeNode* temp = T->left;
T->left=T->right;
T->right=temp;
mirror_tree(T->left);
mirror_tree(T->right);
}
(4)樹的子結構判斷(劍指offer 18)
給pRoot1和pRoot2,判斷pRoot2是不是pRoot1子結構。
第一步:遍歷pRoot1中的每個節點,找pRoot2->val;(遞迴的先序遍歷即可完成)
第二步:判斷以該節點為根的子樹是不是和pRoot2一樣。
bool has_tree(TreeNode* T1,TreeNode* T2)
{
if(T2==NULL)
return true;
if(T1==NULL)
return false;
if(T1->value!=T2->value)
return false;
return has_tree(T1->left,T2->left)&&has_tree(T1->right,T2->right);
}
bool has_sub(TreeNode* T1,TreeNode* T2)
{
bool result=false;
if(T1!=NULL && T2!=NULL)
{
if(T1->value==T2->value)
result=has_tree(T1,T2);
if(!result)
result=has_sub(T1->left,T2);
if(!result)
result=has_sub(T1->right,T2);
}
return result;
}
(5)尋找二叉樹中和為某一值的路徑(劍指offer25)
解題思路:儲存當前已有路徑,當迴圈到根節點時,進行判斷
void findPath(TreeNode* Tree, vector<TreeNode*>& nodes, int num, int target)
{
if(Tree == NULL)return;
num += Tree->val;
nodes.push_back(Tree);
if(Tree->left == NULL && Tree->right == NULL){
if(num == target){
for(auto i : nodes)cout<<i->val;
cout << endl;
}
}
else{
if(Tree->left != NULL){
findPath(Tree->left, nodes, num, target);
}
if(Tree->right != NULL){
findPath(Tree->right, nodes, num, target);
}
}
nodes.pop_back();
}
(6)序列判斷
判斷某一序列是不是二叉搜尋樹的後序遍歷序列(劍指offer24)
思路:對某一個節點:①尋找前面全部小於根值的一部分為左子樹序列,判斷剩餘一部分是不是都大於根值②判斷左子樹序列是不是二叉搜尋樹③判斷右子樹序列是不是二叉搜尋樹
bool back_tree(int* arr,int length)
{
if(arr==NULL||length<=0)
return false;
int root=arr[length-1];
int i=0;
while(arr[i]<root && i<length-1)
i++;
int j=i;
for(;j<length-1;j++)
{
if(arr[j]<root)
return false;
}
bool left=true;
if(i>0)
left=back_tree(arr,i);
bool right=true;
if(i<length-1)
right=back_tree(arr+i,length-i-1);
return (left&&right);
}
(7)二叉搜尋樹轉換成雙向連結串列(劍指offer27)
//找樹的最左節點,即轉換之後連結串列的頭結點
TreeNode* find_most_left(TreeNode* T)
{
if(T==NULL)
return NULL;
TreeNode* pCur = T;
while(pCur->left!=NULL)
pCur = pCur->left;
return pCur;
}
//進行整棵樹的轉換
TreeNode* convert(TreeNode* T)
{
TreeNode* phead=find_most_left(T);
TreeNode* last_node=NULL;
convert_node(T,&last_node);
return phead;
}
//進行單個節點的轉換
void convert_node(TreeNode* T,TreeNode** lastnode)
{
if(T==NULL)
return;
if(T->left!=NULL)
convert_node(T->left,lastnode);
T->left=*lastnode;
if(*lastnode!=NULL)
(*lastnode)->right=T;
*lastnode=T;
if(T->right!=NULL)
convert_node(T->right,lastnode);
}
類似的:二叉樹轉換成單鏈表(leetcode 114)
思路:採用一種類似於後序遍歷的方式,先處理右子節點,再處理左子節點最後處理根節點,用一個指標記錄之前處理的節點,代表當前節點應該銜接哪一個節點
public:
//採用一種偽後序遍歷的方式
void flatten(TreeNode* root) {
if(root == NULL)return;
flatten(root->right);
flatten(root->left);
root->left = NULL;
root->right = pPre;
pPre = root;
}
private:
TreeNode* pPre = NULL;
(8)樹的兩節點最低公共祖先問題(劍指offer50)
①二叉搜尋樹
只需要判斷節點的值,如果給的兩節點val同時小於(或者大於)根節點val,那麼公共節點一定在左子樹(或者右子樹)中,直到找到第一個節點值位於兩個給定節點值之間
TreeNode* findParent(TreeNode* Tree, TreeNode* Node1, TreeNode* Node2)
{
if(Tree == NULL)return NULL;
if(Tree->val > Node1->val && Tree->val > Node2->val)
return findParent(Tree->left, Node1, Node2);
if(Tree->val < Node1->val && Tree->val < Node2->val)
return findParent(Tree->right, Node1, Node2);
return Tree;
}
②普通二叉樹
也可以採用遞迴的形式判斷是否在左右子樹中,不過開銷很大。
高效的做法:先求出根節點到兩個節點的路徑,進而轉換成求兩條路徑的公共節點(類似於求兩條單鏈表的第一個公共節點)
bool FindNodePath(TreeNode* T,vector<TreeNode*>& path,int nodeKey)
{
path.push_back(T);
if(T->value==nodeKey)
return true;
bool found = false;
if(T->left!=NULL)
found=FindNodePath(T->left,path,nodeKey);
if(!found && T->right!=NULL)
found=FindNodePath(T->right,path,nodeKey);
if(!found)
path.pop_back();
return found;
}
TreeNode* GetLastCommonNode(vector<TreeNode*>& path1,vector<TreeNode*>& path2)
{
vector<TreeNode*>::iterator it1=path1.begin();
vector<TreeNode*>::iterator it2=path2.begin();
TreeNode* result=NULL;
while(it1!=path1.end() && it2!=path2.end())
{
if(*it1==*it2)
result=*it1;
it1++;
it2++;
}
return result;
}
(9)二叉樹中節點的最大距離解題思路:最大距離為三種情況:①左子樹中最大距離②右子樹中最大距離③左子樹到根最大+右子樹到根最大
int FarestNode(TreeNode* Tree, int& leftMax, int& rightMax)
{
if(Tree == NULL){
leftMax = 0;
rightMax = 0;
return 0;
}
int maxLL,maxLR,maxRL,maxRR;
int farestLeft,farestRight; //左右子樹的最大距離
if(Tree->left != NULL){
farestLeft = FarestNode(Tree->left, maxLL, maxLR);
leftMax = max(maxLL, maxLR) + 1;
}else{
farestLeft = 0;
leftMax = 0;
}
if(Tree->right != NULL){
farestRight = FarestNode(Tree->right, maxRL, maxRR);
rightMax = max(maxRL, maxRR) + 1;
}else{
farestRight = 0;
rightMax = 0;
}
return max(max(farestLeft, farestRight), leftMax + rightMax);
}
注:求深度的時候,通常將depth資訊作為引數進行遞迴,這個時候引數必須是引用傳參!!(10)判斷一棵樹是否是完全二叉樹
主要是利用廣度優先搜尋,如果遍歷到了空節點那麼,之後應該都為空節點,否則就不是完全二叉樹
bool isCompleteBinaryTree(TreeNode* Tree)
{
if(Tree == NULL)return false;
queue<TreeNode*> q;
q.push(Tree);
TreeNode* pCur;
//與正常的層次遍歷不同的是如果左右節點為空節點,也將其放入到佇列中
while((pCur = q.front()) != NULL){
q.push(pCur->left);
q.push(pCur->right);
q.pop();
}
//此時已經遍歷到空節點,按照完全二叉樹的定義,後面的節點應該都是空節點
while(!q.empty()){
pCur = q.front();
q.pop();
if(pCur != NULL)return false;
}
return true;
}
(11)判斷一顆二叉樹是否是對稱的
解題思路:定義一種新的遍歷序列(先右子樹後左子樹,然後看它與二叉樹的前序遍歷結果是否一樣)
bool func(TreeNode* left,TreeNode* right)
{
if(left==NULL&&right==NULL)
return true;
if(left==NULL||right==NULL)
return false;
if(left->value==right->value)
{
return func(left->left,right->right)&&func(left->right,right->left);
}
return false;
}
(12)二叉搜尋樹中的第k個節點
TreeNode* KthNodeCore(TreeNode* T,int& k)
{
TreeNode* result= NULL;
if(T->left!=NULL)
result=KthNodeCore(T->left,k);
if(result==NULL)
{
if(k==1)
result=T;
--k;
}
if(result==NULL&&T->right!=NULL)
result = KthNodeCore(T->right,k);
return result;
}
(13)給正整數n,用1,2,3....n構建所有可能的二叉搜尋樹(leetcode 95)
解題思路:利用遞迴進行求解
vector<TreeNode*> generateTrees(int n) {
if(n < 1)return vector<TreeNode*>();
return generateTreesCore(1, n);
}
vector<TreeNode*> generateTreesCore(int start, int end)
{
vector<TreeNode*> result;
if(start > end){
result.push_back(NULL);
return result;
}
if(start == end){
result.push_back(new TreeNode(start));
return result;
}
vector<TreeNode*> left,right;
for(int i = start; i <= end; ++i){
left = generateTreesCore(start, i-1);
right = generateTreesCore(i+1, end);
for(auto lNode: left){
for(auto rNode: right){
TreeNode* root = new TreeNode(i);
root->left = lNode;
root->right = rNode;
result.push_back(root);
}
}
}
return result;
}
(14)之字形列印二叉樹(劍指offer 61)
解題思路:利用兩個棧進行層次遍歷,一個棧負責先左後右入棧,一個棧負責先右後左入棧
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
vector<vector<int>>result;
if(root == NULL)return result;
stack<TreeNode*> s[2];
int currentStack = 0;
s[currentStack].push(root);
vector<int> line;
while(!s[0].empty() || !s[1].empty()){
TreeNode* pCurrent = s[currentStack].top();
s[currentStack].pop();
line.push_back(pCurrent->val);
if(currentStack == 0){ //如果是第一個棧就從左往右入棧,這樣就能保證從右往左出棧
if(pCurrent -> left != NULL)s[1 - currentStack].push(pCurrent->left);
if(pCurrent -> right != NULL)s[1 - currentStack].push(pCurrent->right);
}
if(currentStack == 1){ //如果是第二個棧就從右往左入棧,這樣就能保證從左往右出棧
if(pCurrent -> right != NULL)s[1 - currentStack].push(pCurrent->right);
if(pCurrent -> left != NULL)s[1 - currentStack].push(pCurrent->left);
}
if(s[currentStack].empty()){
currentStack = 1 - currentStack;
result.push_back(line);
line.clear();
}
}
return result;
}