Реализация QuickSort (включение или исключение сводного элемента во время разделения)

Я пытаюсь разобраться в алгоритме быстрой сортировки. Я пробовал разные реализации. Проблема, с которой я столкнулся, связана с включением/исключением поворотного элемента.

Рассмотрим эти три реализации.

  1. Этот код вызовет ошибку в коде VS, и код VS просто остановится. Обратите внимание, что элемент поворота (ind) включается в left только один раз (по крайней мере, мне так кажется) и не добавляется отдельно в последней строке функции:

    def quick_sort(arr):
        if len(arr)<=1:
            return arr
        ind = arr[0]
        left = [i for i in arr if i <= ind]           
        right = [i for i in arr if i > ind]     
        return quick_sort(left) + quick_sort(right)    
    
  2. Если я исключим ind из left (используя < вместо <=) и добавлю [ind] в последней строке, это сработает. НО в отсортированном списке не будет дубликатов исходного списка:

    numbers = [5, 2, 3, 2, 7]
    print("Sum of numbers:", quick_sort(numbers))
    
    def quick_sort(arr):
        if len(arr)<=1:
            return arr
        ind = arr[0]
        left = [i for i in arr if i < ind]          
        right = [i for i in arr if i > ind]     
        return quick_sort(left) + [ind] + quick_sort(right)    
    
  3. Наконец, эта реализация отлично работает, правильно сортируя все значения. НО я не понимаю, в чем разница между этой реализацией и первой. Почему первый вызывает ошибку, а этот работает? Но в обоих случаях мы включаем опорную точку (ind) только один раз?

    numbers = [5, 2, 3, 2, 7]
    print("Sum of numbers:", quick_sort(numbers))
    
    def quick_sort(arr):
        if len(arr)<=1:
            return arr
        ind = arr[0]
        left = [i for i in arr if i < ind]  
        middle = [i for i in arr if i == ind]         
        right = [i for i in arr if i > ind]     
        return quick_sort(left) + middle + quick_sort(right) 
    

Я проверил предыдущие вопросы и ответы по этой теме, но ничего по этой проблеме не нашел.

В версии №1, если опорная точка оказывается самым высоким значением в списке, то left в конечном итоге содержит ВСЕ, и вы вызываете quick_sort() с точно тем же списком, что и раньше - что, конечно, снова делает то же самое, поэтому у вас есть бесконечное количество рекурсия. Чтобы рекурсия работала, каждый вызов должен быть строго ближе к базовому (нерекурсивному) случаю.

jasonharper 08.05.2024 21:51

Спасибо за ваш ответ! Проблема заключалась в том, что для меня № 1 и 3 выглядели одинаково, потому что оба включают в себя поворотный элемент. Но, очевидно, я упустил из виду, что этап, на котором мы включаем этот элемент, имеет значение, поэтому в № 1 мы передаем left+pivot рекурсивному вызову, а в № 3 мы избегаем этого и при необходимости добавляем элемент поворота на более позднем этапе.

Dmitr Egorov 08.05.2024 23:01
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Реализация №1 будет страдать от бесконечной рекурсии, когда left принимает все значения в заданном списке (а right остается пустым). В этом случае прогресса нет, и рекурсивный вызов снова решит ту же проблему, что приведет к тому же повторному рекурсивному вызову... и т. д. Это происходит, например, с вводом [2, 1].

Вы уже правильно определили проблему со второй реализацией. Если сводное значение имеет дубликаты, они удаляются из результата. Эту проблему решает третья реализация.

И вторая, и третья реализации решают проблему, с которой сталкивается первая реализация: гарантировано, что рекурсивные вызовы будут иметь дело со строго более короткими списками, поскольку гарантировано, что одно или все значения сводки больше не являются членами списка, который передается рекурсивному вызову.

Не имеет отношения к вашему вопросу, но быстрая сортировка обычно реализуется как алгоритм на месте, т. е. без создания новых списков во время процесса. Вместо этого данный список видоизменяется посредством свопов. См. Википедию о быстрой сортировке.

Спасибо за быстрый ответ! Позвольте мне уточнить. Означает ли это, что в №3 мы обязательно исключаем опорный элемент из левого списка, а затем отдельно добавляем его на более позднем этапе алгоритма, если какие-то элементы ему равны? В этом случае мы включаем элемент поворота, но избегаем его использования на этапе рекурсии. Это верно?

Dmitr Egorov 08.05.2024 22:53

Да это верно. В #3 и left, и right гарантированно будут короче arr, поскольку мы откладываем вхождения опорных значений (они не включены ни в left, ни в right) для добавления между двумя отсортированными результатами, которые мы получаем из рекурсии.

trincot 08.05.2024 23:14

Еще раз спасибо, это было очень полезно! Я думал, что понял это, но потом подумал, что если мы выберем средний элемент в качестве опорного элемента (len(arr)//2), а затем включим его в левый (используя <=), он снова перейдет к бесконечной рекурсии. Но в этом случае подмассив делится при каждом вызове и, следовательно, становится меньше, даже если мы включили опорную точку. Как же тогда? я смущен

Dmitr Egorov 09.05.2024 02:47

«Но в этом случае подмассив становится... меньше»: вы все равно рискуете, что выбранный стержень (из середины) будет наибольшим значением в массиве. Неважно, откуда вы берете опорную точку: если есть риск, что это наибольшее значение, вы все равно рискуете, что все значения в массиве попадут в левый раздел. Тривиальный пример: у вас есть arr=[1,1,1,1,1,1]. Неважно, какую единицу вы возьмете в качестве опорной (среднюю, левую или...), вы всегда будете заполнять left всеми этими значениями.

trincot 09.05.2024 07:49

Еще раз большое спасибо, я понял это, основываясь на вашем предыдущем ответе. На этот раз я пропустил, что во время разделения, если все числа в подмассиве меньше или равны стержню, не только числа не изменятся и не перейдут в левую часть, но ПОРЯДОК останется прежним, поэтому средний элемент также будет оставайся таким же. Теперь ясно, что он застрянет в бесконечной рекурсии. Надеюсь, на этот раз мое понимание окончательное :) Ваш ответ был очень полезен.

Dmitr Egorov 09.05.2024 08:48

Другие вопросы по теме