برنامه نویسی

تأیید درخت دودویی معکوس – انجمن 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;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا