高级排序算法(二)

快速排序(Quick Sort)

算法思想:在待排序表L[1...n]中任取一个元素pivot作为基准,通过一趟排序将带排序表划分为独立的两部分L[1...k-1]和L[k+1...n],使得L[1...k-1]中所有元素小于pivot,L[k+1...n]中所有元素大于或等于pivot,则pivot放在了其最终位置L(k)上,这个过程称为一趟快速排序。而后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素放在了最终的位置上。

算法演示:

快速排序

基本代码如下:

// 对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r) {

    T v = arr[l];

    // arr[l+1...j] < v; arr[j+1...i) > v
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (arr[i] < v) {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;
}

// 对arr[l...r]部分进行快速排序
template<typename T>
void __quickSort(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

template<typename T>
void quickSort(T arr[], int n) {

    __quickSort(arr, 0, n - 1);
}

好了,按照惯例我们将同时调用快速排序和归并排序进行测试,看看哪种算法的运行效率更高,其结果如下(随机数据):

Quick Sort : 0.029 s
Merge Sort : 0.03 s

我们从结果中发现快速排序算法的运行效率与优化后的归并排序算法的效率不相上下。不知大家有没有发现归并排序算法前“优化后”这三个字加粗显示,这么做其实是为了想告诉大家,我们的快速排序算法还有优化的空间,而归并排序算法已是目前最优的结果。

那么,我们将怎么样优化快速排序呢?我们先回想一下对归并排序的优化操作,在归并排序算法中,我们针对数据较少时采用插入排序,类似的,我们也可以进行这样的操作。

有一个重要的问题,我们始终忽略了。这就是我们没有测试在近乎有序的随机数据情况下,两种排序算法的运行效率。大家若学习过数据结构这门课程就会知道,快速排序有个致命的缺陷——在处理近乎有序的随机数据时,其时间复杂度会变为O(n2)。

为什么会成这种结果呢?这是因为我们在对待排序表进行划分时,不像归并排序算法一样一分为二,而是先找到一个元素pivot作为基准,使得待排序列表在基准之前的元素均小于它,在基准后面的元素均大于它。当快速排序算法处理近乎有序的随机数据时,这种划分操作就会类似于冒泡排序算法的处理操作,所以在这种情况下时间复杂度就变为O(n2)。

为了解决这种问题,我们就不再采用选用待排序表第一个元素作为基准(请大家不要被严奶奶版的数据结构中快速排序算法的讲解所束缚,推荐在学习数据结构时翻阅“黑皮书”对数据结构做进一步了解),而选用待排序列表中尽可能中间的元素作为基准,即随机选择一个数。(注:这里不过多叙述其原因,具体请参考算法导论或“黑皮”版数据结构与算法分析。)

改进后的快速排序算法基本代码如下:

// 对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition(T arr[], int l, int r) {

    swap(arr[l], arr[rand() % (r - l + 1) + l]);

    T v = arr[l];

    // arr[l+1...j] < v; arr[j+1...i) > v
    int j = l;
    for (int i = l + 1; i <= r; i++) {
        if (arr[i] < v) {
            swap(arr[j + 1], arr[i]);
            j++;
        }
    }
    swap(arr[l], arr[j]);
    return j;
}

// 对arr[l...r]部分进行快速排序
template<typename T>
void __quickSort(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition(arr, l, r);
    __quickSort(arr, l, p - 1);
    __quickSort(arr, p + 1, r);
}

template<typename T>
void quickSort(T arr[], int n) {

    srand(time(NULL));
    __quickSort(arr, 0, n - 1);
}

让我们看看运行的结果吧(近乎有序的随机数据)。

Quick Sort : 0.035 s
Merge Sort : 0.005 s

我们发现快速排序算法的运行效率虽比上归并排序,但其性能已经远远好于优化前的性能。

除此之外,我们的快速排序还可进行优化。例如在含有大量重复数据的情况下,我们的快速排序算法的运行效率依旧不高。这里我们向大家展示一下快速排序算法的龟速!

首先,我们按如下代码修改main()中的代码:

int main() {

    int n = 100000;
    int *arr_1 = SortTestHelper::generateRandomArray(n, 0, 10);
    int *arr_2 = SortTestHelper::copyIntArray(arr_1, n);

    SortTestHelper::testSort("Quick Sort", quickSort, arr_1, n);
    SortTestHelper::testSort("Merge Sort", mergeSort, arr_2, n);

    delete[] arr_1;
    delete[] arr_2;
    return 0;
}

然后我们运行程序,看看其运行结果:

Quick Sort : 1.459 s
Merge Sort : 0.017 s

在处理含有大量重复数据时,快速排序算法的运行效率可称为龟速啊!这是因为我们在对待排序表进行划分操作时,由于数据中含有大量的重复数据,会有很大概率上将待排序表划分得极度不平衡,从而导致快速排序算法退化为时间复杂度为O(n2)。

那么对于这种情况,我们优化思路的算法演示为:

实际上,图中两侧的数据应该是如下图所示:

二路快速排序

优化的基本代码如下:

// 对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p]; arr[p+1...r] > arr[p]
template<typename T>
int __partition2(T arr[], int l, int r) {

    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // arr[l+1...i) <= v; arr(j...r] >= v
    int i = l + 1, j = r;
    while (true) {
        while (i <= r && arr[i] < v) i++;
        while (j >= l + 1 && arr[j] > v) j--;
        if (i > j) break;
        swap(arr[i], arr[j]);
        i++;
        j--;
    }
    swap(arr[l], arr[j]);
    return j;
}

// 对arr[l...r]部分进行快速排序
template<typename T>
void __quickSort2(T arr[], int l, int r) {

    if (l >= r)
        return;

    int p = __partition2(arr, l, r);
    __quickSort2(arr, l, p - 1);
    __quickSort2(arr, p + 1, r);
}

template<typename T>
void quickSort2(T arr[], int n) {

    // 设置当前的时间值为种子,那么种子总是变化的,所以以该种子产生的随机数总是变化的
    srand(time(NULL));
    __quickSort2(arr, 0, n - 1);
}

那我们来看看这次优化后的结果吧。

Quick Sort : 1.487 s
Merge Sort : 0.018 s
Quick Sort 2 : 0.016 s

通过这次优化,我们的快速排序算法的运行效率有了显著地提升。实际上,我们将这种方式的快速排序算法称之为双路快速排序算法。除此之外,在处理含有大量重复数据的数据时,我们还有一个更为经典的快速排序算法,通常我们将其称为三路快速排序算法。

其实这个排序算法的思路很简单,其算法演示如下图所示:

三路快速排序

三路快速排序算法的基本代码如下:

// 三路快速排序
// 将arr[l...r]分为 < v; == v; > v 三部分
// 之后递归对 < v; > v 两部分进行三路快速排序
template<typename T>
void __quickSort3(T arr[], int l, int r) {

    if (l >= r)
        return;

    // partition操作
    swap(arr[l], arr[rand() % (r - l + 1) + l]);
    T v = arr[l];

    // arr[l+1...lt] < v
    int lt = l;
    // arr[gt...r] > v
    int gt = r + 1;
    // arr[lt+1...i) == v
    int i = l + 1;

    while (i < gt) {
        if (arr[i] < v) {
            swap(arr[i], arr[lt + 1]);
            lt++;
            i++;
        } else if (arr[i] > v) {
            swap(arr[i], arr[gt - 1]);
            gt--;
        } else {
            // arr[i] == v
            i++;
        }
    }
    swap(arr[l], arr[lt]);

    __quickSort3(arr, l, lt - 1);
    __quickSort3(arr, gt, r);
}

template<typename T>
void quickSort3(T arr[], int n) {

    // 设置当前的时间值为种子,那么种子总是变化的,所以以该种子产生的随机数总是变化的
    srand(time(NULL));
    __quickSort3(arr, 0, n - 1);
}

好了,我们调用一些三路快速排序算法并运行程序,其结果如下:

Quick Sort : 1.393 s
Merge Sort : 0.018 s
Quick Sort 2 : 0.015 s
Quick Sort 3 : 0.006 s
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • 一、直接插入排序 直接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的元素记录,按其关键字...
    kevin16929阅读 558评论 0 0
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,183评论 0 52
  • 概述:排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    每天刷两次牙阅读 3,730评论 0 15
  • 情人玫瑰和爱情 在我心中却是肮脏煽情与交易 我不再相信神圣 因为失去了神圣的你
    雨野阅读 249评论 4 6
  • 一天,一家三口出门。我习惯性地对儿子说:“宝贝,你的这件羽绒太薄了,适合在家穿,外面冷,去换一件厚的吧。”儿子嘟囔...
    简遐思阅读 436评论 0 3