前文
2021.1.20 より前に書かれたもの
私は LeetCode を磨いていません。この期間は始まったばかりで、目が見えなくなることだらけです。Zhihu にアクセスして問題の解決方法を検索するだけで、答えもさまざまです。その中でも、質問をモジュール化して書くのが最も効果的だと思います。これにより、方向性がわかるだけでなく、質問間のつながりが見つけやすくなり、経験をまとめることが容易になります。「Labudadong のアルゴリズムチートシート」を読んだので、バイナリツリーから始めることにしました。この本によれば、二分木における 3 つの走査方法をマスターすれば、二分木の問題のほとんどは解けるそうです。ここ数日何度かブラッシングをして、いくつかの二分木の魅力も感じています。確かに、それは再帰と反復にほかならず、これら 2 つの核となる考え方をマスターすると、いくつかのアイデアが得られるようになります。問題を磨く過程での最大の問題は、答えを読みたくて仕方がなくなることです。おそらくこれは、自分の頭を使いたくない、ただ特定の答えを作りたいだけの人々の怠惰なのかもしれません。考え方。これはお勧めできません。ゆっくり修正していきたいと思います。すべては学ぶことで得られると私は常に信じています。一緒に進歩していきましょう。
最前列のヒント
この記事は主に自分のメモと記録を目的としており、リコウ公式サイトのトピック番号を入力すると任意のトピックを検索することができます。LeetCodeではサブモジュールの話題も提供していますが、サブモジュールはあまり良くないと思うので、自分なりにまとめてみます。質問はすべて Java で実装されています。私のメモが間違っていると思われる場合は、教えてください。私のメモがくだらない場合は、質問のリストを見ていただければ、質問を見つける時間を節約できるかもしれません。
必読書
バイナリ ツリーのトピックは主に 2 つのアルゴリズム、再帰と BFS です。再帰には、バイナリ ツリーの前順序、順序、後順序の走査が含まれます。多くのトピックの基本テンプレートは、これら 3 つの走査モードです。このうちプリオーダートラバーサルの話題が多くを占めており、まずルートノードに対する操作を決定し、次にルートノードの左右のサブツリーに対する操作を決定する。BFS は主にバイナリ ツリー シーケンス トラバーサルの要件であり、多くのトピックは層シーケンス トラバーサルによって実現できます。さらに、Morris トラバーサルは一定空間での二分木トラバーサルを実現できるので、チェックしてみる価値があります。
LeetCode バイナリ ツリー トピック コレクション
- 前文
- 1. 2 つの数値の P1 和
- 2. P94バイナリツリーのインオーダートラバーサル
- 3. P95 異なる二分探索木 II
- 4. P96 さまざまな二分探索木 I
- 五、P98検証二分探索木
- 6. P99 復元二分探索木
- セブン、P100同木
- 8、P101 対称二分木
- 9、P102 二分木のレベル順走査
- 10. P103 二分木のジグザグ列走査
- 11. P104バイナリツリーの最大深さ
- 12. P105 プレオーダーおよびインオーダーのトラバーサル シーケンスからバイナリ ツリーを構築する
- 13. P106 順序および事後探索シーケンスから二分木を構築する
- 14. P107 二分木のレベル順走査 II
- 15. P108 順序付き配列を二分探索木に変換する
- 十六、P109 順序付きリンクリスト変換二分探索木
- 17、P876 リンク リストの中間ノード
- 十八、P1382 二分探索木のバランスをとるため
- 19、P110 バランス バイナリ ツリー
- 20. P111二分木の最小深さ
- 21. P112 パスサムⅠ
- 22. P113 パスサムⅡ
- 23. P114 二分木をリンクリストに展開する
- 24. P116 は、各ノードの右隣のノード ポインタを埋めます。
- 25. P117 各ノードの右隣のノードポインタ II を埋める
- 26、P226 二分木を反転する
- 27、P654 最大のバイナリ ツリー
- 28、P652 重複サブツリーを探しています
1. 2 つの数値の P1 和
これは偽のバイナリ ツリー トピックであり、LeetCode のコード作成とトピック送信手順を理解するために使用されます。
- トピックの説明
整数配列 nums と整数ターゲット値 target が与えられた場合、配列内で合計がターゲット値となる 2 つの整数を見つけて、それらの配列の添字を返してください。
各入力に対して答えは 1 つだけであると想定できます。ただし、配列内の同じ要素を 2 回使用することはできません。
回答は任意の順序で返すことができます。
- ソリューション
(1) 暴力的な列挙
は、配列を 2 回走査し、nums[j] = target - nums[i] を見つけ、2 つの for ループで解決できます。これは最も基本的な問題解決方法であり、ほとんどのアプリケーションで使用されます。場合によってはタイムアウトになってしまいます。
(2) ハッシュテーブル
方法 1 の難点は、target - nums[i] を見つけることですが、target - nums[i] を格納しておけば、その後の検索時に添え字の値に応じたランダムアクセスが可能になります。保存方法は、ハッシュテーブルにtarget - nums[i]の値があれば見つかったことを意味し、ハッシュ添字を直接返す、というものです。 target - nums[i] 値の値がない場合は、それ自体に一致する残りの値が常に存在するように、独自の nums[i] を追加します。
(3) 二分探索
以前 Luogu でブラシを使っていたとき、二分探索モジュールでこの問題があったことを思い出します。配列 nums で二分検索を使用します。検索のターゲットは target - nums[i] で、成功した場合は添え字値 Mid が返され、失敗した場合は -1 が返されます。LeetCode のトピックは失敗しません。つまり、ターゲット値に等しい数値が常に 2 つ存在します。
- コード
/**
* 1, 暴力求解
* 略
*/
/**
* 2,哈希表
*/
class Solution {
public int[] twoSum(int[] nums, int target) {
HashMap<Integer,Integer> maptable = new HashMap<Integer,Integer>();
int len = nums.length;
for( int i = 0 ; i < len ; i++){
if(maptable.containsKey(target - nums[i])){
return new int[]{
maptable.get(target - nums[i]),i};
}else{
maptable.put(nums[i],i);
}
}
return new int[0];
}
}
/**
* 3,二分查找
* 结果超时
*/
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] tmp = new int[2];
for(int i =0; i < nums.length; i++){
tmp[0] = i;
tmp[1] = BinarySearch(nums, target - nums[i]);
}
return tmp;
}
public int BinarySearch(int[] nums, int x){
//二分查找要对nums先进行排序
Arrays.sort(nums);
int l = 0;
int r = nums.length;
int mid = (l + r) / 2;
while(l < r){
if(nums[mid] == x){
return mid;
}
if(nums[mid] < x){
l = mid + 1;
}
if(nums[mid] > x){
r = mid;
}
}
return -1;//题目给出的数组总能找到答案,这个返回值有无均可。
}
}
- 結果分析
(1) 暴力的解法の計算量は O(N2)
(2) ハッシュテーブルの計算量は O(N)、検索時間は O(1)
(3) 二分探索の計算量は O( NlogN)
- アルゴリズムの概要
この種のトピックは、実際には私たちが学ぶための考え方です。つまり、1 つの質問に複数の解決策があり、将来的には 1 つの解決策に複数の質問、つまりその種のテンプレート トピックを使用する可能性があります。ルーティンさえ分かれば、ちょっとしたブレインストーミングでテーマを思いつくことはできるかもしれませんが、もちろん、より難しいテーマについては、まだ深く掘り下げて慎重に考える必要があると思います。自分の意見を持つのが一番です。
2. P94バイナリツリーのインオーダートラバーサル
- トピックの説明
バイナリ ツリーのルート ノード ルートを指定すると、その順序トラバーサルを返します。
- ソリューション
(1) 再帰
再帰は非常に簡単で、最初にルートノードをたどり、次に左右のノードを順番にたどります。
(2) 反復
反復は再帰より少し面倒です。スタックを自分で維持する必要があり、再帰時にスタックを維持できますが、経験によれば、再帰は本質的に関数呼び出しであるため、反復の方が再帰よりも時間がかかりません。 、反復によりこの部分の時間が節約されます。反復の考え方は次のとおりです。ツリーの左端のノードを見つけ、順序トラバーサルのルールに従って左ノード、ルート ノード、右ノードを順番に出力します。では、このアイデアをどのように実現するのでしょうか? 左端のノードが見つかった場合にのみ出力されることがわかりました。ルート ノードから左端のノードまでのバイナリ ツリーを想像してください。左端のノードが最初に出力され、ルート ノードが最後の出力になります。これは、スタックの先入れ後出し機能? したがって、スタックを使用して実装します。
- コード
//递归方法
class Solution {
//递归问题要记得将list设置成全局
List<Integer> list = new ArrayList<Integer>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null){
return list;
}
// if(root.left != null)
inorderTraversal(root.left);
list.add(root.val);
// if(root.right != null)
inorderTraversal(root.right);
return list;
}
}
//迭代方法
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<Integer>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode cur = root;
while(cur != null || !stack.empty()){
//这是一个或运算,有一个为真值就可以进入循环,当两者都为false时才跳出循环
while(cur != null){
stack.add(cur);
cur = cur.left;
}
cur = stack.pop();
list.add(cur.val);
cur = cur.right;
}
return list;
}
}
- 結果分析
どちらの方法も同じ時間計算量 O(n) を持ちます。
- アルゴリズムの概要
これら 2 つのメソッドはマスターしなければならないメソッドです。これは多くのトピックのプロトタイプである可能性があり、この再帰と順序どおりの走査の反復を利用して他のいくつかのトピックを達成できるからです。振り返ってみると、問題の解決策がどれだけ似ているかがわかります。変更したい点を見つけるだけです。
3. P95 異なる二分探索木 II
- トピックの説明
整数 n を指定すると、1 ... n 個のノードで構成されるすべての二分探索ツリーが生成されます。
- ソリューション
私の考え:ツリーを生成するときは
、1、ツリーの生成 2、ツリーの出力 1 の 2 つのステップに分けるべきです。
各ルート ノードは異なるツリーを生成でき、ルート ノード i の左側のサブツリーは 1...i で構成され、右側のサブツリーは i+1...n で構成されます。
2. Main に実装する必要があります。自分で生成したツリーを返すだけで済みます。
問題解決のアイデア:
ルート ノード i の左側のサブツリーは 1...i で構成され、右側のサブツリーは i+1...n で構成されます。左のサブツリーと右のサブツリーを再帰的に分割し、最後にサブツリーをマージします。左のサブツリー セット内のツリーは、右のサブツリー セット内のルート ノードに接続されます。
- コード
class Solution {
public List<TreeNode> generateTrees(int n) {
if(n == 0){
return new LinkedList<TreeNode>();
}
return generateTrees(1, n);
}
public List<TreeNode> generateTrees(int start, int end) {
List<TreeNode> allTrees = new LinkedList<TreeNode>();
if(start > end){
allTrees.add(null);
return allTrees;
}
//枚举所有可能的根节点
for(int i =start; i<= end; i++){
//生成左子树
List<TreeNode> leftTrees = generateTrees(start, i-1);
//生成右子树
List<TreeNode> rightTrees = generateTrees(i+1, end);
//子树合并
for(TreeNode left : leftTrees){
for(TreeNode right : rightTrees){
TreeNode cur = new TreeNode(i);
cur.left = left;
cur.right = right;
allTrees.add(cur);
}
}
}
return allTrees;
}
}
- 結果分析
複雑すぎてどうやって分析すればいいのか分からないのでアドバイスをお願いします。
- アルゴリズムの概要
まだ再帰的に部分木を分割していますが、分割するときに特徴を探す必要があります。たとえば、この質問に含まれる特徴は、二分探索木の左の部分木のルート ノードと右の部分木のルート ノードの大小関係です。ルート ノード i 左のサブツリーは 1...i で構成され、右のサブツリーは i+1...n で構成されます。バイナリ ツリー全体は、左と右のサブツリー セットのデカルト積です。P96 を実行した後、理解が深まるかもしれません。
4. P96 さまざまな二分探索木 I
- トピックの説明
動的プログラミング、見ないでください。元の問題はサブ問題に分解でき、サブ問題は再利用できます。二分
探索ツリーの場合、左側のサブツリーはルート ノード以下でなければならず、右側のサブツリーはルート ノード以下である必要があります。サブツリーはルート ノード以上でなければなりません
整数 n が与えられた場合、ノード 1 ... n を持つ二分探索ツリーはいくつありますか?
- ソリューション
ノードが空の場合はケースが 1 つだけあり、ノードが 1 の場合はケースが 1 つだけ
G(n): 長さ n のシーケンスの二分探索木の数
F(i): i を次のようにとります。ルートノードの二分木の数
G(n) = F(1)...+F(n)
i がルートノードの場合、左側のサブツリーノードの数は i-1、右側のサブツリーノードの数は ni になります。
F( i) = G(i-1) G(ni)
、つまり、左側のサブツリー セットの数と右側のサブツリー セットの数 上記 2 つの式を組み合わせると、 G(n) = G(0)
が得られます。
*G(n-1) + G(1)*G(n-2)...
- コード
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0] = 1;//F(0) = 1
dp[1] = 1;//F(1) = 1
for(int i = 2 ; i <= n; i++){
for(int j = 1; j <= i ; j++){
dp[i] += dp[j - 1] * dp[i - j];
//G(n) = G(0)*G(n-1) + G(1)*G(n-2)...
}
}
return dp[n];
}
}
- 結果分析
時間計算量は O(n) です。
- アルゴリズムの概要
これは動的プログラミングのトピックであり、このバイナリ ツリー シリーズのトピックとは基本的にまったく関係がありません。そのため、新しく開設された別のノート「動的プログラミング、気にしないでください」に含まれています。上記の質問 P95 では主にすべてのバイナリ ツリーを生成できますが、この質問では数値を返すことのみが必要であり、これにより、別の方法で解決することが決定されます。
五、P98検証二分探索木
- トピックの説明
二分木が与えられた場合、それが有効な二分探索木であるかどうかを判断します。
二分探索ツリーに次の特性があるとします。
ノードの左側のサブツリーには、現在のノードよりも小さい数値のみが含まれます。
ノードの右側のサブツリーには、現在のノードより大きい数値のみが含まれます。
左右のサブツリーはすべて、それ自体が二分探索ツリーでなければなりません。
- ソリューション
インオーダートラバーサルでは、最初に配列を保存すると、配列内の順序が増分されます。これは false
[1 2 3 4 5 6]—>[1 5 3 4 2 6]
- コード
class Solution {
public boolean isValidBST(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
inorder(root, list);
int len = list.size();
// boolean flag;
for(int i = 0; i < len - 1; i++){
//题目描述中等于也是不符合的。
if(list.get(i).val >= list.get(i+1).val){
return false;
}
}
return true;
}
public void inorder(TreeNode root, List<TreeNode> list){
if(root == null){
return ;
}
inorder(root.left, list);
list.add(root);
inorder(root.right, list);
}
}
- 結果分析
ノードを順番に走査し、サイズを何度も比較するには、追加の配列ストレージが必要です。
- アルゴリズムの概要
順序どおりの走査は非再帰的な方法で実現できるため、このトピックに対する私の方法はまだ改善する必要があります。
6. P99 復元二分探索木
- トピックの説明
二分探索ツリーのルート ノード root を考えると、このツリー内の 2 つのノードが誤って交換されました。このツリーの構造を変更せずに復元してください。
- ソリューション
二分探索木の性質を利用して、インオーダートラバーサルはデジタルシーケンスの順序で出力され、順序に従わない 2 つのノードを見つけてそれらを交換するだけで、正しい二分木が得られます。 。
- コード
class Solution {
public void recoverTree(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>();
inorder(root, list);
findSwap(root, list);
}
//中序遍历
public List<TreeNode> inorder(TreeNode root, List<TreeNode> list){
if(root == null){
return list;
}
inorder(root.left, list);
list.add(root);
inorder(root.right, list);
return list;
}
//寻找需要交换的两个结点并交换
public void findSwap(TreeNode root, List<TreeNode> list){
int len = list.size();
TreeNode x = null;
TreeNode y = null;
//因为是两两比较,所以比较到倒数第二个就可以了
for(int i = 0; i < len-1; i++){
if(list.get(i).val > list.get(i+1).val){
y = list.get(i+1);
if(x == null){
x = list.get(i);
}
}
}
int tmp;
if(x != null && y != null){
tmp = x.val;
x.val = y.val;
y.val = tmp;
}
}
}
- 結果分析
空間複雑度は O(n) です。トピックの要件を満たしていません。
- アルゴリズムの概要
この質問にはモリス トラバーサル ソリューションがあり、将来的には実装してみることができます。
セブン、P100同木
- トピックの説明
2 つのバイナリ ツリーが与えられた場合、それらが同じかどうかを確認する関数を作成します。
2 つのツリーは、構造的に同一であり、ノードの値が同じであれば、同一とみなされます。
- ソリューション
再帰
- コード
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
//用递归或广度优先搜索
//先判断两个父节点是否相同
if(p == null && q == null){
return true;
}else if(p == null || q == null){
return false;
}else if(p.val != q.val){
return false;
}
// 如果不为空就判断值是否相同
else {
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
}
- アルゴリズムの概要
このトピックは非常に単純です。
8、P101 対称二分木
- トピックの説明
二分木が与えられた場合、それが鏡像対称であるかどうかを確認します。
- ソリューション
(1) 再帰的方法
ポインタ p、q を 2 つ設定し、1 つは左に、もう 1 つは右に移動し、それらが等しいかどうかをチェックします。
(2) 反復方式
考え方は再帰と同じですが、再帰をキューの格納形式に変換する点が異なります
。
- コード
//方法一
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return check(root.left, root.right);
}
public boolean check(TreeNode p, TreeNode q){
if(p == null && q == null){
return true;
}
if(p == null || q == null){
return false;
}
if(p.val != q.val){
return false;
}
return check(p.left, q.right) && check(p.right, q.left);
}
}
//方法二
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return check(root, root);
}
public boolean check(TreeNode p, TreeNode q){
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(p);//两个根节点入队列
queue.offer(q);
while(!queue.isEmpty()){
p = queue.poll();
q = queue.poll();
if(p == null && q == null){
//???
continue;
}
if(p == null || q== null){
//这就相当于两个结点不一致
return false;
}
if(p.val != q.val){
//结点值不一致
return false;
}
//比较两个对称位置的结点知否相同
queue.offer(p.left);
queue.offer(q.right);
queue.offer(p.right);
queue.offer(q.left);
}
return true;
}
}
- 結果分析
どちらの方法もすべてのノードを走査します。
- アルゴリズムの概要
質問が異なれば解決策も異なりますので、アルゴリズムの意味をただ覚えるだけでなくしっかりと理解できるようになりたいと思います。
9、P102 二分木のレベル順走査
- トピックの説明
二分木が与えられた場合、その木を順番にたどって得られたノード値を返してください。(つまり、レイヤーごとに、左から右にすべてのノードを訪問します)。
- ソリューション
レイヤー順序トラバーサルの問題を解決するには、キューによって実現する必要があります。キューの先入れ先出し特性は、レイヤーごとのトラバーサルの要件と一致しています。これは実際には BFS のアイデアです。最初にルートノードをキューに入れ、ルートノードがキューから出たらその左右の子ノードをキューに入れて、2層目のノードを取得するという順番で操作することで、レイヤー順序のトラバーサルの結果を取得できます。さらに、トピックの要件に従って、2 次元配列を返す、つまり各層を分離する必要がありますが、このとき、ソース プログラムを少し変更するだけで済みます。上位層のノードがキューから出てくると、下位層のノードが入ったばかりであり、上位層のノードがすべてキューから出てくると、下位層のノードが入ったばかりであることがわかります。 。したがって、キューのサイズをリアルタイムで返せば、レイヤーの出力の問題は解決できます。
- コード
//层序遍历模板
class Solution {
public void levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
}
}
//按层返回的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new ArrayDeque<>();
List<List<Integer>> list = new ArrayList<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
List<Integer> l = new ArrayList<>();
for(int i =0 ; i < n; i++){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
}
list.add(l);
}
return list;
}
}
- 結果分析
各ノードはキューに 1 回入り、時間計算量は O(N) です。
- アルゴリズムの概要
上記のコード部分では、レイヤー順序のトラバーサルのテンプレートを示しましたが、レイヤー順序のトラバーサルだけを見ると非常に簡単で、キューを使用するだけです。トピックの要件を満たしている場合は、若干の改善が可能です。これは、通常質問を作成する場合には当てはまりません。一連のテンプレートは、いくつかの質問を打ち負かして返す可能性があります。これには、1 つのインスタンスから要約して推論を引き出すのが得意であることが必要です。
層シーケンスのトラバーサルのトピックについては、8 つのトピックを含む別のメモがあります。リンクは次のとおりです: 8 つのトピックを含む
コードのセット、層シーケンスのトラバーサルは失われない。
10. P103 二分木のジグザグ列走査
- トピックの説明
バイナリ ツリーを指定すると、そのノード値のジグザグ レベル順序トラバーサルを返します。(つまり、最初は左から右にトラバースし、次に右から左にというように、レイヤー間を交互にトラバースします)。
- ソリューション
(1) 階層トラバーサル + マーキング
まず二分木の階層順序トラバーサルを実装し、これをベースにいくつかの修正を加えてジグザグ階層順序トラバーサルを実現します。ルートノードが第 0 層の場合、奇数番目の層ノードが逆の順序で出力されることがわかります。奇数レベルまたは偶数レベルをマークする変数レベルを追加します。そのためには、キューなどのデータ構造に精通している必要があります。Java の ArrayDeque はパフォーマンスが良く、機能が豊富で、スタック、キュー、両端キューをシミュレートできるため、誰でも使用することをお勧めします。この方法には問題があります。要素を取得するために ArrayDeque の先頭と末尾のみを使用し、要素を追加するときは必ず最後に追加するため、出力結果が乱れて要件を満たしていません。
(2) 改善 1: 要素は依然としてチームの先頭で取得されますが、要素を追加する場合、奇数偶数のレイヤーに従って、先頭に右から左に追加され、末尾に左から左に追加されます。右。ただし、合格した例は 13/33 件のみですが、エラーはまだ存在します。
(3) 改善2: 取得する要素は依然としてチームの先頭で取得され、追加された要素はチームの最後に追加されますが、ノードの値を追加する際にパリティ層に従って追加されます。その代わり。
- コード
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
int level = 0;
while(!queue.isEmpty()){
level += 1;
Deque<Integer> l = new LinkedList<Integer>();
int n = queue.size();
for(int i = 0; i < n ; i++){
//始终在队列头部取结点
TreeNode node = queue.poll();
if(level % 2 == 0){
//偶数层,从右到左输出,加到双端队列头部
l.addFirst(node.val);
}else{
//奇数层,从左向右输出,加到双端队列尾部
l.addLast(node.val);
}
if(node.left != null)
queue.add(node.left);
if(node.right != null)
queue.add(node.right);
}
list.add(new LinkedList<>(l));//需要进行类型转换
}
return list;
}
}
- 結果分析
各ノードは 1 回トラバースされ、空間の複雑さは O(n) になります。
- アルゴリズムの概要
話題を変えますが、実際には、これに基づいて層シーケンスのトラバーサルの理解を調べ、データ構造がどのように使用されているかを調べることになります。したがって、データ構造をよく練習すれば、アルゴリズムによる問題解決のための別のツールを手に入れることができます。
11. P104バイナリツリーの最大深さ
- トピックの説明
二分木が与えられた場合、その最大の深さを見つけます。
バイナリ ツリーの深さは、ルート ノードから最も遠いリーフ ノードまでの最長パス上のノードの数です。
説明: リーフノードは、子ノードのないノードを指します。
- ソリューション
(1) 再帰で実装します。バイナリ ツリーの深さを計算することは、ルート ノードからリーフ ノードまでの最も遠いノードの数を計算することであることがわかります。では、どうやって計算するのでしょうか?最も簡単に考えられるのは、ノードの数を計算することです。リーフ ノードに移動するときに 1 を加算するだけです。深さの計算はどうなるでしょうか。ルート ノードの左側のサブツリーのノード数と右側のサブツリーのノード数を調べて、2 つのサイズを比較します。
(2) 幅優先検索の実装。考え方を変えて、二分木の最大の深さを求める、つまり二分木の層数を求めることですよね。上の 2 つの質問はレイヤー順序のトラバーサルに関するものであることを思い出してください。トピックでバイナリ ツリーのノードを 2 次元配列の形式で層ごとに返す必要がある場合は、BFS をわずかに変更し、現在の層のすべてのノードをキューから取り出して毎回展開します。したがって、あと 1 つ設定するだけで済みます。可変レコード層の数でこの問題は解決しないでしょうか?
- コード
// DFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
int left = maxDepth(root.left) + 1;
int right = maxDepth(root.right) + 1;
return Math.max(left, right);
}
}
//BFS
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){
return 0;
}
Queue<TreeNode> queue = new ArrayDeque<>();
queue.add(root);
int count = 0;//记录层数
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
count ++;
}
return count;
}
}
- 結果分析
時間の点では、両者に違いはなく、すべてのノードを通過する必要があるため、時間計算量は O(n) です。スペースにはわずかな違いがあり、再帰ではスタックを維持する必要があり、スタックのサイズはバイナリ ツリーの深さに依存し、BFS メソッドはキューのサイズに依存します。
- アルゴリズムの概要
3 問続けて解くと、二分木の幅優先探索についてはある程度理解できたので、今後は同様の問題を独立して解けるようになりたいと思っています。
12. P105 プレオーダーおよびインオーダーのトラバーサル シーケンスからバイナリ ツリーを構築する
- トピックの説明
ツリーの事前順序および順序トラバーサルに基づいてバイナリ ツリーを構築します。
注:
ツリー内に重複する要素はないと仮定できます。
- ソリューション
再帰を使用して問題を解決します。プレオーダートラバーサルによれば二分木のルートノードを知ることができ、インオーダートラバーサルによればルートノードの左右のノードを知ることができます。再帰的思考: ルート ノードを見つけて、その左右の子ノードを探します。左右の子ノードは独自のルート ノードを見つけて、すべてのノードが走査されるまで再帰します。
具体的には: 事前順序走査に従ってルート ノードを見つけてから、ルート ノードに従って順序通り走査でルート ノードの位置を見つけて、左右のサブツリーの数を決定できるようにします。 2 つの配列に対する 2 つのポインタで、それぞれ前方と後方の位置をマークします。位置が順序どおりでない場合は、再帰の終了となります。順序どおりの走査ではルート ノードの位置を繰り返し見つける必要があるため、ハッシュ テーブルを使用して順序どおりの走査でルート ノードの位置をマークします。
- コード
class Solution {
//哈希表设置为全局
HashMap<Integer, Integer> map;
public TreeNode buildTree(int[] preorder, int[] inorder) {
map = new HashMap<>();
//创建哈希表
int n = preorder.length;
for(int i =0; i< n ;i++){
map.put(inorder[i], i);
}
return myBuild(preorder, inorder, 0, n-1, 0, n-1);
}
public TreeNode myBuild(int[] preorder, int[] inorder, int pre_left, int pre_right, int in_left, int in_right){
//递归出口
if(pre_left > pre_right){
return null;
}
//或者
if(in_left > in_right){
return null;
}
//创建根节点
TreeNode root = new TreeNode(preorder[pre_left]);
//获取根节点在中序的位置
int root_index = map.get(preorder[pre_left]);
//左子树的大小
int subTree = root_index - in_left;
//获取左右子树并划分
//左子树
root.left = myBuild(preorder, inorder, pre_left+1, pre_left+subTree, in_left, root_index-1);
//右子树
root.right = myBuild(preorder, inorder, pre_left+subTree+1, pre_right, root_index+1, in_right);
return root;
}
}
- 結果分析
時間計算量は O(n) です。
- アルゴリズムの概要
この質問は比較的大きな問題であり、LeetCode では中程度の難易度として定義されていますが、解決策を読む前に質問するつもりはありません。おそらく私にはできない側面がいくつかあります:
(1) 再帰を使用し、2 つの走査特性を使用してバイナリ ツリーを構築することだけを考えていましたが、左側と左側を分割するためにノードの数を使用するとは予想していませんでした。右のサブツリー。
(2) ノードの接続方法がわかりません。私が以前に行った二分木の再帰問題を分析すると、このように考えることができると思います。どのような状況で再帰終了が発生するのでしょうか? つまり、ノードが空の場合は null を返し、サブツリーの分割にノードが 1 つだけある場合はルート ノードに接続され、再帰関数がレイヤーごとに返す場合は接続されて 1 つのノードが形成されます。完成したツリー。
(3) 再帰的な出口が何になるのかさえ考えずに。
13. P106 順序および事後探索シーケンスから二分木を構築する
- トピックの説明
ツリーの順走査および事後走査に基づいてバイナリ ツリーを構築します。
注:
ツリー内に重複する要素はないと仮定できます。
- ソリューション
前の質問のアイデアを試してみましょう。後続の走査の最後の要素はルート ノードであり、順序どおりの走査でルート ノードの位置を見つけることで、左右のサブツリーを分割できます。左右の部分木の数に応じて後順走査で次の部分木の分割範囲を求めます。
再帰的に。
- コード
class Solution {
//哈希表来存储
private HashMap<Integer, Integer> map;
public TreeNode buildTree(int[] inorder, int[] postorder) {
map = new HashMap<>();
int n = inorder.length;
for(int i = 0; i < n; i++){
map.put(inorder[i], i);
}
return myBuild(inorder, postorder, 0, n-1, 0, n-1);
}
public TreeNode myBuild(int[] inorder, int[] postorder, int in_left, int in_right, int post_left, int post_right){
//递归出口
if(in_left > in_right){
return null;
}
if(post_left > post_right){
return null;
}
//创建根节点
TreeNode root = new TreeNode(postorder[post_right]);
//获取根节点在中序位置
int rootindex = map.get(root.val);
//获取左子树大小
int subTree = rootindex - in_left;
//连接左子树
root.left = myBuild(inorder, postorder, in_left, rootindex-1, post_left, post_left+subTree-1);
//连接右子树
root.right = myBuild(inorder, postorder, rootindex+1, in_right, post_left+subTree, post_right-1);
return root;
}
}
- 結果分析
時間計算量は O(n) です。時間はユーザーの 98% を上回っていますが、スペースはユーザーの 16% しか上回っておらず、スペースの利用効率が良くないことがわかります。
- アルゴリズムの概要
サブツリー分割の範囲と境界点には特に注意してください。注意しないと狂気を引き起こす可能性があります。
14. P107 二分木のレベル順走査 II
- トピックの説明
バイナリ ツリーを指定すると、そのノード値のボトムアップのレベル順の走査を返します。(つまり、リーフ ノードが配置されているレイヤーからルート ノードが配置されているレイヤーまで、レイヤーごとに左から右にトラバースします)
- ソリューション
このトピックと P102 Binary Tree Layer Order Traversal I の違いは、ノードの各層を逆の順序で出力することです。最も簡単な方法は、以前のベースに補助スタックを追加することです。もう一つの方法はリストを変更する方法で、要素を追加するたびにその要素を先頭に追加して逆出力を実現します。
- コード
//方法一
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Stack<List<Integer>> stack = new Stack<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
stack.add(l);
}
while(!stack.isEmpty()){
list.add(stack.pop());
}
return list;
}
}
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
List<List<Integer>> list = new LinkedList<>();
Stack<List<Integer>> stack = new Stack<>();
Queue<TreeNode> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
List<Integer> l = new ArrayList<>();
int n = queue.size();
while(n > 0){
TreeNode node = queue.poll();
l.add(node.val);
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
list.add(0, l);
//public void add(int index,E element):将元素添加到指定索引位置
}
return list;
}
}
- 結果分析
時間計算量は O(n) です。
- アルゴリズムの概要
2 つの方法の時間とスペースの消費量は同様ですが、後者の方がわずかに優れています。以前の P102 と比較すると、わずかな変更のみが必要です。ですから、質問を増やしても問題はありません。あ、デメリットとしてはハゲるかもしれません。
15. P108 順序付き配列を二分探索木に変換する
- トピックの説明
昇順にソートされた配列を高さのバランスが取れた二分探索ツリーに変換します。
この質問において、高さバランスの取れた二分木とは、二分木の各ノードの左右の部分木の高さの差の絶対値が 1 を超えないことを意味します。
- ソリューション
このトピックでは、配列を昇順で与えています。つまり、要素がルート ノードであることがわかっている限り、ルート ノードの左右のサブツリーは、当然のことながら要素の左側と右側の要素になります。配列内で。再帰的除算はもう終わりではないでしょうか? このトピックでは、高さのバランスがとれたバイナリ ツリーを構築するように求めています。つまり、ルート ノードの両側のノードの数は基本的に同じである必要があるため、毎回、分割のために中央の要素が選択されます。毎回、要素の半分がルートノードの左右のサブツリーとして分割されるので、高さは同じになります。左側に 2 つ、右側に 2 つあると思います。数値を作成するときは、最初に次のことを行う必要があります。左右のノードを作成します。これはまだ同じ高さではありません。
また、答えは一意ではありません。
- コード
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return DFS(nums, 0, nums.length - 1);
}
public TreeNode DFS(int[] nums, int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode node = new TreeNode(nums[mid]);
node.left = DFS(nums, left, mid - 1);
node.right = DFS(nums, mid + 1, right);
return node;
}
}
- 結果分析
時間計算量: O(n)。
空間計算量: O(logn)、n は配列の長さです。空間計算量は戻り値を考慮しないため、空間計算量は主に再帰スタックの深さに依存し、再帰スタックの深さは O(logn) です。
- アルゴリズムの概要
この質問は簡単に言えますが、私はそれが簡単ではないと思い、部分木の分割、再帰、BST の性質について調べました。ある時点で行き詰まったら、うまくいかなくなると思います。
- 似たような話題
P1382、P876、P109
十六、P109 順序付きリンクリスト変換二分探索木
- トピックの説明
要素が昇順に並べ替えられた単一リンク リストを指定して、それを高さのバランスが取れた二分探索ツリーに変換します。
この質問において、高さバランスの取れた二分木とは、二分木の各ノードの左右の部分木の高さの差の絶対値が 1 を超えないことを意味します。
- ソリューション
この質問と前の質問の違いは、この質問ではリンク リストが使用されており、要素の場所を直接特定できないことです。では、除算のルートノードを見つけるにはどうすればよいでしょうか?
最も簡単な方法は、まずリンクされたリストを配列に変換してから、上記の方法に従って計算することです。
もう 1 つの方法は、2 つのポインタを設定する方法です。つまり、高速ポインタと低速ポインタの方法です。2 ステップを速く、1 ステップを遅くすることで、ちょうど 2 倍の関係になります。これにより、高速ポインタが最後に到達したときに、低速ポインタが正確に位置します。中間の位置。
- コード
//方法一
class Solution {
public TreeNode sortedListToBST(ListNode head) {
//别学我,哈哈哈,太多零了
int[] nums = new int[1000000];
int i =0;
while(head != null){
nums[i ++] = head.val;
head = head.next;
}
//这里的i-1要特别注意
return DFS(nums, 0, i - 1);
}
public TreeNode DFS(int[] nums, int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode node = new TreeNode(nums[mid]);
node.left = DFS(nums, left, mid - 1);
node.right = DFS(nums, mid + 1, right);
return node;
}
}
class Solution {
public TreeNode sortedListToBST(ListNode head) {
return DFS(head, null);
}
public ListNode getMid(ListNode left, ListNode right){
ListNode slow = left;
ListNode fast = left;
while(fast != right && fast.next != right){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
public TreeNode DFS(ListNode left, ListNode right){
if(left == right){
return null;
}
ListNode mid = getMid(left, right);
TreeNode node = new TreeNode(mid.val);
node.left = DFS(left, mid);
node.right = DFS(mid.next, right);
return node;
}
}
- 結果分析
- アルゴリズムの概要
これら 2 つの問題のうち最も基本的なものは、ルート ノードの場所の検索です。次のタイトルはこの問題を説明したものです。
17、P876 リンク リストの中間ノード
- トピックの説明
ヘッド ノードが head である空でない単一リンク リストを指定すると、リンク リストの中間ノードを返します。
中間ノードが 2 つある場合は、2 番目の中間ノードを返します。
- ソリューション
前述の高速ポインタ方式と低速ポインタ方式
- コード
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
十八、P1382 二分探索木のバランスをとるため
- トピックの説明
二分探索木が指定された場合は、バランスの取れた二分探索木を返してください。新しく生成されたツリーは、元のツリーと同じノード値を持つ必要があります。
二分探索木において、各ノードの 2 つのサブツリー間の高さの差が 1 を超えない場合、その二分探索木はバランスが取れていると呼ばれます。
施工方法が複数ある場合はいずれかをご返却ください。
- ソリューション
順序付き配列と順序付きリンク リストをバランスのとれたバイナリ ツリーに変換する方法はすでに実行済みなので、この問題に対する最も簡単な解決策は、最初にバイナリ ツリーを順番にたどって、結果を配列に格納し、次にそれに応じて分割することです。以前の方法に戻ります。
問題の復習に注意:二分探索木をバランスさせるためには二分探索木が前提であり、通常の二分木であればこの方法は使えません。
- コード
class Solution {
private ArrayList<Integer> list;
public TreeNode balanceBST(TreeNode root) {
if(root == null){
return null;
}
list = new ArrayList<>();
inorder(root);
return DFS(0, list.size() - 1);
}
public void inorder(TreeNode root){
if(root == null){
return ;
}
inorder(root.left);
list.add(root.val);
inorder(root.right);
}
public TreeNode DFS(int left, int right){
if(left > right){
return null;
}
int mid = (left + right)/2;
TreeNode root = new TreeNode(list.get(mid));
root.left = DFS(left, mid - 1);
root.right = DFS(mid + 1, right);
return root;
}
}
- 結果分析
インオーダートラバーサルでは各ノードが 1 回トラバースされ、ツリーの構築時に各ノードが 1 回トラバースされます。時間計算量は O(n) です。
- アルゴリズムの概要
上記の 3 つの本体も同様の方法で解くことができ、そのうちの 1 つの本質を理解している限り、1 つの事例から他のケースについての推論を引き出すことができます。
19、P110 バランス バイナリ ツリー
- トピックの説明
与えられた二分木が、高さのバランスがとれた二分木であるかどうかを判断します。この質問では、高さバランスの取れた二分木は、二分木の各ノードの左右のサブツリー間の高さの差の絶対値が 1 を超えないもの
として定義されます。
- ソリューション
平衡二分木かどうかを判断するためなので、左右のノードの高さを計算して比較すればよく、高さの差の絶対値が1以下であれば平衡二分木、そうでなければ平衡二分木となります。 、 そうではない。
バランスのとれたバイナリ ツリー内の各サブツリーもバランスのとれたバイナリ ツリーでなければならないことに注意してください。
- コード
//方法一
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null){
return true;
}
int left = balanced(root.left);
int right = balanced(root.right);
if(Math.abs(left - right) <= 1
&& isBalanced(root.left)//左子树
&& isBalanced(root.right)//右子树
){
return true;
}
return false;
}
public int balanced(TreeNode root){
if(root == null){
return 0;
}
int left = balanced(root.left) + 1;
int right = balanced(root.right) + 1;
return left>right ? left : right;
}
}
上記のメソッドでは、高さを計算するためにバランス メソッドを繰り返し呼び出す必要があり、非効率的です。isBalance メソッドで繰り返し呼び出されていることがわかりますが、どうすれば最適化できるでしょうか? 1 つ目の方法は、最初にツリー全体を判断し、次にサブツリーを判断することです。
最初に部分木が正しいかどうかを判断すれば、判断を繰り返す必要はありません。
//方法二
class Solution {
public boolean isBalanced(TreeNode root) {
if(root == null){
return true;
}
return balanced(root) != -1;
}
public int balanced(TreeNode root){
if(root == null){
return 0;
}
int left = balanced(root.left);
int right = balanced(root.right);
if(Math.abs(left - right) > 1
|| left == -1 || right == -1
){
return -1;
}
return left>right ? left+1 : right+1;
}
}
- 結果分析
最初の方法は 2 番目の方法よりも効率が低く、より多くの関数呼び出しが必要になります。前者はトップダウン、後者はボトムアップです。
- アルゴリズムの概要
これは本質的に再帰的であるため、二分木の高さの計算方法を理解している限り、この問題は簡単に解決できます。
20. P111二分木の最小深さ
- トピックの説明
二分木が与えられた場合、その最小の深さを見つけます。
最小深さは、ルート ノードから最も近いリーフ ノードまでの最短パス上のノードの数です。
注: リーフ ノードは、子ノードを持たないノードを指します。
- ソリューション
この話題に聞き覚えはありますか? はい、二分木の高さを計算するだけではないでしょうか? じゃあ来て。
注: 前のトピックでは最大値を返しましたが、ここでは最小値を返す必要があります。問題が発生します。ルート ノードの左側のサブツリーが空で、右側のサブツリーが空でない場合、最小値は深さは右側でなければなりません サブツリーが優先され、最小値が返される場合は、左側のサブツリーの 0 が返されます 変更方法は、ルート ノードを出口として使用する代わりに、再帰的出口として葉ノードを使用することです。それは空です。
- コード
class Solution {
public int minDepth(TreeNode root) {
//只判断这个会出错
if(root == null){
return 0;
}
if(root.left == null && root.right == null){
return 1;
}
int tmp = Integer.MAX_VALUE;
if(root.left != null)
tmp = Math.min(minDepth(root.left) , tmp);
if(root.right != null)
tmp = Math.min(minDepth(root.right) , tmp);
return tmp + 1;
}
}
- 結果分析
時間計算量は O(N) です。
- アルゴリズムの概要
バイナリツリーの最大深さとの違いに注意してください
21. P112 パスサムⅠ
- トピックの説明
バイナリ ツリーのルート ノード root と、ターゲットの合計を表す整数の targetSum を指定して、ツリー内にルート ノードからリーフ ノードへのパスがあるかどうかを判断します。このパス上のすべてのノード値の合計は次のようになります。目標合計 targetSum に等しい。
- ソリューション
方法 1: 再帰。
各パスの合計を計算し、この合計がターゲットと同じであれば、パスが見つかります。しかし、これには問題があります。パスの合計を再帰的に計算すると何を返すのでしょうか? 合計値が返された場合、左側のサブツリーをトラバースして右側のサブツリーをトラバースすると、合計値は右側のサブツリーにも影響します。方法を変更すると、トラバース時にパスとターゲットを sum - root.val に変更し、各比較は現在のノードがターゲットと同じかどうかになります。
方法 2:
BFS バイナリ ツリーのトラバーサルは再帰と BFS を使用して実装されることが多いため、合計するときに BFS を使用することもできます。BFS を使用するには、新しいキューを開いて各ノード パスの合計値を記録する必要があります。
- コード
//方法一
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
//判断该树是否为空
if(root == null){
return false;
}
//判断是否为叶节点
if(root.left == null && root.right == null){
return root.val == targetSum;
}
return hasPathSum(root.left, targetSum - root.val)||hasPathSum(root.right, targetSum - root.val);
}
}
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
Queue<TreeNode> queue = new ArrayDeque<>();
Queue<Integer> sum = new ArrayDeque<>();
//判断该树是否为空
if(root == null){
return false;
}
queue.add(root);
sum.add(root.val);
while(!queue.isEmpty()){
TreeNode node = queue.poll();
int pathSum = sum.poll();
//判断是否为叶节点
if(node.left == null && node.right == null){
//不能直接返回,否则二者不相等将无法继续判断
if(pathSum == targetSum){
return true;
}
}
if(node.left != null){
queue.add(node.left);
sum.add(pathSum + node.left.val);
}
if(node.right != null){
queue.add(node.right);
sum.add(pathSum + node.right.val);
}
}
return false;
}
}
- 結果分析
時間計算量は同じで、どちらも O(n) です。
- アルゴリズムの概要
再帰と BFS テンプレートをマスターしてみませんか。
22. P113 パスサムⅡ
- トピックの説明
バイナリ ツリーとターゲット合計が与えられた場合、その合計が指定されたターゲット合計に等しい、ルート ノードからリーフ ノードまでのすべてのパスを見つけます。
説明: リーフノードは、子ノードのないノードを指します。
- ソリューション
この質問と前の質問の違いは、この質問では判断が必要なだけでなく、すべてのパスを出力する必要があることです。これは、パスを記録するために単一のデータ構造を設定する必要があることを意味します。
- コード
class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
//记录最终结果
res = new LinkedList<>();
//记录路径
path = new LinkedList<>();
DFS(root, targetSum);
return res;
}
public void DFS(TreeNode root, int targetSum){
if(root == null){
return ;
}
path.offerLast(root.val);
int sum = targetSum - root.val;
//判断是否为叶节点
if(root.left == null && root.right == null){
if(sum == 0){
res.add(new LinkedList<>(path));
}
}
DFS(root.left, sum);
DFS(root.right, sum);
path.pollLast();
}
}
- 結果分析
各ノードは 1 回横断され、時間計算量は O(n) です。
- アルゴリズムの概要
問題の解き方を見ずにできたのでレベルが低いので改善が必要です。
23. P114 二分木をリンクリストに展開する
- トピックの説明
バイナリ ツリーのルート ノード ルートを指定して、それを単一リンク リストに展開してください。
展開された単一リンク リストでも TreeNode を使用する必要があります。ここで、右のサブポインタはリンク リスト内の次のノードを指し、左のサブポインタは、ポインタは常に null です。
展開された単一リンク リストは、バイナリ ツリーの事前順序トラバーサルと同じ順序になっている必要があります。
- ソリューション
タイトルの意味は元のツリーに変更を加えられるようにすることであるため、新しい TreeNode を宣言することは受け入れられません。まず、事前順序走査に従ってリンク リストにツリーを保存し、次にリンク リストをトピックで必要なツリーに変換します。
改良版:
LeetCode の説明: プリオーダー トラバーサルによって各ノードが訪問される順序は、ルート ノード、左側のサブツリー、右側のサブツリーであることに注意してください。ノードの左側の子が空の場合、ノードを展開する必要はありません。ノードの左側の子が空でない場合は、ノードの左側のサブツリーの最後のノードが訪問された後に、ノードの右側の子が訪問されます。このノードの左側のサブツリーで最後に訪問したノードは、左側のサブツリーの右端のノードであり、このノードの先行ノードでもあります。したがって、問題は現在のノードの先行ノードを見つけることになります。
具体的な方法は、現在のノードの左の子ノードが空でない場合、その左のサブツリーで右端のノードを先行ノードとして見つけ、現在のノードの右の子ノードを先行ノードの右の子ノードに割り当てることです。次に、現在のノードの左の子を現在のノードの右の子に割り当て、現在のノードの左の子を null に設定します。現在のノードを処理した後、すべてのノードが処理されるまで、リンク リスト内の次のノードの処理を続けます。
私の理解:
バイナリ ツリーをリンク リストに変換する場合、プリオーダー トラバーサルの順序に従い、プリオーダー トラバーサルは常に左のサブツリーをトラバースした後、右のサブツリーをトラバースします。具体的には、現在のノードの左のサブツリーをトラバースします。サブツリーの右端のノードに移動し、右のサブツリーをトラバースします。この場合、右のサブツリー全体を右端のノードのサブツリーとして使用し、次に左のサブツリー全体をルート ノードの右のサブツリーとして使用し、前後に循環すると、新しいスペースを申請できません。これで連結リストの変換は完了です。
- コード
//方法一
class Solution {
List<TreeNode> list = new ArrayList<>();
public void flatten(TreeNode root) {
if(root == null){
return ;
}
preorder(root);
for(int i = 0; i< list.size()-1; i++){
TreeNode cur = list.get(i);
TreeNode right = list.get(i+1);
cur.left = null;
cur.right = right;
}
}
public void preorder(TreeNode root){
if(root == null){
return ;
}
list.add(root);
preorder(root.left);
preorder(root.right);
}
}
//方法二
class Solution {
public void flatten(TreeNode root) {
TreeNode cur = root;
//判断当前节点是否为空
while(cur != null){
//判断是否有左子树
if(cur.left != null){
//不为空,找到其最右节点
TreeNode tmp = cur.left;
while(tmp.right != null){
tmp = tmp.right;
}
//找到之后,将当前根节点的右子树赋值给它
tmp.right = cur.right;
//将整个左子树作为根节点的右子树
cur.right = cur.left;
//根节点左子树置空
cur.left = null;
}
cur = cur.right;
}
}
}
- 結果分析
時間計算量は O(n) です。
- アルゴリズムの概要
ノードの値ではなく、ツリー ノードが配列に格納されることに注意してください。ノードを保存することによってのみ、ノードの情報を使用して完全なツリーを構築できますが、値を保存するだけであり、ツリーを完全に構築することはできません。
2 番目の方法では、新しい空間を申請しないこと、つまり空間複雑度が O(1) であることを実現できます。
このトピックを完了したら、モリス アルゴリズムを試してみることができます。P99 はモリス アルゴリズムを使用することもできます。つまり、モリスはバイナリ ツリーをトラバースします。
24. P116 は、各ノードの右隣のノード ポインタを埋めます。
- トピックの説明
完全な二分木があるとすると、そのすべての葉ノードは同じレベルにあり、各親ノードには 2 つの子ノードがあります。バイナリ ツリーは次のように定義されます:
struct Node { int val; Node *left; Node *right; Node *next; }このポインターが次の右のノードを指すように、次のポインターをそれぞれ埋めます。次の右のノードが見つからない場合、次のポインタは NULL に設定されます。最初は、すべての次のポインターが NULL に設定されます。
- ソリューション
(1) 空間複雑度は O(n) です。レイヤー順序トラバーサルの考え方によれば、キューに入れられたノードの各レイヤーについて、それがこのレイヤーの最後のノードである場合は NULL を指し、それ以外の場合は次のキューに入れられたノードを指します。
(2) 空間複雑度は O(1) です。2 種類のネクスト ポインタを決定します。1 つはルート ノードの左右のノード間の接続で、もう 1 つは異なる親ノードの左右のサブツリー間の接続です。具体的な方法: この層の左端のノードを見つけて、その 2 種類の接続を決定し、この層の他のノードに対して同じことを行います。
(3) 再帰的方法。
- コード
//方法一
class Solution {
public Node connect(Node root) {
Deque<Node> queue = new ArrayDeque<>();
if(root != null){
queue.add(root);
}
while(!queue.isEmpty()){
int n = queue.size();
while(n > 0){
Node node = queue.poll();
//判断是不是本层最后一个节点
if(n == 1){
node.next = null;
}else{
node.next = queue.element();
//element 只返回不移出
}
if(node.left != null){
queue.add(node.left);
}
if(node.right != null){
queue.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
Node leftmost = root;//最左节点
//如果最左节点的左子树为空,那么说明到了最后一层了
while(leftmost.left != null){
Node head = leftmost;
//head就是当前层的各个节点,为空说明当前层完成了遍历
while(head != null){
//第一种连接:根节点的左右子树
head.left.next = head.right;
//第二种连接:不同根节点之间的连接
if(head.next != null){
//当前节点在该层有next节点
head.right.next = head.next.left;
}
//继续本层的下一个节点
head = head.next;
}
//当前层完成后,继续下一层
//去下一层的最左节点
leftmost = leftmost.left;
}
return root;
}
}
//方法三
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
if(root.left != null){
root.left.next = root.right;
if(root.next != null){
root.right.next = root.next.left;
}
connect(root.left);
connect(root.right);
}
return root;
}
}
- 結果分析
3 つの方法の時間計算量は同等で、後の 2 つはスペースの観点からこのトピックの要件を満たしています。
- アルゴリズムの概要
階層トラバーサルは、階層トラバーサルの意味を理解していればシンプルで理解しやすいですが、定空間法の難しい部分は 2 種類のネクスト ポインタをどのように決定するかであり、方法が決定してから試行できます。
そして次の質問との違いを見てください。
25. P117 各ノードの右隣のノードポインタ II を埋める
- トピックの説明
バイナリ ツリー
struct Node { int val; Node *left; Node *right; Node *next; }が次のポインターのそれぞれに入力されるとすると、このポインターが次の右のノードを指すようにします。次の右のノードが見つからない場合、次のポインタは NULL に設定されます。最初は、すべての次のポインターが NULL に設定されます。
- ソリューション
この質問と前の質問の違いは、この質問が完全なバイナリ ツリーではないことです。これにより、ネクスト ポインタの接続タイプが変更されます。
(1) 空間の複雑さ O(n) 階層トラバーサルの方法が今でも使用されています。
(2) 空間複雑度 O(1). このとき、次の接続は 2 種類ではなく、複数種類になります。
これによりスレッドが失われます。左端のノードが空の場合は、それも見つけることができません。解決策は、ダミー ノードを左端に追加して、次のポインターを連結することです。考え方は前の質問と同じで、このレイヤーをトラバースしながら、次のレイヤーの次のポインターを作成し、この方法に従ってレイヤーごとにトラバースします。(各レイヤーをリンクされたリストと考えてください)
- コード
//方法一
class Solution {
public Node connect(Node root) {
Deque<Node> q = new ArrayDeque<>();
if(root != null){
q.add(root);
}
while(!q.isEmpty()){
int n = q.size();
while(n > 0){
Node node = q.poll();
if(n != 1){
node.next = q.element();
}
if(node.left != null){
q.add(node.left);
}
if(node.right != null){
q.add(node.right);
}
n --;
}
}
return root;
}
}
//方法二
class Solution {
public Node connect(Node root) {
if(root == null){
return root;
}
//cur负责第 N 层的遍历
Node cur = root;
while(cur != null){
//哑节点在N + 1层
Node dummy = new Node(0);
//pre负责第 N + 1 层的遍历
Node pre = dummy;
while(cur != null){
if(cur.left != null){
pre.next = cur.left;
pre = pre.next;
}
if(cur.right != null){
pre.next = cur.right;
pre = pre.next;
}
//N层的节点遍历
cur = cur.next;
}
//去下一层
cur = dummy.next;
}
return root;
}
}
- アルゴリズムの概要
特定の層のノード間に次のポインタが確立されると、この層のノードは実際にリンクされたリストを形成します。したがって、最初に特定の層の次のポインタを確立してから、この層を横断する場合、キューを使用する必要はなくなります。
26、P226 二分木を反転する
- トピックの説明
二分木を反転します。
- ソリューション
このトピックは、P101 対称バイナリ ツリーに関連付ける必要があります。
- コード
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null){
return null;
}
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
27、P654 最大のバイナリ ツリー
- トピックの説明
重複要素のない整数配列 nums を指定します。この配列から直接再帰的に構築される最大バイナリ ツリーは、次のように定義されます。
バイナリ ツリーのルートは、配列 nums 内の最大の要素です。
左側のサブツリーは、配列内の最大値の左側の部分から再帰的に構築された最大のバイナリ ツリーです。
右サブツリーは、配列内の最大値の右部分から再帰的に構築された最大のバイナリ ツリーです。
指定された配列 nums から構築された最大のバイナリ ツリーを返します。
- ソリューション
同様のトピック: P105、P106
アイデア:
まず、配列内の最大の数値を見つけてルート ノードを構築し、その数値の両側の数値に従ってルート ノードの左右のサブツリーを構築します。次に、再帰的にツリーを構築します。
- コード
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return myBuild(nums, 0, nums.length-1);
}
public TreeNode myBuild(int[] nums, int left, int right){
//递归出口
if(left > right){
return null;
}
//首先找到最大值的下标
int rootindex = Maxi(nums, left, right);
//创建根节点
TreeNode root = new TreeNode(nums[rootindex]);
//左子树
root.left = myBuild(nums, left, rootindex-1);
//右子树
root.right = myBuild(nums, rootindex+1, right);
return root;
}
public int Maxi(int[] nums,int left, int right){
//返回指定范围内的最大值的下标值
int j = right;
int maxnum = nums[j];
for(int i = left ; i < right ; i++){
if(nums[i] > maxnum){
maxnum = nums[i];
j = i;
}
}
return j;
}
}
- 結果分析
時間計算量は O(n2) です。
- アルゴリズムの概要
2 つの類似したトピックと同様に、これらは再帰的にツリーを構築しています。
28、P652 重複サブツリーを探しています
- トピックの説明
バイナリ ツリーを指定すると、重複するサブツリーをすべて返します。同じタイプの繰り返しサブツリーの場合は、それらのいずれかのルート ノードを返すだけで済みます。
2 つのツリーは、同じ構造と同じノード値を持つ場合に複製されます。
- ソリューション
バイナリ ツリーをシリアル化します。各サブツリーのシーケンスを HashMap に保存し、重複がある場合は繰り返しの数を記録します。
- コード
class Solution {
Map<String, Integer> map;
List<TreeNode> list;
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
map = new HashMap<>();
list = new ArrayList<>();
collect(root);
return list;
}
public String collect(TreeNode root){
//序列化
if(root == null){
return "#";
}
//先保存的是子树,根据先序遍历保存
//注意中序遍历可能会出错,因为有些情况序列化之后结果相同
String s = root.val + ',' + collect(root.left) + ',' + collect(root.right);
//将出现的次数加入map
map.put(s, map.getOrDefault(s, 0) + 1);
//map.getOrDefault(s, 0):没有s返回0,有的话返回出现的次数。
if(map.get(s) == 2){
//有相同的则将根节点加入列表
list.add(root);
}
return s;
}
}
- 結果分析
時間計算量は O(N2) です。
- アルゴリズムの概要
連載のコンセプトはファーストコンタクトです。