Order of tensorflow.Dataset.concatenate operations
@Leengit 

Can you please elaborate about your feature and please specify the use cases for this feature.

Thank you! I would like the documentation page for [`tensorflow.Dataset.concatenate`](https://www.tensorflow.org/api_docs/python/tf/data/Dataset#concatenate) to mention that if multiple datasets are to be concatenated by repeated use of this two-dataset `concatenate` method then it is safest to accomplish this with a divide-and-conquer approach or, at the very least, the documentation should indicate that the straightforward approach of appending datasets one-by-one can be problematic.

Alternatively, we can expand the `tensorflow.Dataset` interface so that the argument to the `concatenate` method can be a list (or tuple, etc.) of datasets, and the implementation would then use the divide-and-conquer approach (above) (or it would use a new `tensorflow` data structure) to concatenate them.

In my case, I have a function that reads a chunk of a very large whole-slide (medical) image.  Because these chunks require different parameters for reading, masking, color normalization, transforming, etc., it is convenient for me to make a single `tensorflow.Dataset` from each.  I then need to concatenate all these `tensorflow.Dataset` objects together. I think the better place to mention the best practices to use the data is https://www.tensorflow.org/guide/data.
In the API document, we mainly mention the usage and it's behavior. Thanks for raising this issue @Leengit. Agreed that the documentation should be updated to warn about performance issues with concatenating a large number of datasets together and suggest an alternative.

For what it's worth, the best way to concatenate a large number of datasets is to use `flat_map` instead:

```python
def concat_datasets(datasets):
  ds = tf.data.Dataset.from_tensor_slices(datasets)
  return ds.flat_map(lambda x: x)
``` I didn't know that one could give a `List[tf.data.Dataset]` to `tf.data.Dataset.from_tensor_slices`!  Please document this approach somewhere where people who are tempted to use the pairwise `concatenate` will see it.
<details><summary>Click to expand!</summary> 
 
 ### Issue Type

Documentation Feature Request

### Have you reproduced the bug with TF nightly?

No

### Source

binary

### Tensorflow Version

2.11.0

### Custom Code

No

### OS Platform and Distribution

Linux Ubuntu 20.04

### Mobile device

_No response_

### Python version

3.9.7

### Bazel version

_No response_

### GCC/Compiler version

9.4.9

### CUDA/cuDNN version

_No response_

### GPU model and memory

NVIDIA RTX A4000 15.74GiB

### Current Behaviour?

If I have a long list of datasets that I wish to concatenate, it matters whether I concatenate them pairwise with heavy left-hand sides or heavy right-hand sides.  I would like to see this in the documentation so that others don't have to learn this on their own.  (Additionally, a static tf.Dataset.concatenate_from_list function that does the right thing for the user would be nice.)  Specifically, for my large list of datasets that contain large amounts of image data that are read from disk via py_functions, batched, and prefetched, the following code snippets give different performances.  `concatenate_by_appending` causes the process to be killed, likely due to an out-of-memory condition.  In contrast, `concatenate_by_prepending` and `concatenate_by_splitting` work well.  I suspect that if I used an aggressive form of `shuffle` then only `concatenate_by_splitting` would work well.

I should clarify it is not the creation of the `concatenate_by_appending` dataset that causes the process to be killed.  The process is killed only once I attempt to `predict` using the combined dataset.  The pre-pending approach is right-heavy for each concatenation operation, and the left-most dataset can be found immediately, as the left branch of the top-most concatenation.  In contrast, the appending approach is left-heavy for each concatenation operation, requires descending through all concatenations to find the left-most dataset, and causes the process to be killed, likely due to resource exhaustion, specifically likely an out-of-memory condition.  At least I think that is what is happening.
### Standalone code to reproduce the issue

```python
def concatenate_by_prepending(dataset_list):
    response = None
    # Note: list traversal is reversed
    for dataset in reversed(dataset_list):
        if response is None:
            response = dataset
        else:
            # Note: Right-heavy concatenation
            response = dataset.concatenate(response)
    return response

def concatenate_by_appending(dataset_list):
    response = None
    # Note: list traversal is forward
    for dataset in dataset_list:
        if response is None:
            response = dataset
        else:
            # Note: Left-heavy concatenation
            response = response.concatenate(dataset)
    return response

def concatenate_by_splitting(dataset_list):
    # Note: recursive, rather than list traversing
    length = len(dataset_list)
    if length == 1:
        return dataset_list[0]
    if length > 1:
        # Note: Left and right are of roughly equal size
        return concatenate_by_splitting(dataset_list[0 : length // 2]).concatenate(
            concatenate_by_splitting(dataset_list[length // 2 : length])
        )
    return None
```


### Relevant log output

_No response_</details>