تأیید درخت دودویی معکوس – انجمن DEV

بررسی مشکل LeetCode: 266. Invert Binary Tree
بیایید نگاهی به مشکل بیندازیم
با توجه به ریشه یک درخت دوتایی، درخت را معکوس کنید و ریشه آن را برگردانید.
یک درخت باینری توسط یک گره ریشه و سپس بین صفر و دو گره فرزند که از آن منشعب می شود، تعریف می شود. این گره های فرزند خود درخت های باینری هستند. به طور سنتی، گره های فرزند به عنوان فرزند راست و چپ برچسب گذاری می شوند. منظور آنها از معکوس کردن درخت دودویی این است که فرزند چپ با فرزند راست عوض می شود. با این حال، عارضه این است که آنها می خواهند این کار را برای هر گره در درخت انجام دهند.
در اینجا تعریف نوع داده ارائه شده است.
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
}
یک پیاده سازی
در اینجا نحوه حل این مشکل در JavaScript/TypeScript آمده است.
function invertTree(root: TreeNode | null): TreeNode | null {
if(root == null) return null;
let leftChild = invertTree(root.left);
let rightChild = invertTree(root.right);
root.right = leftChild;
root.left = rightChild;
return root;
};
ساختارهای داده بازگشتی مانند یک درخت اغلب با توابع بازگشتی حل می شوند. ما با حالت پایه شروع می کنیم، جایی که بازگشت متوقف می شود. در این حالت، تابع ما تهی بودن درخت را بررسی می کند و در صورت وجود، null را برمی گرداند. هر شاخه از درخت با برگهای پوچ به پایان می رسد. به صورت بازگشتی همه عناصر فرزند را معکوس می کند و سپس فرزند چپ و راست را تعویض می کند. این تابع درخت را در جای خود اصلاح می کند و سپس ریشه اصلاح شده را برمی گرداند.
چیزهای زیادی در حال انجام است، اما اجازه میدهیم مشخصات عملکرد را در Dafny ببینیم تا ایدهای در مورد عملکرد این تابع داشته باشیم.
method invertBinaryTree(root: TreeNode?) returns (newRoot: TreeNode?)
modifies if root != null then root.repr else {}
requires root != null ==> root.Valid()
ensures root == null ==> newRoot == null
ensures root != null ==> newRoot != null && newRoot.repr == old(root.repr) && newRoot.Valid()
ensures root != null ==> newRoot == root && newRoot.right == old(root.left) && root.left == old(root.right)
ensures root != null ==> forall node :: node in newRoot.repr ==> node.right == old(node.left) && node.left == old(node.right)
decreases if root == null then {} else root.repr
{
if root != null {
var leftChild := invertBinaryTree(root.left);
var rightChild := invertBinaryTree(root.right);
root.right := leftChild;
root.left := rightChild;
return root;
}else{
return null;
}
}
تعریف اساسی مسئله به این خط خلاصه می شود.
ensures root != null ==> newRoot == root && newRoot.right == old(root.left) && root.left == old(root.right)
Dafny مکانیزمی دارد، عملگر old() که برای اشیاء heap/mutable میتواند به مقداری که یک متغیر قبل از تغییر در نقطه خاصی از متد داشته است، اشاره کند. در این حالت می گوییم ریشه برگشتی دارای یک مقدار سمت چپ است که برابر با مقدار سمت راست قدیمی است و به همین ترتیب مقدار سمت راست برابر با مقدار سمت چپ قدیمی است.
از آنجایی که هدف این روش معکوس کردن کل درخت باینری است، همچنین باید مطمئن شویم که تمام گرههای درخت معکوس هستند.
ensures root != null ==> forall node :: node in newRoot.repr ==> node.right == old(node.left) && node.left == old(node.right)
کلاس ها در دفنی
برای نمایش ساختارهای داده قابل تغییر در کلاس های Dafny روش ترجیحی است و آنها چند قرارداد دارند که برای کمک به مشخصات در نظر گرفته شده است.
اولین مورد است repr
ویژگی ghost یک نمونه کلاس، مجموعه ای از تمام اشیاء را که توسط نمونه و خود نمونه نگهداری می شود را نشان می دهد. این یک متغیر شبح است که در واقع بخشی از کد کامپایل شده نیست، اما استفاده از آن برای تأیید بسیار راحت است.
ensures root != null ==> newRoot != null && newRoot.repr == old(root.repr) && newRoot.Valid()
اگر به این خط مشخصات نگاه کنیم، به ما می گوید که پس از اجرای متد، درخت هنوز معتبر است و تمام گره های موجود هنوز در درخت هستند و هیچ گره جدیدی اضافه نشده است.
دومی است Valid()
روشی که محمولی است که تضمین می کند که هر نمونه به نحوی به خوبی شکل گرفته است.
class TreeNode {
var val: int;
var left: TreeNode?;
var right: TreeNode?;
ghost var repr: set<TreeNode>;
constructor(val: int, left: TreeNode?, right: TreeNode?)
requires left != null ==> left.Valid()
requires right != null ==> right.Valid()
requires left != null && right != null ==> left.repr !! right.repr
ensures this.val == val
ensures this.left == left
ensures this.right == right
ensures left != null ==> this !in left.repr
ensures right != null ==> this !in right.repr
ensures Valid()
{
this.val := val;
this.left := left;
this.right := right;
var leftRepr := if left != null then {left}+left.repr else {};
var rightRepr := if right != null then {right}+right.repr else {};
this.repr := {this} + leftRepr + rightRepr;
}
ghost predicate Valid()
reads this, repr
decreases repr
{
this in repr &&
(this.left != null ==>
(this.left in repr
&& this !in this.left.repr
&& this.left.repr < repr
&& this.left.Valid())
)
&& (this.right != null ==>
(this.right in repr
&& this !in this.right.repr
&& this.right.repr < repr
&& this.right.Valid())
) && (this.left != null && this.right != null ==> this.left.repr !! this.right.repr && this.repr == {this} + this.left.repr + this.right.repr)
&& (this.left != null && this.right == null ==> this.repr == {this} + this.left.repr)
&& (this.right != null && this.left == null ==> this.repr == {this} + this.right.repr)
&& (this.right == null && this.left == null ==> this.repr == {this})
}
}
متد Valid کمی پرمخاطب است، اما کاملاً قدرتمند است و تضمین میکند که هر نمونه درختی در یک درخت معتبر فقط در یک شاخه قرار دارد و زیرمجموعهای از گرهها یا گرهها است. repr
در والد گنجانده شده است repr
، و همه نمونه های فرزند آن خود معتبر هستند. همچنین تضمین می کند که هیچ گره والد در زیر مجموعه گره های فرزند خود قرار نگیرد.
این روش برای راستیآزمایی مهم است زیرا تضمین میکند که همه موارد مرتبط را در روشهای خود پوشش میدهیم. به این معنی که گره هیچ عنصر فرزندی ندارد، یک فرزند در سمت چپ یا راست یا هر دو فرزند دارد. سپس تضمین می کند که روش ما کارهای قبلی را کپی یا برگردانده نمی کند زیرا تضمین می کند که هر گره فقط یک بار بازدید می شود.
روش کلاس
اگرچه این روش به عنوان یک متد مستقل توصیف شد، اما میتوانست به عنوان متد کلاس نیز پیادهسازی شود.
method invertBinaryTree() returns (newRoot: TreeNode?)
modifies this.repr
requires this.Valid()
ensures newRoot == this && newRoot.right == old(this.left) && newRoot.left == old(this.right)
ensures newRoot.repr == old(this.repr) && newRoot.Valid()
ensures forall node :: node in this.repr ==> node.right == old(node.left) && node.left == old(node.right)
ensures newRoot.Valid()
decreases repr
{
var leftChild: TreeNode? := null;
if left != null {
leftChild := left.invertBinaryTree();
}
var rightChild: TreeNode? := null;
if right != null {
rightChild := right.invertBinaryTree();
}
right := leftChild;
left := rightChild;
return this;
}