アルゴリズム検討8日目:幅優先探索/深さ優先探索--2

目次

1. バイナリツリーをマージする 

1. 深さ優先検索

複雑さの分析

2. 幅優先検索

複雑さの分析

次に、各ノードの右隣のノード ポインタを入力します。

 1. レベルトラバーサル:

複雑さの分析

2. 確立された次のポインタを使用する

アイデア

アルゴリズム

複雑さの分析

1. バイナリツリーをマージする 

617. バイナリ ツリーをマージする - LeetCode https://leetcode.cn/problems/merge-two-binary-trees/?plan=algorithms&plan_progress=gzwnnxs

1. 深さ優先検索


深さ優先検索を使用して 2 つのバイナリ ツリーをマージできます。ルート ノードから開始して 2 つのバイナリ ツリーを同時に走査し、対応するノードをマージします。

2 つのバイナリ ツリーの対応するノードは、次の 3 つの状況で存在する可能性があり、状況ごとに異なるマージ方法が使用されます。

2 つのバイナリ ツリーの対応するノードが空の場合、マージされたバイナリ ツリーの対応するノードも空になります。

2 つのバイナリ ツリーの対応するノードのうち 1 つだけが空の場合、マージされたバイナリ ツリーの対応するノードは空ではないノードになります。

2 つのバイナリ ツリーの対応するノードが空でない場合、マージされたバイナリ ツリーの対応するノードの値は、2 つのバイナリ ツリーの対応するノードの値の合計になります。この場合、2 つのノードは次の値を必要とします明示的にマージする必要があります。

ノードをマージした後、ノードの左側と右側のサブツリーをそれぞれマージする必要があります。これは再帰的なプロセスです。

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == nullptr) {
            return t2;
        }
        if (t2 == nullptr) {
            return t1;
        }
        auto merged = new TreeNode(t1->val + t2->val);
        merged->left = mergeTrees(t1->left, t2->left);
        merged->right = mergeTrees(t1->right, t2->right);
        return merged;
    }
};

複雑さの分析

時間計算量: O(min(m,n))、ここで、m と n はそれぞれ 2 つのバイナリ ツリーのノードの数です。深さ優先探索は 2 つのバイナリ ツリーに対して同時に実行され、2 つのバイナリ ツリーの対応するノードが空でない場合にのみ、ノードは明示的にマージされます。より小さいバイナリ ツリー、ノード数。

空間計算量: O(min(m,n))、ここで、m と n はそれぞれ 2 つのバイナリ ツリーのノードの数です。空間の複雑さは再帰呼び出しのレベル数に依存します。再帰呼び出しのレベル数は、小さい方のバイナリ ツリーの最大高さを超えることはありません。最悪の場合、バイナリ ツリーの高さはノードの数と等しくなります。 。

2. 幅優先検索


幅優先検索を使用して 2 つのバイナリ ツリーをマージすることもできます。まず、2 つのバイナリ ツリーが空かどうかを確認します。両方のバイナリ ツリーが空の場合、マージされたバイナリ ツリーも空になります。一方のバイナリ ツリーだけが空の場合、マージされたバイナリ ツリーは空ではない別のバイナリ ツリーになります。

両方のバイナリ ツリーが空でない場合は、最初にマージされたルート ノードの値を計算し、次にマージされたバイナリ ツリーと 2 つの元のバイナリ ツリーのルート ノードから幅優先検索を開始し、最初から開始して各バイナリ ツリーを同時に走査します。ルートノードを作成し、対応するノードをマージします。

マージされたバイナリ ツリーのノードと 2 つの元のバイナリ ツリーのノードをそれぞれ格納するために 3 つのキューが使用されます。最初に、各バイナリ ツリーのルート ノードが対応するキューに追加されます。各キューからノードが取り出されるたびに、元の2つの二分木ノードの左右の子ノードが空かどうかが判定される。2 つの元のバイナリ ツリーの現在のノードの少なくとも 1 つのノードの左の子ノードが空でない場合、マージされたバイナリ ツリーの対応するノードの左の子ノードも空ではありません。右側の子ノードについても同様です。

マージされたバイナリ ツリーの左の子ノードが空でない場合、マージされたバイナリ ツリーの左の子ノードと左のサブツリー全体は、2 つの元のバイナリ ツリーの左の子ノードに基づいて計算される必要があります。次の 2 つのシナリオを考えてみましょう。

両方の元のバイナリ ツリーの左の子ノードが空でない場合、マージされたバイナリ ツリーの左の子ノードの値は、左の子の後の 2 つの元のバイナリ ツリーの左の子ノードの値の合計になります。マージされたバイナリ ツリーのノードが作成され、各バイナリ ツリーの左側の子ノードを対応するキューに追加します。

2 つの元のバイナリ ツリーの左側の子ノードの 1 つが空の場合、つまり、一方の元のバイナリ ツリーの左側のサブツリーが空の場合、マージされたバイナリ ツリーの左側のサブツリーは、もう一方の元のバイナリ ツリーの左側のサブツリーになります。この時点では、空ではない左側のサブツリーをトラバースし続ける必要があるため、左側の子ノードをキューに追加する必要はありません。

右側の子ノードおよび右側のサブツリーの処理方法は、左側の子ノードおよび左側のサブツリーと同じです。

class Solution {
public:
    TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
        if (t1 == nullptr) {
            return t2;
        }
        if (t2 == nullptr) {
            return t1;
        }
        auto merged = new TreeNode(t1->val + t2->val);
        auto q = queue<TreeNode*>();
        auto queue1 = queue<TreeNode*>();
        auto queue2 = queue<TreeNode*>();
        q.push(merged);
        queue1.push(t1);
        queue2.push(t2);
        while (!queue1.empty() && !queue2.empty()) {
            auto node = q.front(), node1 = queue1.front(), node2 = queue2.front();
            q.pop();
            queue1.pop();
            queue2.pop();
            auto left1 = node1->left, left2 = node2->left, right1 = node1->right, right2 = node2->right;
            if (left1 != nullptr || left2 != nullptr) {
                if (left1 != nullptr && left2 != nullptr) {
                    auto left = new TreeNode(left1->val + left2->val);
                    node->left = left;
                    q.push(left);
                    queue1.push(left1);
                    queue2.push(left2);
                } else if (left1 != nullptr) {
                    node->left = left1;
                } else if (left2 != nullptr) {
                    node->left = left2;
                }
            }
            if (right1 != nullptr || right2 != nullptr) {
                if (right1 != nullptr && right2 != nullptr) {
                    auto right = new TreeNode(right1->val + right2->val);
                    node->right = right;
                    q.push(right);
                    queue1.push(right1);
                    queue2.push(right2);
                } else if (right1 != nullptr) {
                    node->right = right1;
                } else {
                    node->right = right2;
                }
            }
        }
        return merged;
    }
};

複雑さの分析

時間計算量: O(min(m,n))、ここで、m と n はそれぞれ 2 つのバイナリ ツリーのノードの数です。幅優先探索は 2 つのバイナリ ツリーに対して同時に実行されます。ノードへのアクセスは、2 つのバイナリ ツリー内の対応するノードが空でない場合にのみ行われるため、アクセスされるノードの数は、バイナリ ツリー内のノードの数を超えません。より小さな二分木。

空間計算量: O(min(m,n))、ここで、m と n はそれぞれ 2 つのバイナリ ツリーのノードの数です。空間の複雑さはキュー内の要素の数に依存しますが、小さいバイナリ ツリー内のノードの数を超えることはありません。

次に、各ノードの右隣のノード ポインタを入力します。

116. 各ノードの次の右のノード ポインターを設定する - LeetCode https://leetcode.cn/problems/populated-next-right-pointers-in-each-node/?plan=algorithms&plan_progress=gzwnnxs

 1. レベルトラバーサル:

質問自体は、バイナリ ツリーの各レベルでノードを接続してリンク リストを形成する必要があります。したがって、直観的には、バイナリ ツリーの階層トラバースを実行できます。階層トラバース プロセスでは、バイナリ ツリーの各レベルのノードを取り出し、それらをトラバースして接続します。

階層トラバーサルは幅優先検索に基づいています。幅優先検索との違いは、幅優先検索では展開のために一度に 1 つのノードのみが取り出されるのに対し、階層トラバーサルでは展開のためにキュー内のすべての要素が取り出されることです。これにより、毎回キューから取り出されて走査される要素が同じ層に属していることが保証されるため、走査プロセス中に各ノードの次のポインタを変更し、次の層の新しいキューを拡張できます。

class Solution {
public:
    Node* connect(Node* root) {
        if (root == nullptr) {
            return root;
        }
        
        // 初始化队列同时将第一层节点加入队列中,即根节点
        queue<Node*> Q;
        Q.push(root);
        
        // 外层的 while 循环迭代的是层数
        while (!Q.empty()) {
            
            // 记录当前队列大小
            int size = Q.size();
            
            // 遍历这一层的所有节点
            for(int i = 0; i < size; i++) {
                
                // 从队首取出元素
                Node* node = Q.front();
                Q.pop();
                
                // 连接
                if (i < size - 1) {
                    node->next = Q.front();
                }
                
                // 拓展下一层节点
                if (node->left != nullptr) {
                    Q.push(node->left);
                }
                if (node->right != nullptr) {
                    Q.push(node->right);
                }
            }
        }
        
        // 返回根节点
        return root;
    }
};

複雑さの分析

時間計算量: O(N)。各ノードは 1 回だけアクセスされます。つまり、ノードはキューからポップされ、次のポインターが確立されます。

空間の複雑さ: O(N)。これは、最終レベルに N/2 ノードが含まれる完全なバイナリ ツリーです。幅優先トラバーサルの複雑さは、レベルの要素の最大数によって異なります。この場合、空間複雑度は O(N) です。

2. 確立された次のポインタを使用する


アイデア

1) ツリーには 2 種類のネクスト ポインタがあります。

最初のケースは、同じ親ノードの 2 つの子ノードを接続する場合です。同一ノードから直接アクセスできるため、以下の操作を行うことで接続が完了します。

node.left.next = node.right


2) 2 番目のケースは、異なる父親の子ノード間の接続を確立しますが、この場合は直接接続することはできません。

各ノードがその親ノードへのポインタを持つ場合、このポインタを通じて次のノードを見つけることができます。ポインタが存在しない場合、接続は次の考え方に従って確立されます。

第Nレベルのノード間にネクストポインタが確立された後、第N+1レベルのノードのネクストポインタが確立される。同じレイヤ内のすべてのノードはネクスト ポインタを介してアクセスできるため、N 番目のレイヤのネクスト ポインタを使用して、N+1 番目のレイヤ ノードのネクスト ポインタを確立できます。

アルゴリズム

1) ルート ノードから開始します。レイヤー 0 にはノードが 1 つしかないため、接続する必要はありません。レイヤー 1 のノードのネクスト ポインターを直接作成するだけです。このアルゴリズムで注意すべき点の 1 つは、N 番目の層ノードの次のポインターを確立するとき、N-1 番目の層にいることです。第N層ノードのネクストポインタが全て確立されたら、第N層に移動し、第N+1層ノードのネクストポインタを確立する。

2)ある層のノードを横断するとき、この層のノードの次のポインタが確立されている。したがって、この層の左端のノードだけを知る必要があり、キューを使用せずにリンク リストでノードをトラバースできます。

3) 上記のアイデアの擬似コードは次のとおりです。

leftmost = root
while (leftmost.left != null) {
    head = leftmost
    while (head.next != null) {
        1) Establish Connection 1
        2) Establish Connection 2 using next pointers
        head = head.next
    }
    leftmost = leftmost.left
}

4) 2 種類のネクスト ポインタ。

        1. 最初のケースでは、2 つの子ノードが同じ親ノードに属しているため、2 つの子ノードの次のポインタは親ノードを介して直接確立できます。

node.left.next = node.right

        2. 2 つ目は、異なる親ノード間で子ノードを接続する場合です。具体的には、1 番目の親ノードの右の子と 2 番目の親ノードの左の子が接続されます。次のポインタは親ノード レベルで確立されているため、最初の親ノードの次のポインタを介して 2 番目の親ノードを直接見つけることができ、その子の間に接続が確立されます。

node.right.next = node.next.left

5) 現在の層の接続が完了したら、次の層に入り、すべてのノードが接続されるまで操作を繰り返します。次の層に入った後、左端のノードを更新し、新しい左端のノードから開始して層内のすべてのノードを走査する必要があります。これは完全な二分木であるため、左端のノードは現在の層の左端のノードの左の子である必要があります。現在の一番左のノードの左の子が存在しない場合は、ツリーの最後のレベルに到達し、すべてのノードの接続が完了したことを意味します。

class Solution {
public:
    Node* connect(Node* root) {
        if (root == nullptr) {
            return root;
        }
        
        // 从根节点开始
        Node* leftmost = root;
        
        while (leftmost->left != nullptr) {
            
            // 遍历这一层节点组织成的链表,为下一层的节点更新 next 指针
            Node* head = leftmost;
            
            while (head != nullptr) {
                
                // CONNECTION 1
                head->left->next = head->right;
                
                // CONNECTION 2
                if (head->next != nullptr) {
                    head->right->next = head->next->left;
                }
                
                // 指针向后移动
                head = head->next;
            }
            
            // 去下一层的最左的节点
            leftmost = leftmost->left;
        }
        
        return root;
    }
};

複雑さの分析

  • 時間計算量: O(N)、各ノードは 1 回だけアクセスされます。

  • スペースの複雑さ: O(1)、追加のノードを保存する必要はありません。

おすすめ

転載: blog.csdn.net/m0_63309778/article/details/126753319