zoukankan      html  css  js  c++  java
  • 七种常见经典排序算法总结(C++)

    最近想复习下C++,很久没怎么用了,毕业时的一些经典排序算法也忘差不多了,所以刚好一起再学习一遍。

    除了冒泡、插入、选择这几个复杂度O(n^2)的基本排序算法,希尔、归并、快速、堆排序,多多少少还有些晦涩难懂,幸好又博客园大神dreamcatcher-cx都总结成了图解,一步步很详细,十分感谢。

    而且就时间复杂度来说,这几种算法到底有什么区别呢,刚好做了下测试。

    代码参考: http://yansu.org/2015/09/07/sort-algorithms.html

    //: basic_sort
    
    #include <iostream>
    #include <vector>
    #include <ctime>
    #include <string>
    
    using namespace std;
    
    // 获取函数名字的宏
    #define GET_NAME(x) #x
    // 生成随机数的宏
    #define random(a,b) (rand()%(b-a+1)+a)
    // 打印容器对象(vector)的宏
    #define PRT(nums) { 
    for(int i =0; i<nums.size(); i++){ 
        cout << nums[i] << " "; 
    }
    }
    
    /*
     冒泡排序
     基本思想: 对相邻的元素进行两两比较,顺序相反则进行交换,这样,每一趟会将最小或最大的元素“浮”到顶端,最终达到完全有序
     图解: http://www.cnblogs.com/chengxiao/p/6103002.html
     考的最多的排序了吧。
     1. 两层循环,最里面判断两个数的大小,大的换到后面(正序)
     2. 内部循环一遍后,最大的数已经到最后面了
     3. 下一次内部循环从0到倒数第二个数(最后一个数通过第一步循环比较已经最大了)
     4. 依次循环下去
     时间复杂度O(n^2),空间复杂度是O(n)
     */
    void bubble_sort(vector<int> &nums)
    {
        for (int i = 0; i < nums.size() - 1; i++) {  // i用来控制已经冒泡的数字个数
            for (int j = 0; j < nums.size() - i - 1; j++) {  // 从最左边遍历到最后一个没有浮动的数字
                if (nums[j] > nums[j + 1]) {
                    nums[j] += nums[j + 1];
                    nums[j + 1] = nums[j] - nums[j + 1];
                    nums[j] -= nums[j + 1];
                }
            }
        }
    }
    
    /*
     插入排序
     基本思想: 每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。
     图解: http://www.cnblogs.com/chengxiao/p/6103002.html
     1. 两层循环,第一层i表示从左开始已经排好虚的部分
     2. 第二层循环,将当前的数以及它前面的所有数两两比较,交换大的数到后面(正序)
     3. 保证前面的数是排序好的,将新读取的数通过遍历前面排好序的部分并比较,插入到合适的位置
     时间复杂度O(n^2),空间复杂度是O(n)
     */
    void insert_sort(vector<int> &nums)
    {
        for (int i = 1; i < nums.size(); i++) {  // i表示从左开始已经排好序的部分
            for (int j = i; j > 0; j--) {  // 从当前数字位置遍历到最左边的数字位置
                if (nums[j] < nums[j - 1]) {
                    int temp = nums[j];
                    nums[j] = nums[j - 1];
                    nums[j - 1] = temp;
                }
            }
        }
    }
    
    /*
     选择排序
     图解: http://www.cnblogs.com/chengxiao/p/6103002.html
     基本思想: 每一趟从待排序的数据元素中选择最小(或最大)的一个元素作为首元素
     1. 两层循环,第一层从左到右遍历,读取当前的数
     2. min存放最小元素,初始化为当前数字
     3. 内部循环遍历和比较当前数字后后面所有数字的大小,如果有更小的,替换min为更小数字的位置
     4. 内部遍历之后检查min是否变化,如果变化,说明最小的数字不在之前初始化的min位置,交换使每次循环最小的元素被移动到最左边。
     时间复杂度O(n^2),空间复杂度是O(n)
     */
    void selection_sort(vector<int> &nums)
    {
        for (int i = 0; i < nums.size(); i++) {  // 从左到右遍历所有数字
            int min = i;  // 每一趟循环比较时,min用于存放较小元素的数组下标,这样当前批次比较完毕最终存放的就是此趟内最小的元素的下标,避免每次遇到较小元素都要进行交换。
            for (int j = i + 1; j < nums.size(); j++) {
                if (nums[j] < nums[min]) {
                    min = j;
                }
            }
            if (min != i) {  //进行交换,如果min发生变化,则进行交换
                int temp = nums[i];
                nums[i] = nums[min];
                nums[min] = temp;
            }
        }
    }
    
    /*
     希尔排序
     图解: http://www.cnblogs.com/chengxiao/p/6104371.html
     基本思想: 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
     1. 最外层循环设置间隔(gap),按常规取gap=length/2,并以gap = gap/2的方式缩小增量
     2. 第二个循环从gap位置向后遍历,读取当前元素
     3. 第三个循环从当前元素所在分组的上一个元素开始(即减去gap的位置),通过递减gap向前遍历分组内的元素,其实就是比较分组内i和i-gap元素的大小,交换大的到后面
    希尔排序的时间复杂度受步长的影响,不稳定。
     */
    void shell_sort(vector<int> &nums)
    {
        for (int gap = int(nums.size()) >> 1; gap > 0; gap >>= 1) {  // 遍历gap
            for (int i = gap; i < nums.size(); i++) {  // 从第gap个元素向后遍历,逐个对其所在组进行直接插入排序操作
                int j = i - gap;   // j是这个分组内i元素的上一个元素
                for (; j >= 0 && nums[j] > nums[i]; j -= gap) {  // 从i向前遍历这个分组内所有元素,把大的交换到后面
                    swap(nums[j + gap], nums[j]);
                }
            }
        }
    }
    
    // 合并两个有序序列
    void merge_array(vector<int> &nums, int b, int m, int e, vector<int> &temp)
    {
    //    cout << "b: " << b << "  " << "m: " << m << "  " << "e: " << e << endl;
        int lb = b, rb = m, tb = b;
        while (lb != m && rb != e)
            if (nums[lb] < nums[rb])
                temp[tb++] = nums[lb++];
            else
                temp[tb++] = nums[rb++];
        
        while (lb < m)
            temp[tb++] = nums[lb++];
        
        while (rb < e)
            temp[tb++] = nums[rb++];
        
        for (int i = b;i < e; i++)
            nums[i] = temp[i];
    //    cout << "temp: ";
    //    PRT(temp);
    //    cout << endl;
    }
    
    //递归对序列拆分,从b(开始)到e(结束)的序列,取中间点(b + e) / 2拆分
    void merge_sort_recur(vector<int> &nums, int b, int e, vector<int> &temp)
    {
        int m = (b + e) / 2;  // 取中间位置m
        if (m != b) {
            merge_sort_recur(nums, b, m, temp);
            merge_sort_recur(nums, m, e, temp);
            merge_array(nums, b, m, e, temp);  // 开始(b)到中间(m) 和 中间(m)到结束(e) 两个序列传给合并函数
        }
    }
    
    /*
     归并排序
     图解: http://www.cnblogs.com/chengxiao/p/6194356.html
     基本思想: 利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
     1. 合并两个有序序列的函数,合并后结果存入临时的temp
     2. 从中间分,一直递归分到最小序列,即每个序列只有一个元素,单位为1(一个元素肯定是有序的)
     3. 然后两两比较合并成单位为2的n/2个子数组,在结果上继续两两合并
    时间复杂度是O(nlogn),空间复杂度是O(n)。
     */
    void merge_sort(vector<int> &nums){
        vector<int> temp;
        temp.insert(temp.begin(), nums.size(), 0);  // 定义和初始化temp用于保存合并的中间序列
        merge_sort_recur(nums, 0, int(nums.size()), temp);
    }
    
    
    // 将启始位置b作为基准,大于基准的数移动到右边,小于基准的数移动到左边
    void quick_sort_recur(vector<int> &nums, int b, int e)
    {
        if (b < e - 1) {
            int lb = b, rb = e - 1;
            while (lb < rb) {  // 遍历一遍,把大于基准的数移动到右边,小于基准的数移动到左边
                while (nums[rb] >= nums[b] && lb < rb)  //默认第一个数nums[b]作为基准
                    rb--;
                while (nums[lb] <= nums[b] && lb < rb)
                    lb++;
                swap(nums[lb], nums[rb]);
            }
            swap(nums[b], nums[lb]);
    //        cout << "nums: ";
    //        PRT(nums);
    //        cout << endl;
            quick_sort_recur(nums, b, lb);
            quick_sort_recur(nums, lb + 1, e);
        }
    }
    
    /*
     快速排序
     图解: http://www.cnblogs.com/chengxiao/p/6262208.html
     基本思想: 快速排序也是利用分治法实现的一个排序算法。快速排序和归并排序不同,它不是一半一半的分子数组,而是选择一个基准数,把比这个数小的挪到左边,把比这个数大的移到右边。然后不断对左右两部分也执行相同步骤,直到整个数组有序。
     1. 用一个基准数将数组分成两个子数组,取第一个数为基准
     2. 将大于基准数的移到右边,小于的移到左边
     3. 递归的对子数组重复执行1,2,直到整个数组有序
    空间复杂度是O(n),时间复杂度不稳定。
     */
    void quick_sort(vector<int> &nums){
        quick_sort_recur(nums, 0, int(nums.size()));
    }
    
    // 调整单个二叉树的根节点和左右子树的位置,构建大顶堆
    // 在左右子树中挑出最大的和根节点比较,把最大的数放在根节点即可
    void max_heapify(vector<int> &nums, int root, int end)
    {
        int curr = root;  // 根结点
        int child = curr * 2 + 1;  // 左子树
        while (child < end) {
            if (child + 1 < end && nums[child] < nums[child + 1]) {
                child++;
            }
            if (nums[curr] < nums[child]) {
                int temp = nums[curr];
                nums[curr] = nums[child];
                nums[child] = temp;
                curr = child;
                child = 2 * curr + 1;
            } else {
                break;
            }
        }
    }
    
    /*
     堆排序
     图解: http://www.cnblogs.com/chengxiao/p/6262208.html
     基本思想: 将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
     堆的概念(i是一个二叉树的根节点位置,2i+1和2i+2分别是左右子树):
     大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
     小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
     1. 由底(最后一个有叶子的根节点n/2-1)自上构建大顶堆
     2. 根节点(0)和末尾交换,末尾变为最大
     3. 对余下的0到n-1个数的根节点(0)二叉树进行大顶堆调整(调用max_heapify)(根节点(0)的叶子节点已经大于下面的所有数字了)
    堆执行一次调整需要O(logn)的时间,在排序过程中需要遍历所有元素执行堆调整,所以最终时间复杂度是O(nlogn)。空间复杂度是O(n)。
     */
    void heap_sort(vector<int> &nums)
    {
        int n = int(nums.size());
        for (int i = n / 2 - 1; i >= 0; i--) { // 构建大顶堆
            max_heapify(nums, i, n);
        }
        
        for (int i = n - 1; i > 0; i--) { // 排序, 将第一个节点和最后一个节点交换,确保最后一个节点最大
            int temp = nums[i];
            nums[i] = nums[0];
            nums[0] = temp;
            max_heapify(nums, 0, i);  // 重新调整最顶部的根节点
        }
    }
    
    void func_excute(void(* func)(vector<int> &), vector<int> nums, string func_name){
        clock_t start, finish;
        start=clock();
        (*func)(nums);
        finish=clock();
    //    PRT(nums);  // 打印每次的排序结果
        cout << endl;
        cout << func_name << "耗时:" << float(finish-start)/float(CLOCKS_PER_SEC)*1000 << " (ms) "<< endl;
    }
    
    int main() {
        vector<int> b;
        srand((unsigned)time(NULL));
        for(int i=0;i<5000;i++)
            b.insert(b.end(), random(1,100));
        cout << "数组长度: " << b.size() << "; ";
    //    PRT(b);  // 打印随机数组
        cout << endl;
    
        void (*pFun)(vector<int> &);
        string func_name;
    
        pFun = bubble_sort;
        func_name = GET_NAME(bubble_sort);
        func_excute(pFun, b, func_name);
    
        pFun = insert_sort;
        func_name = GET_NAME(insert_sort);
        func_excute(pFun, b, func_name);
    
        pFun = selection_sort;
        func_name = GET_NAME(selection_sort);
        func_excute(pFun, b, func_name);
        
        pFun = shell_sort;
        func_name = GET_NAME(shell_sort);
        func_excute(pFun, b, func_name);
        
        pFun = merge_sort;
        func_name = GET_NAME(merge_sort);
        func_excute(pFun, b, func_name);
        
        pFun = quick_sort;
        func_name = GET_NAME(quick_sort);
        func_excute(pFun, b, func_name);
        
        pFun = heap_sort;
        func_name = GET_NAME(heap_sort);
        func_excute(pFun, b, func_name);
    } ///:~

    在数组很小的情况下,没有太大区别。但是较长数组,考的最多的冒泡排序就明显比较吃力了~

    具体原因只能从时间复杂度上面来看,但为什么差这么多,我也不是完全明白~

    运行结果,排序算法分别耗时:

    数组长度: 5000; 
    
    bubble_sort耗时:183.4 (ms) 
    
    insert_sort耗时:106.525 (ms) 
    
    selection_sort耗时:68.036 (ms) 
    
    shell_sort耗时:1.096 (ms) 
    
    merge_sort耗时:1.226 (ms) 
    
    quick_sort耗时:1.398 (ms) 
    
    heap_sort耗时:1.514 (ms) 
    Program ended with exit code: 0
  • 相关阅读:
    5.4 省选模拟赛 修改 线段树优化dp 线段树上二分
    一本通 高手训练 1782 分层图 状压dp
    luogu P3830 [SHOI2012]随机树 期望 dp
    5.2 省选模拟赛 或许 线型基
    luogu P4562 [JXOI2018]游戏 组合数学
    一本通 高手训练 1781 死亡之树 状态压缩dp
    luogu P4726 【模板】多项式指数函数 多项式 exp 牛顿迭代 泰勒展开
    4.28 省选模拟赛 负环 倍增 矩阵乘法 dp
    HDU 1756 Cupid's Arrow 计算几何 判断一个点是否在多边形内
    一本通 高手训练 1763 简单树 可持久化线段树 树链刨分 标记永久化
  • 原文地址:https://www.cnblogs.com/yuanzhaoyi/p/8655159.html
Copyright © 2011-2022 走看看