為什麼自己寫的qsort比不上C語言庫里自帶的qsort效率高?

代碼完全按照演算法導論寫的,而且在數的個數超過1000000的時候還會崩潰(等很長時間卻不出結果)。請問有類似情況的嗎?還是我自己的代碼或者電腦的問題?

實在抱歉忘了貼代碼我也是醉了

——————————————————

#include&

int partition(int *a,int l,int r)
{
int x=a[r];
int i=l-1;
int j;
for (j=l;j&<=r-1;j++) { if (a[j]&<=x) { i++; int temp=a[i]; a[i]=a[j]; a[j]=temp; } } x=a[i+1]; a[i+1]=a[r]; a[r]=x; return i+1; } void qsort(int *a,int l,int r) { if (l&

上面是我看完書之後寫的,測試排了10個數感覺還不錯,結果今天隨機生成了1000000個數測試卻發現了問題所以想問一下。


快速排序水很深啊。我不貼代碼,主要講講優化思路和手段吧。

1. 合理選擇pivot

你就直接選擇分區的第一個或最後一個元素做pivot肯定是不合適的。對於已經排好序,或者接近排好序的情況,會進入最差情況,時間複雜度衰退到O(N^{2} )

pivot選取的理想情況是:讓分區中比pivot小的元素數量和比pivot大的元素數量差不多。較常用的做法是三數取中(midian of three),即從第一項、最後一項、中間一項中取中位數作為pivot。當然這並不能完全避免最差情況的發生。所以很多時候會採取更小心、更嚴謹的pivot選擇方案(對於大數組特別重要)。比如先把大數組平均切分成左中右三個部分,每個部分用三數取中得到一個中位數,再從得到的三個中位數中找出中位數。

我在javascript v8引擎中看到了另外一種選擇pivot的方式:認為超過1000項的數組是大數組,每隔200左右(不固定)選出一個元素,從這些元素中找出中位數,再加入首尾兩個元素,從這個三個元素中找出中位數作為pivot。

By the way,現實環境中,你要對一個預先有一定順序的數組做排序的需求太太太普遍了,這個優化必須要有。

2. 更快地分區

我看到題主的做法是從左向右依次與pivot比較,做交換,這樣做其實效率並不高。舉個簡單的例子,一個數組[2, 1, 3, 1, 3, 1, 3],選第一個元素作為pivot,如果按題主的方式,每次發現比2小的數會引起一次交換,一共三次。然而,直觀來說,其實只要將第一個3和最後一個1交換就可以達到這三次交換的效果。所以更理想的分區方式是從兩邊向中間遍歷的雙向分區方式。實現的話你可以參考樓上 @林麵包的實現。

3. 處理重複元素的問題

假如一個數組裡的元素全部一樣大(或者存在大量相同元素),會怎麼樣?這是一個邊界case,但是會令快速排序進入最差情況,因為不管怎麼選pivot,都會使分區結果一邊很大一邊很小。那怎麼解決這個問題呢?還是修改分區過程,思路跟上面說的雙向分區類似,但是會更複雜,我們需要小於pivot、等於pivot、大於pivot三個分區。既然說了不貼代碼,那就點到為止吧,有興趣可以自己找別人實現看看。

4. 優化小數組效率

這一點很多人都提到了。為什麼要優化小數組?因為對於規模很小的情況,快速排序的優勢並不明顯(可能沒有優勢),而遞歸型的演算法還會帶來額外的開銷。於是對於這類情況可以選擇非遞歸型的演算法來替代。好,那就有兩個問題:多小的數組算小數組?替換的演算法是什麼?

通常這個閾值設定為16(v8中設定的是10),替換的演算法一般是選擇排序。據說閾值的設定是要考慮更好地利用cpu緩存,這個問題我就不是很清楚了,不深入。同樣,對於分區得到的小數組是要立刻進行選擇排序,還是等分區全部結束了之後,再統一進行選擇排序,這個問題也會存在一定的緩存命中的區別,我也不懂,不深入。

5. 監控遞歸過程

這裡我要說的是內省排序。想想看,我們已經做了一些努力來避免快速排序演算法進入最壞的情況。但事實上可能並不如我們想像地那麼理想。理想情況下,快速排序演算法的遞歸嘗試會到多深呢?這個很好回答:log N。好,如果現在遞歸深度已經到了log N,我會覺得很正常,畢竟不太可能每次都是最好情況嘛;那如果此時遞歸深度達到2	imes log N呢?我想你應該慌了,比理想情況遞歸深了一倍還沒有結束。而此時,我覺得可以認為可能已經進入最差情況了,繼續使用快速排序只會更遭,可以考慮對這個分區採用其他排序演算法來處理。通常我們會使用堆排序。為啥要用堆排序?因為它的平均和最差時間複雜度都是O(NlogN)。這就是內省排序的思想。

6. 優化遞歸

我想先說明一點:內省排序雖然會避免遞歸過深,但它的目的並不是為了優化遞歸。

在分區過程中,我們其實是把一個大的問題分解成兩個小一點的問題分別處理。這時我們需要考慮一下,這兩個小問題哪個更小。先處理更小規模的問題,再處理更大規模的問題,這樣可以減小遞歸深度,節約棧開銷。

樓上也有人提到了尾遞歸。對於支持尾遞歸的語言,自然是極好的,小規模的問題先遞歸,減少遞歸深度,大規模的問題直接通過尾遞歸優化掉,不進入遞歸棧。

然而並不是所有的語言都支持尾遞歸⊙︿⊙,比如python(據說)和javascript。在javascript的v8引擎中,我看到它是用一個循環變相手動實現了一個與尾遞歸效果一樣的優化,棒棒噠。

7. 並行

既然快速排序演算法是典型的分治演算法,那麼對於分解下來的小問題是可以在不同的線程中並行處理的。當然對於javascript還是不適用,嗯,我是做前端的。


/********************************/

當日更新:修改了一些筆誤還有一處我的理解錯誤=_=都怪我眼殘

當日更新:優化了一下注釋排版

/********************************/

以glibc的源碼開始剖析吧。

/*
首先實現qsort需要有一個swap:
*/

#define SWAP(a, b, size)
do
{
size_t __size = (size);
char *__a = (a), *__b = (b);
do
{
char __tmp = *__a;
*__a++ = *__b;
*__b++ = __tmp;
} while (--__size &> 0);
} while (0)

/*
為了閱讀方便,我把用於宏定義續行用的去掉了,方便看到語法高亮。

SWAP的定義很簡單,只是以char為單位長度進行交換。實際上,對於足夠大的對象,這裡還有
一定的優化空間,但qsort不能假定你的對象足夠大。這也是qsort統計性能不如C++的
std::sort的原因之一。

所謂qsort並不是單純的快速排序,當快速排序劃分區間小到一定程度時,改用插入排序可以在
更少的耗時內完成排序。glibc將這個小區間定義為元素數不超過常數4的區間:
*/

#define MAX_THRESH 4

/*
可以認為對每一個小區間的插入排序耗時是不超過一個常數值(即對4個元素進行插入排序的最
差情況)的,這樣插入排序總耗時也可以認為是線性的,不影響總體時間複雜度。

接下來這段不難理解:
*/

typedef struct
{
char *lo;
char *hi;
} stack_node;

#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) /* 這顯然是要手動維護棧來模擬遞歸,避免實際遞歸的函數調用開銷了。題主自己的實現在元素數 量過多的時候會崩潰,估計就是遞歸過深爆棧了。 比較值得一提的是STACK_SIZE的選擇。由於元素數量是由一個size_t表示的,size_t的最大 值應該是2 ^ (CHAR_BIT * sizeof(size_t)),快速排序的理想「遞歸」深度是對元素總數 求對數,所以理想的「遞歸」深度是CHAR_BIT * sizeof(size_t)。雖然理論上最差情況下 「遞歸」深度會變成2 ^ (CHAR_BIT * sizeof(size_t)),但是一方面我們不需要從頭到尾 快速排序(區間足夠小時改用插入排序),另一方面,我們是在假設元素數量等於size_t的上 限...這都把內存給擠滿了=_=所以最後glibc決定直接採用理想「遞歸」深度作為棧大小上限。 接下來該進入qsort的正文了: */ void _quicksort (void *const pbase, size_t total_elems, size_t size, __compar_d_fn_t cmp, void *arg) { /* qsort實際上是通過調用這個_quicksort實現的。最後的arg是個workaround,用來搞定 qsort_r的。暫且不去理會。下面是一些準備工作: */ char *base_ptr = (char *) pbase; const size_t max_thresh = MAX_THRESH * size; if (total_elems == 0) return; /* 明確了對內存操作是以sizeof(char)為單位的。max_thresh實際上是改用插入排序時,維護 當前區間的兩個指針(類型均為char*)之間的距離。另外,如果元素數量為0,qsort啥也不 干,函數直接返回。 */ if (total_elems &> MAX_THRESH)
// 如果元素數大於終止區間(4個元素),則進行快速排序
{
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;

/*
這裡採用的是中值劃分,用於降低特定序列導致遞歸惡化影響。也就是說,對區間里的首元
素、中間元素和尾元素先進行排序。排序方式是...呃,是冒泡。反正也就3個元素嘛。
*/

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; 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; /* 和傳統的劃分方式不同,首先把中間當作鍵值在左側找到一個不小於mid的元素,右側找到一個 不大於mid的元素 */ 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; } /* 如果左右側找到的不是同一個元素,那就交換之。如果左右側任意一側已經達到mid,就把mid往 另一邊挪。因為鍵值已經被「丟」過去了。 */ else if (left_ptr == right_ptr) { left_ptr += size; right_ptr -= size; break; } } /* 如果左右側是同一元素,劃分其實已經大功告成了。 */ while (left_ptr &<= right_ptr); /* 接下來,將劃分出來的所有大於的終止區間的區間壓入棧準備下一次劃分,連這裡都會發生喪心 病狂的優化...不能浪費已經在棧里的原區間上下界=_= */ if ((size_t) (right_ptr - lo) &<= max_thresh) { if ((size_t) (hi - left_ptr) &<= max_thresh) POP (lo, hi); else lo = left_ptr; } else if ((size_t) (hi - left_ptr) &<= max_thresh) hi = right_ptr; else if ((right_ptr - lo) &> (hi - left_ptr))
{
PUSH (lo, right_ptr);
lo = left_ptr;
}
else
{
PUSH (left_ptr, hi);
hi = right_ptr;
}
}
}

/*
至此快速排序工作結束。
*/

#define min(x, y) ((x) &< (y) ? (x) : (y)) // 頭一回學宏「函數」都應該見過這貨吧 /* 接下來是插入排序 */ { /* 眾所周知,傳統的插入排序中每一趟插入(假設是向前插入),我們都會迭代有序區間,如果已 經迭代到了區間頭部,那就把該元素插在區間首部;否則如果前一個元素不大於待插入元素,則 插在該元素之後。 這種插入方式會導致每一次迭代要進行兩次比較:一次比較當前迭代位置與區間首部,一次比較 兩個元素大小。但事實上,我們可以用一個小技巧省掉前一次比較————插入排序開始前,先將容 器內最小元素放到容器首部,這樣就可以保證每趟插入你永遠不會迭代到區間首部,因為你總能 在中途找到一個不小於自己的元素。 注意,如果區間大於終止區間,搜索最小元素時我們不必遍歷整個區間,因為快速排序保證最小 元素一定被劃分到了第一個終止區間,也就是頭4個元素之內。 */ char *const end_ptr = base_ptr[size * (total_elems - 1)]; char *tmp_ptr = base_ptr; char *thresh = min(end_ptr, base_ptr + max_thresh); char *run_ptr; 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); /* 準備工作完畢,這下可以放心地幹掉迭代位置的比較了。 */ 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;
}
}
}
}
}

/*
至此,qsort工作完成。
*/


因為libc里的qsort不一定是quicksort啊

a libc qsort() shootout of sorts


你倒是把代碼貼出來啊!!

還有其它答主。

人家問的是qsort啊不是std::sort!!

更新:

題主貼代碼了。掃了一眼發現可能存在的問題:

swap操作似乎不那麼高效。

我寫個好點的。睡前手機碼字,將就看吧。

void qsort(int *l, int *r) {
int key = *l,*bg = l, *ed = r;
while( l &< r ) { while( l &< r *r &>= key)
--r;
*l = *r;
while( l &< r *l &<= key) ++l; *r = *l; } *l = key; if(bg &< l - 1) qsort( bg, l - 1 ); if( l + 1 &< ed) qsort( l + 1, ed); }

擦,下次再用手機擼代碼我就是瓜。(已改

知乎強行吞我的空格ˊ_&>ˋ


你在main函數內建立大數組是會爆棧的,參見http://www.zhihu.com/question/37047634/answer/70170894


非常抱歉,因為無知,說錯話了。感謝知友 呂青松 的討論 和 高godlike 的批評。

今天特意下載了 glibc 的源碼看了看,庫函數並不是都用彙編實現的,庫中有非常多的部分是用C語言直接寫的,包括 qsort,在 stdlib/qsort.c 裡面實現。

下面對此問題重新進行回答,以分析 glibc 中的代碼為主,分析一下為什麼庫中的 qsort 效率那麼高。完整的代碼貼在回答的最後。

-------------------------------- 以下是原回答 ---------------------------------------------

C庫是用彙編寫的,而且做了好多優化。這跟自己用高級語言寫有非常大的區別的。

-------------------------------- 以下是新回答 ---------------------------------------------

看了 glibc 中的實現,發現他的確有很多地方已經做到了接近完美:

1、他沒有使用遞歸,而是手動用宏實現了一個棧:

/* 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. */
/* 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). */
#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)

光是這一項,節省的函數調用的開銷就是不可估量的。服了!

2、選取中間元:他用了一種很巧妙的方法,減小「壞的」中間元出現的概率,同時開銷還不是很大。首先「中間元」的位置選取:小段的正中間;其次「中間元的值」定義為:最左、最右、中間這三個數的中間值,並且在交換之後,首先完成這三個數的相對位置關係的正確性。上代碼:

/* 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. */

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:;

對效率的要求到了近乎苛刻的程序,一次多餘的比較都不做。

3、效率的核心:小段內將比中間元小的放左邊,大的放右邊,這個過程對效率的影響是很大的。他用了一個非常棒的方法,左、右指針分別向中間推近,將位於左邊且大於中間元的元素,與位於右邊且小於中間元的元素位置調換。元素比較的次數沒有減少,但是元素交換的次數已經非常少了。他的代碼中有兩行是當 mid 與一邊指針相撞時,將 mid 重新定義為另一邊的指針。這樣做的好處是可以增加一定的平衡程度。上代碼:

/* 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. */
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);

循環很緊湊。但是我個人發現一個小小的問題,當 *left_ptr == *mid == *right_ptr 時,也會發生交換,所以個人覺得上面的兩個小 while 語句如果換成 &<= 應該更好。

4、只使用快排演算法處理多於四個元素的情況,對不超過四個元素的小數組,單獨處理。這樣效率可以更高一點。在快排過程中,也遵循這一原則,這樣節省的時間就是對數級的了。

分析至此。貼上完整代碼如下。讀一讀這樣的代碼,也是一種享受,一種提高。筆者受益匪淺。再次感謝兩位知友的賜教。

/* Copyright (C) 1991-2015 Free Software Foundation, Inc.
This file is part of the GNU C Library.
Written by Douglas C. Schmidt (schmidt@ics.uci.edu).

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, see
&. */

/* 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 &
#include & #include &
#include &

/* Byte-wise swap two items of size SIZE. */
#define SWAP(a, b, size)
do
{
size_t __size = (size);
char *__a = (a), *__b = (b);
do
{
char __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. */
#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. */
/* 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). */
#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: 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. 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. 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. 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)! */ void _quicksort (void *const pbase, size_t total_elems, size_t size, __compar_d_fn_t cmp, void *arg) { 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. */

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. */ 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!). */

#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); 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;
}
}
}
}
}


1. 輸入shuffle了嗎?或者隨機選擇pivot了么?快排的最壞情況是O(n^2)

2. 對於輸入比較小的情況(&<15?),插入排序一般比快排快, 可以判斷當前輸入大小切換成插入排序


qsort有很多優化方法,根據不同情況進行優化,可以大大提高效率。比如如何選擇基準值等等。

維基百科quick sort詞條里還提到了幾種優化的思路,包括:

1.利用尾遞歸:先排小的那一半,然後用尾調用來排剩下的那一半。

2.如果元素很少,換用別的非遞歸的排序方法來減少操作。

3.當每個遞歸函數要處理的元素少於k個時,提前終止,然後進行插入排序。

PS: 你倒是把代碼貼出來啊!!!


glibc的qsort函數並不一定是快速排序,大多時候是歸併排序,即使快速排序也不是教科書的遞歸演算法,而是非遞歸演算法

http://blog.zheezes.com/an-in-depth-understanding-of-qsort.html


C標準裡面並沒有要求qsort的實現方式,不同的庫可能會使用不同的優化方式來提高速度,所以你這是亂比。


首先,棧的大小一般4m,100w個int就爆棧了。


既然是《演算法導論》里看來的,那麼暫且認為題主自己用C語言實現了一個函數名叫qsort的Quicksort,可是C語言里的qsort的實現並不一定是Quicksort的標準實現。

C語言不是很清楚,但是微軟的C++實現中,對於標準庫std::sort()的實現採用了introsort(語言標準里不會限制各廠家用什麼演算法實現,只會限定sort要滿足一定的複雜度以及是否stable等等)。

introsort中混合了quicksort、heapsort以及insertion sort。(用了交換、選擇、插入三種排序思想,又稱為混合排序)。

introsort對比quicksort在很多方面做了改進,可以參考維基(https://www.wikiwand.com/en/Introsort)。


為什麼我自己畫的圖標沒有自帶的圖標好看?

PS: 你倒是把代碼貼出來啊!!!


你這個快排沒分組樞紐元大法,沒隨機化,甚至還會爆棧,你這個寫法只是寫個思想真的。


據說algorithm庫的sort函數是特別優化過的,針對不同的數量級採用不同的演算法,部分環節使用彙編語言。

你就算直接貼教科書里的代碼去跑,也不可能比algorithm庫的函數跑得快。我試過,至少10倍的差距。


1百萬就爆了? 先把區間斷的那段遞歸,再把區間大的那段遞歸,估計很難爆嘎


畢業工作多年,表示不想看這種代碼,第一點,可讀性差,第二點,健壯性差,第三點,既然叫快速排序,遞歸效率肯定差,數組大了棧溢出肯定崩潰


你在逗我吧,排了10個數,十個數需要用快排嗎,隨便這個冒泡也看不出來啥吧,快排的寫法分三種,分別是單向,雙向,三向,而且還和數據本身的分布有關,用不好的話還會退回到N的平方級別,快排的水還是很深的,可以參考下編程珠璣 演算法 演算法導論這三本書上對他的講解


堆棧掛了吧

int a[11]={};


因為C語言庫里的qsort不單純是Quicksort,還採用了一些其他的優化方案,包括但不限於:

  1. 在遞歸到subarray的length較小時,改用插入排序。
  2. 使用三數取中值或Tukey ninther演算法而不是直接取第一個來選取pivot元素,以避免輸入序列已經被排序而導致的worst case。
  3. 使用3-way partitioning策略以避免輸入序列有大量重複元素情況下的性能問題。


推薦閱讀:

Mathematica 是怎麼做不定積分的?能不能用幾個簡單的例子,簡述一下求不定積分的演算法?
程序員如何通過《演算法導論》學習?這本書適不適合演算法基礎薄弱的程序員?
如何生成多個互不重疊的不同半徑圓?
c++自學演算法導論 利用遞歸二分法 快速排序 請問哪裡寫v錯了?
stl partition演算法有兩種寫法,哪種效率高?

TAG:演算法 | C編程語言 | 演算法導論書籍 | 演算法與數據結構 |