【每日算法】交換排序算法之快速排序


恩,重頭戲開始了,快速排序是各種筆試面試最愛考的排序算法之一,且排序思想在很多算法題里面被廣泛使用。是需要重點掌握的排序算法。

1)算法簡介

快速排序是由東尼·霍爾所發展的一種排序算法。其基本思想是基本思想是,通過一趟排序將待排記錄分隔成獨立的兩部分,其中一部分記錄的關鍵字均比另一部分的關鍵字小,則可分別對這兩部分記錄繼續進行排序,以達到整個序列有序。

2)算法描述和分析

快速排序使用分治法來把一個串(list)分為兩個子串行(sub-lists)。

步驟為:
1、從數列中挑出一個元素,稱為 "基准"(pivot),
2、重新排序數列,所有元素比基准值小的擺放在基准前面,所有元素比基准值大的擺在基准的后面(相同的數可以到任一邊)。在這個分區退出之后,該基准就處於數列的中間位置。這個稱為分區(partition)操作。
3、遞歸地(recursive)把小於基准值元素的子數列和大於基准值元素的子數列排序。

遞歸的最底部情形,是數列的大小是零或一,也就是永遠都已經被排序好了。雖然一直遞歸下去,但是這個算法總會退出,因為在每次的迭代(iteration)中,它至少會把一個元素擺到它最后的位置去。

算法偽代碼描述:

function quicksort(q)
    var list less, pivotList, greater
    if length(q) ≤ 1 {
        return q
    } else {
        select a pivot value pivot from q
        for each x in q except the pivot element
            if x < pivot then add x to less
            if x ≥ pivot then add x to greater
            add pivot to pivotList
        return concatenate(quicksort(less), pivotList, quicksort(greater))
    }

在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他Ο(n log n) 算法更快,因為它的內部循環(inner loop)可以在大部分的架構上很有效率地被實現出來。

最差時間復雜度: O(n^2)
最優時間復雜度: O(n log n)
平均時間復雜度: O(n log n)
最差空間復雜度: 根據實現的方式不同而不同

3)算法圖解、flash演示、視頻演示

圖解:
快速排序會遞歸地進行很多輪,其中每一輪稱之為快排的partition算法,即上述算法描述中的第2步,非常重要,且在各種筆試面試中用到該思想的算法題層出不窮,下圖為第一輪的partition算法的一個示例。

快速排序

Flash:
可一步步參見http://ds.fzu.edu.cn/fine/resources/FlashContent.asp?id=86中的快排過程
視頻 舞動的排序算法
http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html

4)算法代碼

    事實上,這個地方需要提一下的是,快排有很多種版本。例如,我們“基准數”的選擇方法不同就有不同的版本,但重要的是快排的思想,我們熟練掌握一種版本,在最后的筆試面試中也夠用了,我這里羅列幾種最有名的版本C代碼。

1、版本一
我們選取數組的第一個元素作為主元,每一輪都是和第一個元素比較大小,通過交換,分成大於和小於它的前后兩部分,再遞歸處理。

代碼如下

/************************************************** 
  函數功能:對數組快速排序                        
  函數參數:指向整型數組arr的首指針arr;           
            整型變量left和right左右邊界的下標    
  函數返回值:空                                   
/**************************************************/  
void QuickSort(int *arr, int left, int right)  
{  
  int i,j;  
  if(left<right)  
  {  
    i=left;j=right;  
    arr[0]=arr[i]; //准備以本次最左邊的元素值為標准進行划分,先保存其值  
    do  
    {  
      while(arr[j]>arr[0] && i<j)   
        j--;        //從右向左找第1個小於標准值的位置j  
      if(i<j)                               //找到了,位置為j  
      {   
        arr[i] = arr[j];  
        i++;  
      }           //將第j個元素置於左端並重置i  
      while(arr[i]<arr[0] && i<j)  
        i++;      //從左向右找第1個大於標准值的位置i  
      if(i<j)                       //找到了,位置為i  
      {   
        arr[j] = arr[i];  
        j--;  
      }           //將第i個元素置於右端並重置j  
    }while(i!=j);  
    arr[i] = arr[0];         //將標准值放入它的最終位置,本次划分結束  
    quicksort(arr, left, i-1);     //對標准值左半部遞歸調用本函數  
    quicksort(arr, i+1, right);    //對標准值右半部遞歸調用本函數  
  }  
}  

2、版本二
隨機選基准數的快排

//使用引用,完成兩數交換  
void Swap(int& a , int& b)  
{  
 int temp = a;  
 a = b;  
 b = temp;  
}  
//取區間內隨機數的函數  
int Rand(int low, int high)  
{  
 int size = hgh - low + 1;  
 return  low + rand()%size;   
}  
    //快排的partition算法,這里的基准數是隨機選取的  
int RandPartition(int* data, int low , int high)  
{      
 swap(data[rand(low,high)], data[low]);//  
 int key = data[low];  
 int i = low;  
   
 for(int j=low+1; j<=high; j++)  
 {  
  if(data[j]<=key)  
  {  
   i = i+1;  
   swap(data[i], data[j]);  
  }              
 }   
 swap(data[i],data[low]);  
 return i;  
}  
//遞歸完成快速排序  
void QuickSort(int* data, int low, int high)  
{  
 if(low<high)  
 {  
  int k = RandPartition(data,low,high);  
  QuickSort(data,low,k-1);  
  QuickSort(data,k+1,high);  
 }  
}  

5)考察點,重點和頻度分析

完全考察快排算法本身的題目,多出現在選擇填空,基本是關於時間空間復雜度的討論,最好最壞的情形交換次數等等。倒是快排的partition算法需要特別注意!頻度極高地被使用在各種算法大題中!詳見下小節列舉的面試小題。

6)筆試面試例題

這里要重點強調的是快排的partition算法,博主當年面試的時候就遇到過數道用該思路的算法題,舉幾道如下:

例題1、最小的k個數,輸入n個整數,找出其中最下的k個數,例如輸入4、5、1、6、2、7、3、8、1、2,輸出最下的4個數,則輸出1、1、2、2。

當然,博主也知道這題可以建大小為k的大頂堆,然后用堆的方法解決。

但是這個題目可也以仿照快速排序,運用partition函數進行求解,不過我們完整的快速排序分割后要遞歸地對前后兩段繼續進行分割,而這里我們需要做的是判定分割的位置,然后再確定對前段還是后段進行分割,所以只對單側分割即可。代碼如下:

void GetLeastNumbers_by_partition(int* input, int n, int* output, int k)  
{  
    if(input == NULL || output == NULL || k > n || n <= 0 || k <= 0)  
        return;  
    int start = 0;  
    int end = n - 1;  
    int index = Partition(input, n, start, end);  
    while(index != k - 1)  
    {  
        if(index > k - 1)  
        {  
            end = index - 1;  
            index = Partition(input, n, start, end);  
        }  
        else  
        {  
            start = index + 1;  
            index = Partition(input, n, start, end);  
        }  
    }  
    for(int i = 0; i < k; ++i)  
        output[i] = input[i];  
}  

例題2、判斷數組中出現超過一半的數字

當然,這道題很多人都見過,而且最通用的一種解法是數對對消的思路。這里只是再給大家提供一種思路,快排partition的方法在很多地方都能使用,比如這題。我們也可以選擇合適的判定條件進行遞歸。代碼如下:

bool g_bInputInvalid = false;  
bool CheckInvalidArray(int* numbers, int length)  
{  
    g_bInputInvalid = false;  
    if(numbers == NULL && length <= 0)  
        g_bInputInvalid = true;  
    return g_bInputInvalid;  
}  
bool CheckMoreThanHalf(int* numbers, int length, int number)  
{  
    int times = 0;  
    for(int i = 0; i < length; ++i)  
    {  
        if(numbers[i] == number)  
            times++;  
    }  
    bool isMoreThanHalf = true;  
    if(times * 2 <= length)  
    {  
        g_bInputInvalid = true;  
        isMoreThanHalf = false;  
    }  
    return isMoreThanHalf;  
}  
int MoreThanHalfNum_Solution1(int* numbers, int length)  
{  
    if(CheckInvalidArray(numbers, length))  
        return 0;  
    int middle = length >> 1;  
    int start = 0;  
    int end = length - 1;  
    int index = Partition(numbers, length, start, end);  
    while(index != middle)  
    {  
        if(index > middle)  
        {  
            end = index - 1;  
            index = Partition(numbers, length, start, end);  
        }  
        else  
        {  
            start = index + 1;  
            index = Partition(numbers, length, start, end);  
        }  
    }  
    int result = numbers[middle];  
    if(!CheckMoreThanHalf(numbers, length, result))  
        result = 0;  
    return result;  
}  

例題3、有一個由大小寫組成的字符串,現在需要對他進行修改,將其中的所有小寫字母排在大寫字母的前面(不要求保持原順序)
這題可能大家都能想到的方法是:設置首尾兩個指針,首指針向后移動尋找大寫字母,尾指針向前移動需找小寫字母,找到后都停下,交換。之后繼續移動,直至相遇。這種方法在這里我就不做討論寫代碼了。

但是這題也可以采用類似快排的partition。這里使用從左往后掃描的方式。字符串在調整的過程中可以分成兩個部分:已排好的小寫字母部分、待調整的剩余部分。用兩個指針i和j,其中i指向待調整的剩余部分的第一個元素,用j指針遍歷待調整的部分。當j指向一個小寫字母時,交換i和j所指的元素。向前移動i、j,直到字符串末尾。代碼如下:

#include <iostream>  
using namespace std;  

void Proc( char *str ) {  
    int i = 0;  
    int j = 0;  
    //移動指針i, 使其指向第一個大寫字母  
    while( str[i] != '\0' && str[i] >= 'a' && str[i] <= 'z' ) i++;  
    if( str[i] != '\0' ) {  
        //指針j遍歷未處理的部分,找到第一個小寫字母  
        for( j=i; str[j] != '\0'; j++ ) {  
            if( str[j] >= 'a' && str[j] <= 'z' ) {  
                char tmp = str[i];  
                str[i] = str[j];  
                str[j] = tmp;  
                i++;  
            }  
        }  
    }  
}  

int main() {  
    char data[] = "SONGjianGoodBest";  
    Proc( data );  
    return 0;  
}  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM