return child post if available otherwise parent post

You’d need at least 2 queries for this task, since it’s irregular.
For example

// collect all child posts
$allChildPosts = get_posts([
  'post_parent__not_in' => [0],
  'posts_per_page' => -1,
]);

// list child posts and filter one post by parent id
$childPosts = [];
foreach ($allChildPosts as $post) {
    $childPosts[$post->post_parent] = $post;
}

// get all parent level posts without descendants
$lonelyPosts = get_posts([
  'post_parent__not_in' => array_keys($childPosts),
  'posts_per_page' => -1,
]);

$allPosts = array_merge($childPosts, $lonelyPosts);
// As a drawback, you'd have to sort them manually, since they are glued together
usort($allPosts, 'sorting_function');

First approach utilizes only 2 db queries, but will become inefficient with large quantity of posts

Second approach:

// get all parent posts
$parentPosts = get_posts([
  'post_parent' => 0,
  'posts_per_page' => -1,
]);

$allPosts = [];
foreach ($parentPosts as $post) {
    $descendants = get_posts([
      'post_parent' => $post->ID,
      'posts_per_page' => 1,
      'orderby' => 'whatever'
    ]);
    if ($descendants) {
        $allPosts[] = $descendants[0];
    } else {
        $allPosts[] = $post;
    }
}

Second one is more straight-forward and also allows to order descendants (otherwise how to understand which of them must be last?), but since it makes numberOfParentPosts + 1 queries it will become inefficient with large quantity of parent posts.

If you need really efficient solution, you’d have to write clever MySQL manually.

But feel free to prove me wrong 🙂