10个数冒泡排序流程图_10大排序算法的简洁代码实现

本文用C++简洁实现了10大经典排序算法,包括冒泡、插入、选择等。对各算法特点如稳定性、时间复杂度等进行说明,还对比了部分算法性能,如插入排序相对冒泡排序更快,快排因原地排序等优势比归并排序、堆排序应用更广泛。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文用C++尽可能简洁得实现了10大经典必知必会的排序算法,并对其中的一些算法特点比如稳定性、时间复杂度等进行了一定的说明。

本文总结表如下,“其他备注”的具体内容在文章下有写出来。

165645edb0a2e674f38e341b87827cbb.png

他们的简洁代码实现和测试脚本如下(从小到大排序):

#include <stdio.h>
#include <vector>
#include <algorithm>
#include <unistd.h>
using namespace std;

// 辅助函数,顺序打印vector v中的数据
void Print(vector<int>& v){
    for(int i=0;i<v.size()-1;i++){
        printf("%d-",v[i]);
    }
    printf("%dn",v[v.size()-1]);
}

void bubble_sort(vector<int>& v){
    int n = v.size();
    for(int i=0;i<n-1;i++){ // 对n-1个数据进行冒泡放置
        bool flag = false;  // 该轮中是否还存在逆序对 
        for(int j=0;j<n-i-1;j++){
            if(v[j]>v[j+1]){
                swap(v[j],v[j+1]);
                if(!flag) flag = true;
            }
        }
        if(!flag) break;
    }
}

void insert_sort(vector<int>& v){
    int n = v.size();
    for(int i=1;i<n;i++){ // 要被取出来插入到别处的元素的下标位置:从1~n-1
        int elem = v[i], j=i-1;
        for(j=i-1;j>=0;j--){ // 要比较的元素的id
            if(v[j]>elem) v[j+1] = v[j];
            else {break;}
        }
        v[j+1]=elem;
    }
}

void select_sort(vector<int>& v){
    int n = v.size();
    for(int i=0;i<n;i++){ // 选择最小后的数后要放置的位置
        int idx=i;
        for(int j=i+1;j<n;j++){
            if(v[j]<v[idx]) idx=j;
        }
        swap(v[i],v[idx]);
    }
}

void shell_sort(vector<int>& v){
    int n = v.size();
    int step = n/2;
    while(step>0){
        // 对每组元素进行插入排序;等价于对step及以后的元素进行插入排序
        for(int i=step;i<n;i++){
            int elem=v[i], j=i-step;
            for(j=i-step;j>=0;j-=step){
                if(v[j]>elem) v[j+step] = v[j];
                else {break;}
            }
            v[j+step] = elem;
        }
        step /= 2;
    } 
}

void merge_fn(vector<int>& v, int st, int ed){
    if(st>=ed) return;
    int mid= st + ((ed-st)>>1);
    merge_fn(v, st, mid);
    merge_fn(v, mid+1, ed);
    // 合并排序后的结果,先申请一个和v[st,ed]等大的临时数组
    vector<int> tmp_v(ed-st+1);
    int i=st, j=mid+1, k=0;
    while(i<=mid || j<=ed){
        tmp_v[k++] = ((i<=mid && v[i]<v[j])||(j>ed))? v[i++] : v[j++];
    }
    // 重新赋值
    for(int i=st;i<=ed;i++) v[i]=tmp_v[i-st];
}

void merge_sort(vector<int>& v){
    merge_fn(v, 0, v.size()-1);
}

void quick_sort_fn(vector<int>& v, int l, int r){
    // 递归出口
    if(l>=r) return;
    
    // 先遍历一次让pivot就位
    int pivot = v[r];
    int i=l, j=l; // i指向第一个大于等于pivot元素的位置(其实就是最后pivot要放置的位置)
                  // j用于遍历
    for(;j<r;j++){
        if(v[j]<pivot){
            swap(v[i],v[j]);
            i++; // 从而保持i往右都是大于等于pivot的元素
        }
    }
    swap(v[r],v[i]); // 最终pivot元素就位,根据pivot被分成了两部分

    // 然后再递归解决两边
    quick_sort_fn(v, l, i-1);
    quick_sort_fn(v, i+1, r);
}

void quick_sort(vector<int>& v){
    quick_sort_fn(v, 0, v.size()-1);
}

void count_sort(vector<int>& v){
    // 1. 产生计数表
    // 1.1 找出最大值
    int max_num = v[0];
    for(int i=0;i<v.size();i++) max_num = max(max_num, v[i]);
    // 1.2 生成计数表
    vector<int> count_table(max_num+1, 0);
    for(int i=0;i<v.size();i++) count_table[v[i]]++;
    for(int i=1;i<count_table.size();i++) count_table[i] += count_table[i-1];
    // 2. 从后往前遍历v,就位在临时数组中
    vector<int> tmp_v(v.size());
    for(int i=v.size()-1;i>=0;i--) tmp_v[--count_table[v[i]]]=v[i];

    // 3. 将tmp_v赋值给v
    for(int i=0;i<tmp_v.size();i++) v[i]=tmp_v[i];
}

void down_adjust(vector<int>& v, int n, int idx){
    int max_idx = idx;
    while(true){
        int tmp_max_idx = max_idx;
        int lch_idx = 2*max_idx+1, rch_idx = 2*max_idx+2;
        if(lch_idx<n) max_idx = v[lch_idx]>v[max_idx]? lch_idx:max_idx;
        if(rch_idx<n) max_idx = v[rch_idx]>v[max_idx]? rch_idx:max_idx;
        if(max_idx==tmp_max_idx) break;
    }
    swap(v[max_idx],v[idx]);
    return;
}

void build_big_heap(vector<int>& v){
    // 从下标是(n-1)/2的元素到0的元素进行下调
    int n=v.size();
    for(int i=(n-1)/2;i>=0;i--){
        down_adjust(v,n,i);
    }
}

void heap_sort(vector<int>& v){
    // 首先是建堆过程,O(n);然后是排序过程
    // 由于排序过程我们希望是原地排序,因此要从最后开始就位元素;因此建立大顶堆。
    build_big_heap(v); // 采用从下往上的建堆方式
    // 每次把堆顶元素换入最后,然后对堆顶元素进行下调
    int n = v.size();
    for(int i=n-1;i>0;--i){
        swap(v[0],v[i]);
        down_adjust(v,i,0); // 总个数要变成i,不能再是n
    }
}

int main(){
    int a[6]={5,3,6,2,1,4};
    vector<int> v(a,a+6);
    Print(v); // 打印排序前的算法
    // 排序算法
    // bubble_sort(v);
    // insert_sort(v);
    // select_sort(v);
    // shell_sort(v);
    // merge_sort(v);
    // quick_sort(v);
    // count_sort(v);
    // heap_sort(v);
    Print(v);  // 打印排序后的算法
    return 0;

}

备注内容如下:

(说明:以下表述中的“就位”是指某个元素放到了最终从小到大排序结束后的位置)

  1. 冒泡排序
    1. 设置提前退出flag时,best time才是O(1);
    2. 相等时不交换才能保证稳定性;
    3. 交换次数=逆序对数=n*(n-1)/2-顺序对数;因此平均有序度就是O(n^2)。由于计算步骤最多的就是比较和交换,也因此平均时间复杂度就是O(n^2)
  2. 插入排序
    1. 插入排序是最像抓牌时的排序算法。
    2. 当要插入的元素和被比较的元素值相同的时候,要保证插在后面那么是稳定的算法;
    3. 虽然插入排序和冒泡排序从时空复杂度看是一样的,但是插入排序相对更快一些;冒泡 排序的主要操作是比较和元素交换,其中交换次数=逆序对数;插入排序的主要操作是比较和 移动,其中移动次数=逆序对数;但是交换需要进行三次赋值,移动只需要一次赋值;并且 冒泡排序的比较次数会比交换次数多很多次,但是插入排序的比较次数最多比移动次数多n次。
  3. 选择排序
    1. 最好复杂度也是O(n^2)的原因是总是要不断重复遍历去找剩下的元素里最小的值;
    2. 稳定性:不能确保稳定性。加入5,8,5,2,9;在第一次交换后,前一个5就到了第二个5的后面。
  4. 希尔排序
    1. 最好的时间复杂度即原来就有序,如果在第一个step的时候,通过flag记录没有发生元素移动,那么就直接退出循环,因此最好的时间复杂的是O(n);
    2. 最坏的时间复杂度和步长的选择有关,已经被证明的步长选择方法最坏的时间复杂度是O(n(logn)^2);
    3. 由于分成了几个组,所以可能发生不稳定的情况。比如有一些相同的元素,其中位置在最后的元素被分在第一组,那么他们之间的顺序就破坏了,因此不具备稳定性。
  5. 归并排序
    1. 时间复杂度的分析,T(n) = 2*T(n/2) + O(n) ;具体的时间时间复杂度参考网页:https://blog.csdn.net/so_geili/article/details/53444816 ;可以使用递推公式、递归树和主方法分析;
    2. 稳定性是可以做到的,只要合并的时候顺序不乱就可以做到稳定性的;
  6. 快速排序
    1. 最好的情况是每次都对半分,这样的话递归树分析复杂度时的高度是最低的;T(n)=2*T(n/2)+n;最坏的情况是每次都有一边没有元素,这样的话高度是n,所以最坏情况的t(n)=O(n^2);
    2. 不稳定的原因是在于,假如最后pivot该放的位置上的元素和pivot正好相等,那么交换后就是不稳定的;
    3. 虽然从时间复杂度上看,归并排序相比快排更好些,但是归并排序不是原地排序的,当数据量很大的时候就会有很多额外的空间开销,因此更多用的还是快排;
    4. 拓展:根据快排的关键代码,我们可以在O(n)的平均时间复杂度,O(1)的空间复杂度下找到第K大的元素。即先选一个pivot然后得到最终位置i后,i和K比较后,递归选择平均一半的区域重复上述操作。
  7. 桶排序
    1. 桶排序要求数据能轻易分成多个桶,并且要尽量均匀;比如用户的年龄排序;
    2. 桶排序的一个桶内数据采用快速排序等方式进行;因此稳定性也由桶内选择的排序算法的稳定性有关;
    3. 桶排序非常适合外部排序,每个桶可以存储为一个文件,最后的合并只需要顺序读取文件即可得到所有排序的结果;
  8. 计数排序
    1. 时间复杂度是O(max{n,k}),其中k是最大的元素值;具体原因看后面的代码就清楚了;顺提,计数排序是特殊的桶排序,即一个桶只放相同的数;
    2. 是稳定的方法, 因为算法中从后往前遍历和就位,因此是稳定的;
    3. 确定就位的方法是:从后往前遍历,假如遍历到x,查表知有y个元素小于等于x,那么x就位的下标就是y-1;然后更新y为y-1;继续遍历。具体可以参照下面的代码。
  9. 基数排序
    1. 例如对很多用户的手机进行排序;由于数据范围很大,不好找合适的桶数量;但是每位的数据范围很小(0-9),因此可以按照位进行桶排序,一共进行11次桶排序即可;
    2. 由于桶排序本身是不稳定的,因此为了确保基数排序效果的稳定,必须要从低位开始往高位排。
  10. 堆排序
    1. 由于其中发生了远距离的交换,因此是非稳定的;
    2. 堆排序分为两步,建堆+排序;建堆如果采用从下至上下调的方法可以实现O(n)的时间复杂度建堆;排序中为实现原地排序,必须从后往前就位元素,是O(nlogn)复杂度;因此总算法的时间复杂度是O(nlogn);
    3. 看起来比快排时间复杂度更好,并且也是原地排序;但是由于以下的原因所以实际中依然还是采用快排更多:
  • CPU缓存没有快排访问友好;因为堆排序中访问数据不是连续的;
  • 堆排序的交换次数大于快排;在建堆的过程中数据如果原来有序那么建堆后无序对数增加;快排中可以先随机选择一个数,然后与最后一个数交换后开始快排,整体上依然是有序状态,最终的数据交换次数少。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值