1. 程式人生 > >七、排序(4)——qsort()

七、排序(4)——qsort()

一、回顧

時間複雜度 是穩定排序 是原地排序
氣泡排序 O(n2)
插入排序 O(n2)
選擇排序 O(n2) ×
快速排序 O(nlogn) ×
歸併排序 O(nlogn) ×
計數排序 O(n+k),k是資料範圍 ×
桶排序 O(n) ×
基數排序 O(dn) d是維度 ×
  • 線性排序演算法的時間複雜度比較低,適用場景比較特殊。
  • 小規模資料進行排序 ==》時間複雜度是 O(n2) 的演算法;
  • 大規模資料進行排序 ==》時間複雜度是O(nlogn) 的演算法 ==》一般也是首選

二、快速排序的優化

關鍵點:合理選擇分割槽點,來避免時間複雜度退化為O(n2

)

理想的分割槽點:被區分點分開的兩個分割槽中,資料的數量差不多。
==》常用、簡單的分割槽演算法

1、三數取中法

從區間的首、尾、中間,分別取出一個數,然後進行比較,取三個數的中位數作為分割槽點。
==》擴充套件:資料大時,“五數取中”、“十數取中”

2、隨機法

每次從要排序的區間中,隨機選擇一個元素作為分割槽點。
特點:雖不能保證每次分割槽點都選的比較好,但是從概率角度分析,不太可能出現每次分割槽點都很差的情況。

3、警惕堆疊溢位

快速排序使用堆疊實現的。==》 遞迴要警惕堆疊溢位

解決方法:
(1)限制遞迴深度。一旦遞迴過深(超過事先設定的閾值),就停止遞迴。
(2)通過在堆上模擬實現一個函式呼叫棧,手動模擬遞迴壓棧、出棧的過程,使得系統棧大小沒有限制。

三、qsort()函式的分析

1、原始碼

/* Copyright (C) 1991,1992,1996,1997,1999,2004 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   Written by Douglas C. Schmidt ([email protected]).

   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.

   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, write to the Free
   Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307 USA.  */

/* If you consider tuning this algorithm, you should consult first:
   Engineering a sort function; Jon Bentley and M. Douglas McIlroy;
   Software - Practice and Experience; Vol. 23 (11), 1249-1265, 1993.  */

#include <alloca.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>

/* Byte-wise swap two items of size SIZE. */
/* 以位元組為單位交換兩個SIZE長度陣列的SWAP巨集 */
#define SWAP(a, b,size)
do                                               
{  
    register size_t __size = (size);
    register char *__a = (a), *__b = (b); 
    do
    {
	har __tmp = *__a;
	*__a++ = *__b;
	*__b++ = __tmp;
    } while (--__size > 0);
} while (0)

/* Discontinue quicksort algorithm when partition gets below this size.
   This particular magic number was chosen to work best on a Sun 4/260. */
/* 當一個數據段的長度小於這個值的時候,將不用快排對其進行排序。因為這個特別的的魔術數在Sun 4/260下的效能最好*/
#define MAX_THRESH 4
/* Stack node declarations used to store unfulfilled partition obligations. */
/* 用來儲存還沒處理的資料段索引*/
typedef struct
{
    char *lo;
    char *hi;
} stack_node;

/* The next 4 #defines implement a very fast in-line stack abstraction. */
/* 下面四個巨集實現了一個非常快速的棧(的確很快,但是現實中真要這樣做,請確保1. 這是核心程式碼。
    2. 這些程式碼百分比正確。要不然出錯了,除錯調死你,維護的人罵死你。)*/
/* The stack needs log (total_elements) entries (we could even subtract log(MAX_THRESH)).
   Since total_elements has type size_t, we get as upper bound for log (total_elements):
   bits per byte (CHAR_BIT) * sizeof(size_t).  */
/* 棧需要log(total_elements)個元素(當然我們也可以在這個基礎上減去log(MAX_THRESH)。)(PS:log(x/y) = log(x) - log(y))
   因為傳入資料個數total_elements的型別是size_t,所以我們可以把棧元素上限設為:size_t的位數(CHAR_BIT) * sizeof(size_t)。
   PS:棧用來記錄的是還沒處理的資料段索引,最壞的情況是分開的兩個資料段其中一個已經不用再分,這個時候棧不許要任何記錄。
   最好的情況是進行log(total_elements)次劃分,此時棧需要記錄log(total_elements)個索引,但是這個演算法在一個分片的元素個數
   小於MAX_THRESH便不再劃分,所以實際只需log(total_elements / MAX_THRESH)個空間。CHAR_BIT在limits.h有定義,意思是一個
   位元組有多少位,因為sizeof是算出一種型別佔幾個位元組,所以(CHAR_BIT) * sizeof(size_t)是當total_elements去最大值的值,也就
   是這裡棧元素個數的上限。*/
#define STACK_SIZE     (CHAR_BIT * sizeof(size_t))
#define PUSH(low, high)  ((void) ((top->lo = (low)), (top->hi = (high)), ++top))
#define POP(low, high)  ((void) (--top, (low = top->lo), (high = top->hi)))
#define STACK_NOT_EMPTY  (stack < top)

/* Order size using quicksort.  This implementation incorporates
   four optimizations discussed in Sedgewick:

這個快排的程式實現了Sedgewick書中討論的四個優化,下面從大到小說明:(大概這意思...)

    1. Non-recursive, using an explicit stack of pointer that store the
      next array partition to sort.  To save time, this maximum amount
      of space required to store an array of SIZE_MAX is allocated on the
      stack.  Assuming a 32-bit (64 bit) integer for size_t, this needs
      only 32 * sizeof(stack_node) == 256 bytes (for 64 bit: 1024 bytes).
      Pretty cheap, actually.
   1. 不用遞迴,用了顯示的指標棧儲存下一段要排序的資料。為了節省時間,為棧申請了最大的儲存空間。假設size_t是一個32位(64位)的整數,這裡僅需要 32 * sizeof(stack_node) = 256 bytes(對於64位:1024bytes)。事實上很小。(一個棧節點兩指標,32位就是2 * 4 個位元組,64位是8 * 2位位元組)

2. Chose the pivot element using a median-of-three decision tree. This reduces the probability of selecting a bad pivot value and
 eliminates certain extraneous comparisons.
   2. 用中值決策樹選擇關鍵值。這減小了選擇一個差關鍵值的可能性和排除特定的無關的比較。

3. Only quicksorts TOTAL_ELEMS / MAX_THRESH partitions, leaving
      insertion sort to order the MAX_THRESH items within each partition.
      This is a big win, since insertion sort is faster for small, mostly
      sorted array segments.
   3. 只用快排對TOTAL_ELEMS / MAX_THRESH個數據段進行了排序,用插入排序對每個資料段的
      MAX_THRESH個數據進行排序。這是一個很好的改進,因為插入排序在處理小的、基本有序的數
      據段時比快排更快。
4. The larger of the two sub-partitions is always pushed onto the
      stack first, with the algorithm then concentrating on the
      smaller partition.  This *guarantees* no more than log (total_elems)
      stack size is needed (actually O(1) in this case)!
   4. 大的資料分段通常先壓入棧內,演算法優先處理小的資料分段。這就保證棧的元素不會超過
      log(total_elems)(事實上這裡只用了常數個空間)。
*/
void _quicksort (void *const pbase, size_t total_elems, size_t size, __compar_d_fn_t cmp, void *arg)
{
    /* 暫存器指標,最快的指標,當然系統不一定會把它放到暫存器。register只是一種建議。*/
    register char *base_ptr = (char *) pbase;

    const size_t max_thresh = MAX_THRESH * size;

    if (total_elems == 0)
        /* Avoid lossage with unsigned arithmetic below.  */
        return;

    if (total_elems > MAX_THRESH)
    {
        char *lo = base_ptr;
        char *hi = &lo[size * (total_elems - 1)];
        /* 因為用了上面棧的巨集,所以下面兩個變數的名字一定不能改...*/
        stack_node stack[STACK_SIZE];
        stack_node *top = stack;

        PUSH (NULL, NULL);

        while (STACK_NOT_EMPTY)
        {
            char *left_ptr;
            char *right_ptr;
            /* Select median value from among LO, MID, and HI. Rearrange
               LO and HI so the three values are sorted. This lowers the
               probability of picking a pathological pivot value and skips a comparison for both the LEFT_PTR and RIGHT_PTR in the while loops. */
               /* 在陣列的第一位、中間一位、最後一位中選出一箇中值。同時也會對第一位和最後
               一位進行排序以達到這三個值都是有序的目的。這降低了選擇一個很爛的關鍵值的
               可能性,同時也跳過了左指標和右指標在while迴圈裡面的一次比較。*/

            char *mid = lo + size * ((hi - lo) / size >> 1);

            if ((*cmp) ((void *) mid, (void *) lo, arg) < 0)
                SWAP (mid, lo, size);
            if ((*cmp) ((void *) hi, (void *) mid, arg) < 0)
                SWAP (mid, hi, size);
            else
                goto jump_over;
            if ((*cmp) ((void *) mid, (void *) lo, arg) < 0)
                SWAP (mid, lo, size);
jump_over:
		;

            left_ptr  = lo + size;
            right_ptr = hi - size;

            /* Here's the famous ``collapse the walls'' section of quicksort.
               Gotta like those tight inner loops!  They are the main reason
               that this algorithm runs much faster than others. */
            /* 這裡就是快排中著名的“collapse the walls(推牆??)”。和那些緊湊
               的內層迴圈非常像!它們是這個演算法比其他演算法快的主要原因。
               PS:瞭解過快排的應該對下面這一段都比較熟悉,就是把比關鍵值小的移
               到陣列左邊,比關鍵值大的移到陣列右邊,把資料分成大小兩段的過程*/
            do
            {
                while ((*cmp) ((void *) left_ptr, (void *) mid, arg) < 0)
                    left_ptr += size;

                while ((*cmp) ((void *) mid, (void *) right_ptr, arg) < 0)
                    right_ptr -= size;
		        if (left_ptr < right_ptr)
                {
                    SWAP (left_ptr, right_ptr, size);
                    if (mid == left_ptr)
                        mid = right_ptr;
                    else if (mid == right_ptr)
                        mid = left_ptr;
                    left_ptr += size;
                    right_ptr -= size;
                }
                else if (left_ptr == right_ptr)
                {
                    left_ptr += size;
                    right_ptr -= size;
                    break;
                }
            }
            while (left_ptr <= right_ptr);

            /* Set up pointers for next iteration.  First determine whether
               left and right partitions are below the threshold size.  If so,
               ignore one or both.  Otherwise, push the larger partition's
               bounds on the stack and continue sorting the smaller one. */
           /*  給下次迭代的指標賦值。首先判斷左右兩段資料的元素個數是否小於閾值
               ,如果是,跳過這一個或兩個分段。如若不是,把大的資料段的開始和結
               束指標入棧,繼續劃分小的資料段。*/

            if ((size_t) (right_ptr - lo) <= max_thresh)
            {
                /* 左右兩個資料段的元素都小於閾值,取出棧中資料段進行劃分*/
                if ((size_t) (hi - left_ptr) <= max_thresh)
                    /* Ignore both small partitions. */
                    POP (lo, hi);
                else
                    /* Ignore small left partition. (只有左邊大於閾值)*/
                    lo = left_ptr;
            }
            else if ((size_t) (hi - left_ptr) <= max_thresh)
                /* Ignore small right partition. (只有右邊大於閾值)*/
                hi = right_ptr;
            else if ((right_ptr - lo) > (hi - left_ptr))
            {
                /* Push larger left partition indices. */
                /* 兩個資料段的元素個數都大於閾值,大的入棧,小的繼續劃分。 */
                PUSH (lo, right_ptr);
                lo = left_ptr;
            }
            else
            {
                /* Push larger right partition indices. */
                /* 兩個資料段的元素個數都大於閾值,大的入棧,小的繼續劃分。 */
                PUSH (left_ptr, hi);
                hi = right_ptr;
            }
        }
    }
    /* Once the BASE_PTR array is partially sorted by quicksort the rest
       is completely sorted using insertion sort, since this is efficient
       for partitions below MAX_THRESH size. BASE_PTR points to the beginning
       of the array to sort, and END_PTR points at the very last element in
       the array (*not* one beyond it!). */
   /*  當陣列經過快排的排序後,已經是整體有序了。剩下的排序由插入排序完成,因
       為資料段小於MAX_THRESH時,插入排序效率更高。此時排序的首指標是陣列的首
       指標,尾指標是陣列的尾指標(不是倒數第二個)*/

#define min(x, y) ((x) < (y) ? (x) : (y))

    {
        char *const end_ptr = &base_ptr[size * (total_elems - 1)];
        char *tmp_ptr = base_ptr;
        char *thresh = min(end_ptr, base_ptr + max_thresh);
        register char *run_ptr;

        /* Find smallest element in first threshold and place it at the
           array's beginning.  This is the smallest array element,
           and the operation speeds up insertion sort's inner loop. */
        /* 找出第一段的最小一個值並把它放在陣列的第一個位置。這是陣列的
           最小元素(用快排排過,應該比較容易理解),這一步可以加入插入
           排序的內層迴圈*/

        for (run_ptr = tmp_ptr + size; run_ptr <= thresh; run_ptr += size)
            if ((*cmp) ((void *) run_ptr, (void *) tmp_ptr, arg) < 0)
                tmp_ptr = run_ptr;

        if (tmp_ptr != base_ptr)
            SWAP (tmp_ptr, base_ptr, size);

        /* Insertion sort, running from left-hand-side up to right-hand-side.  */
        /* 從左到右執行插入排序。*/

        run_ptr = base_ptr + size;
        while ((run_ptr += size) <= end_ptr)
        {
            /*上面說的加速內層迴圈,就是不用在這裡判斷*/
            tmp_ptr = run_ptr - size;
            /*一直往回找直到找到大於或等於當前元素的元素*/
            while ((*cmp) ((void *) run_ptr, (void *) tmp_ptr, arg) < 0)
                tmp_ptr -= size;

            /*把當前元素移到找出元素的後面去*/
            tmp_ptr += size;
            if (tmp_ptr != run_ptr)
            {
                char *trav;

                trav = run_ptr + size;
                while (--trav >= run_ptr)
                {
                    char c = *trav;
                    char *hi, *lo;
            /*這個內層迴圈只是把每個元素的最後一位移到後面一個元素去*/
            for (hi = lo = trav; (lo -= size) >= tmp_ptr; hi = lo)
                        * hi = *lo;
                    *hi = c;
                }
            }
        }
    }
}

2、分析

通過多種排序演算法實現的:

  • 當資料量較小是,qsort() 會優先使用歸併演算法來排序輸入資料(原因:空間複雜度為O(n),空間換時間)
  • 當資料量太大,qsort() 使用快速排序,分割槽點選擇方法:“三數取中法”
  • 遞迴太深會導致堆疊溢位問題的解決方法:qsort()實現了一個堆上的棧,手動模擬遞迴來解決該問題。
  • 當排序的區間中的元素小於等於4時,qsort()就退化為插入排序,不再使用遞迴來做快速排序。原因:在小規模資料面前,O(n2時間複雜度並不一定比O(nlogn)的演算法執行時間長)
    • 時間複雜度代表增長趨勢,但是在表示的過程中,我們會省略低階、係數和常數。例:O(knlogn + c)時間複雜度中的 k 和 c 可能還是一個比較大的數。