快速排序C++及Python实现及优化

快速排序和冒泡排序一样,也是交换排序。不过它是一种不稳定的排序算法(因为关键字的比较和交换是跳跃进行的)。

算法思想:快速排序首先选一个轴值(pivot,也有叫基准的),将待排序记录划分成独立的两部分,左侧的元素均小于轴值,右侧的元素均大于或等于轴值,然后对这两部分再重复,直到整个序列有序,过程是和二叉搜索树相似,就是一个递归的过程。

算法稳定性:不稳定

算法复杂度:

平均时间复杂度: O(n\log n), 递归式 C(n) = n-1 + \frac{1}{n}\sum_{j=0}^{n-1}[C(j) + C(n-j-1)] \approx 2n\log n

最坏时间复杂度: O(n^2) , 当待排序的序列为正序或逆序排列时,每一次取到的元素就是数组中最小/最大的,这种情况其实就是冒泡排序了,需要 (n-1)+(n-2)+…+2+1= O(n^2)

最好时间复杂度: O(n\log n) , 每次都恰好五五分,一次递归共需比较n次,递归深度为 \log n


# include <iostream>

void swap(int A[], int i, int j)
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}

void partition(int A[], int start, int end)
{
int i = start;
int j = end;
while(i<j)
{
while(i<j && A[i] <= A[j])
j--;
swap(A, i, j);
while(i<j && A[i]<= A[j])
i++;
swap(A, i, j);
}
return i;
}

void QuickSort(int A[], int first, int end)
{
int pivot = partition(A, first, end);

QuickSort(A, start, pivot-1)
QuickSort(A, pivot+1, end)
}

迭代版:

void quickSortIterative (int arr[], int l, int h)
{
    // Create an auxiliary stack
    int stack[ h - l + 1 ];

    // initialize top of stack
    int top = -1;

    // push initial values of l and h to stack
    stack[ ++top ] = l;
    stack[ ++top ] = h;

    // Keep popping from stack while is not empty
    while ( top >= 0 )
    {
        // Pop h and l
        h = stack[ top-- ];
        l = stack[ top-- ];

        // Set pivot element at its correct position
        // in sorted array
        int p = partition( arr, l, h );

        // If there are elements on left side of pivot,
        // then push left side to stack
        if ( p-1 > l )
        {
            stack[ ++top ] = l;
            stack[ ++top ] = p - 1;
        }

        // If there are elements on right side of pivot,
        // then push right side to stack
        if ( p+1 < h )
        {
            stack[ ++top ] = p + 1;
            stack[ ++top ] = h;
        }
    }
}


Python代码:

def quick_sort(array, l, r):
    if l < r:
        q = partition(array, l, r)
        quick_sort(array, l, q - 1)
        quick_sort(array, q + 1, r)
 
def partition(array, l, r):
    x = array[r]
    i = l - 1
    for j in range(l, r):
        if array[j] <= x:
            i += 1
            array[i], array[j] = array[j], array[i]
    array[i + 1], array[r] = array[r], array[i+1]
    return i+1


快速排序优化:

选择基准的方式:
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。

最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列

有三种选择基准的方法:固定位置、随机选取基准、三数取中(median-of-three)

固定位置:基本的快速排序选取第一个或最后一个元素作为基准。但是,这是一种很不好的处理方法。如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为冒泡排序,时间复杂度为Θ(n^2)。

随机选取基准:这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。一位前辈做出了一个精辟的总结:“随机化快速排序可以满足一个人一辈子的人品需求。”

三数取中:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。

这里仅给出最好的三数取中代码:

/*函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴*/  
int SelectPivotMedianOfThree(int arr[],int low,int high)  
{  
    int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标  

    //使用三数取中法选择枢轴  
    if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]  
    {  
        swap(arr[mid],arr[high]);  
    }  
    if (arr[low] > arr[high])//目标: arr[low] <= arr[high]  
    {  
        swap(arr[low],arr[high]);  
    }  
    if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]  
    {  
        swap(arr[mid],arr[low]);  
    }  
    //此时,arr[mid] <= arr[low] <= arr[high]  
    return arr[low];  
    //low的位置上保存这三个位置中间的值  
    //分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了  
}

优化1:当待排序序列的长度分割到一定大小后,使用插入排序

原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排

截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。

if (high - low + 1 < 10)  
{  
    InsertSort(arr,low,high);  
    return;  
}
    QuickSort(arr,low,high);

优化2:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割 ,即三路快排。

当待排序元素序列中有大量的重复排序码时,简单的快速排序算法的效率将会降到非常之低。如果把重复元素聚在一起,对其他元素进行划分,则效率会提高很多。

思路:将数组分为三部分,分别对应于小于、等于和大于基准pivot元素v的子序列。

Dijkstra三向切分方法为从左到右遍历(扫描)数组一次,维护一个指针lt使得a[left..lt-1]中的元素都小于v,一个指针gt使得a[gt+1..right]中的元素都大于v,一个指针i使得a[lt..i-1]中的元素都等于v,而a[i..gt]中的元素是还未扫描的。

一开始令i = left + 1,对a[i]与v进行比较,根据比较情况作出不同的处理:

a[i]小于v,将a[lt]和a[i]交换,将lt和i加1;

a[i]大于v,将a[gt]和a[i]交换,将gt减1;

a[i]等于v,将i加1

以上操作会不断缩小gt - i的值,直到i > gt扫描结束。这时就成了下图切分后的情况。

指针 lt 前面的元素都小于pivot,指针 gt之后的元素都大于pivot,在指针 lt 和 i 之间的元素等于pivot,i 从左往右移动,遇到等于pivot的元素,就 i 加1,lt 不用动;所以让 i 从left+1开始(因为left就是pivot,所以 i 会加1,和left+1开始是一样的),遇到小于pivot的元素,就扔到 lt 前面去(即交换 lt 和 i 上的元素),然后 lt 和 i 都加1,可以看出 lt 一直指向 pivot;如果元素大于pivot,就扔到 gt 后面去,gt减1,直到 i 坐标大于 gt,退出循环。

举个具体例子:

a[i]=3小于6,交换a[lt]a[i]:6和3,lti指针后移。

a[i]=8大于6,交换a[gt]a[i]:7和8,gt指针前移。

a[i]=7也大于6,交换a[gt]a[i]:1和7,gt指针继续前移。

a[i]=1小于6,交换a[lt]a[i]:6和1,lti指针后移。

2,1也都小于6,lt 和 i 交换后,指针都后移,直到 i = 6, 只有 i 后移,直到 i = 5, 交换6和5,lti指针后移。然后不满足i <= gt的条件啦,跳出循环

代码:

void sort (int [] a, int low, int high) {
    if(low>=high)  { return; }
    int lt = low, gt = high, i =low+1;
    int v = a[low];
    while(i <= gt) {
      if(a[i] > v) { exchange(a, i, gt--);  }
      else if(a[i] < v) { exchange(a, i++, lt++); }
      else{ i++; }
    }
    sort(a, low, lt-1);
    sort(a, gt+1, high);
  }

Dijkstra的三分区快速排序虽然在快速排序发现不久后就提出来了,但是对于序列中重复值不多的情况下,它比传统的2分区快速排序需要更多的交换次数。

Bentley 和D. McIlroy在普通的三分区快速排序的基础上,对一般的快速排序进行了改进。在划分过程中,i遇到的与v相等的元素交换到最左边,j遇到的与v相等的元素交换到最右边,i与j相遇后再把数组两端与v相等的元素交换到中间

这个方法不能完全满足只扫描一次的要求,但它有两个好处:首先,如果数据中没有重复的值,那么该方法几乎没有额外的开销;其次,如果有重复值,那么这些重复的值不会参与下一趟排序,减少了无用的划分。

下面是采用 Bentley&D. McIlroy 三分区快速排序的算法改进:

void swap(int[] arr, int a, int b) {
    int temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}
void quickSort(int[] a, int left, int right) {
    if (right <= left)
        return;

    // p指向序列左边等于 v 的位置, q指向序列右边等于 v 的位置
    // i指向从左向右扫描时的元素, j指向从右向左扫描时的元素
    int p, q, i, j;
    i = p = left, j = q = right;
    int v = a[left];
    while (i < j) {
        while (i < j && a[j] >= v) {
            // 找到与 v 相等的元素将其交换到q所指的位置
            if (a[j] == v) {
                swap(a, j, q);
                q--;
            }
            j--;
        }
        a[i] = a[j];
        while (i < j && a[i] <= v) {
            // 找到与 v 相等的元素将其交换到p所指的位置
            if (a[i] == v) {
                swap(a, i, p);
                p++;
            }
            i++;
        }
        a[j] = a[i];
    }
    a[i] = v;
    // 因为 i 和 j 最后指向 v 位置,故而需要左、右移一位,再将等于 v 元素交换到中间v旁边
    i--;
    p--;
    while (p >= left) {
        swap(a, i, p);
        i--;
        p--;
    }
    j++;
    q++;
    while (q <= right) {
        swap(a, j, q);
        j++;
        q++;
    }
    // 递归遍历左右子序列
    quickSort(a, left, i);
    quickSort(a, j, right);
}

  具体过程:在处理过程中,会有两个步骤

    第一步,在划分过程中,把与key相等元素放入数组的两端

    第二步,划分结束后,把与key相等的元素移到枢轴周围

  举例:

    待排序序列 6 4 6 7 1 6 7 6 8 6 枢轴key:6

    第一步,在划分过程中,把与key相等元素放入数组的两端

    结果为:6 4 1 6(枢轴) 7 8 7 6 6 6

    此时,与6相等的元素全放入在两端了

    第二步,划分结束后,把与key相等的元素移到枢轴周围

    结果为:1 4 66(枢轴) 6 6 6 7 8 7

    此时,与6相等的元素全移到枢轴周围了

    之后,在1 4 和 7 8 7两个子序列进行快排

概括:这里效率最好的快排组合 是:三数取中+插排+聚集相等元素,它和STL中的Sort函数效率差不多。


番外:

单链表的快排实现为:

1.使第一个节点为pivot.

2.创建2个指针(p,q),p指向头结点,q指向p的下一个节点.

3.q开始遍历,如果发现q的值比pivot中心点的值小,则此时p=p->next,然后交换当前p的值和q的值,q遍历到链表尾即可.

4.把头结点的值和p的值执行交换.此时p节点为中心点,并且完成1轮快排

5.使用递归的方法即可完成排序

    void swap(int *a,int *b){
        int t=*a;
        *a=*b;
        *b=t;
    }
    ListNode *partion(ListNode *pBegin,ListNode *pEnd){
        if(pBegin==NULL||pBegin==pEnd||pBegin->next==pEnd)    return pBegin;
        int key=pBegin->val;    //选择pBegin作为基准元素
        ListNode *p=pBegin,*q=pBegin->next;
        while(q!=pEnd){   //从pBegin开始向后进行一次遍历
            if(q->val<key){
                p=p->next;
                swap(&p->val,&q->val);
            }
            q=q->next;
        }
        swap(&p->val,&pBegin->val);
        return p;
    }
    void quick_sort(ListNode *pBegin,ListNode *pEnd){
        if(pBegin==pEnd||pBegin->next==pEnd)    return;
        ListNode *mid=partion(pBegin,pEnd);
        quick_sort(pBegin,mid);
        quick_sort(mid->next,pEnd);
    }
    ListNode* sortList(ListNode* head) {
        if(head==NULL||head->next==NULL)    return head;
        quick_sort(head,NULL);
        return head;
    }


参考:

blog.csdn.net/yjw123456

编辑于 2019-08-26

文章被以下专栏收录