۱۱ - ۱۷درخت باینری
حالا نظرمون رو به یه تایپ مشابهِ لیست جلب میکنیم. نوعساز ِ درختهای باینری یه آرگومان میگیره، و مثلِ لیست بازگشتی ِه:
data BinaryTree a =
Leaf
| Node (BinaryTree a) a (BinaryTree a)
deriving (Eq, Ord, Show)این درخت در هر گره یه مقدار از تایپِ a داره. هر گره ممکنه یه گره ِ انتهایی باشه، به اسمِ برگ، یا میتونه شاخه بده و دوتا زیردرخت داشته باشه. زیردرختها هم از تایپِ BinaryTree a هستن، پس این تایپ بازگشتی ِه. هر درخت باینری میتونه درخت باینری ِ دیگهای رو در خودش ذخیره کنه، و همین به درختها عُمقِ دلخواه میده.
در بعضی موارد، درختهای باینری برای ساختاردهی و دسترسی به دادهها عملکردِ بهتری نسبت به لیستها دارن، مخصوصاً اگه به نحوهی ترتیببندیِ مقادیر اشرافِ کامل داشته باشین، طوری که بدونین کِی برین "چپ" و کی برین "راست" تا به مقدارِ موردِ نظر برسین. از طرف دیگه، درختی که فقط از راست منشعب بشه، فرقی با لیست معمولی نداره. فعلاً خیلی با این چیزها کار نداریم، بعداً از استفاده و کاربرد صحیح از ساختارهای داده بیشتر صحبت میکنیم. بجاش چندتا تابع برای پردازش مقادیرِ BinaryTree مینویسین.
افزودن به داخلِ درختها
اولین چیزی که باید حواسمون بهش باشه، اینه که برای مرتب کردن مقادیر در درختها، Ord رو لازم داریم. اینطوری اگه چیزی کوچکتره، میتونیم بفرستیمش یه جاهایی سمت چپ، یا اگه بزرگتر از گره ِ حاضر ِه، بِره یه جاهایی سمت راست. این "چپ کوچکتر، راست بزرگتر،" ترتیب رایجی برای درختهای باینری ِه – میشه برعکس هم باشه و فرقی نداره، ولی اینطوری کلاً آشناتر ِه، چون این ترتیب رو خیلی جاها دیدیم، مثلاً در محور اعداد. در هر صورت، هدف اینه که بدونیم کجا دنبالِ مقادیر بزرگتر یا کوچکتر از مقداری که روش هستیم بگردیم.
تابعِ insert یه مقدار رو داخلِ یه درخت وارد میکنه، یا اگه درختی هنوز وجود نداره، از طریقِ افزودن ِ مقادیر، راهی برای ساختِ یه درخت رو در اختیار میذاره. باز هم یادآوری میکنیم که مقادیر در هسکل تغییرناپذیر اند. مقدار رو به یه درختی که داریم اضافه نمیکنیم؛ هر بار که میخوایم یه مقدار به درختی اضافه بشه، یه درخت ِ جدید رو کامل میسازیم:
insert' :: Ord a
=> a
-> BinaryTree a
-> BinaryTree a
insert' b Leaf = Node Leaf b Leaf
insert' b (Node left a right)
| b == a = Node left a right
| b < a = Node (insert' b left) a right
| b > a = Node left a (insert' b right)فرمان پایه در insert' دو کار انجام میده. افزودن به یه درخت ِ خالی (Leaf) و شروع به ساختِ یه درخت ِ جدید، و همچنین حالتی که به انتهای درخت میرسیم رو به عهده میگیره. این سادگی اجازه میده تفاوتهای جزئی بین این دو حالت رو نادیده بگیریم.
-- میشه "درخت خالی" همون حالت Leaf
Prelude> let t1 = insert' 0 Leaf
Prelude> t1
Node Leaf 0 Leaf
Prelude> let t2 = insert' 3 t1
Prelude> t2
Node Leaf 0 (Node Leaf 3 Leaf)
Prelude> let t3 = insert' 5 t2
Prelude> t3
Node Leaf 0
(Node Leaf 3
(Node Leaf 5 Leaf))درختهای باینری و مشخصاتشون رو بعداً در کتاب بررسی میکنیم. الان نمیخوایم روی مشخصات خودِ درختهای باینری تمرکز کنیم، بلکه با ساختارِ تایپشون کار داریم. شاید تمرینهای زیر به نظرتون سخت و خسته کننده بیان، اما به درک عمیقتر از طرز کار تایپهای بازگشتی کمک میکنن.
برای BinaryTree تابعِ map بنویسین
با توجه به تعریفِ BinaryTree در بالا، یه تابعِ نگاشت برای این ساختار داده بنویسین. واقعاً نیازی نیست چیزی از درختهای باینری بدونین تا این توابع رو بنویسین. همون ساختاری که در تعریف تایپ میبینین تنها چیزیه که لازم دارین. فقط باید توابعِ بازگشتی رو بنویسین.
هیچ الگوریتمِ خاصی نیاز نیست، ما هم انتظار نداریم درختها رو متعادل یا مرتب نگه دارین. یادتون هم باشه که ما هرگز یک بار هم چیزی رو تغییر ندادیم. فقط از داده ِ ورودی مقادیرِ جدید ساختیم. با توجه به این، تابعِ mapTree هم که تعریف میکنین، درخت ِ موجود رو تغییر نمیده – یه درخت ِ جدید بر مبنای اونی که هست میسازه (مثلِ نگاشت ِ توابع روی لیستها).
دقت کنین که برای این تابع نیازی به insert' ندارین. ساختارِ درخت ِ ورودی رو حفظ کنین.
mapTree :: (a -> b)
-> BinaryTree a
-> BinaryTree b
mapTree _ Leaf = Leaf
mapTree f (Node left a right) =
Node undefined undefined undefined
testTree' :: BinaryTree Integer
testTree' =
Node (Node Leaf 3 Leaf)
1
(Node Leaf 4 Leaf)
mapExpected =
Node (Node Leaf 4 Leaf)
2
(Node Leaf 5 Leaf)
-- mapTree تست تأیید برای
mapOkay =
if mapTree (+1) testTree' == mapExpected
then print "yup okay!"
else error "test failed!"این زیر چندتا راهنمایی برای نوشتنِ mapTree آوردیم.
اولین تطبیقِ الگو در تابعِ mapTree همون فرمان پایه هست (درخت ِ ورودی یه مقدارِ Leaf ِه). در این حالت نمیشه f رو اعمال کنیم چون a نداریم، پس کلاً نادیده گرفتیمش. به این دلیل که تحت هر شرایطی باید یه مقدار با تایپِ BinaryTree b برگردونیم، ما هم یه مقدارِ Leaf برای خروجی نوشتیم.
در تطبیقِ الگو ِ دومِ تابعِ mapTree، یه Node برمیگردونیم. دقت کنین که دادهساز ِ Node سه آرگومان میگیره:
Prelude> :t Node
Node :: BinaryTree a
-> a
-> BinaryTree a
-> BinaryTree aپس باید یه BinaryTree، یه مقدارِ مجرد، و یه BinaryTree ِ دیگه بهش بدین. جملات زیر در اختیارتون هستن:
۱.
f :: (a -> b)۲.
left :: BinaryTree a۳.
a :: a۴.
right :: BinaryTree a۵.
mapTree :: (a -> b)
-> BinaryTree a
-> BinaryTree bحالا Node ِ خروجی باید یه مقدار از تایپِ b، و مقادیرِ BinaryTree با تایپِ b داخلشون داشته باشه. دو تابع در اختیارتون هست. یکی (a -> b) و یکی هم BinaryTreeهای با تایپ a رو به BinaryTreeهای با تایپِ b نگاشت میده. ببینیم چه میکنین.
چندتا پیشنهاد که ممکنه تو این تمرین کمکتون کنن:
۱.
اول از همه الگوهایی که تابع باید روشون منطبق بشه رو جدا کنین.
۲.
کُدِ فرمان پایه رو اول بنویسین.
۳.
اول سعی کنین مراحلِ خوداتکایی رو دستی بنویسین، بعد اونها رو به یک مرحلهای که بازگشتی هست خلاصه کنین.
درختهای باینری رو به لیست تبدیل کنین
تابعهایی که مقادیرِ BinaryTree رو به لیست تبدیل میکنن بنویسین. حتماً تابعتون رو با اون تست امتحان کنین.
preorder :: BinaryTree a -> [a]
preorder = undefined
inorder :: BinaryTree a -> [a]
inorder = undefined
postorder :: BinaryTree a -> [a]
postorder = undefined
testTree :: BinaryTree Integer
testTree =
Node (Node Leaf 1 Leaf)
2
(Node Leaf 3 Leaf)
testPreorder :: IO ()
testPreorder =
if preorder testTree == [2, 1, 3]
then putStrLn "Preorder fine!"
else putStrLn "Bad news bears."
testInorder :: IO ()
testInorder =
if preorder testTree == [1, 2, 3]
then putStrLn "Ineorder fine!"
else putStrLn "Bad news bears."
testPostorder :: IO ()
testPorstorder =
if preorder testTree == [1, 3, 2]
then putStrLn "Posteorder fine!"
else putStrLn "Bad news bears."
main :: IO ()
main = do
testPreorder
testInorder
testPostorderبرای BinaryTree تابعِ foldr بنویسین
با توجه به تعریفی که برای BinaryTree دادیم، یه کاتامورفیسم برای درختهای باینری بنویسین.
-- هر ترتیب پیمایشیای قبوله
foldTree :: (a -> b -> b)
-> b
-> BinaryTree a
-> b