目次
1. バイナリツリーの事前順序走査
1. 再帰
アイデアとアルゴリズム
まず第一に、バイナリ ツリーの事前順序トラバーサルとは何かを理解する必要があります: ルート ノード - 左サブツリー - 右サブツリーと同じ方法でツリーをトラバースします。左サブツリーまたは右サブツリーにアクセスするときは、同じ方法に従います。ツリー全体を横断するまで横断します。したがって、走査プロセス全体は当然再帰的であり、再帰関数を直接使用してこのプロセスをシミュレートできます。
preorder(root) を定義して、現在ルート ノードに到達している回答を表します。定義によれば、最初にルート ノードの値を答えに追加し、次に preorder(root.left) を再帰的に呼び出してルート ノードの左側のサブツリーを走査し、最後に preorder(root.right) を再帰的に呼び出すだけです。ルート ノードの右側のサブツリーをトラバースする場合、つまり [はい] の場合、再帰終了の条件は空のノードに遭遇することです。
class Solution {
public:
void preorder(TreeNode *root, vector<int> &res) {
if (root == nullptr) {
return;
}
res.push_back(root->val);
preorder(root->left, res);
preorder(root->right, res);
}
vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
preorder(root, res);
return res;
}
};
複雑さの分析
時間計算量: O(n)、n はバイナリ ツリーのノードの数です。各ノードは 1 回だけ通過されます。
空間計算量: O(n)、再帰的プロセス中のスタックのオーバーヘッドです。平均的な場合、O(logn) です。最悪の場合、ツリーはチェーン状になり、O(n) になります。 。
2. 反復
アイデアとアルゴリズム
反復を使用して、メソッド 1 の再帰関数を実装することもできます。この 2 つのメソッドは同等です。違いは、再帰中にスタックが暗黙的に維持され、反復中にこのスタックを明示的にシミュレートする必要があることです。実装の残りの部分と詳細は同じです。詳細については、以下のコードを参照してください。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode*> stk;
TreeNode* node = root;
while (!stk.empty() || node != nullptr) {
while (node != nullptr) {
res.emplace_back(node->val);
stk.emplace(node);
node = node->left;
}
node = stk.top();
stk.pop();
node = node->right;
}
return res;
}
};
複雑さの分析
時間計算量: O(n)、nn はバイナリ ツリーのノードの数です。各ノードは 1 回だけ通過されます。
空間計算量: O(n)。反復プロセス中の明示的なスタックのオーバーヘッドです。平均的な場合、O(logn) です。最悪の場合、ツリーはチェーン状になり、O(n )。
3.モリストラバーサル
アイデアとアルゴリズム
線形時間で一定のスペースのみを占有するプリオーダー トラバーサルを実装する賢い方法があります。この方法は、JH Morris によって 1979 年の論文「Traversing Binary Trees Simply and Couldaly」で初めて提案されたため、Morris トラバーサルと呼ばれています。
Morris トラバーサルの中心となるアイデアは、ツリー内の多数の空きポインタを利用して、スペースのオーバーヘッドを究極的に削減することです。その事前注文トラバーサル ルールは次のように要約されます。
新しい一時ノードを作成し、そのノードをルートにします。
現在のノードの左側の子ノードが空の場合は、現在のノードを回答に追加し、現在のノードの右側の子ノードをトラバースします。
現在のノードの左側の子ノードが空でない場合は、現在のノードの左側のサブツリーで順序トラバーサルの下で現在のノードの先行ノードを検索します。
先行ノードの右の子ノードが空の場合は、先行ノードの右の子ノードを現在のノードに設定します。次に、現在のノードを回答に追加し、先行ノードの右側の子ノードを現在のノードに更新します。現在のノードは、現在のノードの左側の子ノードに更新されます。
先行ノードの右の子ノードが現在のノードである場合、その右の子ノードを空にリセットします。現在のノードは、現在のノードの右側の子ノードに更新されます。
トラバースが完了するまで、ステップ 2 と 3 を繰り返します。
このようにして、モリス走査法を使用してバイナリ ツリーを事前順序で走査し、線形時間および一定空間の走査を実現できます。
class Solution {
public:
vector<int> preorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
TreeNode *p1 = root, *p2 = nullptr;
while (p1 != nullptr) {
p2 = p1->left;
if (p2 != nullptr) {
while (p2->right != nullptr && p2->right != p1) {
p2 = p2->right;
}
if (p2->right == nullptr) {
res.emplace_back(p1->val);
p2->right = p1;
p1 = p1->left;
continue;
} else {
p2->right = nullptr;
}
} else {
res.emplace_back(p1->val);
}
p1 = p1->right;
}
return res;
}
};
複雑さの分析
時間計算量: O(n)、nn はバイナリ ツリーのノードの数です。左サブツリーのないノードは 1 回だけ訪問され、左サブツリーのあるノードは 2 回訪問されます。
空間複雑度: O(1)。すでに存在するポインター (ツリーの空きポインター) のみを操作するため、一定量の追加スペースのみが必要です。
2. 二分木の順序通りの走査
1. 再帰
アイデアとアルゴリズム
まず第一に、バイナリ ツリーの順序トラバーサルとは何かを理解する必要があります。左のサブツリー - ルート ノード - 右のサブツリーと同じ方法でツリーをトラバースし、いつ左のサブツリーまたは右のサブツリーにアクセスするかを理解する必要があります。も同様の方法で、ツリー全体が走査されるまで走査します。したがって、走査プロセス全体は当然再帰的であり、再帰関数を直接使用してこのプロセスをシミュレートできます。
現在ルート ノードに到達している答えを表すように inorder(root) を定義します。定義によれば、必要なのは inorder(root.left) を再帰的に呼び出してルート ノードの左側のサブツリーを走査し、その値を追加することだけです。ルート ノードから答えを取得し、inorder を再帰的に呼び出します (root.right)。ルート ノードの右のサブツリーをトラバースします。再帰的終了の条件は、空のノードに遭遇することです。
class Solution {
public:
void inorder(TreeNode* root, vector<int>& res) {
if (!root) {
return;
}
inorder(root->left, res);
res.push_back(root->val);
inorder(root->right, res);
}
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
inorder(root, res);
return res;
}
};
複雑さの分析
時間計算量: O(n)、nn はバイナリ ツリー ノードの数です。バイナリ ツリー トラバーサルでは、各ノードは 1 回だけ訪問されます。
空間の複雑さ: O(n)。空間の複雑さは再帰的なスタックの深さに依存し、バイナリ ツリーがチェーンの場合、スタックの深さは O(n) レベルに達します。
2. 反復
反復を使用してメソッド 1 の再帰関数を実装することもできます。この 2 つのメソッドは同等です。違いは、再帰中にスタックが暗黙的に維持されることですが、反復中にこのスタックを明示的にシミュレートする必要があることです。その他はすべて同じです。具体的な実装は以下のコードで確認できます。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> stk;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
res.push_back(root->val);
root = root->right;
}
return res;
}
};
複雑さの分析
時間計算量: O(n)、nn はバイナリ ツリー ノードの数です。バイナリ ツリー トラバーサルでは、各ノードは 1 回だけ訪問されます。
空間の複雑さ: O(n)。空間の複雑さはスタックの深さに依存し、バイナリ ツリーがチェーンの場合、スタックの深さは O(n) レベルに達します。
3. モリスの順序トラバーサル
アイデアとアルゴリズム
Morris トラバーサル アルゴリズムは、バイナリ ツリーをトラバースするためのもう 1 つの方法であり、非再帰的な順序トラバーサルの空間複雑さを O(1) に削減できます。
モリス走査アルゴリズムの全体的な手順は次のとおりです (現在走査されているノードが x であると仮定します)。
1. x に左の子がない場合、まず x の値を応答配列に追加し、次に x の右の子、つまり x =x.right にアクセスします。 2. x に左の子がある場合、右端のノードを見つけます
。 x の左側のサブツリー (つまり、順序トラバーサルにおける左側のサブツリーの最後のノード、順序トラバーサルにおける x の先行ノード) 上で、それを先行ノードとして記録します。\textit{predecessor}predecessor の右側の子が空かどうかに応じて、次の操作を実行します。
先行者の右の子が空の場合は、その右の子を x にポイントし、次に x の左の子、つまり x =x.left にアクセスします。
先行者の右の子が空でない場合、その右の子はこの時点で x を指します。これは、x の左側のサブツリーを走査したことを示します。先行者の右の子を空のままにし、x の値を配列を回答し、x にアクセスします。右側の子は x=x.right です。
完全なツリーにアクセスするまで、上記の操作を繰り返します。
実際、プロセス全体で必要な作業はあと 1 つだけです。現在通過しているノードが x であると仮定し、x の左側のサブツリーにある右端のノードの右側の子をポイントし、このポインタを通じて通過したことを知ることができます。左側のサブツリーをスタックを通じて維持する必要がなく、スタックのスペースの複雑さが解消されます。
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
TreeNode *predecessor = nullptr;
while (root != nullptr) {
if (root->left != nullptr) {
// predecessor 节点就是当前 root 节点向左走一步,然后一直向右走至无法走为止
predecessor = root->left;
while (predecessor->right != nullptr && predecessor->right != root) {
predecessor = predecessor->right;
}
// 让 predecessor 的右指针指向 root,继续遍历左子树
if (predecessor->right == nullptr) {
predecessor->right = root;
root = root->left;
}
// 说明左子树已经访问完了,我们需要断开链接
else {
res.push_back(root->val);
predecessor->right = nullptr;
root = root->right;
}
}
// 如果没有左孩子,则直接访问右孩子
else {
res.push_back(root->val);
root = root->right;
}
}
return res;
}
};
複雑さの分析
時間計算量: O(n)、nn は二分探索ツリー内のノードの数です。モリス トラバーサルの各ノードは 2 回訪問されるため、合計の時間計算量は O(2n)=O(n) になります。
空間複雑度: O(1)。
3. 二分木の事後ソート
1. 再帰
アイデアとアルゴリズム
まず第一に、バイナリ ツリーのポストオーダー トラバーサルとは何かを理解する必要があります: 左のサブツリー - 右のサブツリー - ルート ノードと同じ方法でツリーをトラバースします。左のサブツリーまたは右のサブツリーにアクセスするときは、同じ方法に従い、ツリー全体を走査するまで走査します。したがって、走査プロセス全体は当然再帰的であり、再帰関数を直接使用してこのプロセスをシミュレートできます。
現在ルート ノードに到達している回答を表す postorder(root) を定義します。定義によれば、postorder(root->left) を再帰的に呼び出してルート ノードの左側のサブツリーを走査し、次に postorder(root->right) を再帰的に呼び出してルート ノードの右側のサブツリーを走査するだけで済み、最後にルート ノードの値を答えに追加します。 以上です。再帰終了の条件は、空のノードに遭遇することです。
class Solution {
public:
void postorder(TreeNode *root, vector<int> &res) {
if (root == nullptr) {
return;
}
postorder(root->left, res);
postorder(root->right, res);
res.push_back(root->val);
}
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
postorder(root, res);
return res;
}
};
複雑さの分析
時間計算量: O(n)、n は二分探索ツリー内のノードの数です。各ノードは 1 回だけ通過されます。
空間計算量: O(n)、再帰的プロセス中のスタックのオーバーヘッドです。平均的な場合、O(logn) です。最悪の場合、ツリーはチェーン状になり、O(n) になります。 。
2. 反復
反復を使用して、メソッド 1 の再帰関数を実装することもできます。この 2 つのメソッドは同等です。違いは、再帰中にスタックが暗黙的に維持され、反復中にこのスタックを明示的にシミュレートする必要があることです。実装の残りの部分と詳細は同じです。詳細については、以下のコードを参照してください。
class Solution {
public:
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
stack<TreeNode *> stk;
TreeNode *prev = nullptr;
while (root != nullptr || !stk.empty()) {
while (root != nullptr) {
stk.emplace(root);
root = root->left;
}
root = stk.top();
stk.pop();
if (root->right == nullptr || root->right == prev) {
res.emplace_back(root->val);
prev = root;
root = nullptr;
} else {
stk.emplace(root);
root = root->right;
}
}
return res;
}
};
複雑さの分析
時間計算量: O(n)、n は二分探索ツリー内のノードの数です。各ノードは 1 回だけ通過されます。
空間計算量: O(n)。反復プロセス中の明示的なスタックのオーバーヘッドです。平均的な場合、O(logn) です。最悪の場合、ツリーはチェーン状になり、O(n )。
3.モリストラバーサル
アイデアとアルゴリズム
線形時間で一定の空間のみを占有するポストオーダートラバーサルを実装する賢い方法があります。この方法は、JH Morris によって 1979 年の論文「Traversing Binary Trees Simply and Couldaly」で初めて提案されたため、Morris トラバーサルと呼ばれています。
Morris トラバーサルの中心となるアイデアは、ツリー内の多数の空きポインタを利用して、スペースのオーバーヘッドを究極的に削減することです。ポストオーダートラバーサルのルールは次のように要約されます。
新しい一時ノードを作成し、そのノードをルートにします。
現在のノードの左側の子ノードが空の場合は、現在のノードの右側の子ノードをトラバースします。
現在のノードの左側の子ノードが空でない場合は、現在のノードの左側のサブツリーで順序トラバーサルの下で現在のノードの先行ノードを検索します。
先行ノードの右の子ノードが空の場合、先行ノードの右の子ノードが現在のノードに設定され、現在のノードが現在のノードの左の子ノードに更新されます。
先行ノードの右の子ノードが現在のノードである場合、その右の子ノードを空にリセットします。現在のノードの左の子ノードから先行ノードまでのパス上のすべてのノードを逆順に出力します。現在のノードは、現在のノードの右側の子ノードに更新されます。
トラバースが完了するまで、ステップ 2 と 3 を繰り返します。
このようにして、モリス トラバーサル法を使用して二分探索ツリーを事後順序でトラバースし、線形時間および一定空間のトラバースを実現できます。
class Solution {
public:
void addPath(vector<int> &vec, TreeNode *node) {
int count = 0;
while (node != nullptr) {
++count;
vec.emplace_back(node->val);
node = node->right;
}
reverse(vec.end() - count, vec.end());
}
vector<int> postorderTraversal(TreeNode *root) {
vector<int> res;
if (root == nullptr) {
return res;
}
TreeNode *p1 = root, *p2 = nullptr;
while (p1 != nullptr) {
p2 = p1->left;
if (p2 != nullptr) {
while (p2->right != nullptr && p2->right != p1) {
p2 = p2->right;
}
if (p2->right == nullptr) {
p2->right = p1;
p1 = p1->left;
continue;
} else {
p2->right = nullptr;
addPath(res, p1->left);
}
}
p1 = p1->right;
}
addPath(res, root);
return res;
}
};
複雑さの分析
時間計算量: O(n)、n はバイナリ ツリーのノードの数です。左サブツリーのないノードは 1 回だけ訪問され、左サブツリーのあるノードは 2 回訪問されます。
空間複雑度: O(1)。すでに存在するポインター (ツリーの空きポインター) のみを操作するため、一定量の追加スペースのみが必要です。