Skip to content

98. Validate Binary Search Tree#30

Open
Ryotaro25 wants to merge 3 commits intomainfrom
problem28
Open

98. Validate Binary Search Tree#30
Ryotaro25 wants to merge 3 commits intomainfrom
problem28

Conversation

@Ryotaro25
Copy link
Copy Markdown
Owner

問題へのリンク
https://leetcode.com/problems/validate-binary-search-tree/description/

問題文(プレミアムの場合)

備考

次に解く問題の予告
Construct Binary Tree from Preorder and Inorder Traversal

フォルダ構成
LeetCodeの問題ごとにフォルダを作成します。
フォルダ内は、step1.cpp、step2.cpp、step3.cpp、recursion.cppとmemo.mdとなります。

memo.md内に各ステップで感じたことを追記します。

return true;
}
queue<NodesAndRange> nodes;
nodes.emplace(root, LONG_MIN, LONG_MAX);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

long long に対応する最小値と最大値は LONG_LONG_MIN と LONG_LONG_MAX です。

C++11 以降の C++ では、 std::numeric_limits::min() と std::numeric_limits::max() を使用することをお勧めいたします。

また、 long はメモリモデルによってサイズが異なります。 LP64 では 64-bit、 LLP64 では 32-bit となります。
https://ja.wikipedia.org/wiki/64%E3%83%93%E3%83%83%E3%83%88

サイズを指定したい場合は int64_t 等を使用することをお勧めいたします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
レビューありがとうございます。

long long に対応する最小値と最大値は LONG_LONG_MIN と LONG_LONG_MAX です。

型ごとに対応するものが存在しますね。気をつけます。

C++11 以降の C++ では、 std::numeric_limits::min() と std::numeric_limits::max() を使用することをお勧めいたします。

numeric_limits::min()のようにlong long型であるとはっきりと伝わるためでしょうか?

long はメモリモデルによってサイズが異なります。

資料もありがとうございます。データモデルという概念をまず知りませんでした。使用するコンパイラによってデータモデルが異なり結果としてプログラムに影響が出るのですね。
int32_tだとmax側の境界値を渡される場合に、if (node->val <= lower_value || node->val >= upper_value)の条件がうまく判定されなくなるのでint64_t にしました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

numeric_limits::min()のようにlong long型であるとはっきりと伝わるためでしょうか?
あくまで個人的な感覚ですが、 LONG_LONG_MIN を使うと C 言語っぽく感じられるためだと思います。 std::numeric_limits<long long>::min() と書くと C++ っぽい感じられるので、そちらのほうが良いと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
なるほどです。説明ありがとうございます。

}

private:
bool CheckIsValidBst(TreeNode* node, long long lower_value,long long upper_value) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

, のあとにスペースを 1 つ空けることをお勧めいたします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

失礼しました。

}

private:
bool CheckIsValidBst(TreeNode* node, long long lower_value,long long upper_value) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

要素数の最大値が 104 のため、ノードが片側に偏って一直線になっていた場合、スタックオーバーフローを引き起こす可能性があります。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
この辺り苦手な箇所なのでじっくり調べてみました。

1TreeNodeあたりの大きさ確認
ツリーnodeが持っている要素は、int valとTreeNode *leftとTreeNode *right
intは2^32まで表せられる32bit。byteに変換すると32 / 8 で4byte
ポインターのサイズは、32bitコンピュータだと4byte、64bitコンピュータの場合は8byte
一般的なPCは64がほとんどと見かけました、なので合計4 + 8 * 2 = 20
https://www.naukri.com/code360/library/what-is-a-size-of-pointer-in-c

上記に加えて、paddingというものが存在する
kazukiii/leetcode@93bc473#r1677572697

知らなかったので下記を読んでみました。
https://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/
64ビットコンピュータでは8byteごとにメモリを管理しているので、この法則を守るため?にpaddingが使われているのですね。今回の場合だと、20byteを管理するために8byte*3の24byteが必要となりその間と埋めるために4byteのpaddingが必要になる。この8byteずつ管理しなさいというのは、プロセッサ側からの約束事。

合計24byteが1TreeNodeあたりに必要な大きさです。

再帰1巡回あたりに必要なサイズは
long longからint64_tに変換して話をします。
int64_tは2^64ビットを扱えるので、64ビット=8byte
CheckIsValidBst(TreeNode* node, int64_t lower_value, int64_t upper_value)なので24 + 8 + 8 で40byteが必要です。

10^4を考慮すると
10^4 * 40 = 400,000 byteが最大で必要となる。
すみませんここからオーバーするかどうかどう確認したらいいのかわかりませんでした🙇

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C 言語の場合の話となりますが、関数呼び出しを行うたびにスタックフレームが作られます。スタックフレームには以下のような値が格納されます。

・引数
・オート変数
・リターンアドレス
・リターン値設定アドレス
・レジスタ退避域
・一時変数

https://www.gaio.co.jp/gaioclub/compiler_blog10/

これを考えると Java の場合も引数以外の情報がスタックフレームに格納されるのではないかと思います。調べてみるとよいと思います。

また、スタックメモリのサイズは起動オプション -Xss で指定することができます。
https://docs.oracle.com/cd/F25597_01/document/products/jrockit/jrdocs/refman/optionX.html
他にも様々なオプションがあります。調べてみることをお勧めいたします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
ありがとうございます!オート変数など初めて聞く名前などこの辺り調べてみます。

Copy link
Copy Markdown

@liquo-rice liquo-rice Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CheckIsValidBst(TreeNode* node, int64_t lower_value, int64_t upper_value)なので24 + 8 + 8 で40byteが必要です。

一つ目の変数はポインタなので、合計で24 bytesではないですか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liquo-rice
コメントありがとうございます。
nodeに対して下記のような構造体が定義されている場合、int valが4バイト、TreeNode *leftとTreeNode *rightがそれぞれ8バイト、そしてpaddingが4バイトで合計24にならないでしょうか?

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TreeNode *leftとTreeNode *rightがそれぞれ8バイト

上と同じく、TreeNode* nodeの大きさは、8バイトです。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@liquo-rice
ありがとうございます。

@nodchip @liquo-rice
下記読みました。
https://www.gaio.co.jp/gaioclub/compiler_blog10/

・引数
 引数は3つで合計24byte
 C++なのですが、Macのlldbというものを使ってサイズの確認をしてみました。
 各引数のサイズは見ることができました。
Screenshot 2024-09-01 at 9 25 37

・オート変数
auto をつけた変数のことなのですね。計算時に特別なものがあるのかと思いました。
今回使っていない

・リターンアドレス
  8byte

・リターン値設定アドレス
こちらも64bitコンピュータとすると8byte

・レジスタ退避域
 わからなかったのでChatGPtに聞いてみたところ16~64byte(環境による)ということでした。

・一時変数
 今回は使っていない。lldb上でも表示されませんでした。

なので合計、1回の関数呼び出し56~106byteが必要。
56byte * 10^4 = 560,000byte = 560KB
106byte * 10^4 = 1,060,000byte = 1,060KB = 1.06 MB

Mac OSの場合Stackメモリーは8mbとございました、なので処理できると思いました🙇

}

private:
struct NodesAndRange {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

細かいけどNodeAndRangeですかね

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

失礼しました。

* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

書いてみたけど、探索中のnodeに紐づく左右だけとなってしまい
[5,4,6,null,null,3,7]を通すことができなかった。修正してみたがstep2やrecursion.cppのようになった

通らないのは子の値としか比較していないので、node->val < node->left->right->val みたいな子よりも下にいる子孫が二分探索木の条件を破る場合を見抜けないからだと思います。

wikipediaを見ると

binary tree, is a rooted binary tree data structure with the key of each internal node being greater than all the keys in the respective node's left subtree and less than the ones in its right subtree.
(強調引用者)

と書いてました。

修正の仕方は、左の子の値ではなく左部分木の最大値、右の子の値ではなく右部分木の最小値とnodeの値を比較すればよいのかなと。
実装してみました。 std::optinal使ってます。

struct TreeStatus {
    bool valid;
    optional<int> min;
    optional<int> max;
};

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return computeTreeStatus(root).valid;
    }

private:
    static TreeStatus computeTreeStatus(const TreeNode* const node) {
        if (!node) return {true, {}, {}};
        if (!node->left && !node->right) return {true, node->val, node->val};
        bool valid = true;
        int min_value = node->val;
        int max_value = node->val;
        if (node->left) {
            const TreeStatus left_status = computeTreeStatus(node->left);
            valid &= left_status.valid && left_status.max.value() < node->val;
            if (!valid) return {false, {}, {}};
            min_value = left_status.min.value();
        }
        if (node->right) {
            const TreeStatus right_status = computeTreeStatus(node->right);
            valid &= right_status.valid && node->val < right_status.min.value();
            if (!valid) return {false, {}, {}};
            max_value = right_status.max.value();
        }
        return {true, min_value, max_value};
    }
};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fhiyo
レビューありがとうございます。そして解説もありがとうございます。

通らないのは子の値としか比較していないので、node->val < node->left->right->val みたいな子よりも下にいる子孫が二分探索木の条件を破る場合を見抜けないからだと思います。

今いるnodeに隣接している子としか比較できていないので通りませんでした。子よりも下にいる子孫の状態を保持しようとすると記載通り、step2やrecursion.cppのような方法しか思いつくことが出来ませんでした。
有効性を確認しならがら深く潜っていって有効な場合は、最大値or最小値を更新するのですね。ありがとうございます。

}

private:
bool CheckIsValidBst(TreeNode* node, int64_t lower_value, int64_t upper_value) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

細かいですが、短くvalidateBstでも良いと思いました

@TORUS0818
Copy link
Copy Markdown

拝見しました。
良いと思いました(あまり有意義なコメントが出来ておらずすみません。。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants