1. 程式人生 > >面試複習-------演算法與資料結構------二叉樹

面試複習-------演算法與資料結構------二叉樹

建立二叉樹

(1)二叉樹的反序列化

給定“6423####51##7##”(先序)這種序列,構造二叉樹(假設只出現0~9數字,如果要擴充套件可以加空格)

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;
}
(2)前序+中序重建二叉樹(劍指offer6

首先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)二叉樹三種遍歷的遞迴版本

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;
}
(2)二叉樹的三種遍歷非遞迴實現

非遞迴主要是利用棧的特性來實現。

//迭代的先序遍歷的思想就是每次都向左下遍歷,當訪問到一個節點時,先
//將該節點的值列印,然後將該節點入棧,一直到為空
//下一步就是取出棧頂,該點是已經遍歷過了的,所以只需要取其右子節點
//繼續向左下遍歷即可
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;
    }