Is This Code Efficient – Or is there a better way?

I’d recommend caching – generating the HTML mark-up and storing that in a cache, and using that rather then generating the list on each page load.

Of course, the cache would have to cleared if a recipe is ever added, removed or changes title.

add_shortcode('recipe_list', 'recipe_query');
function recipe_query( $atts, $content ){

     //Get mark-up from cache (if present)
     $html = get_transient( 'wpse123038_recipe_list' );

     if( !$html ){
          //Nothing in cache - generate mark-up
          $html="";
          ....

          //Store in cache (for a long time - a year?)
          set_transient( 'wpse123038_recipe_list', $html, 60*60*24*365 );     
     }

     return $html;
}

Now everytime we add/remove a post (or may be its published and then we set it to ‘draft’) – basically anytime it might go from not being in the list, to being on the list :). (Note, you’ll also want to clear the cache whenever the recipe title changes…)

function wpse123038_recipe_status_changes( $new_status, $old_status, $post ) {

    //Check if status has changed
    if( $new_status == $old_status ){
        return;
    }

    //Check post type
    if( 'recipes' != get_post_type( $post) ){
         return;
    }

    //Check if we're publishing on 'unpublishing' it.
    if( 'publish' == $new_status || 'publish' == $old_status ){
         delete_transient( 'wpse123038_recipe_list' );

         //For bonus points, regenerate the cache here :)
    }

}
add_action( 'transition_post_status', 'wpse123038_recipe_status_changes', 10, 3 );

So when ever a recipe is published, or is ‘unpublished’ the cache is cleared. This means the next visitor will have to wait for the mark-up to be generated again.

Of course, we’re usually less concerned about page load times admin-side, so when the cache is cleared, you could regenerate it then. Giving a (usually) seamless experience on the front-end.