07、重建二叉樹
題目:
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
返回如下的二叉樹:
3
/ \
9 20
/ \
15 7
分析:
首先分析前序遍歷和中序遍歷的特點:前序遍歷—>根左右; 中序遍歷---->左根右
所以,前序遍歷中,第一個數一定是根節點,又因爲不含重複的數字,所以我們只要在中序遍歷中找到這個數字,則在這個數字左邊的數一定是左子樹,右側的數一定是右子樹。
這樣我們就可以遞歸地構建左子樹和右子樹,之後再和根節點相連即可。
算法思路:
遞歸形式:
- 遞歸函數形式爲 build (preorder, inorder, rootindex, inleftbegin, inrightend);
- preorder 代表前序遍歷數組
- inorder代表中序遍歷數組
- rootindex代表根節點的索引(從0到 length - 1)
- inleftbegin 代表在中序遍歷數組中,左子樹的起始索引值
- inrightend 代表在中序遍歷數組中,右子樹的終止索引值
- 初始化遞歸形式爲 build (preorder, inorder, 0, 0, inorder.length - 1)
- 之後,先根據根節點的索引值,拿到根節點數據,建立根節點
- 接着,在inorder數組上,在索引爲 inleftbegin~~inrightend之間,查找值等於根節點值的位置,拿到索引 i
- 之後,遞歸構建左子樹和右子樹
- 左子樹:在中序遍歷數組上,數據必然落在 inleftbegin~~~i-1之間。而它的根節點必然就是前序遍歷中根節點對應數值的下一個數。
- 遞歸形式:build (preorder, inorder, rootindex + 1, inleftbegin, i - 1);
- 右子樹:在中序遍歷數組上,數據必然落在 i+1~~~~inrightend 之間。而它的根節點必然就是在前序遍歷中根節點對應數值的索引,加上上面左子樹的節點數量之後對應的值。
- 遞歸形式:build ( preorder, inorder, rootindex + (i - inleftbegin + 1), i + 1, inrightend );
- 遞歸結束的條件是
- 根節點的索引超出範圍了
- 中序遍歷數組中左子樹的起始索引大於右子樹的終止索引了
- 沒有在中序遍歷數組中找到對應的根節點的值
- 左子樹:在中序遍歷數組上,數據必然落在 inleftbegin~~~i-1之間。而它的根節點必然就是前序遍歷中根節點對應數值的下一個數。
- 之後,只要將左右子樹和根節點連接起來即可。
具體代碼:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || inorder == null){
return null;
}
return build(preorder,inorder,0,0,inorder.length-1);
}
public TreeNode build(int[] preorder,int[] inorder, int rootindex, int inleftbegin, int inrightend){
if(inleftbegin>inrightend || rootindex >= preorder.length){
return null;
}
TreeNode root = new TreeNode(preorder[rootindex]); //根節點
//在中序遍歷中尋找根節點
int i = inleftbegin;
while(i<=inrightend && inorder[i] != preorder[rootindex]){
i++;
}
if(i>inrightend){ //沒有找到
return null;
}
TreeNode left = build(preorder,inorder,rootindex+1,inleftbegin,i-1);
TreeNode right = build(preorder,inorder,rootindex+(i-inleftbegin+1),i+1,inrightend);
root.left = left;
root.right = right;
return root;
}
}
進一步優化:
可以發現,在上面的代碼中,每次都要進行大量的遍歷才能夠找到根節點在中序遍歷數組中的位置,這會浪費很多時間。
所以,藉助於每個節點的值都不相同這個特性,我們可以先進行一次遍歷,將中序遍歷數組中每個值作爲鍵,將它對應的索引作爲值,以鍵值對的形式存儲起來,這樣在查找一個數值時就是O(1)的時間複雜度了。
但是,提高了算法的空間複雜度。
代碼如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null|| inorder == null){
return null;
}
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i],i);
}
return build(preorder,inorder,map,0,0,inorder.length-1);
}
public TreeNode build(int[] preorder,int[] inorder, Map<Integer,Integer> map, int rootindex, int inleftbegin, int inrightend){
if(inleftbegin>inrightend || rootindex >= preorder.length){
return null;
}
TreeNode root = new TreeNode(preorder[rootindex]); //根節點
//在中序遍歷中尋找根節點
Integer ii = map.get(preorder[rootindex]);
if(ii == null){
return null;
}
int i = ii;
TreeNode left = build(preorder,inorder,map,rootindex+1,inleftbegin,i-1);
TreeNode right = build(preorder,inorder,map,rootindex+(i-inleftbegin+1),i+1,inrightend);
root.left = left;
root.right = right;
return root;
}
}
非遞歸形式(棧實現):
思路:
- 其實和遞歸的思路是一樣的,只是通過棧結構來實現而已。因爲所有的遞歸必然可以等價地通過一個棧來實現!!!
- 其中關鍵的地方可能就是數據在棧裏面如何存放的問題。就是什麼數據要進棧,如何進棧出棧?如何對應?
- 這裏採用兩個棧來實現。
- 第一個棧裏面存放樹的節點,棧頂元素就是當前要處理的當前樹的根節點。
- 第二個棧內主要存放當前這個樹的所有節點在前序和中序遍歷數組中的位置範圍。
- 通過四個數字來表示範圍。
- 棧頂的四個數就對應着目前第一個棧中棧頂元素所對應的子樹的所有節點的範圍。
- 這兩個棧中的元素要互相對應。
- 當棧爲空時,就代表已經處理完畢了。
思路可能寫的不是特別清晰,直接看代碼可能更清楚一點………………
代碼:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 判斷數組是否爲空時,一定要判斷兩個條件,一是數組的引用是否爲null,二是數組的長度是否爲0
if(preorder == null || preorder.length == 0 ||
inorder == null || inorder.length == 0){
return null;
}
// 存放中序遍歷值和腳標的對應關係,提高查找效率
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i<inorder.length; i++){
map.put(inorder[i],i);
}
Stack<Integer> p = new Stack<>(); //存放位置信息
Stack<TreeNode> nodes = new Stack<>(); //存放節點
TreeNode root = new TreeNode(preorder[0]); // 建立根節點
nodes.push(root); //入棧
// 以當前nodes棧中棧頂節點爲根節點的樹,該樹上的所有節點在中序和前序遍歷數組內的位置範圍
// 初始情況肯定就是包含所有的元素
p.push(0);
p.push(preorder.length - 1);
p.push(0);
p.push(inorder.length - 1);
while(!nodes.isEmpty()){ // 循環結束條件是棧爲空
int inend = p.pop(); // 先取出位置信息,注意順序要和入棧順序相反
int inbegin = p.pop();
int preend = p.pop();
int prebegin = p.pop();
TreeNode curroot = nodes.pop(); //取出當前的根節點
curroot.val = preorder[prebegin]; //當前根節點對應的值必然是當前前序遍歷的第一個元素
int inindex = map.get(preorder[prebegin]); //得到當前根節點在中序遍歷中的索引位置
int leftlen = inindex - inbegin; // 當前根節點的左子樹上的所有節點個數
if(leftlen > 0){ // 大於零,說明存在左子樹
TreeNode leftNode = new TreeNode(-1); //建立左子樹根節點
curroot.left = leftNode; // 和當前的根節點建立連接
nodes.push(leftNode); // 左節點入棧
// 將左節點leftNode視爲根節點時,其對應樹上的所有節點在兩個數組中的位置範圍
// 也就是當前根節點curroot對應的左子樹上所有節點在兩個數組中的位置範圍
p.push(prebegin+1);
p.push(prebegin+leftlen);
p.push(inbegin);
p.push(inindex-1);
}
int rightlen = inend - inindex; // 當前根節點的右子樹的節點個數
if(rightlen > 0){ // 大於零,說明存在右子樹
TreeNode rightNode = new TreeNode(-1); //建立右子樹根節點
curroot.right = rightNode;
nodes.push(rightNode); //右節點入棧
p.push(prebegin+leftlen+1); //curroot右子樹的所有節點對應的位置範圍
p.push(preend);
p.push(inindex+1);
p.push(inend);
}
}
return root;
}
}
非遞歸形式(隊列實現)
寫完棧的非遞歸實現之後,觀察上面的代碼可以發現,其實我們在整個構建的過程中,節點入棧之後,每一個節點之間其實並沒有很強的依賴關係。哪一個節點先進行構建貌似並沒有很大差別。
這就催生了另一個相法,如果我不用棧結構,使用隊列結構可以實現嗎?
答案是肯定的!
其實,採用棧結構,相當於我們通過深度優先的思路在進行樹的重建,那自然我們也可以根據廣度優先來進行樹的重建呀。
採用廣度優先,就對應着使用隊列結構來進行樹的重建。
代碼如下(和上面類似,改變的僅僅是將棧換成了隊列):
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder == null || preorder.length == 0 || inorder == null || inorder.length == 0){
return null;
}
Map<Integer,Integer> map = new HashMap<>(); // 存放中序遍歷值和腳標的對應關係
for(int i = 0; i<inorder.length; i++){
map.put(inorder[i],i);
}
Queue<Integer> p = new LinkedList<>(); //存放位置信息
Queue<TreeNode> nodes = new LinkedList<>(); //存放節點
TreeNode root = new TreeNode(preorder[0]);
nodes.offer(root);
p.offer(0);
p.offer(preorder.length - 1);
p.offer(0);
p.offer(inorder.length - 1);
while(!nodes.isEmpty()){
int prebegin = p.poll();
int preend = p.poll();
int inbegin = p.poll();
int inend = p.poll();
TreeNode curroot = nodes.poll(); //取出當前的根節點
curroot.val = preorder[prebegin]; //當前根節點對應的值必然是前序遍歷的當前第一個元素
int inindex = map.get(preorder[prebegin]); //得到當前根節點在中序遍歷中的索引位置
int leftlen = inindex - inbegin; // 當前節點的左子樹的所有節點個數
if(leftlen > 0){ // 大於零,說明存在左子樹
TreeNode leftNode = new TreeNode(-1); //建立左子樹根節點
curroot.left = leftNode; // 和當前的根節點建立連接
nodes.offer(leftNode); // 左節點入隊列
p.offer(prebegin+1); // 左節點作爲根節點時,對應的樹的節點在兩個遍歷數組中的位置
p.offer(prebegin+leftlen);
p.offer(inbegin);
p.offer(inindex-1);
}
int rightlen = inend - inindex; // 當前根節點的右子樹的節點個數
if(rightlen > 0){ // 大於零,說明存在右子樹
TreeNode rightNode = new TreeNode(-1); //建立右子樹根節點
curroot.right = rightNode;
nodes.offer(rightNode);
p.offer(prebegin+leftlen+1);
p.offer(preend);
p.offer(inindex+1);
p.offer(inend);
}
}
return root;
}
}