[C++] 七种常见排序算法的实现及运行效率对比(算法课实验)

简介

本文实现以下七种排序算法:

  • 插入排序

  • 选择排序

  • 冒泡排序

  • 希尔排序

  • 堆排序

  • 快速排序

  • 归并排序

使用 C++ 完成,并测试运行时间。

算法说明

以下针对每种算法,从网络摘取简介,并附实现代码。

所有实现以对 int 数组排序,目标为从小到大为例。感兴趣的读者可以自行编写更通用的写法。

插入排序

插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

void insertSort(int* first, int* last) {
    for (int* p = first + 1; p < last; p++) {
        int* curr = p;
        for (int* q = p - 1; q >= first; q--) {
            if (*q > *curr) {
                swap(*q, *curr);
                curr--;
            }
            else {
                break;
            }
        }
    }
}

选择排序

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

void selectSort(int* first, int* last) {
    for (int* p = first; p < last; p++) {
        int* tar = p;
        for (int* q = p + 1; q < last; q++) {
            if (*q < *tar) {
                tar = q;
            }
        }

        swap(*tar, *p);
    }
}

冒泡排序

重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。

void bubbleSort(int* first, int* last) {
    for (int* i = first; i < last; i++) {
        for (int* j = i; j < last; j++) {
            if (*i > *j) {
                swap(*i, *j);
            }
        }
    }
}

希尔排序

希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。

void shellSort(int* first, int* last) {
    int step = last - first;
    int*& arr = first;

    while (step > 1) {
        step /= 2;
        
        for (int i = 0; i < step; i++) {
            for (int j = i + step; j < last - first; j += step) {
                for (int k = j - step; k >= 0; k -= step) {
                    if (arr [k] > arr [k + step]) {
                        swap(arr [k], arr [k + step]);
                    }
                    else {
                        break;
                    }
                }
            }
        }
    }
}

堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。

void heapSort(int* first, int* last) {
    int*& arr = first;
    int len = last - first;
    
    auto lc = [](int x) { return 2 * x + 1; };
    auto rc = [](int x) { return 2 * x + 2; };

    auto adjust = [&](int tar, int borderIdx) {
        int curIdx = tar;

        while (true) {
            int curMaxIdx = curIdx;
            if (lc(curIdx) < borderIdx && arr[lc(curIdx)] > arr[curMaxIdx]) {
                curMaxIdx = lc(curIdx);
            }
            if (rc(curIdx) < borderIdx && arr[rc(curIdx)] > arr[curMaxIdx]) {
                curMaxIdx = rc(curIdx);
            }

            if (curMaxIdx != curIdx) {
                swap(arr[curIdx], arr[curMaxIdx]);
                curIdx = curMaxIdx;
            }
            else {
                break;
            }
        }
    };

    for (int i = len / 2 - 1; i >= 0; i--) {
        adjust(i, len);
    }

    for (int i = len - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        adjust(0, i);
    }
}

快速排序

在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

注意:不可使用递归形式完成。否则当数据量较大的时候,会导致栈溢出。

void quickSort(int* first, int* last) {
    vector<pair<int*, int*>> q;

    q.push_back(make_pair(first, last));

    while (!q.empty()) {
        pair<int*, int*> currPair = q.back();
        q.pop_back();

        int* currFirst = currPair.first;
        int* currLast = currPair.second;

        if (currFirst + 1 >= last) {
            return;
        }

        int* lp = currFirst;
        int* rp = currLast - 1;

        while (lp < rp) {
            
            while (lp < rp && *lp <= *rp) { // 右边界收缩。
                rp--;
            }

            swap(*lp, *rp);
            
            while (lp < rp && *lp <= *rp) { // 左边界收缩。
                lp++;
            }

            swap(*lp, *rp);
        }

        if (lp - 1 > currFirst) {
            q.push_back(make_pair(currFirst, lp));
        }

        if (rp + 1 < currLast - 1) {
            q.push_back(make_pair(rp + 1, currLast));
        }
    }
}

归并排序

归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

void mergeSort(int* first, int* last) {
    // 迭代法。

    int* arrA = first;
    int size = last - first;
    int* arrB = new int[size];

    int* currSource = arrA;
    int* currTarget = arrB;

    for (int seqLen = 1; seqLen < size; seqLen *= 2) {

        for (int i = 0; i < size; i += seqLen * 2) {
            if (i + seqLen >= size) {
                memcpy(currTarget + i, currSource + i, (size - i) * sizeof(int));
                break;
            }

            int crA = i;
            int crB = i + seqLen;
            int tailA = i + seqLen;
            int tailB = min(i + 2 * seqLen, size);


            for (int j = i; j < i + 2 * seqLen && j < size; j++) {
                if (crA == tailA) {
                    currTarget[j] = currSource[crB];
                    crB += 1;
                }
                else if (crB == tailB) {
                    currTarget[j] = currSource[crA];
                    crA += 1;
                }
                else if (currSource[crA] < currSource[crB]) {
                    currTarget[j] = currSource[crA];
                    crA += 1;
                }
                else {
                    currTarget[j] = currSource[crB];
                    crB += 1;
                }
            }
        }

        swap(currSource, currTarget);
    }

    if (currSource == arrB) {
        memcpy(arrA, arrB, size * sizeof(int));
    }

    delete[] arrB;
}

包装

为方便测试,对程序加入命令行选择能力,并加入计时和结果检查。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <algorithm>
#include <chrono>
#include <string>
#include <cmath>
#include <vector>
#include <conio.h>
using namespace std;

#undef swap
#undef sort

int getTimeMillisec() {
    return chrono::duration_cast<chrono::milliseconds>
        (chrono::system_clock::now().time_since_epoch()).count();
}

void swap(int& a, int& b) {
    int t = a; 
    a = b;
    b = t;
}

void bubbleSort(int* first, int* last) {...}
void quickSort(int* first, int* last) {...}
void selectSort(int* first, int* last) {...}
void insertSort(int* first, int* last) {...}
void mergeSort(int* first, int* last) {...}
void shellSort(int* first, int* last) {...}
void heapSort(int* first, int* last) {...}

enum class Algo {
    QUICK, BUBBLE, SELECT, MERGE, INSERT, SHELL, HEAP
};

int sort(int* first, int* last, Algo algo = Algo::QUICK) {
    auto beg = getTimeMillisec();

    if (algo == Algo::QUICK) {
        quickSort(first, last);
    } else if (algo == Algo::BUBBLE) {
        bubbleSort(first, last);
    } else if (algo == Algo::SELECT) {
        selectSort(first, last);
    } else if (algo == Algo::MERGE) {
        mergeSort(first, last);
    } else if (algo == Algo::INSERT) {
        insertSort(first, last);
    } else if (algo == Algo::SHELL) {
        shellSort(first, last);
    } else if (algo == Algo::HEAP) {
        heapSort(first, last);
    }

    auto now = getTimeMillisec();
    return now - beg;
}

int main(int argc, const char* argv[]) {
    ios::sync_with_stdio(false);
    srand(time(0));

    string algoStr = "quick";
    if (argc >= 2) {
        algoStr = argv[1];
    } else {
        cout << "Usage: [Algorithm] [optional: qsop]" << endl;
        cout << endl;
        cout << "Available algorithms are:" << endl;
        cout << "    quick, bubble, merge, heap, insert, shell, select." << endl;
        cout << endl;
        cout << "if quicksort-optimization is required, add \"qsop\" to the arguments." << endl;
        cout << endl;
        cout << "Example:" << endl;
        cout << argv[0] << " quick op : optimized quick sort." << endl;
        return 0;
    }

    bool isOpRequired = false;
    if (argc >= 3 && strcmp(argv[2], "qsop") == 0) {
        isOpRequired = true;
    }

    Algo algo = Algo::QUICK;
    if (algoStr == "quick") {
        algo = Algo::QUICK;
        cout << "algo set: quick sort." << endl;
    } else if (algoStr == "bubble") {
        algo = Algo::BUBBLE;
        cout << "algo set: bubble sort." << endl;
    } else if (algoStr == "merge") {
        algo = Algo::MERGE;
        cout << "algo set: merge sort." << endl;
    } else if (algoStr == "heap") {
        algo = Algo::HEAP;
        cout << "algo set: heap sort." << endl;
    } else if (algoStr == "insert") {
        algo = Algo::INSERT;
        cout << "algo set: insert sort." << endl;
    } else if (algoStr == "shell") {
        algo = Algo::SHELL;
        cout << "algo set: shell sort." << endl;
    } else if (algoStr == "select") {
        algo = Algo::SELECT;
        cout << "algo set: select sort." << endl;
    }

    int n;
    cin >> n;
    int* arr = new int[n];

    for (int i = 0; i < n; i++) {
        cin >> arr[i];
    }

    if (isOpRequired) {
        // 随机打乱
        for (int i = 0; i < n / 33; i++) {
            swap(arr[rand() % n], arr[rand() % n]);
        }
    }

    time_t timeUsage = sort(arr, arr + n, algo);

    for (int i = 0; i < n; i++) {
        if (i < 6 || n - i < 6) {
            cout << arr[i] << ' ';
        }
        else if (i == 10) {
            cout << "... ";
        }

        if (i > 0 && arr[i - 1] > arr[i]) {
            cout << endl;
            cout << "Seq Error: at arr[" << i << "]." << endl;
            cout << "exit? [y/n]: ";
            while (true) {
                char c = _getch();
                if (c == 'Y' || c == 'y') {
                    cout << c << endl;
                    i = n; // break the outer loop.
                    break;
                } else if (c == 'N' || c == 'n') {
                    cout << c << endl;
                    break;
                }
            }
        }
    }
    cout << endl;
    cout << "time usage: " << timeUsage << " ms." << endl;
    delete[] arr;
    return 0;
}

测试文件生成程序

使用简短的 Kotlin 程序生成测试文件。通过命令行方式控制生成数据的规模等。

随机

import java.io.File
fun main(args: Array<String>) {
    if (args.size < 2) {
        println("Usage: [data size] [outfile dir]")
        println("Example: name 1000 ./out1000.txt")
        return
    }
    
    val s = args[0].toInt()
    val file = File(args[1])
    file.printWriter().use { out ->
        out.println(s)
        for (i in 0 until s) {
            out.print("${(0..s).random()} ")
        }
        out.println()
    }
}

有序

import java.io.File
enum class Order {
    ASC, DES
}
fun main(args: Array<String>) {
    if (args.size < 2) {
        println("Usage: [order: \"ass\" or \"rev\"] [outfile] [optional: data size]")
        return
    }
    
    val size = if (args.size >= 3) args[2].toInt() else 10000
    val order = if (args[0].lowercase() == "des") Order.DES else Order.ASC
    val file = File(args[1])
    
    file.printWriter().use { out ->
        out.println(size)
        var curr = if (order == Order.ASC) 0 else size
        for (i in 0 until size) {
            out.print("$curr ")
            if ((0..100).random() < 70) {
                curr += if (order == Order.ASC) 1 else -1
            }
        }
        out.println()
    }
}

测试结果

测试分别运行于笔记本电脑和 arm64 服务器。采用自动测试并输出结果的模式,各完成64种组合的排序并计时。

最终,得到以下测试结果。

其中,快速排序(优化)快速排序的基础上,随机打乱 3% 的数据,从而避免极端情况使快速排序算法复杂度退化。

计时单位:毫秒 (ms)

笔记本

插入 选择 冒泡 希尔 快速 快速(优化) 归并
10 0 0 0 0 0 0 0 0
100 0 0 0 0 0 0 0 0
1K 0 1 2 0 0 0 0 0
10K 30 75 105 1 0 1 0 1
100K 2811 7613 10950 9 7 5 6 6
1M 279138 (279s) 783016 (13min) 1226214 (20min) 150 103 64 69 79
10K 正序 0 79 11 0 0 11 1 0
10K 逆序 56 81 35 0 1 11 0 0

服务器

插入 选择 冒泡 希尔 快速 快速(优化) 归并
10 0 0 0 0 0 0 0 0
100 0 0 0 0 0 0 0 0
1K 1 2 5 0 0 0 0 1
10K 150 187 434 3 3 2 3 1
100K 15163 18739 43051 46 46 25 25 20
1M 1579726 (26min) 1904198 (32min) 4480618 (75min) 738 630 290 286 243
10K 正序 0 186 186 1 3 162 7 0
10K 逆序 298 254 526 1 3 175 4 1

排序算法比较

下表为各种排序算法的性能指标。

排序算法 时间复杂度(平均) 时间复杂度(最坏) 空间复杂度 稳定 备注
插入 O(n2) O(n2) O(1)
希尔 O(n1.3) O(n2) O(1)
选择 O(n2) O(n2) O(1)
O(nlogn) O(nlogn) O(1)
冒泡 O(n2) O(n2) O(1)
快速 O(nlogn) O(n2) O(nlogn) 数列有序时,快速排序严重退化
快速(优化) O(nlogn) O(nlogn) O(1) 排序前随机打乱数列
归并 O(nlogn) O(nlogn) O(n) 需要一个辅助数组

从前方实验中可以看出,O(n2) 的复杂度在数据量较大的时候耗时严重。

当数列有序时,快速排序和冒泡的效率几乎一致。将数列进行小幅度随机打乱可以规避此问题。

优缺点分析

算法 优点 缺点
插入 无额外内存开销。稳定。 效率低下。
希尔 比选择效率高。无额外内存开销。 效率依旧低下。
选择 无额外内存开销。 效率低下。
效率高。无额外内存开销。
冒泡 稳定。 数据移动次数过多。效率低下。
快速 时间复杂度低。 计算过程空间复杂度较高。不稳定。数列有序时,效率退化。
快速(优化) 不论数列是否有序,都有较高效率。 计算过程空间复杂度较高。不稳定。
归并 效率高。额外空间开销较小。 存在额外空间开销。

数据生成环境

软件

Kotlinc: 1.6.10 (JRE 17.0.1+12)

Java: 17 (64 bit)

测试环境:笔记本电脑

硬件

CPU型号:i7-11370H

内存频率:LPDDR4X 4266MHz

软件

操作系统:Windows 11 Pro 21H1 (22000.527) x86_64

C++编译器:MSVC v143 (以 release x64 模式编译)

测试环境:服务器

硬件

CPU: 鲲鹏920 arm64 2.6GHz

软件

操作系统:Ubuntu 20.04 focal (aarch64 Linux 5.4.0-100-generic)

C++编译器:gcc 9.3.0

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

推荐阅读更多精彩内容