MAINT Split `Splitter` into a `BaseSplitter` and a `Splitter` subclass to allow easier inheritance

### Summary
With #24678, we make it easier for the `Criterion` class to be inherited. However, the Splitter class can also leverage this improvement. We should separate the current `Splitter` class into an abstract base class for "any type of splitting" `BaseSplitter` and an abstract supervisededly splitter class that requires `y` labels `Splitter`. By keeping the names the same, this change is mostly backwards-compatible.

Moreover, we should refactor what `criterion.init` does into its two intended functionalities: i) setting data and ii) moving pointers around and updating criterion statistics.

### Proposed improvement
Based on discussion below, we want to preserve the `y` parameter passing chain of `Tree -> Splitter -> Criterion`. With the exception of `splitter.init`, all other functions can and should be part of the base class without any notion of whether or not the splitter is supervised, or unsupervised. In order to achieve this, we need to separate where `y` is passed to the criterion (currently it is done within `node_reset`.

1. Refactor `criterion.init` into two functions for initializing the data and resetting the pointers (https://github.com/scikit-learn/scikit-learn/blob/b728b2e8b192857f3522007328fa66909ed68787/sklearn/tree/_criterion.pyx#L44-L69):
i) `criterion.init(y, sample_weight, weighted_n_samples, samples)`, which initializes the data for criterion.
ii) `criterion.set_pointer(start, end)`, which sets the pointers to the start/end of the samples we consider
2. Refactor `splitter.init` to pass the value of `y` to the criterion via the new `criterion.init` function (https://github.com/scikit-learn/scikit-learn/blob/b728b2e8b192857f3522007328fa66909ed68787/sklearn/tree/_splitter.pyx#L96)
3. Refactor `splitter.node_reset` to call `criterion.set_pointer` instead of `criterion.init`. (https://github.com/scikit-learn/scikit-learn/blob/9c9c8582dff9f4563aa130ef89f155bad0051493/sklearn/tree/_splitter.pyx#L180)
4. Refactor `Splitter` into `BaseSplitter` and `Splitter` class. The `BaseSplitter` will contain all the methods except for `init`. `Splitter` will subclass `BaseSplitter` and require the `init` function.

This makes Splitter easier to subclass and also removes some of the interdependency of parameters between Tree, Splitter and Criterion.

Once the changes are made, one should verify:

1. If `tree` submodule's Cython code still builds (i.e. `make clean` and then `pip install --verbose --no-build-isolation --editable .` should not error out)
2. verify unit tests inside `sklearn/tree` all pass
3. verify that the asv benchmarks do not show a performance regression.

`asv continuous --verbose --split --bench RandomForest upstream/main <new_branch_name>` and then for side-by-side comparison `asv compare main <new_branch_name>`

### Reference

As discussed in #24577 , I wrote up a doc on proposed improvements to the tree submodule that would:
1. make it easier for 3rd party packages to subclass existing sklearn tree code and
7. make it easier for sklearn itself to make improvements to the tree code with many of the modern improvements to trees

cc: @jjerphan 

@jshinm I think this is a good next issue to tackle because it is more involved, but does not require making any backward incompatible changes to the sklearn tree code.

COMMENT:
> `node_reset` could be an abstract method of `BaseSplitter` and defined as a reset of the pointer in `Splitter` for our supervised splitter. Someone could just define it as `return None` if there is no need in some unsupervised cases.

In an unsupervised setting, `node_reset` can be the exact same as supervised setting. That is `node_reset` is just about resetting the "samples" i.e. rows (can be rows of `y`, `X` or both presumably). That really just involves the following parameters:

```
samples : array-like, dtype=SIZE_t
            A mask on the samples, showing which ones we want to use
        start : SIZE_t
            The first sample to use in the mask
        end : SIZE_t
            The last sample to use in the mask
```

The rest is just extra imo. Thus the goal is to separate the notion of initializing the data vs initializing the sample-mask/pointers. Sure someone could define it as `return None`, but at least for unsupervised, I noticed that `splitter.node_reset` is still useful, so why not keep it as part of the abstract `BaseSplitter`? Lmk if this is along the lines of what you're thinking.

Labels: module:tree, cython, Refactor