Function builds a binary decision tree with given dataset and it expand tree nodes in priority order. Tree model is
    stored in a dictionary and it has following structure:
    {parent_identifier: [(child_identifier, highest_estimated_feature_index , split_value, distribution_of_labels,
    depth, feature_type)]}

    x: numpy array - dataset with features
    y: numpy array - dataset with labels
    t: list - features types
    randomized: boolean - if True, algorithm estimates sqrt(num_of_features)+1 randomly selected features each
    iteration.
    If False, it estimates all features in each iteration.
    max_tree_nodes: integer - number of tree nodes to expand.
    min_samples_leaf: float - minimal number of samples in leafs.
    class_majority: float - purity of the classes in leafs.
    measure: measure function - information gain or mdl.
    split_fun: split function - discretization of continuous features can be made randomly or with equal label frequency.

def fit(x, y, t, randomized, max_tree_nodes, min_samples_leaf, min_samples_split, class_majority, measure, accuracy,
        separate_max):
    """
    Function builds a binary decision tree with given dataset and it expand tree nodes in priority order. Tree model is
    stored in a dictionary and it has following structure:
    {parent_identifier: [(child_identifier, highest_estimated_feature_index , split_value, distribution_of_labels,
    depth, feature_type)]}

    x: numpy array - dataset with features
    y: numpy array - dataset with labels
    t: list - features types
    randomized: boolean - if True, algorithm estimates sqrt(num_of_features)+1 randomly selected features each
    iteration.
    If False, it estimates all features in each iteration.
    max_tree_nodes: integer - number of tree nodes to expand.
    min_samples_leaf: float - minimal number of samples in leafs.
    class_majority: float - purity of the classes in leafs.
    measure: measure function - information gain or mdl.
    split_fun: split function - discretization of continuous features can be made randomly or with equal label frequency.
    """

    depth = 0  # depth of the tree
    node_id = 1  # node identifier

    # conditions of continuous and discrete features.
    operation = {"c": (np.less_equal, np.greater), "d": (np.in1d, np.in1d)}
    tree = {0: [(node_id, -1, "", dict(Counter(y)), depth, "")]}  # initialize tree model
    mapping = range(len(x[0]))  # global features indices

    # sqrt(num_of_features) is estimated if randomized == True.
    # If randomized == False, all indices are estimated at each node.
    rand_attr = int(np.ceil(np.sqrt(len(x[0]))))
    recalculate = True
    while recalculate:
        recalculate = False
        est_indices = rand_indices(x, rand_attr) if randomized else range(len(x[0]))
        # estimate indices with given measure
        est = [measure(x[:, i], y, t[i], accuracy, separate_max) for i in est_indices]
        try:
            max_est, split = max(est)  # find highest estimated split
        except:
            recalculate = True

    best = est_indices[est.index((max_est, split))]  # select feature index with highest estimate

    queue = Queue.PriorityQueue()  # initialize priority queue
    # put datasets in the queue
    queue.put((max_est, (node_id, x, y, mapping, best, split, depth)))

    while not queue.empty() and len(tree) * 2 < max_tree_nodes:
        _, (parent_id, x, y, mapping, best, split, depth) = queue.get()

        # features indices are mapped due to constantly changing subsets of data
        best_map = mapping[best]

        for j in range(2):  # for left and right branch of the tree
            selection = range(len(x[0]))  # select all indices for the new subset
            new_mapping = [i for i in mapping]  # create a mapping of indices for a new subset

            if t[best_map] == "d" and len(split[j]) == 1:
                # if feature is discrete with one value in split, we cannot split it further.
                selection.remove(best)  # remove feature from new dataset
                new_mapping.remove(best_map)  # remove mapping of feature

            # select rows of new dataset that satisfy condition (less than, greater than or in)
            indices = operation[t[best_map]][j](x[:, best], split[j]).nonzero()[0]
            # create new subsets of data
            sub_x, sub_y = x[indices.reshape(len(indices), 1), selection], y[indices]
            if j == 0 and (len(sub_y) < min_samples_leaf or len(x) - len(sub_y) < min_samples_leaf):
                break

            node_id += 1  # increase node identifier
            y_dist = Counter(sub_y)  # distribution of labels in the new node

            # connect child node with its parent and update tree model
            tree[parent_id] = tree.get(parent_id, []) + [(node_id, best_map,
                                                          set(split[j]) if t[best_map] == "d" else split[j],
                                                          dict(y_dist), depth + 1, t[best_map])]

            # select new indices for estimation
            est_indices = rand_indices(sub_x, rand_attr) if randomized and len(sub_x[0]) > rand_attr else range(
                len(sub_x[0]))
            # check label majority
            current_majority = y_dist[max(y_dist, key=y_dist.get)] / float(len(sub_y))

            # if new node satisfies following conditions it can be further split
            if current_majority < class_majority and len(sub_y) > min_samples_split and est_indices != -1:
                # estimate selected indices
                est = [measure(sub_x[:, i], sub_y, t[new_mapping[i]], accuracy, separate_max) for i in est_indices]
                try:
                    max_est, new_split = max(est)  # find highest estimated split
                except:
                    continue
                # select feature index with highest estimate
                new_best = est_indices[est.index((max_est, new_split))]
                # put new datasets in the queue with inverse value of estimate (priority order)
                queue.put(
                    (max_est * -1 * len(sub_y), (node_id, sub_x, sub_y, new_mapping, new_best, new_split, depth + 1)))
    return tree