📜  快速排序

📅  最后修改于: 2021-04-24 14:56:54             🧑  作者: Mango

像合并排序一样,QuickSort是一种分而治之算法。它选择一个元素作为枢轴,并围绕拾取的枢轴对给定的数组进行分区。 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++
/* 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


输出
Sorted array: 
1 5 7 8 9 10 

QuickSort分析
通常,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是否就位?
根据就地算法的广义定义,它有资格作为就地排序算法,因为它仅使用多余的空间来存储递归函数调用,而不使用操纵输入。

什么是3向QuickSort?
在简单的QuickSort算法中,我们选择一个元素作为枢轴,围绕枢轴对数组进行分区,然后在枢轴的左右两侧递归子数组。
考虑具有许多冗余元素的阵列。例如,{1、4、2、4、2、4、1、2、4、1、2、2、2、2、4、1、4、4、4}。如果在Simple QuickSort中将4选作枢轴,我们将只修复1个4并递归处理剩余的事件。在3 Way QuickSort中,数组arr [l..r]分为3部分:
a)小于枢轴的arr [l..i]元素。
b)arr [i + 1..j-1]元素等于数据透视。
c)大于枢轴的arr [j..r]元素。
参见此实现。

如何实现链表的QuickSort?
单链接列表上的QuickSort
双向链接列表上的QuickSort

我们可以迭代地实现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

快照:

场景scene01369

scene01801scene02377scene02881场景03025scene03385scene03889

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

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