面试基础算法及编程 第三弹(树(二叉树)相关:主要考察指针相关的操作)
生活随笔
收集整理的這篇文章主要介紹了
面试基础算法及编程 第三弹(树(二叉树)相关:主要考察指针相关的操作)
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
// # -*- coding:utf-8 -*-
// # @Author: Mr.chen(ai-chen2050@qq.com)
// # @Date: 2018-08-17 16:32:55 // 注:此為第三彈,主要講解樹(shù)(但是面試中大多提到的都是二叉樹(shù))相關(guān)面試筆試題,要求手寫。
// 在樹(shù)的面試題中也會(huì)涉及到大量的指針,對(duì)于二叉樹(shù)來(lái)說(shuō)最應(yīng)該先關(guān)注的就是它的前中后序遍歷,
// 對(duì)于它們來(lái)說(shuō)都有遞歸和非遞歸遍歷,還有層次遍歷,說(shuō)明如下圖所示:
?
? ? ?
? ? ? ?
/* 1、重建二叉樹(shù),輸入某二叉樹(shù)的前序和中序,請(qǐng)重建該二叉樹(shù)。假設(shè)輸入的前序和中序序列都不含重復(fù)的數(shù)字,例如輸入前序的序列為{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6} 思路:首先前序序列中的第一個(gè)節(jié)點(diǎn)一定是根節(jié)點(diǎn),中序序列中,根節(jié)點(diǎn)在中間,其左右子樹(shù)在其兩邊,故我們可以根據(jù)中序找到根節(jié)點(diǎn),左邊的即為左子樹(shù),右邊的即為右左子樹(shù),之后再其左右兩邊的序列遞歸去構(gòu)造生成二叉樹(shù)。代碼如下: */#include<iostream> #include<set> #include<map> #include<stack> #include<deque> #include<list> #include<stdexcept>using namespace std;// 二叉樹(shù)的節(jié)點(diǎn)結(jié)構(gòu)體為:如下。 struct BiTreeNode {int m_value;BiTreeNode* m_pLeft;BiTreeNode* m_pRight; };BiTreeNode* Construct(int* preOrder,int* inOrder,int length) {if(preOrder == NULL || inOrder == NULL || length <= 0)return NULL;return ConstructCore(preOrder,preOrder+length-1,inOrder,inOrder+length-1); }BiTreeNode* ConstructCore(int* sPreOrder,int* ePreOrder,int* sInOrder,int* eInOrder) {// 前序遍歷的第一個(gè)節(jié)點(diǎn)是根節(jié)點(diǎn)的值int rootValue = sPreOrder[0];BiTreeNode* root = new BiTreeNode();root->m_value = rootValue;root->m_pLeft = root->m_pRight = NULL;// 定義遞歸出口if(sPreOrder == ePreOrder){if(sInOrder == eInOrder && *sPreOrder == *sInOrder)return root;else throw std::exception("Invalid input.");}// 在中序中找到根節(jié)點(diǎn)的值int* rootInOrder = sInOrder;while(rootInOrder < eInOrder && *rootInOrder != rootValue)++ rootInOrder;if(rootInOrder == eInOrder && *rootInOrder != rootValue)throw std::exception("Invalid input...")// 左子樹(shù)的節(jié)點(diǎn)個(gè)數(shù)int leftLength = rootInOrder - sInOrder;int* leftPreOrderEnd = sPreOrder + leftLength;if(leftLength > 0) //構(gòu)建左子樹(shù) root->m_pLeft = ConstructCore(sPreOrder + 1,leftPreOrderEnd,sInOrder,rootInOrder - 1);if(leftLength < ePreOrder - sPreOrder) //構(gòu)建右子樹(shù)root->m_pRight = ConstructCore(leftPreOrderEnd+1,ePreOrder,rootInOrder+1,eInOrder)return root; }/* 2、求二叉樹(shù)的深度:輸入一顆二叉樹(shù)的根節(jié)點(diǎn),求該樹(shù)的深度。從根節(jié)點(diǎn)到頁(yè)節(jié)點(diǎn)依次經(jīng)過(guò)的節(jié)點(diǎn)(含根和葉節(jié)點(diǎn))形成樹(shù)的一顆路徑,最長(zhǎng)路徑的長(zhǎng)度即為樹(shù)的深度。思路:我們可以從另外一個(gè)角度來(lái)考慮樹(shù)的深度。如果樹(shù)只有一個(gè)節(jié)點(diǎn),則深度為 1,如果根節(jié)點(diǎn)只有左子樹(shù)而沒(méi)有右子樹(shù),則樹(shù)的深度應(yīng)該是左子樹(shù)加 1,反之,是右子樹(shù)加 1,故我們很容易想到遞歸實(shí)現(xiàn)。 */ int treeDepth(BiTreeNode* pRoot) {if(NULL == pRoot)return 0;// 計(jì)算左右子樹(shù)的深度int nLeft = treeDepth(pRoot->m_pLeft);int nRight = treeDepth(pRoot->m_pRight);return nLeft > nRight? (nLeft + 1):(nRight + 1); }/* 2.1 面試官加深難度, 追問(wèn): 輸入一顆二叉樹(shù)的根節(jié)點(diǎn),判斷它是不是平衡二叉樹(shù),如果某二叉樹(shù)中任意節(jié)點(diǎn)的左右子樹(shù)深度相差不超過(guò) 1,那么它就是一顆平衡二叉樹(shù)。法一:需要重復(fù)遍歷一個(gè)節(jié)點(diǎn)多次,基于上述代碼的改進(jìn),法二:采用后序遍歷的方法遍歷每一個(gè)節(jié)點(diǎn),在遍歷一個(gè)節(jié)點(diǎn)之前,我們就已經(jīng)遍歷了該節(jié)點(diǎn)的左右子樹(shù)。只要在遍歷一個(gè)節(jié)點(diǎn)的時(shí)候,記錄它的深度,就可以一邊遍歷,一邊判斷該節(jié)點(diǎn)是否平衡的了。 *///法一:需要重復(fù)遍歷一個(gè)節(jié)點(diǎn)多次 bool isBalanced(BiTreeNode* pRoot) {if(NULL == pRoot)return true;int left = treeDepth(pRoot->m_pLeft);int right = treeDepth(pRoot->m_pRight);int diff = left - right;if(diff > 1 || diff < -1)return false;// 遞歸判斷,左右子樹(shù)return isBalanced(pRoot->m_pLeft) && isBalanced(pRoot->m_pRight); } // 由于重復(fù)遍歷會(huì)影響性能,故我們推薦下面這種基于后序遍歷的方法。// 法二:每個(gè)節(jié)點(diǎn)只遍歷一次 bool isBalanced(BiTreeNode* pRoot,int* pDepth) {if(NULL == pRoot){*pDepth = 0;return true;}int left,right;if(isBalanced(pRoot->m_pLeft, &left) && isBalanced(pRoot->m_pRight, &right)){int diff = left - right;if(diff <= 1 && diff >= -1){*pDepth = 1 + (left > right? left:right );return true;}}return false; }// 調(diào)用時(shí),只需給上面的函數(shù)傳入一個(gè)二叉樹(shù)的根節(jié)點(diǎn)和表示節(jié)點(diǎn)深度的整形變量即可。 bool isBalancedTree(BiTreeNode* pRoot) {int depth = 0;return isBalanced(pRoot, &depth); } /* 3、樹(shù)的子結(jié)構(gòu),題目:輸入兩顆二叉樹(shù) A 和 B,判斷 B 是不是 A 的子結(jié)構(gòu),子結(jié)構(gòu)定義如下圖:一般來(lái)說(shuō),樹(shù)的指針的操作比起鏈表來(lái)說(shuō)更多,如果面試官想加大難度,則會(huì)考察樹(shù),在涉及二叉樹(shù)的題目中,由于涉及到很多的指針操作,故一定要注意檢查邊界條件,即檢查空指針,如果沒(méi)有檢查,而導(dǎo)致程序崩潰,則是很尷尬和忌諱的事情。故在操作指針時(shí),一定要注意這個(gè)指針是不是為 NULL,如果是,該怎么處理。本題可以分兩步來(lái)看,第一步是從樹(shù) A 中查找與根節(jié)點(diǎn)值一樣的節(jié)點(diǎn),實(shí)際上是對(duì)樹(shù)的遍歷,一般遍歷樹(shù)可以有遞歸和迭代循環(huán),一般遞歸代碼較為簡(jiǎn)單,一般會(huì)優(yōu)先考慮,第二步是判斷 A 樹(shù)中以 R 為根節(jié)點(diǎn)的子樹(shù)是不是和樹(shù) B 具有相同的結(jié)構(gòu),同樣,我們也可以采用遞歸的方法來(lái)做。 */? ? ? ? ? ??
// 第一步: 遞歸遍歷,尋找相同節(jié)點(diǎn) bool hasSubTree(BiTreeNode* pRoot1,BiTreeNode* pRoot2) {bool result = false;// 異常值檢測(cè)if(NULL != pRoot1 && NULL != pRoot2){if(pRoot1->m_value == pRoot2->m_value)result = doseTree1HasTree2(pRoot1,pRoot2);if(!result)result = hasSubTree(pRoot1->m_pLeft,pRoot2);if(!result)result = hasSubTree(pRoot1->m_pRight,pRoot2);}return result; }// 第二步:比較子結(jié)構(gòu),同樣可以用遞歸的方法 bool doseTree1HasTree2(BiTreeNode* pRoot1,BiTreeNode* pRoot2) {// 定義遞歸出口if(NULL == pRoot2)return true;if(NULL == pRoot1)return false;if(pRoot1->m_value != pRoot2->m_value)return false;return doseTree1HasTree2(pRoot1->m_pLeft,pRoot2->m_pLeft) &&doseTree1HasTree2(pRoot1->m_pRight,pRoot2->m_pRight); } /* 4、二叉樹(shù)的鏡像(畫(huà)圖(幾何的視角去看待)讓抽象問(wèn)題形象化),二叉樹(shù)的鏡像對(duì)大多數(shù)沒(méi)有接觸過(guò)得人來(lái)說(shuō),算是一個(gè)新的感念,此時(shí)可以通過(guò)幾何畫(huà)圖的方式來(lái)讓問(wèn)題明朗化。如下圖:通過(guò)觀察圖,我們來(lái)總結(jié)解決該問(wèn)題的方法。首先交換根節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn),此時(shí)子節(jié)點(diǎn)的子節(jié)點(diǎn)的左右子樹(shù)的相對(duì)關(guān)系保持不變。故還需要交換子節(jié)點(diǎn)的子節(jié)點(diǎn)的左右子樹(shù),知道葉子節(jié)點(diǎn)。思路如下圖,所示。通過(guò)前序遍歷這棵樹(shù)的每個(gè)節(jié)點(diǎn),如果該節(jié)點(diǎn)有子節(jié)點(diǎn),就交換該節(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn),當(dāng)交換完所有的非葉子節(jié)點(diǎn)的左右子節(jié)點(diǎn)后,就得到了樹(shù)的鏡像。 */? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ?解題思路圖,如下:?
? ? ? ? ? ? ? ?
void mirrorRecursively(BiTreeNode* pRoot) {// 定義遞歸出口if(pRoot == NULL || (pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL))return;// 首先交換根節(jié)點(diǎn)BiTreeNode* pTemp = pRoot->m_pLeft;pRoot->m_pLeft = pRoot->m_pRight;pRoot->m_pRight = pTemp;// recursivelyif(pRoot->m_pLeft)mirrorRecursively(pRoot->m_pLeft);if(pRoot->m_pRight)mirrorRecursively(pRoot->m_pRight) }// 當(dāng)使用循環(huán)時(shí),需要一個(gè)棧來(lái)保存遍歷到的當(dāng)前的根結(jié)點(diǎn) void mirrorCircularly(BiTreeNode* pRoot) {// 用棧保存根結(jié)點(diǎn)std::stack<BiTreeNode* > stackRoot;if(pRoot == NULL)return;stackRoot.push(pRoot);// 開(kāi)始循環(huán)while(!stackRoot.empty()){//從棧中獲取根結(jié)點(diǎn)BiTreeNode* pNode = stackRoot.top();stackRoot.pop();// 首先交換根節(jié)點(diǎn)BiTreeNode* pTemp = pNode->m_pLeft;pNode->m_pLeft = pNode->m_pRight;pNode->m_pRight = pTemp;if(pNode->m_pLeft)stackRoot.push(pNode->m_pLeft);if(pNode->m_pRight)stackRoot.push(pNode->m_pRight);} }/* 5、從上到下打印二叉樹(shù),從上到下打印二叉樹(shù)的每一個(gè)節(jié)點(diǎn),同一層的節(jié)點(diǎn)按照從左到右的順序打印。即層次遍歷。其實(shí),樹(shù)是一種簡(jiǎn)化的圖,該題也對(duì)應(yīng)圖的廣度優(yōu)先遍歷。我們來(lái)嘗試分析一下,由于是層次遍歷,故最先打印頭結(jié)點(diǎn),然后我們需要一個(gè)容器來(lái)保存頭結(jié)點(diǎn)的兩個(gè)子節(jié)點(diǎn),并且是先左后右,且容器應(yīng)該保證先進(jìn)先出(隊(duì)列),之后只要隊(duì)列里面的節(jié)點(diǎn)有孩子節(jié)點(diǎn),我們就應(yīng)該將孩子節(jié)點(diǎn)加載到隊(duì)列末尾,以便后續(xù)遍歷。直到葉子節(jié)點(diǎn)。 */// 由于 STL 已經(jīng)幫我們實(shí)現(xiàn)了一個(gè)很好的雙端隊(duì)列 deque,故我們這里直接采用 deque 作為數(shù)據(jù)結(jié)構(gòu) void printFromTopToButtom(BiTreeNode* pRoot) {if(NULL === pRoot)return;std::deque<BiTreeNode* > treeDeque;// 壓入根節(jié)點(diǎn)treeDepth.push_back(pRoot);// 循環(huán)迭代while(treeDeque.size()){BiTreeNode* pNode = treeDeque.front();treeDeque.pop_front();std::cout<< pNode->m_value <<std::endl;// 將 pNode 的子節(jié)點(diǎn)加入隊(duì)列if(pNode->m_pLeft)treeDeque.push_back(pNode->m_pLeft);if(pNode->m_pRight)treeDeque.push_back(pNode->m_pRight);} } // 本題的擴(kuò)展圖的廣度優(yōu)先遍歷/* 6、二叉搜索樹(shù)的后序遍歷序列,題目:輸入一個(gè)整數(shù)數(shù)組,判斷該數(shù)組是不是某二叉搜索樹(shù)的后序遍歷結(jié)果。如果是返回True, 不是返回False,假設(shè)輸入的兩個(gè)數(shù)組的任意兩個(gè)數(shù)字都不相同。分析:特殊的地方是二叉搜索樹(shù)是具有排序?qū)傩缘?#xff08;左子樹(shù)小于根節(jié)點(diǎn),根節(jié)點(diǎn)小于右子樹(shù)),加上后序遍歷,故我們需要分析出它的背后的規(guī)律。在后序遍歷中,最后一個(gè)數(shù)字是樹(shù)的根節(jié)點(diǎn)的值,數(shù)組中前面的數(shù)字可以分為兩部分,第一部分是左子樹(shù)節(jié)點(diǎn)的值,他們都比根節(jié)點(diǎn)小;第二部分是右子節(jié)點(diǎn)的值,它們都比根節(jié)點(diǎn)大。接下來(lái)用同樣的方法,確定于數(shù)組每一部分對(duì)應(yīng)子樹(shù)的結(jié)構(gòu)。這其實(shí)是一個(gè)遞歸的過(guò)程。 */// 找到規(guī)律后再寫代碼,就容易多了。 bool verifySquenceOfBST(int sequence[],int length) {if(sequence == NULL || length <= 0)return false;// 后序遍歷最后一個(gè)節(jié)點(diǎn)就是根節(jié)點(diǎn)int root = sequence[length-1];//找到左子樹(shù)的序列,在二叉搜索樹(shù)中,左子樹(shù)的節(jié)點(diǎn)小于根節(jié)點(diǎn)int i = 0;for(; i< length-1; ++i){if(sequence[i] > root)break;}//在二叉搜索樹(shù)中,右子樹(shù)的節(jié)點(diǎn)大于根節(jié)點(diǎn)int j = i;for(; j<length-1; ++j){if(sequence[j] < root)return false;}// 判斷左子樹(shù)是不是二叉搜索樹(shù)bool left = true;if(i>0)left = verifySquenceOfBST(sequence,i);// 判斷右子樹(shù)是不是二叉搜索樹(shù)bool right = true;if(i < length-1)right = verifySquenceOfBST(sequence+i, length - i +1);return left && right; }/* 7、二叉樹(shù)中和為某一個(gè)值的路徑,題目:輸入一個(gè)二叉樹(shù)和一個(gè)整數(shù),打印出二叉樹(shù)中節(jié)點(diǎn)值的和為輸入整數(shù)的所有路徑。從樹(shù)的根節(jié)點(diǎn)開(kāi)始往下一直到葉節(jié)點(diǎn)所經(jīng)過(guò)的節(jié)點(diǎn)形成一條路徑。事例,如下圖,對(duì)于樹(shù)的路徑和這樣一個(gè)新概念,很難一下想到完整的思路,故可以先從一兩個(gè)具體的例子入手,尋找規(guī)律。首先加和是從根到葉子,故即遍歷中,只有前序遍歷是先根節(jié)點(diǎn),故應(yīng)該是前序遍歷,其次具體思路可也參見(jiàn)下圖說(shuō)明。這個(gè)題明顯是所謂的回溯搜索的過(guò)程,參見(jiàn) labuladong 算法小抄。此外應(yīng)為要輸出整條路徑,所以需要將之前遍歷過(guò)得節(jié)點(diǎn)保存下來(lái),這里采用 vector. 在遍歷完,不滿足條件后需要回溯,即進(jìn)行逆操作。 */? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ?
void findPath(BiTreeNode* pRoot,int expectedSum) {if(pRoot == NULL)return;// 用 vector 模擬 stackstd::vector<int> path;int currentSum = 0;FindPath(pRoot,expectedSum,path,currentSum); }void FindPath(BiTreeNode* pRoot,int expectedSum,std::vector<int>& path,int ¤tSum) {// 加入根節(jié)點(diǎn)的值,壓入棧currentSum += pRoot->m_value;path.push_back(pRoot->m_value);// 如果是葉節(jié)點(diǎn),并且路徑上的葉節(jié)點(diǎn)上值之和等于輸入的值,則打印出這條路徑bool isLeaf = pRoot->m_pLeft == NULL && pRoot->m_pRight == NULL;if(currentSum == expectedSum && isLeaf){printf("The path is Find: ");std::vector<int>::iterator iter = path.begin();for(; iter < path.end(); ++iter){print("%d\t", *iter);}printf("\n");}// 如果不是葉節(jié)點(diǎn)就遍歷它的子節(jié)點(diǎn)if(pRoot->m_pLeft != NULL)FindPath(pRoot->m_pLeft,expectedSum,path,currentSum);if(pRoot->m_pRight != NULL)FindPath(pRoot->m_pRight,expectedSum,path,currentSum);// 在返回父節(jié)點(diǎn)的時(shí)候,在路徑上刪除當(dāng)前節(jié)點(diǎn),并在 currentSum 中減去當(dāng)前的值currentSum -= pRoot->m_value;path.pop_back(); } /* 8、求樹(shù)中兩個(gè)結(jié)點(diǎn)的最低公共祖先。注:由于這里涉及到樹(shù)的類型,不同的類型有不同的解法,這里就默認(rèn)是最為普通的樹(shù)。如果是二叉搜索樹(shù),則是比較方便的。因?yàn)槎嫠阉鳂?shù)是排過(guò)序的,如果當(dāng)前節(jié)點(diǎn)比輸入的兩個(gè)節(jié)點(diǎn)的值都大,說(shuō)明兩個(gè)節(jié)點(diǎn)都在左子樹(shù),反之,在右子樹(shù)。若在左邊,則下一步遍歷左子節(jié)點(diǎn),若在右邊,則遍歷右子節(jié)點(diǎn)。依次遞歸遍歷直到找到兩個(gè)第一個(gè)在兩個(gè)節(jié)點(diǎn)值之間的節(jié)點(diǎn),即為最低公共祖先。如果不是二叉搜索樹(shù),是一般的樹(shù),且二叉樹(shù)都不是,但是有指向父節(jié)點(diǎn)的指針。則可以將其中一條路徑轉(zhuǎn)換為從輸入節(jié)點(diǎn)到根節(jié)點(diǎn)的鏈表,此時(shí)問(wèn)題就可以轉(zhuǎn)化為兩個(gè)鏈表求取第一個(gè)公共子節(jié)點(diǎn)的問(wèn)題。如果也沒(méi)有指向父節(jié)點(diǎn)的指針,就是最為普通的樹(shù)。則可以采用輔助空間存儲(chǔ)從根節(jié)點(diǎn)到輸入節(jié)點(diǎn)的鏈表,然后在求取最后公共子節(jié)點(diǎn)的問(wèn)題。算法如下: */ bool GetNodePath(TreeNode* pRoot,TreeNode* pNode,std::list<TreeNode* > & path) {if(pRoot == pNode)return true;path.push_back(pRoot);bool found = false;std::vector<TreeNode* >::iterator i = pRoot->m_vChildrne.begin();while(!found && i<pRoot->m_vChildrne.end()){found = GetNodePath(*i,pNode,path);++i;}if(!found)path.pop_back();return found; }TreeNode* getLastCommonNode(const std::list<TreeNode* > &path1,const std::list<TreeNode* > & path2) {std::list<TreeNode* >::const_iterator iter1 = path1.begin();std::list<TreeNode* >::const_iterator iter2 = path2.begin();TreeNode* pLastNode = NULL;while(iter1 != path1.end() && iter2 != path2.end()){if(*iter1 == *iter2)pLastNode = *iter1;++ iter1;++ iter2;}return pLastNode; }TreeNode* getLastCommonParent(TreeNode* pRoot,TreeNode* pNode1,TreeNode* pNode2) {if(pRoot == NULL || pNode1 == NULL || pNode2 == NULL)return NULL;std::list<TreeNode* > path1;GetNodePath(pRoot,pNode1,path1);std::list<TreeNode* > path2;GetNodePath(pRoot,pNode2,path2);return GetLastCommonNode(path1,path2); }/* 9、求最小的 K 個(gè)數(shù),若果不能改變數(shù)組順序,建議直接使用堆結(jié)構(gòu)(適合處理海量數(shù)據(jù))。如 stl 里面的優(yōu)先級(jí)隊(duì)列 priority_queue,或者使用紅黑樹(shù)結(jié)構(gòu)的 set、map 等。這里使用 set */ typedef std::multiset<int, std::greater<int> > intSet; typedef std::multiset<int, std::greater<int> >::iterator setIterator;void GetLeastNumber(const std::vector<int> & data,intSet & leastNumbers,int k) {leastNumbers.clear();if(k<1 || k > data.size())return;vector<int>::const_iterator iter = data.begin();for(; iter != data.end(); ++iter){// 先插入 k 個(gè)值if((leastNumbers.size()) < k)leastNumbers.intsert(*iter);else { // 然后在調(diào)整setIterator iterGreastest = leastNumbers.begin();if(*iter < *(leastNumbers.begin())){leastNumbers.erase(iterGreastest);leastNumbers.insert(*iter);}}} }總結(jié)
以上是生活随笔為你收集整理的面试基础算法及编程 第三弹(树(二叉树)相关:主要考察指针相关的操作)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ubuntu12.04 安装 php5.
- 下一篇: 华为S1720, S2700, S570