七种常见经典排序算法总结(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++) {  // 从左到右遍历所有数字
        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. 两层循环,第一层从左到右遍历,读取当前的数
 2. 第二层循环,将当前的数以及它前面的所有数两两比较,交换大的数到后面(正序)
 3. 保证前面的数是排序好的,将新读取的数通过遍历前面排好序的部分并比较,插入到合适的位置
 时间复杂度O(n^2),空间复杂度是O(n)
 */
void insert_sort(vector<int> &nums)
{
    for (int i = 1; i < nums.size(); 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

原文地址:https://www.cnblogs.com/yuanzhaoyi/p/8655159.html

时间: 2024-11-06 07:28:41

七种常见经典排序算法总结(C++)的相关文章

java几种常见的排序算法总结

[java] view plain copy /*************几种常见的排序算法总结***************************/ package paixu; public class PaiXu { final int MAX=20; int num[]=new int[MAX]; { System.out.print("生成的随机数组是:"); for(int i=0;i<20;i++){ num[i]=(int)(Math.random()*100)

几种常见的排序算法

1.插入类排序 在一个已经有序的序列中,插入一个新的记录.有直接插入排序.折半插入排序.希尔排序. 插入类排序 直接插入排序 1 void InsertSort(int R[], int n) 2 { 3 int i, j; 4 int temp; 5 for (i = 1; i < n; ++i) 6 { 7 temp = R[i]; 8 j = i - 1; 9 while (j >= 0 && temp < R[j]) 10 { 11 R[j+1] = R[j];

常见经典排序算法学习总结,附算法原理及实现代码(插入、shell、冒泡、选择、归并、快排等)

博主在学习过程中深感基础的重要,经典排序算法是数据结构与算法学习过程中重要的一环,这里对笔试面试最常涉及到的7种排序算法(包括插入排序.希尔排序.选择排序.冒泡排序.快速排序.堆排序.归并排序)进行了详解.每一种算法都有基本介绍.算法原理分析.算法代码. 转载请注明出处:http://blog.csdn.net/lsh_2013/article/details/47280135 插入排序 1)算法简介 插入排序(Insertion Sort)的算法描述是一种简单直观的排序算法.它的工作原理是通过

Python全栈开发之5、几种常见的排序算法以及collections模块提供的数据结构

在面试中,经常会遇到一些考排序算法的题,在这里,我就简单了列举了几种最常见的排序算法供大家学习,说不定以后哪天面试正好用上,文章后半段则介绍一下collections模块,因为这个模块相对于python提供的基本数据结构(list,tuple,dict)不被人们所熟悉,但是如果你对他们了解的话,用起来也是非常方便高效的. 排序算法 一.冒泡排序(BubbleSort) 步骤: 比较相邻的元素,如果第一个比第二个大,就交换他们两个. 循环一遍后,最大的数就“浮”到了列表最后的位置. 将剩下的数再次

常见经典排序算法

插入排序: 算法简介:接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止.时间复杂度为O(n^2). 最稳定的排序算法但是效率很低 代码实现: void InsertSort(int *arr,int n) {                  for (int index = 0; index < n-1; ++index)                 {             

几种常见的排序算法分析学习

目录(?)[-] 冒泡排序 选择排序 1 直接插入排序 1 二分查找插入排序 希尔入排序 快速排序 归并排序 总结 本篇博客知识点 分别描述了 冒泡,选择,直接插入,二分插入,希尔,快速以及归并排序.同时还有Java实现代码,算法分析和示意图 冒泡排序 算法描述 设待排序记录序列中的记录个数为n 一般地,第i趟起泡排序从1到n-i+1 依次比较相邻两个记录的关键字,如果发生逆序,则交换之. 其结果是这n-i+1个记录中,关键字最大的记录被交换到第n-i+1的位置上,最多作n-1趟. 算法实例 经

java基础之几种常见的排序算法

一,冒泡排序 1.原理: 从数组的第一个位置开始两两比较array[index]和array[index+1],如果array[index]大于array[index+1]则交换array[index]和array[index+1]的位置,止到数组结 束: 从数组的第一个位置开始,重复上面的动作,止到数组长度减一个位置结束: 从数组的第一个位置开始,重复上面的动作,止到数组长度减二个位置结束: ....... 2. 时间复杂度:O(N2),进行了(n-1)*(n-2)....=n*(n-1)/2

几种常见的排序算法Java实现总结

public class MySort { final int MAX=20; int num[]=new int[MAX]; { System.out.print("生成的随机数组是:"); for(int i=0;i<20;i++){ num[i]=(int)(Math.random()*100); System.out.print(num[i]+" "); } System.out.println(); } int num2[]=new int[MAX]

几种常见的排序算法总结

*****选择排序***** 方法描述:首先找到第一个最小的数,和第一个数交换:然后从剩下的找出最小的与第二个交换,以此类推.效率: 长度为N的数组,大约N2/2比较,N次交换特点: 1.运行时间和输入无关,有序数组,全部相等的数组,随机数组所用时间一样,没有充分利用输入的初始状态. 2.数据移动最少,与输入成线性关系.代码: sort(int[] a){ int n = a.length; for(int i = 0; i < n; i++){ int min = i; for(int j =