1. 程式人生 > >【資料結構】有關二叉樹的面試題

【資料結構】有關二叉樹的面試題

【摘要】電腦科學中,二叉樹是每個節點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。二叉樹常被用於實現二叉查詢樹和二叉堆。二叉樹是遞迴定義的,因此,與二叉樹有關的題目基本都可以用遞迴思想解決,當然有些題目非遞迴解法也應該掌握,如非遞迴遍歷節點等等。本文努力對二叉樹相關題目做一個較全的整理總結,希望對找工作的同學有所幫助。

本文內容如下所示 
1. 求二叉樹中的節點個數 
2. 求二叉樹的深度 
3. 前序遍歷,中序遍歷,後序遍歷 
4. 分層遍歷二叉樹(按層次從上往下,從左往右) 
5. 將二叉查詢樹變為有序的雙向連結串列 
6. 求二叉樹第K層的節點個數 
7. 求二叉樹中葉子節點的個數 
8. 判斷兩棵二叉樹是否結構相同 
9. 判斷二叉樹是不是平衡二叉樹 
10. 求二叉樹的映象 
11. 求二叉樹中兩個節點的最低公共祖先節點 
12. 求二叉樹中節點的最大距離 
13. 由前序遍歷序列和中序遍歷序列重建二叉樹 
14. 判斷二叉樹是不是完全二叉樹

15.判斷一棵樹是否為另一棵樹的子樹

二叉樹節點定義

struct BinaryTreeNode
{
    int m_nValue;
    BinaryTreeNode* m_pLeft;
    BinaryTreeNode* m_pRight;
};

1、求二叉樹中的節點個數

遞迴解法: 
(1)如果二叉樹為空,節點個數為0 
(2)如果二叉樹不為空,二叉樹節點個數 = 左子樹節點個數 + 右子樹節點個數 + 1 
參考程式碼如下:

int GetNodeNum(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL) // 遞迴出口
        return 0;
    return GetNodeNum(pRoot->m_pLeft) + GetNodeNum(pRoot->m_pRight) + 1;
}

2、求二叉樹的深度

遞迴解法: 
(1)如果二叉樹為空,二叉樹的深度為0 
(2)如果二叉樹不為空,二叉樹的深度 = max(左子樹深度, 右子樹深度) + 1 
參考程式碼如下:

int GetDepth(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL) // 遞迴出口
        return 0;
    int depthLeft = GetDepth(pRoot->m_pLeft);
    int depthRight = GetDepth(pRoot->m_pRight);
    return depthLeft > depthRight ? (depthLeft + 1) : (depthRight + 1); 
}

3、前序遍歷,中序遍歷,後序遍歷

前序遍歷遞迴解法: 
(1)如果二叉樹為空,空操作 
(2)如果二叉樹不為空,先訪問根節點,然後遍歷左子樹,再遍歷右子樹 
參考程式碼如下:

void PreOrderTraverse(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return;
    printf("%d\n",pRoot->data); // 顯示結點資料
    PreOrderTraverse(pRoot->m_pLeft); // 前序遍歷左子樹
    PreOrderTraverse(pRoot->m_pRight); // 前序遍歷右子樹
}

中序遍歷遞迴解法 
(1)如果二叉樹為空,空操作。 
(2)如果二叉樹不為空,先遍歷左子樹,然後訪問根節點,再遍歷右子樹 
參考程式碼如下: 

void InOrderTraverse(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return;
    InOrderTraverse(pRoot->m_pLeft); // 中序遍歷左子樹
    printf("%d\n",pRoot->data); // 顯示結點資料
    InOrderTraverse(pRoot->m_pRight); // 中序遍歷右子樹
}

後序遍歷遞迴解法 
(1)如果二叉樹為空,空操作。 
(2)如果二叉樹不為空,先遍歷左子樹,然後遍歷右子樹,訪問根節點 
參考程式碼如下:

void InOrderTraverse(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return;
    InOrderTraverse(pRoot->m_pLeft); // 後序遍歷左子樹
    InOrderTraverse(pRoot->m_pRight); // 後序遍歷右子樹
    printf("%d\n",pRoot->data); // 顯示結點資料
}

4、分層遍歷二叉樹(按層次從上往下,從左往右)

相當於廣度優先搜尋,使用佇列實現。佇列初始化,將根節點壓入佇列。當佇列不為空,進行如下操作:彈出一個節點,訪問,若左子節點或右子節點不為空,將其壓入佇列。

void LevelTraverse(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return;
    queue<BinaryTreeNode *> q;
    q.push(pRoot);
    while(!q.empty())
    {
        BinaryTreeNode * pNode = q.front();
        q.pop();
        Visit(pNode); // 訪問節點
        if(pNode->m_pLeft != NULL)
            q.push(pNode->m_pLeft);
        if(pNode->m_pRight != NULL)
            q.push(pNode->m_pRight);
    }
    return;
}

 5、將二叉查詢樹變為有序的雙向連結串列


要求不能建立新節點,只調整指標。 
遞迴解法: 
(1)如果二叉樹查詢樹為空,不需要轉換,對應雙向連結串列的第一個節點是NULL,最後一個節點是NULL 
(2)如果二叉查詢樹不為空: 
如果左子樹為空,對應雙向有序連結串列的第一個節點是根節點,左邊不需要其他操作; 
如果左子樹不為空,轉換左子樹,二叉查詢樹對應雙向有序連結串列的第一個節點就是左子樹轉換後雙向有序連結串列的第一個節點,同時將根節點和左子樹轉換後的雙向有序鏈 表的最後一個節點連線; 
如果右子樹為空,對應雙向有序連結串列的最後一個節點是根節點,右邊不需要其他操作; 
如果右子樹不為空,對應雙向有序連結串列的最後一個節點就是右子樹轉換後雙向有序連結串列的最後一個節點,同時將根節點和右子樹轉換後的雙向有序連結串列的第一個節點連 接。 
參考程式碼如下:

/******************************************************************************
引數:
pRoot: 二叉查詢樹根節點指標
pFirstNode: 轉換後雙向有序連結串列的第一個節點指標
pLastNode: 轉換後雙向有序連結串列的最後一個節點指標
******************************************************************************/
void Convert(BinaryTreeNode * pRoot, 
             BinaryTreeNode * & pFirstNode, BinaryTreeNode * & pLastNode)
{
    BinaryTreeNode *pFirstLeft, *pLastLeft, * pFirstRight, *pLastRight;
    if(pRoot == NULL) 
    {
        pFirstNode = NULL;
        pLastNode = NULL;
        return;
    }

    if(pRoot->m_pLeft == NULL)
    {
        // 如果左子樹為空,對應雙向有序連結串列的第一個節點是根節點
        pFirstNode = pRoot;
    }
    else
    {
        Convert(pRoot->m_pLeft, pFirstLeft, pLastLeft);
        // 二叉查詢樹對應雙向有序連結串列的第一個節點就是左子樹轉換後雙向有序連結串列的第一個節點
        pFirstNode = pFirstLeft;
        // 將根節點和左子樹轉換後的雙向有序連結串列的最後一個節點連線
        pRoot->m_pLeft = pLastLeft;
        pLastLeft->m_pRight = pRoot;
    }

    if(pRoot->m_pRight == NULL)
    {
        // 對應雙向有序連結串列的最後一個節點是根節點
        pLastNode = pRoot;
    }
    else
    {
        Convert(pRoot->m_pRight, pFirstRight, pLastRight);
        // 對應雙向有序連結串列的最後一個節點就是右子樹轉換後雙向有序連結串列的最後一個節點
        pLastNode = pLastRight;
        // 將根節點和右子樹轉換後的雙向有序連結串列的第一個節點連線
        pRoot->m_pRight = pFirstRight;
        pFirstRight->m_pLeft = pRoot;
    }

    return;
}

6、求二叉樹第K層的節點個數

遞迴解法: 
(1)如果二叉樹為空或者k<1返回0 
(2)如果二叉樹不為空並且k==1,返回1 
(3)如果二叉樹不為空且k>1,返回左子樹中k-1層的節點個數與右子樹k-1層節點個數之和 
參考程式碼如下:

int GetNodeNumKthLevel(BinaryTreeNode * pRoot, int k)
{
    if(pRoot == NULL || k < 1)
        return 0;
    if(k == 1)
        return 1;
    int numLeft = GetNodeNumKthLevel(pRoot->m_pLeft, k-1); // 左子樹中k-1層的節點個數
    int numRight = GetNodeNumKthLevel(pRoot->m_pRight, k-1); // 右子樹中k-1層的節點個數
    return (numLeft + numRight);
}

7、求二叉樹中葉子節點的個數

遞迴解法: 
(1)如果二叉樹為空,返回0 
(2)如果二叉樹不為空且左右子樹為空,返回1 
(3)如果二叉樹不為空,且左右子樹不同時為空,返回左子樹中葉子節點個數加上右子樹中葉子節點個數 
參考程式碼如下:

int GetLeafNodeNum(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return 0;
    if(pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL)
        return 1;
    int numLeft = GetLeafNodeNum(pRoot->m_pLeft); // 左子樹中葉節點的個數
    int numRight = GetLeafNodeNum(pRoot->m_pRight); // 右子樹中葉節點的個數
    return (numLeft + numRight);
}

 8、判斷兩棵二叉樹是否結構相同


不考慮資料內容。結構相同意味著對應的左子樹和對應的右子樹都結構相同。 
遞迴解法: 
(1)如果兩棵二叉樹都為空,返回真 
(2)如果兩棵二叉樹一棵為空,另一棵不為空,返回假 
(3)如果兩棵二叉樹都不為空,如果對應的左子樹和右子樹都同構返回真,其他返回假 
參考程式碼如下:

 

bool StructureCmp(BinaryTreeNode * pRoot1, BinaryTreeNode * pRoot2)
{
    if(pRoot1 == NULL && pRoot2 == NULL) // 都為空,返回真
        return true;
    else if(pRoot1 == NULL || pRoot2 == NULL) // 有一個為空,一個不為空,返回假
        return false;
    bool resultLeft = StructureCmp(pRoot1->m_pLeft, pRoot2->m_pLeft); // 比較對應左子樹 
    bool resultRight = StructureCmp(pRoot1->m_pRight, pRoot2->m_pRight); // 比較對應右子樹
    return (resultLeft && resultRight);
} 

 9、 判斷二叉樹是不是平衡二叉樹

遞迴解法: 
(1)如果二叉樹為空,返回真 
(2)如果二叉樹不為空,如果左子樹和右子樹都是AVL樹並且左子樹和右子樹高度相差不大於1,返回真,其他返回假 
參考程式碼:

bool IsAVL(BinaryTreeNode * pRoot, int & height)
{
    if(pRoot == NULL) // 空樹,返回真
    {
        height = 0;
        return true;
    }
    int heightLeft;
    bool resultLeft = IsAVL(pRoot->m_pLeft, heightLeft);
    int heightRight;
    bool resultRight = IsAVL(pRoot->m_pRight, heightRight);
    if(resultLeft && resultRight && abs(heightLeft - heightRight) <= 1) // 左子樹和右子樹都是AVL,並且高度相差不大於1,返回真
    {
        height = max(heightLeft, heightRight) + 1;
        return true;
    }
    else
    {
        height = max(heightLeft, heightRight) + 1;
        return false;
    }
}

10、求二叉樹的映象

遞迴解法: 
(1)如果二叉樹為空,返回空 
(2)如果二叉樹不為空,求左子樹和右子樹的映象,然後交換左子樹和右子樹 
參考程式碼如下:

BinaryTreeNode * Mirror(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL) // 返回NULL
        return NULL;
    BinaryTreeNode * pLeft = Mirror(pRoot->m_pLeft); // 求左子樹映象
    BinaryTreeNode * pRight = Mirror(pRoot->m_pRight); // 求右子樹映象
        // 交換左子樹和右子樹
    pRoot->m_pLeft = pRight;
    pRoot->m_pRight = pLeft;
    return pRoot;
}

11、求二叉樹中兩個節點的最低公共祖先節點

遞迴解法: 
(1)如果兩個節點分別在根節點的左子樹和右子樹,則返回根節點 
(2)如果兩個節點都在左子樹,則遞迴處理左子樹;如果兩個節點都在右子樹,則遞迴處理右子樹 
參考程式碼如下

bool FindNode(BinaryTreeNode * pRoot, BinaryTreeNode * pNode)
{
    if(pRoot == NULL || pNode == NULL)
        return false;

    if(pRoot == pNode)
        return true;

    bool found = FindNode(pRoot->m_pLeft, pNode);
    if(!found)
        found = FindNode(pRoot->m_pRight, pNode);

    return found;
}

BinaryTreeNode * GetLastCommonParent(BinaryTreeNode * pRoot, 
                                     BinaryTreeNode * pNode1, 
                                     BinaryTreeNode * pNode2)
{
    if(FindNode(pRoot->m_pLeft, pNode1))
    {
        if(FindNode(pRoot->m_pRight, pNode2))
            return pRoot;
        else
            return GetLastCommonParent(pRoot->m_pLeft, pNode1, pNode2);
    }
    else
    {
        if(FindNode(pRoot->m_pLeft, pNode2))
            return pRoot;
        else
            return GetLastCommonParent(pRoot->m_pRight, pNode1, pNode2);
    }
}

分析: 
遞迴解法效率很低,有很多重複的遍歷,下面看一下非遞迴解法。 
非遞迴解法: 
先求從根節點到兩個節點的路徑,然後再比較對應路徑的節點就行,最後一個相同的節點也就是他們在二叉樹中的最低公共祖先節點 
參考程式碼如下

bool GetNodePath(BinaryTreeNode * pRoot, BinaryTreeNode * pNode, 
                 list<BinaryTreeNode *> & path)
{
    if(pRoot == pNode)
    {   
        path.push_back(pRoot);
        return true;
    }
    if(pRoot == NULL)
        return false;
    path.push_back(pRoot);
    bool found = false;
    found = GetNodePath(pRoot->m_pLeft, pNode, path);
    if(!found)
        found = GetNodePath(pRoot->m_pRight, pNode, path);
    if(!found)
        path.pop_back();
    return found;
}
BinaryTreeNode * GetLastCommonParent(BinaryTreeNode * pRoot, BinaryTreeNode * pNode1, BinaryTreeNode * pNode2)
{
    if(pRoot == NULL || pNode1 == NULL || pNode2 == NULL)
        return NULL;
    list<BinaryTreeNode*> path1;
    bool bResult1 = GetNodePath(pRoot, pNode1, path1);
    list<BinaryTreeNode*> path2;
    bool bResult2 = GetNodePath(pRoot, pNode2, path2);
    if(!bResult1 || !bResult2) 
        return NULL;
    BinaryTreeNode * pLast = NULL;
    list<BinaryTreeNode*>::const_iterator iter1 = path1.begin();
    list<BinaryTreeNode*>::const_iterator iter2 = path2.begin();
    while(iter1 != path1.end() && iter2 != path2.end())
    {
        if(*iter1 == *iter2)
            pLast = *iter1;
        else
            break;
        iter1++;
        iter2++;
    }
    return pLast;
}

在上述演算法的基礎上稍加變化即可求二叉樹中任意兩個節點的距離了。

12、求二叉樹中節點的最大距離


即二叉樹中相距最遠的兩個節點之間的距離。 
遞迴解法: 
(1)如果二叉樹為空,返回0,同時記錄左子樹和右子樹的深度,都為0 
(2)如果二叉樹不為空,最大距離要麼是左子樹中的最大距離,要麼是右子樹中的最大距離,要麼是左子樹節點中到根節點的最大距離+右子樹節點中到根節點的最大距離,同時記錄左子樹和右子樹節點中到根節點的最大距離。 
參考程式碼如下:

 

int GetMaxDistance(BinaryTreeNode * pRoot, int & maxLeft, int & maxRight)
{
    // maxLeft, 左子樹中的節點距離根節點的最遠距離
    // maxRight, 右子樹中的節點距離根節點的最遠距離
    if(pRoot == NULL)
    {
        maxLeft = 0;
        maxRight = 0;
        return 0;
    }
    int maxLL, maxLR, maxRL, maxRR;
    int maxDistLeft, maxDistRight;
    if(pRoot->m_pLeft != NULL)
    {
        maxDistLeft = GetMaxDistance(pRoot->m_pLeft, maxLL, maxLR);
        maxLeft = max(maxLL, maxLR) + 1;
    }
    else
    {
        maxDistLeft = 0;
        maxLeft = 0;
    }
    if(pRoot->m_pRight != NULL)
    {
        maxDistRight = GetMaxDistance(pRoot->m_pRight, maxRL, maxRR);
        maxRight = max(maxRL, maxRR) + 1;
    }
    else
    {
        maxDistRight = 0;
        maxRight = 0;
    }
    return max(max(maxDistLeft, maxDistRight), maxLeft+maxRight);
}

13、由前序遍歷序列和中序遍歷序列重建二叉樹


二叉樹前序遍歷序列中,第一個元素總是樹的根節點的值。中序遍歷序列中,左子樹的節點的值位於根節點的值的左邊,右子樹的節點的值位 
於根節點的值的右邊。 
遞迴解法: 
(1)如果前序遍歷為空或中序遍歷為空或節點個數小於等於0,返回NULL。 
(2)建立根節點。前序遍歷的第一個資料就是根節點的資料,在中序遍歷中找到根節點的位置,可分別得知左子樹和右子樹的前序和中序遍 
歷序列,重建左右子樹。

 

BinaryTreeNode * RebuildBinaryTree(int* pPreOrder, int* pInOrder, int nodeNum)
{
    if(pPreOrder == NULL || pInOrder == NULL || nodeNum <= 0)
        return NULL;
    BinaryTreeNode * pRoot = new BinaryTreeNode;
    // 前序遍歷的第一個資料就是根節點資料
    pRoot->m_nValue = pPreOrder[0];
    pRoot->m_pLeft = NULL;
    pRoot->m_pRight = NULL;
    // 查詢根節點在中序遍歷中的位置,中序遍歷中,根節點左邊為左子樹,右邊為右子樹
    int rootPositionInOrder = -1;
    for(int i = 0; i < nodeNum; i++)
        if(pInOrder[i] == pRoot->m_nValue)
        {
            rootPositionInOrder = i;
            break;
        }
    if(rootPositionInOrder == -1)
    {
        throw std::exception("Invalid input.");
    }
    // 重建左子樹
    int nodeNumLeft = rootPositionInOrder;
    int * pPreOrderLeft = pPreOrder + 1;
    int * pInOrderLeft = pInOrder;
    pRoot->m_pLeft = RebuildBinaryTree(pPreOrderLeft, pInOrderLeft, nodeNumLeft);
    // 重建右子樹
    int nodeNumRight = nodeNum - nodeNumLeft - 1;
    int * pPreOrderRight = pPreOrder + 1 + nodeNumLeft;
    int * pInOrderRight = pInOrder + nodeNumLeft + 1;
    pRoot->m_pRight = RebuildBinaryTree(pPreOrderRight, pInOrderRight, nodeNumRight);
    return pRoot;
}

同樣,有中序遍歷序列和後序遍歷序列,類似的方法可重建二叉樹,但前序遍歷序列和後序遍歷序列不同恢復一棵二叉樹,證明略。

14、判斷二叉樹是不是完全二叉樹


若設二叉樹的深度為h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層所有的結點都連續集中在最左邊,這就是完全 
二叉樹。 
有如下演算法,按層次(從上到下,從左到右)遍歷二叉樹,當遇到一個節點的左子樹為空時,則該節點右子樹必須為空,且後面遍歷的節點左 
右子樹都必須為空,否則不是完全二叉樹

 

bool IsCompleteBinaryTree(BinaryTreeNode * pRoot)
{
    if(pRoot == NULL)
        return false;
    queue<BinaryTreeNode *> q;
    q.push(pRoot);
    bool mustHaveNoChild = false;
    bool result = true;
    while(!q.empty())
    {
        BinaryTreeNode * pNode = q.front();
        q.pop();
        if(mustHaveNoChild) // 已經出現了有空子樹的節點了,後面出現的必須為葉節點(左右子樹都為空)
        {
            if(pNode->m_pLeft != NULL || pNode->m_pRight != NULL)
            {
                result = false;
                break;
            }
        }
        else
        {
            if(pNode->m_pLeft != NULL && pNode->m_pRight != NULL)
            {
                q.push(pNode->m_pLeft);
                q.push(pNode->m_pRight);
            }
            else if(pNode->m_pLeft != NULL && pNode->m_pRight == NULL)
            {
                mustHaveNoChild = true;
                q.push(pNode->m_pLeft);
            }
            else if(pNode->m_pLeft == NULL && pNode->m_pRight != NULL)
            {
                result = false;
                break;
            }
            else
            {
                mustHaveNoChild = true;
            }
        }
    }
    return result;
}

15.判斷一棵樹是否為另一棵樹的子樹

用pRoot2去比對pRoot1看pRoot1中是否有pRoot2的結構,利用遞迴。先檢視根節點,如果相等則繼續檢視左子樹,最後是右子樹。如果根節點不相等則直接返回false。

注意:空樹是任何樹的子樹

 bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
    {
        if(!pRoot1 || !pRoot2)
            return false;
      if(pRoot1->val == pRoot2->val)
      {
          if(isHasSubtree(pRoot1,pRoot2))
              return true;
      }
        if(isHasSubtree(pRoot1->left,pRoot2))
            return true;
        if(isHasSubtree(pRoot1->right,pRoot2))
            return true;
        
        return false;
    }
    
    bool isHasSubtree(TreeNode* pRoot1,TreeNode* pRoot2)
    {
        if(pRoot2 == NULL)
            return true;
        if(pRoot1 == NULL)
            return false;
        if(pRoot1->val == pRoot2->val)
        {
        if(isHasSubtree(pRoot1->left,pRoot2->left) && isHasSubtree(pRoot1->right,pRoot2->right))
            return true;
        }
        return false;
    }