1. 程式人生 > >[C/C++基礎知識] 一篇就讓你徹底搞懂qsort快速排序的文章

[C/C++基礎知識] 一篇就讓你徹底搞懂qsort快速排序的文章

一. C語言實現qsort快速排序

        這段介紹參考百度百科,編譯器函式庫自帶的快速排序函式qsort。使用qsort()排序並用 bsearch()搜尋是一個比較常用的組合,使用方便快捷。
        標頭檔案:stdlib.h
        用 法: void qsort(void *base, int nelem, int width, int (*fcmp)(const void *, const void *));
        引數: 1 待排序陣列首地址
                   2 陣列中待排序元素數量
                   3 各元素的佔用空間大小
                   4 指向函式的指標,用於確定排序的順序

        整數快排
int cmp1(const void *a, const void *b)  
        {  
return *(int*)a - *(int*)b;  
        } 
        qsort(num, len, sizeof(int), cmp1);

#include <stdio.h>  
#include <stdlib.h>  
  
//整數從小到大排序
int cmp1(const void *a, const void *b)  
{  
    return *(int*)a - *(int*)b;  
}  
//整數從大到小排序  
int cmp2(const void *a, const void *b)  
{  
    return *(int*)b - *(int*)a;  
}  
  
int main()  
{  
    int num[8] = {4,11,2,-3,15,4,0,7};    
    int len = 8;  
    int i;  
  
    printf("整數從小到大排序:\n");  
    qsort(num, len, sizeof(int), cmp1);  
    for(i = 0; i < len; i ++)  
    {  
        printf("%d ", num[i]);  
    }  
    printf("\n\n");  
  
    printf("整數從大到小排序:\n");  
    qsort(num, len, sizeof(int), cmp2);  
    for(i = 0; i < len; i ++)  
    {  
        printf("%d ", num[i]);  
    }  
    printf("\n");  
  
	system("PAUSE");
    return 0;  
}   
        執行結果如下圖所示:

        二維陣列快排
int cmp(const void *a, const void *b)  
        {  
return ((int*)a)[0] - ((int*)b)[0];  
        }
        qsort(num, len, sizeof(int)*2, cmp);

#include <stdio.h>  
#include <stdlib.h>  
  
//二維陣列按照num[i][0]從小到大排序
int cmp(const void *a, const void *b)  
{  
    return ((int*)a)[0] - ((int*)b)[0];  
}  

int main()  
{  
    int num[6][2] = {
		4, 1,
	   11, 2,
		2, 3,
	   -3, 4,
	   15, 5,
	    0, 7,
	};    
    int len = 6;  
    int i;  
  
    printf("排序前:\n"); 
	for(i = 0; i < len; i ++)  
    {  
        printf("%4d %4d\n", num[i][0], num[i][1]);  
    } 
	printf("\n"); 

    qsort(num, len, sizeof(int)*2, cmp);   
     
    printf("排序後:\n");  
    for(i = 0; i < len; i ++)  
    {  
        printf("%4d %4d\n", num[i][0], num[i][1]);  
    } 
    printf("\n");  
  
	system("PAUSE");
    return 0;  
}  
        執行結果如下圖所示,其中num[i][0]和num[i][1]同時移動。

字串快排
int cmp(const void *a, const void *b)  
        {  
             return *(char*)a - *(char*)b;  
        }   
        qsort(str, sum, sizeof(char)*10, cmp);

#include <stdio.h>  
#include <stdlib.h>  
  
int cmp(const void *a, const void *b)  
{  
    return *(char*)a - *(char*)b;  
}  

int main()  
{  
    char str[7][10] = {
		"Monday",
		"Tuesday",
		"Wednesday",
		"Thursday",
	    "Friday",
	    "Staturday",
	    "Sunday"
	};    
    int sum = 7;  
    int i;  
  
    printf("排序前:\n"); 
	for(i = 0; i < sum; i++)  
    {  
        printf("%s\n", str[i]);  
    } 
	printf("\n"); 

    qsort(str, sum, sizeof(char)*10, cmp);   
     
    printf("排序後:\n");  
    for(i = 0; i < sum; i++)  
    {  
         printf("%s\n", str[i]);  
    } 
    printf("\n");  
  
	system("PAUSE");
    return 0;  
}   
        同int型別一樣,輸出如下所示:

double快排
int cmp(const void *a, const void *b)  
        {  
            return *(double*)a > *(double*)b ? 1 : -1;  
        }  
        qsort(num, sum, sizeof(double), cmp); 

#include <stdio.h>  
#include <stdlib.h>  
  
int cmp(const void *a, const void *b)  
{  
    return *(double*)a > *(double*)b ? 1 : -1;  
}  

int main()  
{  
    double num[7] = {
		12.2324,
		-4.3457,
		0.00000,
		5.64375,
	    1.25879,
	    0.00001,
	    7.85435
	};    
    int sum = 7;  
    int i;  
  
    printf("排序前:\n"); 
	for(i = 0; i < sum; i++)  
    {  
        printf("%10f\n", num[i]);  
    } 
	printf("\n"); 

    qsort(num, sum, sizeof(double), cmp);   
     
    printf("排序後:\n");  
    for(i = 0; i < sum; i++)  
    {  
         printf("%10f\n", num[i]);  
    } 
    printf("\n");  
  
	system("PAUSE");
    return 0;  
}   
        輸出如下圖所示:

        結構體排序
        int compare1(const void *a,const void *b)  
        {  
            return ((Student*)a)->score - ((Student*)b)->score;  
        }
        qsort(s, N, sizeof(Student), compare1);

#include <stdio.h>  
#include <stdlib.h>  
#define N 6  
  
typedef struct  
{  
    char name[15];  
    int  score;  
  
}Student;  
  
int compare1(const void *a,const void *b)  
{  
    return ((Student*)a)->score - ((Student*)b)->score;  
}  
  
int compare2(const void *a,const void *b)  
{  
    return *(((Student*)a)->name) - *(((Student*)b)->name);  
}  

int main()  
{  
    Student s[N] =  
    {  
    "Zhang San", 94,  
    "Li Si",     80,  
    "You",       94,  
    "I",        100,  
    "He",        72,  
    "She",       60  
    };  
  
    int i; 
	printf("按照分數排序:\n");
    qsort(s, N, sizeof(Student), compare1);  
    for(i = 0; i < N; i++)  
    {  
        printf("%-15s : %d\n", s[i].name, s[i].score);   
    }  
    printf("\n");  
  
    qsort(s, N, sizeof(Student), compare2);  
	printf("按照姓名排序:\n");
    for(i = 0; i < N; i++)  
    {  
        printf("%-15s : %d\n", s[i].name, s[i].score);   
    }  
	system("PAUSE");
    return 0;  
}  
        程式碼輸出如下圖所示:


二. 快速排序原理及手寫快排原始碼


        分治法的思想將一個大問題分割成小規模問題各個擊破,分而治之,常用遞迴解決。常用的包括二分查詢、歸併排序和快速排序等應用。其中思想如下所示:
divide-and-conquer(P)
        {
                  if(|P|<=n0) adhoc(p);             //解決小規模問題
                            divide P into smaller subinstances P1,P2…Pk;
                  for(i=1; i<=k; i++)  {
                            yi = divide-and-conquer(Pi); //分解
                  }
                  return merge(y1,y2…yk);         //合併子演算法
        }

        關於快速排序,由於它的時間複雜度在O(N*logN)效率較高,經常使用。該演算法的基本思想:
1. 先從數列中取出一個數作為基準數。
        2. 分割槽過程,將比這個數大的全放到它右邊,小於或等於它的全放到它的左邊。
        3. 再對左右區間重複第二步,直到各區間只有一個數。

MoreWindows對快速排序作了進一步的說明:挖坑填數+分治法 
        以一個數組作為示例,取區間第一個數為基準數。


        初始時,i = 0; j = 9;   X = a[i] = 72
        由於已經將a[0]中的數儲存到X中,可以理解成在陣列a[0]上挖了個坑,可以將其它資料填充到這來。
        從j開始向前找一個比X小或等於X的數。當j=8,符合條件,將a[8]挖出再填到上一個坑a[0]中。a[0]=a[8]; i++;  這樣一個坑a[0]就被搞定了,但又形成了一個新坑a[8],這怎麼辦了?
        簡單,再找數字來填a[8]這個坑。這次從i開始向後找一個大於X的數,當i=3,符合條件,將a[3]挖出再填到上一個坑中a[8]=a[3]; j--; 陣列變為:


        i = 3;   j =7;   X=72
        再重複上面的步驟,先從後向前找,再從前向後找。
        
從j開始向前找,當j=5,符合條件,將a[5]挖出填到上一個坑中,a[3] = a[5]; i++;
        從i開始向後找,當i=5時,由於i==j退出。此時,i = j = 5,而a[5]剛好又是上次挖的坑,因此將X填入a[5]。陣列變為:


        可以看出a[5]前面的數字都小於它,a[5]後面的數字都大於它。因此再對a[0…4]和a[6…9]這二個子區間重複上述步驟就可以了。
        對挖坑填數進行總結:
        1. i =L; j =R; 將基準數挖出形成第一個坑a[i]。
        2. j--由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。
        3. i++由前向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。
        4. 再重複執行2,3二步,直到i==j,將基準數填入a[i]中。
        照著這個總結很容易實現挖坑填數的程式碼:
        分治法:

void quick_sort1(int s[], int l, int r)  
{  
    if (l < r)  
    {  
        int i = AdjustArray(s, l, r);//先成挖坑填數法調整s[]  
        quick_sort1(s, l, i - 1); // 遞迴呼叫   
        quick_sort1(s, i + 1, r);  
    }  
}  
       挖坑填數:
int AdjustArray(int s[], int l, int r) //返回調整後基準數的位置  
{  
    int i = l, j = r;  
    int x = s[l]; //s[l]即s[i]就是第一個坑  
    while (i < j)  
    {  
        // 從右向左找小於x的數來填s[i]  
        while(i < j && s[j] >= x)   
            j--;    
        if(i < j)   
        {  
            s[i] = s[j]; //將s[j]填到s[i]中,s[j]就形成了一個新的坑  
            i++;  
        }  
  
        // 從左向右找大於或等於x的數來填s[j]  
        while(i < j && s[i] < x)  
            i++;    
        if(i < j)   
        {  
            s[j] = s[i]; //將s[i]填到s[j]中,s[i]就形成了一個新的坑  
            j--;  
        }  
    }  
    //退出時,i等於j。將x填到這個坑中。  
    s[i] = x;  
  
    return i;  
} 
       PS:去一些大公司面試,經常會讓你手寫快排、二叉樹非遞迴遍歷、連結串列翻轉等


三. LeetCode關於Two Sum的快排實現

        下面講一個經典的題目。做過LeetCode的肯定做個這個題目,LeetCode的第一題Two Sum,求兩個數的和。連結:https://leetcode.com/problems/two-sum/

Given an array of integers, find two numbers such that they add up to a specific target number.

The function twoSum should return indices of the two numbers such that they add up to the target, where index1 must be less than index2. Please note that your returned answers (both index1 and index2) are not zero-based.

You may assume that each input would have exactly one solution.

Input: numbers={2, 7, 11, 15}, target=9
Output: index1=1, index2=2


        找到兩個數字和為target,並輸出兩個值的下標。由於陣列可能是無序狀態,故需要排序後輸出即可 。但你需要注意一下幾點:
        1.首先想到的方法是兩層迴圈遍歷普通排序,時間複雜度O(N^2)會TLE
        2.輸出下標不是從0開始,故i+1即可
        3.輸出時負數需要判斷小的在前
           易錯:Input:[-1,-2,-3,-4,-5] -8  Output:[5,3]  Expected:[3,5]
        4.錯誤 Input:[3,2,4] 6  Output:[1,3]  Expected:[2,3]輸出原始位置,所以使用快速排序怎樣解決呢?方案一採用結構,方案二採用二維陣列呼叫qsort:
              (1)先定義結構NODE儲存<值,下標>在進行qsort排序
              (2)再一層迴圈前後兩個指標移動遍歷求和,時間複雜度O(N*logN)
         由於結果唯一,可以通過左右下標移動實現,如果sum<target移動左下標
        5.通過Hash實現時間複雜度O(n)
程式碼如下:
//定義結構 因為需要陣列值和下標同時排序
typedef struct NODE {
    int value;
    int pos;
}node;

//呼叫cmp排序實現結構排序
int cmp(const void *a, const void *b)
{
    return ((node *)a)->value - ((node *)b)->value;
}

int* twoSum(int* nums, int numsSize, int target) {
    int *result;
    int i,j;
    int sum;
    node *numNode;
    result=(int*)malloc(sizeof(int)*2);
    numNode=(node*)malloc(sizeof(node)*numsSize);
    
    for(i=0; i<numsSize; i++) {
        numNode[i].value=nums[i];  
        numNode[i].pos=i;
    }
    
    qsort(numNode,numsSize,sizeof(node),cmp);

    for(i=0,j=numsSize-1; i<j; ) {
        sum=numNode[i].value+numNode[j].value;
        if(sum==target) {
            if(numNode[i].pos<numNode[j].pos) { //小在前
                result[0]=numNode[i].pos+1;
                result[1]=numNode[j].pos+1;
                break;
            }
            else {
                result[0]=numNode[j].pos+1;
                result[1]=numNode[i].pos+1;
                break;
            }
        }
        else if(sum<target) { //左移
            i++;
        }    
        else { //右移
            j--;
        }
    }
    return result;
}
        當然你也可以呼叫unordered_map實現雜湊表O(n)演算法:
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> vec;
        unordered_map<int,int> map;
        for(int i=0; i<nums.size(); i++){
            if(map.find(target-nums[i]) != map.end()){
                vec.push_back(map[target-nums[i]]+1);
                vec.push_back(i+1);
                return vec;
            }
          map[nums[i]]=i;
        }
    }
};

        希望文章對你有所幫助吧!如果有錯誤或不足之處,還請海涵。最後分析一個今天的故事:
        今天去一個小公司面試(百度百科沒有該公司詞條),那個Leader感覺很不尊重人,一方面總是不屑的搖頭與嘲笑人,一方面又總和旁邊的人聊天,還不斷打斷人說話。確實我普通話帶有西南口音,但是我就不明白了,難道這能影響到我寫程式嗎?雖然他直言不諱的說我程式語言不說精通,連熟悉都達不到,但是我前面都說了我不精通任何東西,只是一般。讓我給他講知識圖譜,他說要用通俗的話,那我就舉例子喬布斯、蘋果等,他又說沒難度,那我就講神經網路,他說他不會程式設計。我不知道怎麼說了~他居然還說百度可能僅僅是通過傳統的搜尋引擎現在返回一個值,如姚明的身高,而沒有真正去做這個知識圖譜,我不知道怎麼吐槽。
         僅僅面試了10分鐘,我就想走了,後面的問題我都說不知道,問我Python的優勢,我說指令碼語言、開發快、語言簡潔,面向物件。他非說是面向過程的,和我爭論怎麼是面向物件的,我說舉個例子建房子,他又說那是老師教的,我讓你講例項。好吧!我承認我比較弱,但是Python是面向物件這種常識都不知道,我只能呵呵了~
         然後他說你這也不會,那也不會,那就簡單講講你認為自己最優秀的地方。我就說我最大的優點就是堅持,不論10年,5年也好,我能做出東西來;同時以後還是想會貴州當老師。他最後說我們要的人還是有要求的,明確告訴你不會給你offer,我當時也回了句:我也不會來的。
        最後離開的時候我給HR說我就是很不爽他,就沒心思答了,而且我表達能力也不好,但還是非常感謝你的推薦。最後當得知他是國內最好的大學畢業的時候,我也非常震驚。我脾氣只有這麼好了,二十幾年的光陰裡沒怎麼生過氣,當時確實生氣。只恨臉皮薄,當時沒損他幾句,但那又顯得不夠大度。
        其實我想說:面試本身就是一個相互學習的過程,確實很多基礎知識我都不記得了,可能也不善言表,但是你一個面試官不尊重面試人員,那我就是看不起你。不論你賺再多的錢,再有本事,學校再厲害,我就是看不起,做人似乎比工作更重要吧!我也大大小小參加了不少的面試,阿里、360、百度都有,各種大牛也遇到過,馬雲、吳恩達、劉知遠的視訊和講座也看過不少,清華北大也都有同學,但是沒見過這麼不尊重人的,面試過程中又嘲諷又聊天的,各種秀英語,還吐槽我普通話。
        先做人,後做事吧!看著比我年輕幾歲,這種秀優越感只能理解為不成熟。
        如果他們公司都是這樣招人的,那早晚也會毀在他們手上的,雖然他們公司現在有很大的潛力;如果我還在他說下幹活,遇到個東西整天吐槽你做得不夠好,給他說程式碼他又說不會,我也早晚會辭職的。總之,以後如果我走到當面試官那一步了,最起碼要尊重面試人員,因為你是在尋財,而不是秀優越感!而且我都不知道哪裡來的優越感,學校嗎?好吧!但我還是看不起你這個人~
        當然面試也讓我知道了自己還存在很多不足,尤其是底層的演算法很多都忘了,最近紮紮實實看書和做LeetCode吧!自己技術還是不夠精通,非常致命,包括同步非同步通訊,執行緒程序程式碼等~但還是感謝那個熱心的HR吧!以後也儘量別參加這種沒有筆試,通過獵頭直接去小公司面試了。
      (By:Eastmount 2015-10-11 清晨6點http://blog.csdn.net/eastmount/