1. 程式人生 > >演算法5:快速排序的套路 | 分治 | 重用 | 定中位

演算法5:快速排序的套路 | 分治 | 重用 | 定中位

之前小程介紹合併排序的套路時,提到了“重用”與“分治”的套路,這兩個套路,在快速排序的演算法設計上,同樣有所體現。

在“重用”套路上,快排跟合排一樣,都是重用自身(因為自身就具有排序功能),讓某段數列變得有序。

在“分治”套路上,相比於合排,快排更具有心思,不是簡單的“分”,而是有設計的巧妙的“分”,這也使得在重用自己變得有序之後,不需要再跟其它段的數列進行合併。

本文介紹快速排序的演算法套路,也就是重用與分治的套路。

重用

在重用方面,很簡單,就是呼叫快速排序自身,讓某段數列有序,這跟合排一樣,這裡不細說了,如果你想了解更多重用套路的概念,可以參考之前的文章。

分治

快速排序的演算法設計,經典套路在於“分治”套路,它的“分”跟合排不一樣。

合排的“分治”很簡單,都是對半地一分為二,分出來的兩段數列,在重用自己變得有序之後,還需要合併起來,因為這兩段數列沒有絕對地哪段更大,需要比較著來合併。

而快排的“分治”套路,是精心設計的,並不是簡單的對半一分為二,而是先在數列中找出一個位置,這個位置上的值,均大於它左邊的數列的值,均小於它右邊的數列的值,也就是出現“左小右大”的排列。那麼,這個位置(以及值)就是最終排序後的位置,是不需要再動的,因為這個位置的左邊的數列都小於這個位置的值,而右邊的數列都大小這個位置的值。於是,可以很自然地,分出“左小”的數列,以及“右大”的數列,對於這兩個數列,再次重用自己就可以變得有序,而且,有序後的兩個數列並不需要再次合併,因為左數列總是小於右數列。

所以,快速排序所體現的套路,最經典的就是分治套路,而且是精心設計的分治。

以上,主要講解了快排的分治與重用的套路,接下來介紹快排更具體的設計思路。

在設計分治的時候,需要找到一個位置,使得“左小右大”,這一個操作,可以視為一個標準作業,叫作“確定中位”,顯然這個操作也是會不斷地被重用到的。

小程介紹一下如何確定中位,這個標準作業也是設計快排演算法的關鍵點。

確定中位前,先要確定中位值。

中位值,坐在中位,左邊的數比中位值小,右邊的數比中位值大。於是,這個中位值就是最終排序後的一個元素,它的位置不需要再調整。然後,就中位兩邊的兩部分,再重複呼叫自身來排序(實際也是定值定位的操作),即可解決問題。

(一)定中位值

這個很隨意。

一般,可以選擇數列最左邊,或最右邊的值作為中位值。

也可隨機地選擇一個值(包括中間的值),然後跟最左或最右的值進行互換,於是又回到了一般的思路。

對於中位值,需要記錄下來,因為數列中的中位值很快就會被其它值覆蓋。

中位值,最終要坐到中位。

(二)定中位

定中位,是實現快排的最複雜的流程。

確定出來的中位,用來放置中位值。

最終,要保證“左小右大”,即中位左邊的數小於(或等於)中位值,而右邊的數大於(或等於)中位值。

小程這裡介紹兩個設計思路。

(1)“左小右大”

參考網上的一個圖,最終ij相逢的位置,就是中位:

設計兩個索引i跟j,分列數列的兩端。不斷地把小值扔到i的位置,然後把大值扔到j的位置,一直保證[start,i]是小值,[j,end]是大值。

最終,i==j,設定好中位值,再返回i。

在拿到中位後,數列分為[start,i-1]跟[i+1,end]兩部分,用次用“快排”的演算法解決它們。

這是“左小右大”的一個演示圖:

可以看到,每一輪的定位,都是不斷地把紅框區域變小,最終就是中位。

另外,有一個細節,當arr[j]覆蓋到左邊後,就相當arr[j]是一個空出來的位置,等著arr[i]來覆蓋,所以,這時一定是i++,再拿arr[i]跟中位值比較。在arr[i]覆蓋到右邊後,也是同樣的道理,一定是j--,再拿arr[j]跟中位值比較。

根據這種設計思想,可以這樣編碼實現(小程再次強調,演算法套路跟編碼是兩個話題):

#include <stdio.h>

int position(int* arr, int i, int j) {
    int v = arr[i];
    while (i < j) {
        while (i < j && arr[j] >= v) {
            j--;
        }
        if (i<j) {
            arr[i++] = arr[j];
        }
        while (i<j && arr[i]<v) {
            i++;
        }
        if (i<j) {
            arr[j--]=arr[i];
        }
    }
    arr[i]=v;
    return i;
}
    
void _quicksort(int* arr, int i, int j) {
    if (i<j) {
        int pos = position(arr, i, j);
        _quicksort(arr, i, pos-1);
        _quicksort(arr, pos+1, j);
    }
}

void quicksort(int* arr, int size) {
    _quicksort(arr, 0, size-1);
}

int main(int argc, char *argv[])
{
    int arr[] = {4, 2, 5, 1, 6, 6, 8, 9, 8, 3};
    int size=sizeof arr/sizeof *arr;
    for (int i = 0; i < size; i ++) {
        printf("%d, ", arr[i]);
    }
    quicksort(arr, size);
    printf("\nafter_sort:\n");
    for (int i = 0; i < size; i ++) {
        printf("%d, ", arr[i]);
    }
    printf("\n");
    return 0;
}
(2)“左小”

把所有小值扔到左邊,最後,中位就是“小值堆”的右邊的第一個位置。

可以取list[end]即最右的數值為中位值(或最左邊也可以)。

設計i、j索引。保證[start,i]為小值,而j用來遍歷所有數值(從start到end),如果發現list[j]小於中位值,則擴大[start,i]並把小值扔到裡面(list[j]與list[i]互換即可),而j的遍歷就是找出小值跟list[++i]互換。

最終,i+1為中位。

示意圖是這樣的:

按這種設計,可以這樣寫程式碼:

#include <iostream>
using namespace std;

template< typename T >
void Exchange( 
        T &  leftElement, 
        T & rightElement 
        )
{
    T  temp = leftElement;
    leftElement = rightElement;
    rightElement = temp;
}
template< typename T >
int  Partition( 
        T List[], 
        int  nStartPos, 
        int  nStopPos
        )
{
    if ( nStartPos < 0 || nStopPos < 0 )
        return -1;
    int  nLessEndPos = nStartPos - 1;
    int  nCurrentDealPos = nStartPos;
    while ( nCurrentDealPos < nStopPos )
    {
        if ( List[nCurrentDealPos] <= List[nStopPos] )
        {
            nLessEndPos ++;
            Exchange( List[nLessEndPos], List[nCurrentDealPos] );
        }
        nCurrentDealPos ++;
    }

    Exchange( List[nLessEndPos+1], List[nStopPos] );
    return nLessEndPos + 1;
}

template< typename T >
void QuickSort( 
        T List[], 
        int nStartPos, 
        int nStopPos
        )
{
    if ( nStartPos < nStopPos )
    {
        int  nMidPos = Partition<T>( List, nStartPos, nStopPos );
        QuickSort( List, nStartPos, nMidPos - 1 );
        QuickSort( List, nMidPos + 1, nStopPos );
    }
}

int main(int argc, const char *argv[])
{
    int arr[] = {4, 2, 5, 1, 6, 6, 8, 9, 8, 3};
    int size=sizeof arr/sizeof *arr;
    for (int i = 0; i < size; i ++) {
        printf("%d, ", arr[i]);
    }
    QuickSort<int>(arr, 0, size-1);
    printf("\nafter_sort:\n");
    for (int i = 0; i < size; i ++) {
        printf("%d, ", arr[i]);
    }
    printf("\n");

    return 0;
}

你不必在意程式碼實現的語言細節,事實上,程式碼實現並不是小程介紹的重點。

至此,如何“定中位”的關鍵設計就介紹完畢了,而快排的設計套路也介紹到這裡。

總結一下,本文介紹了快速排序的重要套路,也就是重用跟分治的套路,特別是分治的設計,因為它是經過精心設計的套路,並非簡單的一分為二。另外,本文也介紹了快排的設計關鍵,也就是如何“定中位”。最後,演示瞭如何用程式碼實現快速排序的演算法設計。


相關推薦

演算法5快速排序套路 | 分治 | 重用 |

之前小程介紹合併排序的套路時,提到了“重用”與“分治”的套路,這兩個套路,在快速排序的演算法設計上,同樣有所體現。 在“重用”套路上,快排跟合排一樣,都是重用自身(因為自身就具有排序功能),讓某段數列變得有序。 在“分治”套路上,相比於合排,快排更具有心思,不是簡單的“分”,而是有設計的巧妙的“分”,這也使得

資料結構與演算法分析筆記與總結(java實現)--排序5快速排序練習題

題目:對於一個int陣列,請編寫一個快速排序演算法,對陣列元素排序。給定一個int陣列A及陣列的大小n,請返回排序後的陣列。測試樣例:[1,2,3,5,2,3],6 [1,2,2,3,3,5] 思路: 快速排序是使用二分思想,通過遞迴來實現排序的。對於一個數組,它先隨機選

排序5快速排序

元素 swap while 實例 pes 步驟 等於 自己 data http://blog.csdn.net/morewindows/article/details/6684558 選擇一個基準元素,通常選擇第一個元素或者最後一個元素,通過一趟掃描,將待排序列分成兩部

排序演算法(六)快速排序(Quick Sort)

基本思想: 1)選擇一個基準元素,通常選擇第一個元素或者最後一個元素, 2)通過一趟排序講待排序的記錄分割成獨立的兩部分,其中一部分記錄的元素值均比基準元素值小。另一部分記錄的 元素值比基準值大。 3)此時基準元素在其排好序後的正確位置 4)然後分別對這兩部分記錄用同樣

排序演算法快速排序

快速排序是一種交換排序。 快速排序由C. A. R. Hoare在1962年提出。 它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分:分割點左邊都是比它小的數,右邊都是比它大的數。 然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此

排序演算法快速排序

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h>

經典演算法之一快速排序

快速排序由於排序效率在同為O(N*logN)的幾種排序方法中效率較高,因此經常被採用,再加上快速排序思想----分治法也確實實用,因此很多軟體公司的筆試面試,包括像騰訊,微軟等知名IT公司都喜歡考這個

排序演算法5快速排序

快速排序 很早就聽說了快速排序的大名,然後看演算法導論的相關視訊也感覺講的很有意思,現在總結一下。針對待排序陣列{a1,a2……an}快速排序可以分解為三步: 尋找基準數,比較通常就是選擇待排序的首專案或者中間專案; 2.根據與基準數的大小關係, 將待排

演算法導論 第七章快速排序 筆記(快速排序的描述、快速排序的效能、快速排序的隨機化版本、快速排序分析)

快速排序的最壞情況時間複雜度為Θ(n^2)。雖然最壞情況時間複雜度很差,但是快速排序通常是實際排序應用中最好的選擇,因為它的平均效能很好。它的期望執行時間複雜度為Θ(n lg n),而且Θ(n lg n)中蘊含的常數因子非常小,而且它還是原址排序的。 快速排序是一種排序演算法,對包含n個數的

最常用的排序快速排序演算法

快速排序(QuickSort)是一種很高效的排序演算法,但實際操作過程中不容易寫出十分正確具有健壯性的程式碼,所以我想講清這個問題,也能使自己理解更加深刻 演算法思想 通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此

演算法快速排序

轉載地址:https://mp.weixin.qq.com/s/e5iF6zIf_ZpaxkVVANf6nA   【資料結構與演算法】 通俗易懂講解 快速排序   快速排序介紹 快速排序(Quick Sort)使用分治法策略。它的基本思想是:選擇一個基準數,通

五分鐘學會一個高難度演算法快速排序

前言 由於LeetCode上的演算法題很多涉及到一些基礎的資料結構,為了更好的理解後續更新的一些複雜題目的動畫,推出一個新系列 -----《圖解資料結構》,主要使用動畫來描述常見的資料結構和演算法。本系列包括十大排序、堆、佇列、樹、並查集、圖等等大概幾十篇。 快速排序 快速排序是由東尼·霍爾所發展的一種

排序演算法5)--快速排序QuickSort

快速排序 時間複雜度: 平均O(nlogn) 最差的情況就是每一次取到的元素就是陣列中最小/最大的,這種情況其實就是氣泡排序了(每一次都排好一個元素的順序) 這種情況時間複雜度,就是氣泡排序的時間複雜度:T[n] = n * (n-1) = n^2 + n; 綜

坐在馬桶上看演算法快速排序

演算法的精髓在於,跟它一比高數也顯得那麼生動活潑…。本文由啊哈磊吐槽而成,話說我還是頭一次見到這麼萌的變數,簡直顛覆我對變數這個兵種、對演算法這個種族的傳統觀念。。 高快省的排序演算法 有沒有既不浪費空間又可以快一點的排序演算法呢?那就是“快速排序”啦!光聽這個名字是不是就覺得很高階呢。 假設我們現在對

演算法訓練營(一)快速排序

#/usr/bin/python #coding:utf8 import random import time import copy testlist = [6,1,2,7,9,3,4,5,10,8] testlist = [6,1,2,7,9,3,4,5,10,8,2,11,8,1

Erlang演算法一章快速排序

Erlang演算法一章:快速排序 快速排序演算法 快速排序演算法 快速排序的核心思想是分而治之,①把陣列列表根據某一取值分成兩段,左邊都比中間值小,右邊都比中間大,再對左右兩邊執行①操作即可。 用Erlang語言描述比較簡單,如下新建q_sor

圖解演算法學習筆記(四)快速排序

本章內容:學習分而治之,快速排序 1) 示例1: 假設你是農場主,有一小塊土地,你要將這塊地均勻分成方塊,且分出的方塊儘可能大。如何分? 你要將這塊地均勻分成方塊,且分出的方塊要儘可能大。顯然,下面的分法不符合要求。 此時,你應該使用D&C策略(div

排序演算法學習快速的桶排序

排序原理 假設現在要對5 3 5 2 8這幾個數進行排序。我們申請一個大小為9的陣列(待排序數中最大數+1),假設為9個木桶,把9個木桶置為0,如圖: 然後在對應桶的編號(這就是為什麼要設定為待排序陣列中最大數+1了)中標記1,如圖在a[5]中標記為

資料結構與演算法 快速排序

void quickSort(vector<int> &arra, int L, int R) { int *p; p = partition2(arra, L, R); cout << p[0] <<

演算法快學筆記(四)快速排序的原理與實現

1. 原理介紹 快速排序是一種排序演算法,速度比選擇排序快得多,其主要基於“分而治之”的思想對集合進行排序,本文將對該演算法進行分析。 2. 分而治之(D&C)的思想 D&C主要指利用遞迴的方式來不斷的縮小需要處理問題的規模,最終使問題容易解決。使用D&C