データ構造とアルゴリズム [ツリー]

二分木のプロパティ

完全二分木

ここに画像の説明を挿入
深さは k で、 2 k − 1 2^{k}-1あります2kノードが1 つの二分木は、完全な二分木です。

完全な二分木

完全二分木の定義は次のとおりです。 完全二分木では、埋められない可能性がある最下層のノードを除いて、各層のノードの数が最大値に達し、最下層のノードがすべて集中しています。レイヤーの一番左の位置。最下層が h 番目の層である場合、この層には 1~2^(h-1) 個のノードが含まれます。

二分木の格納方法

連鎖記憶域と順次記憶域を含む
連鎖記憶域のバイナリ ツリーは理解に役立つため、通常、バイナリ ツリーの連鎖記憶域を使用します。
したがって、二分木は配列で表現できることを誰もが理解する必要があります。

ここに画像の説明を挿入

二分木チェーン ストレージ コード

struct TreeNode{
    
    
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x):val(x),left(NULL),right(NULL){
    
    }
};

二分木をトラバースする方法

トラバーサル方式には 2 つのタイプがあります。

二分木の走査方法については、まず深さと幅から区別されます。

  1. 深さ優先トラバーサル: 最初に深く進み、リーフ ノードに遭遇したら戻ります。
  2. 幅優先トラバーサル: レイヤーごとにトラバースします。
    これらの 2 つのトラバーサルは、グラフ理論の最も基本的な 2 つのトラバーサル手法であり、後でグラフ理論を紹介するときに紹介します。

次に、深さ優先トラバーサルと幅優先トラバーサルをさらに拡張して、トラバーサル方法をより詳細に区別します。

  • 深さ優先トラバーサル
    • Preorder traversal (再帰法、反復法)
    • 順序通りのトラバーサル (再帰法、反復法)
    • ポストオーダー トラバーサル (再帰法、反復法)
  • 幅優先トラバーサル
    • 深さ優先トラバーサルの階層的トラバーサル(反復法)
      :前・中・後トラバーサルの3つの順序があり、生徒によってはこれら3つの順序を区別できず、混乱することが多いので、ここでテクニックを教えます。
      ここでの前、中、後は、実際には中間ノードの走査順序を指します. ご存じのとおり、前、中、後という順序は、中間ノードの位置を指します。次のノードのトラバーサル順序を見ると、中間ノードの順序がいわゆるトラバーサル メソッド名の由来であることがわかります。
  • 先行トラバーサル: 中央、左、右
  • 順序通りのトラバーサル: 左中央右
  • ポストオーダー トラバーサル: 左、右、中央

トラバーサルの実装

最後に、バイナリ ツリーでの深さ優先トラバーサルと幅優先トラバーサルの実装について話しましょう。二分木に関連するトピックを扱うとき、再帰的な方法を使用して深さ優先トラバーサルを実現することがよくあります。
前にスタックについて話したとき、スタックは実際には先入れ先出しの再帰的な実装構造であると言いました。つまり、スタックの助けを借りて、実際には前中後順序トラバーサルのロジックを非再帰的に実現できます。(スタックの構造による再帰的な操作を避ける)

幅優先トラバーサルの実装は、通常、二分木層をトラバースするには先入れ先出し構造が必要なため、先入れ先出しキューの特性によって決定されるキューによって実現されます。レイヤーごと。

実際、ここでは、スタックとキューのアプリケーション シナリオについて学習しました。
具体的な実装については後で説明しますが、ここではまずこれらの理論的基礎を理解する必要があります。

二分木と再帰 (二分木の再帰的走査)

二分木に関して言えば, 再帰について話さなければなりません. 多くの学生は再帰に慣れている人もそうでない人もいます. 再帰コードは一般的に非常に短いですが, 読みやすいといつも書いても役に立たない.

不適切な再帰的書き込みの根本的な原因は、それが体系的ではなく、再帰的な方法論がないことです。二分木の前後順の再帰的な書き方を通じて、再帰的な方法論を決定し、複雑な再帰的なトピックを扱います。

まず、再帰アルゴリズムを作成するたびに、まず次の 3 つの要素を決定します。

  1. 再帰関数のパラメーターと戻り値を決定する:再帰プロセス中に処理する必要があるパラメーターを決定し、このパラメーターを再帰関数に追加し、各再帰の戻り値が何であるかを明確にして、戻り値の型を決定します。再帰関数の
  2. 終了条件を決定する:再帰アルゴリズムを記述した後、実行中に、スタック オーバーフロー エラーが発生することがよくあります。つまり、終了条件が書き込まれていないか、または終了条件が正しく書き込まれていません。再帰の各層の情報を保存します。再帰が終了しない場合、オペレーティング システムのメモリ スタックは必然的にオーバーフローします。
  3. **単層再帰のロジックを決定する: **再帰の各層で処理する必要がある情報を決定します。ここで、再帰を実現するために自分自身を繰り返し呼び出すプロセスも繰り返されます。

3 つの要素を割り込んで見つけるにはどうすればよいでしょうか。
プレオーダートラバーサルを例に、その感覚を探ってみましょう!
1. 再帰関数のパラメーターと戻り値を決定します: 事前注文トラバーサル ノードの値を出力したいので、ノードの値をパラメーターに入れるためにベクトルを渡す必要があります。これとは別に、データを処理する必要はありません 戻り値があるため、再帰関数の戻り値の型は void です コードは次のとおりです : ベクトルを除いて、処理する必要はありませ
ん戻り値を持つ必要はありませんが、TreeNode *cur を渡す目的は言及されていませんが、私の推測では、ツリー自体を処理するには、現在処理されているツリーに渡す必要があります。ポインターノードの値を格納するためにベクトルを使用する必要がある理由については、わかりません。後でわかることを願っています)

void traversal(TreeNode* cur, vector<int>& vec)

2. 終了条件を決定します。再帰プロセスでは、再帰の終了をどのように計算しますか? preorder トラバーサルの場合、現在トラバースされているノードが空の場合、再帰が終了したことを意味するため、現在トラバースされたノードが空の場合は、直接戻ります。コードは次のとおりです。

if (cur == NULL) return;

3. 単一レベル再帰のロジックを決定します。事前順序トラバーサルは、中央、左、右の順序であるため、単一レベル再帰のロジックは、最初に中間ノードの値を取得することです (概念単一レベルの再帰ロジックの意味がありません!! まず、単一層の再帰とは何ですか? 再帰を実現するために単一層の再帰を繰り返し呼び出すプロセスとは? 私の理解では、新しいデータが読み込まれるたびに、データをどのように処理する必要がありますか? これが単層再帰のデータ処理ロジックです!!! データ処理が完了したら、それが再帰のロジックです。 !! 続いて再帰のロジック、例えばここで再帰によって処理される次のデータは左部分木のノードなので、左部分木に対して再帰を実行し、次に右部分木を処理し、右部分で再帰を続けますsubtree here!! したがって、ここでいう一段再帰とは、本質的には、データ処理プロセス + 後で処理する必要があるデータのロジック、つまり、後で処理する必要があるロジック データの再帰的な配置が実際にはここでこの再帰的なデータ処理メソッドを呼び出すだけですが、再帰の順序は問題のニーズに基づいている必要があります。たとえば、ここでは事前順序トラバーサルなどです。そのため、読み込みルートを出力して結果に格納する必要があります。まず、左右のサブツリーのデータを表示します!!!)

したがって、コードは次のようになります

vec.push_back(cur->val);    // 中
traversal(cur->left, vec);  // 左
traversal(cur->right, vec); // 右

ここまではまだ理解できないのですが、その理由は TreeNode* の cur と vector &vec の意味がわからないからです。! !

しかし、読者がコード全体を繰り返し読んでいると、突然次のことに気付くでしょう。

class Solution {
    
    
public:
    void traversal(TreeNode* cur, vector<int>& vec) {
    
    
        if (cur == NULL) return;
        vec.push_back(cur->val);    // 中
        traversal(cur->left, vec);  // 左
        traversal(cur->right, vec); // 右
    }
    vector<int> preorderTraversal(TreeNode* root) {
    
    
        vector<int> result; //vector就是我们的遍历结果存储向量
        traversal(root, result);//遍历需要输入树的根节点
        return result;
    }
};

Vector はトラバーサル結果の格納ベクトルです。トラバーサルはツリーに従って段階的に下っていく必要があるため、ツリーのルートを渡す必要があり、ツリーのルートを渡す必要があるため、パラメータ TreeNode* cur を渡す必要があります。

したがって、順序内および順序後のトラバーサルのロジックを記述します。

  1. **再帰関数のパラメーターと戻り値: 再帰、つまりデータ処理ユニットには、受信ツリーのベクトルと受信ストレージ リストが含まれ、値を返す必要はなく、データ処理を行うだけです。
  2. **終了条件:スペースポイントに遭遇したら、すぐに戻る
  3. 単層再帰のロジック: ノード データを読み取るのに適した場所を見つけ、その後の再帰的なデータ フローを調整します。

順序通りのトラバーサル

void traversal(TreeNode *cur, vector<int>& vec){
    
    
	if(cur==NULL) return;
	traversal(cur->left, vec);
	vec.posh_back(cur->val);
	traversal(cur->right, vec);
}

ポスト オーダー トラバーサル

void traversal(TreeNode *cur, vector<int>& vec){
    
    
	if (cur==NULL) return;
	traversal(cur->left, vec);
	traversal(cur->right, vec);
	vec.push_back(cur->val);
}

バイナリ ツリーの非再帰的トラバーサル (反復トラバーサル)

反復の概念: Baidu 百科事典
反復は、フィードバック プロセスを繰り返す活動であり、通常、その目的は、望ましい目標または結果に近づくことです。この繰り返しを「イテレーション」と呼び、イテレーションの結果を次のイテレーションの初期値として使用します。
コンピュータでは、通常、ループ構造プログラミングを使用して、この反復プロセスを実現します。(他の2つのタイプ: 構造化プログラミング、関数で実装されたモジュラープログラミング「Tan Haoqiang C プログラミング」を選択)

スタックとキューの部分で、再帰の実装が次のようになっていることがわかっています。各再帰呼び出しは、ローカル変数、パラメーター値、および関数の戻りアドレスをスタックにプッシュします (再帰が発生すると、次のように理解することもできます)。 、現在の再帰 スタックの現在の実行状態がスタックにプッシュされます) 再帰が戻ると、最後の再帰のパラメーターがスタックの一番上からポップされるため、再帰は前のレベルに戻ることができます。(再帰が戻ったとき、以前にスタックにプッシュされた再帰状態がスタックから順次フェッチされて実行され、再帰に遭遇したときに再び同じ操作が使用されることも理解できます)

したがって、スタックを使用してバイナリ ツリーのトラバーサルを実現することもできます。これは本質的に、スタックを使用して再帰をシミュレートするプロセスです。このプロセスは反復と呼ばれます。

予約注文トラバーサルの繰り返し書き込み

class Solution {
    
    
public:
    vector<int> preorderTraversal(TreeNode* root) {
    
    
        stack<TreeNode*> st;
        vector<int> result;
        if (root == NULL) return result;
        st.push(root);
        while (!st.empty()) {
    
    
            TreeNode* node = st.top();// 中                 
            st.pop();
            result.push_back(node->val);
            if (node->right) st.push(node->right);// 右(空节点不入栈)           
            if (node->left) st.push(node->left);// 左(空节点不入栈)             
        }
        return result;
    }
};

この時点で、反復法を使用して事前注文トラバーサルを作成することは難しくないように見えますが、実際には難しくありません。

このとき、先行トラバーサル コードの順序を少し変更して、インオーダー トラバーサルを出しますか?

実際、それは本当にうまくいきません!

But then, when you use the iterative method to write in-order traversal, you will find that the routine is different. 先行順序トラバーサルの現在のロジックを直接順序トラバーサルに適用することはできません。

インオーダー トラバーサル (反復的なコード記述)

class Solution{
    
    
public:
	vector<int> inorderTraversal(TreeNode* root){
    
    
		vector<int> result;
		stack<TreeNode*> st;
		TreeNode* cur = root;
		while(cur!=NULL||!st.empty()){
    
    
		//只要当前的指针不为空或者栈不空
		if(cur!=NLL){
    
    //如果指针不空
			st.push(cur);//访问节点入栈
			cur=cur->left;//继续往左走
			
		}else{
    
    
			//当往左走空了,开始出栈
			cur = st.top();
			st.pop();
			result.push_back(cur->val);
			cur = cur->right; //往右走
		}
		}
		return result;
	}
	
};

ポスト オーダー トラバーサル

class Solution{
    
    
public:
   vector<int> postorderTraversal(TreeNode* root){
    
    
   	stack<TreeNode*> st;
   	vector<int> result;
   	//注意,当root为空时要返回;
   	if (root==NULL) return;
   	st.push(root);
   	while(!st.empty()){
    
    
   			TreeNode* node = st.top();
   			st.pop();
   			result.push_back(node->val);
   			if (node->left!=NULL) st.push(node->left);
   			if (node->right!=NULL) st.push(node->right);
   		}
   	reverse(result.begin(),result.end());
   	return result;

   }
}

二分木の繰り返し探索(前後順の統一形式)

class Solution {
    
    
public:
    vector<int> inorderTraversal(TreeNode* root) {
    
    
        vector<int> result;
        stack<TreeNode*> st;
        if (root != NULL) st.push(root);
        while (!st.empty()) {
    
    
            TreeNode* node = st.top();
            if (node != NULL) {
    
    
                st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中
                if (node->right) st.push(node->right);  // 添加右节点(空节点不入栈)

                st.push(node);                          // 添加中节点
                st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。

                if (node->left) st.push(node->left);    // 添加左节点(空节点不入栈)
            } else {
    
     // 只有遇到空节点的时候,才将下一个节点放进结果集
                st.pop();           // 将空节点弹出
                node = st.top();    // 重新取出栈中元素
                st.pop();
                result.push_back(node->val); // 加入到结果集
            }
        }
        return result;
    }
};

二分木レベルの順序トラバーサル

2組のコード(覚えるだけ)

反復実装


再帰的な実装

バックトラッキングによる二分木再帰

「コード カプリス レコード」アルゴリズム ビデオ オープン クラス: 再帰はバックトラッキングをもたらします。| LeetCode: 257. バイナリ ツリーのすべてのパス(opens new window)、ビデオと組み合わせてこの問題の解決策を見ると、誰もがこの問題を理解するのにより役立つと思います。
バックトラッキング (バックトラッキングとも呼ばれます)
バックトラッキング関数は、実際には再帰関数です。バックトラッキング関数の単一の実装がないため、バックトラッキングは再帰に含まれる概念です。

バックトラッキング法を理解する方法: バックトラッキングは再帰的なプロセスであり, 再帰を終了する必要があります. バックトラッキング法によって解決される問題は n 分木に抽象化できます. この木の幅は集合のサイズです.対処したい問題を for ループでトラバースします; この木の深さが再帰の深さであり、終了後は層ごとに逆になります。

ここに画像の説明を挿入

一般的に言えば、バックトラッキングメソッドの再帰関数には戻り値がありません.つまり、void.これらの再帰関数の名前は一般的にバックトラッキングです.もちろん、誰もが独自の習慣を持っている可能性がありますが、業界では一般的にこのように呼ばれています. . バックトラッキングメソッドのパラメータは一般的に 多い場合が多いので最初からパラメータを設定しておくと都合が悪い. ロジック部分を書く際にここにパラメータを追加すればよい. 次に、再帰を終了する必要があるため、終了条件を実行する必要があります. 終了条件に達したら、通常は結果を収集します. ほとんどの問題 (サブセットの問題を除く) はリーフ ノードで結果を収集し、サブセットの問題のみが各ノードで結果を収集します。[ここでは理解しにくい。具体的な問題と組み合わせる必要がある]. 終了条件は、通常はリーフ ノードで結果を収集する必要があるため、どのような結果を収集する必要がありますか? たとえば、組み合わせの問題: これらの結果を組み合わせて結果セットに入れることを忘れないでください. 終了条件を処理した後、単一レベル検索のロジックに入ります.
単一レベル検索のロジックは一般に for ループです. この for ループのパラメータは, コレクション内の各要素を処理するのに適しています. 通常, コレクション内の各要素は for ループに置かれ, コレクションはfor ループ. の各要素は、すべての子ノードの数 (処理ノードの数) に対応することもできます.処理ノードはどのノードを処理しますか? たとえば、組み合わせの問題: for ループでノードを処理するプロセス中に、1 と 2 を配列に入れます。そのため、終了条件では、結果を収集するときに 1 と 2 が結果セットに入れられます。処理ノードの下には、再帰、再帰関数、再帰プロセスがあります。つまり、ツリー ダイアグラムで層ごとに下に移動し、再帰の一番下がバックトラック操作です。バックトラック操作は、処理ノードの操作を元に戻すことです。バックトラック操作の意味は元に戻すことです: 組み合わせの問題で、1、2、3、4 が 2 を選択した場合、1、2 がある場合、2 のみがキャンセルされ、3 が入れられると、1、3 のみが削除されます。なれ

おすすめ

転載: blog.csdn.net/adreammaker/article/details/128594745