📜  快速排序

📅  最后修改于: 2021-09-16 11:14:18             🧑  作者: Mango

与归并排序一样,快速排序是一种分而治之的算法。它选取一个元素作为枢轴并围绕选取的枢轴对给定数组进行分区。有许多不同版本的 quickSort 以不同的方式选择枢轴。

  1. 始终选择第一个元素作为枢轴。
  2. 始终选择最后一个元素作为枢轴(在下面实现)
  3. 选择一个随机元素作为枢轴。
  4. 选择中位数作为枢轴。

quickSort 中的关键过程是 partition()。分区的目标是,给定一个数组和数组的一个元素 x 作为主元,将 x 放在已排序数组中的正确位置,并将所有较小的元素(小于 x)放在 x 之前,并将所有较大的元素(大于 x)放在之后X。所有这些都应该在线性时间内完成。

递归 QuickSort函数的伪代码:

/* low  --> Starting index,  high  --> Ending index */
quickSort(arr[], low, high)
{
    if (low < high)
    {
        /* pi is partitioning index, arr[pi] is now
           at right place */
        pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);  // Before pi
        quickSort(arr, pi + 1, high); // After pi
    }
}

快速排序

分区算法
可以有多种方式进行分区,以下伪代码采用CLRS书中给出的方法。逻辑很简单,我们从最左边的元素开始,跟踪较小(或等于)元素的索引为 i。在遍历时,如果我们找到一个较小的元素,我们将当前元素与 arr[i] 交换。否则我们忽略当前元素。

/* low  --> Starting index,  high  --> Ending index */
quickSort(arr[], low, high)
{
    if (low < high)
    {
        /* pi is partitioning index, arr[pi] is now
           at right place */
        pi = partition(arr, low, high);

        quickSort(arr, low, pi - 1);  // Before pi
        quickSort(arr, pi + 1, high); // After pi
    }
}

partition() 的伪代码

/* This function takes last element as pivot, places
   the pivot element at its correct position in sorted
    array, and places all smaller (smaller than pivot)
   to left of pivot and all greater elements to right
   of pivot */
partition (arr[], low, high)
{
    // pivot (Element to be placed at right position)
    pivot = arr[high];  
 
    i = (low - 1)  // Index of smaller element and indicates the 
                   // right position of pivot found so far

    for (j = low; j <= high- 1; j++)
    {
        // If current element is smaller than the pivot
        if (arr[j] < pivot)
        {
            i++;    // increment index of smaller element
            swap arr[i] and arr[j]
        }
    }
    swap arr[i + 1] and arr[high])
    return (i + 1)
}

partition() 的图示:

arr[] = {10, 80, 30, 90, 40, 50, 70}
Indexes:  0   1   2   3   4   5   6 

low = 0, high =  6, pivot = arr[h] = 70
Initialize index of smaller element, i = -1

Traverse elements from j = low to high-1
j = 0 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 0 
arr[] = {10, 80, 30, 90, 40, 50, 70} // No change as i and j 
                                     // are same

j = 1 : Since arr[j] > pivot, do nothing
// No change in i and arr[]

j = 2 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 1
arr[] = {10, 30, 80, 90, 40, 50, 70} // We swap 80 and 30 

j = 3 : Since arr[j] > pivot, do nothing
// No change in i and arr[]

j = 4 : Since arr[j] <= pivot, do i++ and swap(arr[i], arr[j])
i = 2
arr[] = {10, 30, 40, 90, 80, 50, 70} // 80 and 40 Swapped
j = 5 : Since arr[j] <= pivot, do i++ and swap arr[i] with arr[j] 
i = 3 
arr[] = {10, 30, 40, 50, 80, 90, 70} // 90 and 50 Swapped 

We come out of loop because j is now equal to high-1.
Finally we place pivot at correct position by swapping
arr[i+1] and arr[high] (or pivot) 
arr[] = {10, 30, 40, 50, 70, 90, 80} // 80 and 70 Swapped 

Now 70 is at its correct place. All elements smaller than
70 are before it and all elements greater than 70 are after
it.

执行:
以下是 QuickSort 的实现:

C++14
/* C++ implementation of QuickSort */
#include 
using namespace std;
 
// A utility function to swap two elements
void swap(int* a, int* b)
{
    int t = *a;
    *a = *b;
    *b = t;
}
 
/* This function takes last element as pivot, places
the pivot element at its correct position in sorted
array, and places all smaller (smaller than pivot)
to left of pivot and all greater elements to right
of pivot */
int partition (int arr[], int low, int high)
{
    int pivot = arr[high]; // pivot
    int i = (low - 1); // Index of smaller element and indicates the right position of pivot found so far
 
    for (int j = low; j <= high - 1; j++)
    {
        // If current element is smaller than the pivot
        if (arr[j] < pivot)
        {
            i++; // increment index of smaller element
            swap(&arr[i], &arr[j]);
        }
    }
    swap(&arr[i + 1], &arr[high]);
    return (i + 1);
}
 
/* The main function that implements QuickSort
arr[] --> Array to be sorted,
low --> Starting index,
high --> Ending index */
void quickSort(int arr[], int low, int high)
{
    if (low < high)
    {
        /* pi is partitioning index, arr[p] is now
        at right place */
        int pi = partition(arr, low, high);
 
        // Separately sort elements before
        // partition and after partition
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
 
/* Function to print an array */
void printArray(int arr[], int size)
{
    int i;
    for (i = 0; i < size; i++)
        cout << arr[i] << " ";
    cout << endl;
}
 
// Driver Code
int main()
{
    int arr[] = {10, 7, 8, 9, 1, 5};
    int n = sizeof(arr) / sizeof(arr[0]);
    quickSort(arr, 0, n - 1);
    cout << "Sorted array: \n";
    printArray(arr, n);
    return 0;
}
 
// This code is contributed by rathbhupendra


Java
// Java implementation of QuickSort
import java.io.*;
 
class GFG{
     
// A utility function to swap two elements
static void swap(int[] arr, int i, int j)
{
    int temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
 
/* This function takes last element as pivot, places
   the pivot element at its correct position in sorted
   array, and places all smaller (smaller than pivot)
   to left of pivot and all greater elements to right
   of pivot */
static int partition(int[] arr, int low, int high)
{
     
    // pivot
    int pivot = arr[high];
     
    // Index of smaller element and
    // indicates the right position
    // of pivot found so far
    int i = (low - 1);
 
    for(int j = low; j <= high - 1; j++)
    {
         
        // If current element is smaller
        // than the pivot
        if (arr[j] < pivot)
        {
             
            // Increment index of
            // smaller element
            i++;
            swap(arr, i, j);
        }
    }
    swap(arr, i + 1, high);
    return (i + 1);
}
 
/* The main function that implements QuickSort
          arr[] --> Array to be sorted,
          low --> Starting index,
          high --> Ending index
 */
static void quickSort(int[] arr, int low, int high)
{
    if (low < high)
    {
         
        // pi is partitioning index, arr[p]
        // is now at right place
        int pi = partition(arr, low, high);
 
        // Separately sort elements before
        // partition and after partition
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}
 
// Function to print an array
static void printArray(int[] arr, int size)
{
    for(int i = 0; i < size; i++)
        System.out.print(arr[i] + " ");
         
    System.out.println();
}
 
// Driver Code
public static void main(String[] args)
{
    int[] arr = { 10, 7, 8, 9, 1, 5 };
    int n = arr.length;
     
    quickSort(arr, 0, n - 1);
    System.out.println("Sorted array: ");
    printArray(arr, n);
}
}
 
// This code is contributed by Ayush Choudhary


Python3
# Python3 implementation of QuickSort 
 
# This Function handles sorting part of quick sort
# start and end points to first and last element of
# an array respectively
def partition(start, end, array):
     
    # Initializing pivot's index to start
    pivot_index = start
    pivot = array[pivot_index]
     
    # This loop runs till start pointer crosses
    # end pointer, and when it does we swap the
    # pivot with element on end pointer
    while start < end:
         
        # Increment the start pointer till it finds an
        # element greater than  pivot
        while start < len(array) and array[start] <= pivot:
            start += 1
             
        # Decrement the end pointer till it finds an
        # element less than pivot
        while array[end] > pivot:
            end -= 1
         
        # If start and end have not crossed each other,
        # swap the numbers on start and end
        if(start < end):
            array[start], array[end] = array[end], array[start]
     
    # Swap pivot element with element on end pointer.
    # This puts pivot on its correct sorted place.
    array[end], array[pivot_index] = array[pivot_index], array[end]
    
    # Returning end pointer to divide the array into 2
    return end
     
# The main function that implements QuickSort
def quick_sort(start, end, array):
     
    if (start < end):
         
        # p is partitioning index, array[p]
        # is at right place
        p = partition(start, end, array)
         
        # Sort elements before partition
        # and after partition
        quick_sort(start, p - 1, array)
        quick_sort(p + 1, end, array)
         
# Driver code
array = [ 10, 7, 8, 9, 1, 5 ]
quick_sort(0, len(array) - 1, array)
 
print(f'Sorted array: {array}')
     
# This code is contributed by Adnan Aliakbar


输出
Sorted array: 
1 5 7 8 9 10 

快速排序分析
QuickSort 花费的时间,一般来说,可以写成如下。

T(n) = T(k) + T(nk-1) + \theta (n)

前两项用于两次递归调用,最后一项用于分区过程。 k 是小于枢轴的元素数。
QuickSort 花费的时间取决于输入数组和分区策略。以下是三种情况。

最坏情况:最坏情况发生在分区过程总是选择最大或最小元素作为主元时。如果我们考虑上面总是选择最后一个元素作为主元的分区策略,最坏的情况会发生在数组已经按升序或降序排序时。以下是最坏情况的复发。

T(n) = T(0) + T(n-1) + \theta (n) 相当于 T(n) = T(n-1) + \theta (n)

上述递归的解为\theta        (n 2 )。

最佳情况:当分区过程总是选择中间元素作为枢轴时,就会出现最好的情况。以下是最佳情况的重复。

T(n) = 2T(n/2) + \theta (n)

上述递归的解为\theta        (nLogn)。可以使用主定理的情况 2 来解决。

平均情况:
要进行平均情况分析,我们需要考虑数组的所有可能排列并计算每个排列所花费的时间,这看起来并不容易。
我们可以通过考虑分区将 O(n/9) 个元素放在一个集合中并将 O(9n/10) 个元素放在另一个集合中的情况来了解平均情况。以下是这种情况的复发。

T(n) = T(n/9) + T(9n/10) + \theta (n)

上面递归的解也是O(nLogn)
尽管 QuickSort 最坏情况下的时间复杂度是 O(n 2 ),这比合并排序和堆排序等许多其他排序算法都要多,但 QuickSort 在实践中更快,因为它的内循环可以在大多数架构上有效实现,并且在大多数情况下真实世界的数据。 QuickSort 可以通过改变主元的选择以不同的方式实现,因此对于给定类型的数据很少发生最坏的情况。但是,当数据很大并且存储在外部存储中时,通常认为合并排序更好。

QuickSort稳定吗?
默认实现不稳定。然而,任何排序算法都可以通过将索引作为比较参数而变得稳定。

QuickSort 是否就地?
根据就地算法的广泛定义,它有资格作为就地排序算法,因为它使用额外的空间仅用于存储递归函数调用,而不是用于操作输入。

什么是三向快速排序?
在简单的 QuickSort 算法中,我们选择一个元素作为主元,围绕主元对数组进行分区,并在主元的左侧和右侧对子数组进行递归。
考虑一个具有许多冗余元素的数组。例如,{1, 4, 2, 4, 2, 4, 1, 2, 4, 1, 2, 2, 2, 2, 4, 1, 4, 4, 4}。如果在 Simple QuickSort 中选择 4 作为主元,我们只修复一个 4 并递归处理剩余的出现。在 3 Way QuickSort 中,数组 arr[l..r] 分为 3 部分:
a) arr[l..i] 元素小于枢轴。
b) arr[i+1..j-1] 元素等于枢轴。
c) arr[j..r] 元素大于枢轴。
见这个实现。

如何为链表实现快速排序?
单向链表上的快速排序
双向链表上的快速排序

我们可以迭代地实现 QuickSort 吗?
是的,请参考迭代快速排序。

为什么快速排序优于 MergeSort 对数组进行排序
快速排序的一般形式是就地排序(即它不需要任何额外的存储空间),而归并排序需要 O(N) 额外的存储空间,N 表示可能非常昂贵的数组大小。分配和取消分配用于归并排序的额外空间会增加算法的运行时间。比较平均复杂度,我们发现两种类型的平均复杂度都是 O(NlogN),但常数不同。对于数组,归并排序由于使用了额外的 O(N) 存储空间而丢失。
快速排序的大多数实际实现都使用随机版本。随机版本的预期时间复杂度为 O(nLogn)。最坏的情况也可能出现在随机版本中,但最坏的情况不会发生在特定模式(如排序数组)中,随机快速排序在实践中效果很好。
快速排序也是一种缓存友好的排序算法,因为它在用于数组时具有良好的引用局部性。
快速排序也是尾递归的,因此完成了尾调用优化。

为什么 MergeSort 比 QuickSort 更适用于链表?
在链表的情况下,情况有所不同,主要是由于数组和链表的内存分配不同。与数组不同,链表节点在内存中可能不相邻。与数组不同,在链表中,我们可以在 O(1) 额外空间和 O(1) 时间内在中间插入项目。因此归并排序的归并操作可以在没有链表额外空间的情况下实现。
在数组中,我们可以进行随机访问,因为元素在内存中是连续的。假设我们有一个整数(4 字节)数组 A,让 A[0] 的地址为 x,然后访问 A[i],我们可以直接访问 (x + i*4) 处的内存。与数组不同,我们不能在链表中进行随机访问。快速排序需要大量此类访问。在访问第 i 个索引的链表中,由于我们没有连续的内存块,我们必须从头到第 i 个节点遍历每个节点。因此,快速排序的开销会增加。归并排序顺序访问数据,随机访问的需求低。

如何优化 QuickSort 以便在最坏的情况下占用 O(Log n) 额外空间?
请参阅 QuickSort 尾调用优化(将最坏情况空间减少到 Log n)
https://youtu.be/PgBzjlCcFvc

快照:

场景00865场景01369

场景01801场景02377场景02881场景03025场景03385场景03889

  • 快速排序测验
  • 最近关于 QuickSort 的文章
  • 排序的编码实践。

参考:
http://en.wikipedia.org/wiki/Quicksort

如果您希望与专家一起参加现场课程,请参阅DSA 现场工作专业课程学生竞争性编程现场课程