This can get a bit demanding to get it right so this following snippet should provide the basic upon which you can research and build a more complete solution:
add_action( 'template_redirect', function() {
if ( is_category() ) {
$term = get_queried_object();
if ( isset( $term->parent ) && $term->parent ) {
global $wp;
$request_url = trailingslashit( get_site_url( get_current_blog_id(), $wp->request ) );
$link = get_term_link( $term );
if ( ! is_wp_error( $link ) ) {
if ( strpos( $request_url, trailingslashit( $link ) ) !== 0 ) {
wp_safe_redirect( $link, 301 );
}
}
}
}
} );
This solution will work with pagination too but it will redirect to the start of pagination in case you are attempting to access an “incorrect” sub-category paged URL (http://www.example.com/category/child/page/2/
will redirect to http://www.example.com/category/parent/child/
— this can be addressed by checking if we have a paginated request and build the redirect link accordingly)