TL;DR

On with TASK #2 from The Weekly Challenge #130. Enjoy!

The challenge

You are given a tree.

Write a script to find out if the given tree is Binary Search Tree (BST).

According to wikipedia, the definition of BST:

A binary search tree is a rooted binary tree, whose internal nodes each store a key (and optionally, an associated value), and each has two distinguished sub-trees, commonly denoted left and right. The tree additionally satisfies the binary search property: the key in each node is greater than or equal to any key stored in the left sub-tree, and less than or equal to any key stored in the right sub-tree. The leaves (final nodes) of the tree contain no key and have no structure to distinguish them from one another.

Example 1

Input:
        8
       / \
      5   9
     / \
    4   6

Output: 1 as the given tree is a BST.

Example 2

Input:
        5
       / \
      4   7
     / \
    3   6

Output: 0 as the given tree is a not BST.

The questions

My question about the definition is whether the leaves are considered only the empty left/right nodes of a node that has a key. Another question would be whether a node with a key always has two non-empty left and right children.

All in all, anyway, it doesn’t really matter for the implementation I have in mind… so it’s more curiosity than anything else.

The solution

The most straightforward approach is, for me, to go recursive. In this case, in each node we will have to consider the following quantities:

  • the key of the node itself;
  • the minimum key and the maximum key of the left child, which we will call $lmin and $lmax;
  • the same quantities for the right child, respectively $rmin and $rmax.

At that node, we have the following:

  • if either the left or the right child don’t comply with the BST rules, then the whole tree does not either. Hence, we have to make sure that they do.
  • At the specific node, we must check that the key is greater than $lmax (i.e. the maximum value on the left side) and that it is also less than $rmin (i.e. the minimum value on the right side).

If both apply, then this particular node is good, and we can go back to the parent node, reporting a success and also $lmin and $rmax as the overall minimum and maximum values.

In case the tree is not perfectly assembled (e.g. a node only has a left or a right side) we will have to cope with the fact and act accordingly.

Perl goes first this time:

#!/usr/bin/env perl
use v5.24;
use warnings;
use experimental 'signatures';
no warnings 'experimental::signatures';

sub check_bst ($root) {
   state $checker = sub ($node) {
      return 1 unless $node;
      my $key = $node->{key};
      my ($lsub, $lmin, $lmax) = __SUB__->($node->{left});
      return 0 unless $lsub;
      ($lmin, $lmax) = ($key, $key - 1) unless defined $lmin;
      my ($rsub, $rmin, $rmax) = __SUB__->($node->{right});
      return 0 unless $rsub;
      ($rmin, $rmax) = ($key + 1, $key) unless defined $rmin;
      return 0 if $key < $lmax || $key > $rmin;
      return (1, $lmin, $rmax);
   };
   return ($checker->($root))[0];
}

sub n ($k, $l = undef, $r = undef) {{key => $k, left => $l, right => $r}}

say check_bst(n(8, n(5, n(4), n(6)), n(9)));
say check_bst(n(5, n(4, n(3), n(6)), n(7)));

I guess that these two lines deserve some additional explanation:

($lmin, $lmax) = ($key, $key - 1) unless defined $lmin;
...
($rmin, $rmax) = ($key + 1, $key) unless defined $rmin;

In case a leg is empty, we get nothing from it (i.e. undef). This inherently means that the sub-tree on that side is compliant, hence:

  • to make the test succeed, we set $lmax to be smaller than the key ($key - 1), and $rmin to be greater than it ($key + 1).
  • on the other hand, the missing extreme is set to be equal to $key, because this is the value we want to send back to the parent’s call.

Time for Raku now, which is a simple translation:

#!/usr/bin/env raku
use v6;

sub check-bst ($root) {
   my sub checker ($node --> Array()) {
      return 1 unless $node;
      my ($key, $left, $right) = $node<key left right>;
      my ($lsub, $lmin, $lmax) = checker($left);
      return 0 unless $lsub;
      ($lmin, $lmax) = ($key, $key - 1) unless defined $lmin;
      my ($rsub, $rmin, $rmax) = checker($right);
      return 0 unless $rsub;
      ($rmin, $rmax) = ($key + 1, $key) unless defined $rmin;
      return 0 if $key < $lmax || $key > $rmin;
      return (1, $lmin, $rmax);
   }
   return checker($root)[0];
}

sub n ($k, $l = Nil, $r = Nil) {(key => $k, left => $l, right => $r).hash}

put check-bst(n(8, n(5, n(4), n(6)), n(9)));
put check-bst(n(5, n(4, n(3), n(6)), n(7)));

And with this… it’s all for this post, I hope you enjoyed it and stay safe anyway!