七大比较排序总结

本文详细介绍了比较排序的多种算法,包括插入排序、冒泡排序、选择排序、归并排序,以及它们的时间复杂度、稳定性、是否需要辅助空间等特性。此外,还提及了希尔排序和快速排序作为不稳定排序的例子,以及稳定排序如归并排序。文章讨论了排序算法的稳定性在多键排序中的重要性,并提供了每种排序算法的基本思想和代码示例。

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

排序分为比较排序和非比较排序,这里先介绍比较排序
插入排序:直接插入排序,希尔排序
交换排序:冒泡排序,快速排序
选择排序:直接选择排序, 堆排序
最后是归并排序。
基于比较的排序都是遵循“决策树模型”,而在决策树模型中,我们能证明给予比较的排序算法最坏情况下的运行时间为Ω(nlgn),证明的思路是因为将n个序列构成的决策树的叶子节点个数至少有n!,因此高度至少为nlgn。(算法导论)

非比较排序:基数排序,计数排序,桶排序
线性时间排序虽然能够理想情况下能在线性时间排序,但是每个排序都需要对输入数组做一些假设,比如计数排序需要输入数组数字范围为[0,k]等。

这里写图片描述
图片来源于网络

文章将从以下几个角度来阐述这些算法:(面试常考点)
1. 每个算法的基本思想
2. 每个算法的稳定性
3. 每个算法的时间复杂度,最好、最坏以及平均
4. 参考代码
5. 是否需要辅助空间

写在文章开始的话:

  • 是否需要额外空间的分类:当排序数据非常多的时候,额外空间会非常大,老式电脑会吃不消
    In-place sort(不占用额外内存或占用常数的内存):插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序。
    Out-place sort:归并排序、计数排序、基数排序、桶排序(后三种属于分比较排序)

  • 是否稳定 的分类:是否稳定的一种简单的判别方式:是都有跨越距离的交换(该距离内的数据没有进行比较),如果有,一般都为不稳定算法;若否,则一般为稳定算法
    stable sort:插入排序、冒泡排序、归并排序、计数排序、基数排序、桶排序。
    unstable sort:选择排序(5 8 5 2 9)、快速排序、堆排序。
    是否稳定有一点要注意:需要注意的是,排序算法是否为稳定的是由具体算法决定的,不稳定的算法在某种条件下可以变为稳定的算法,而稳定的算法在某种条件下也可以变为不稳定的算法。
    例如大于小于等于号的问题,该是大于不要写大于等于,否则很可能改变稳定性。 冒泡排序中的比较,>写成>=,两个数相等也会交换位置,这样就改变了稳定性!!

  • 为什吗要考虑稳定性
    排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位排序后元素的顺序在高位也相同时是不会改变的
    这里写图片描述


下面开始具体介绍

1. 直接插入排序
思想:插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。
插入排序的工作方式像玩扑克牌时顺序放牌一样。开始时,左手为空并且桌子上的牌面向下。然后,每次从桌上拿一张牌并将它插入左手中正确的位置。为了找到正确的位置,我们从右到左将它与已在手中的每张牌比较,原来的牌是排好序的,如果比刚拿出的牌大,则将其再向右移一个位置,直到找到一个比刚拿出来的牌更小的,此时将这张牌放到该位置。
这里写图片描述
这里写图片描述

稳定性:在插入排序中,K1是已排序部分中的元素,当K2和K1比较时,直接插到K1的后面(没有必要插到K1的前面,这样做还需要移动!!),因此,插入排序是稳定的。
时间复杂度
最好的情况下:正序有序(从小到大),这样只需要比较n次,不需要移动。因此时间复杂度为O(n)
最坏的情况下:逆序有序,这样每一个元素就需要比较n次,共有n个元素,因此实际复杂度为O(n­2)
平均情况下:O(n­2)
辅助空间:无
代码

 void insertSort(int A[],int n){
     for(int i=1;i<n;++i){
         int j=i-1;
         int temp=A[i];
         while(j>0 && A[j]>A[i]){
             A[j+1]=A[j];
             --j;
         }
         A[j+1]=temp;
     }
 }

2. 希尔排序
思想:希尔排序也是一种插入排序方法,实际上是一种分组插入方法。先取定一个小于n的整数d1作为第一个增量,把表的全部记录分成d1个组,所有距离为d1的倍数的记录放在同一个组中,在各组内进行直接插入排序;然后,取第二个增量d2(<d1),重复上述的分组和排序,直至所取的增量dt=1(dt < dt-1 < … < d2 < d1),即所有记录放在同一组中进行直接插入排序为止
这里写图片描述
稳定性:不稳定
由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。(有个猜测,方便记忆:一般来说,若存在不相邻元素间交换,则很可能是不稳定的排序。)
时间复杂度
最好情况:由于希尔排序的好坏和步长d的选择有很多关系,因此,目前还没有得出最好的步长如何选择(现在有些比较好的选择了,但不确定是否是最好的)。所以,不知道最好的情况下的算法时间复杂度。
最坏情况下:O(N*logN),最坏的情况下和平均情况下差不多。
平均情况下:O(N*logN)
额外空间:无
代码

void shellSort(int A[],int n){
    int d=n/2
    int j=0,temp=0;
    while(d>0){
        //内部插入排序
        for(int i=d;i<n;i++){
            j=i-d;
            temp=A[i];
            while(j>0 && A[j]>temp){
                A[j+d]=A[j];
                j-=d;
            }
            A[j+d]=temp;
        }
        d=d/2;
    }
}

3.直接选择排序
思想:选择一个值array[0]作为标杆,然后循环找到除这个值外最小的值(查找小于标杆的最小值),交换这两个值,这时最小值就被放到了array[0]上,然后再将array[1]作为标杆,从剩下未排序的值中找到最小值,并交换这两个值
这里写图片描述
稳定性:由于每次都是选取未排序序列中的最小元素x与该序列中的第一个元素交换,因此跨距离了,很可能破坏了元素间的相对位置,因此选择排序是不稳定的!例如:5 4 6 5 3 7 第一次选择3 和第一个5 交换,就改变了稳定性
时间复杂度:
平均情况:循环n次,每一次都要寻找最小值,所以是O(n*n)
最好情况:最坏情况:循环n次,若有序,虽然能第一次选择到最小值,但是还是会和其他数字比较,所以还是O(n*n)。但是减少了交换次数

是否需要额外空间:不需要
代码

void selectSort(int A[],int n){
    int min=0;
    for(int i=0;i<n-1;++i){
        min=A[i];
        for(int j=i+1;j<n;++j){
            if(A[j]<min)
                min=A[j];
        }
        if(min!=A[i])
            swap(&A[i],&min);
    }
}

4.堆排序
思想:利用完全二叉树中双亲节点和孩子节点之间的内在关系,在当前无序区中选择关键字最大(或者最小)的记录。也就是说,以最小堆为例,根节点为最小元素,较大的节点偏向于分布在堆底附近。
若想了解堆排序,要了解最大堆性质,如何建堆,再进行堆排序这三个步骤(后续将从这三个方面进行阐述)
稳定性:堆排序要进行跨越式的交换,不稳定
时间复杂度:待续
代码:待续

5.冒泡排序(交换排序)
思想:将相邻的两个数比较,将较小的数调到前头;有n个数就要进行n-1趟比较,第一次比较中要进行n-1次两两比较,在第j趟比较中,要进行n-j次两两比较。
依次比较相邻的两个数,将小数放在前面,大数放在后面。即首先比较第1个和第2个数,将小数放前,大数放后。然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。重复以上过程,直至最终完成排序。
稳定性:两两交换,一定是稳定的
复杂度:
对于原始未改善的冒泡排序,时间复杂度三种情况都是O(n*n),因为无论是否有序,都要两两进行比较;
但是针对于有序序列,可以加入一个变量,在一趟比较中如果一次交换都没有发生,则通过变量退出循环排序,因为这时候已经是排序好的序列了。这时候复杂度最好是O(n)。
代码

//1
void bubbleSort(int A[],int n){
    for(int i=0;i<n-1;++i){//一共进行n趟,每趟中两两比较
        for(int j=n-1;j>i;--j){
            if(A[j]<A[j-1])
                swap(&A[j],&A[j-1]);
        }
    }
}

//2 改进
void bubbleSort(int A[],int n){
    for(int i=0;i<n-1;++i){//一共进行n趟,每趟中两两比较
        bool flag=false;
        for(int j=n-1;j>i;--j){
            if(A[j]<A[j-1])
                swap(&A[j],&A[j-1]);
                flag=true;      
        }
        if(!flag)
            break;
    }
}

6.快速排序(交换排序)
思想它是由冒泡排序改进而来的。在待排序的n个记录中任取一个记录(通常取第一个记录),把该记录放入适当位置后,数据序列被此记录划分成两部分。所有关键字比该记录关键字小的记录放置在前一部分,所有比它大的记录放置在后一部分,并把该记录排在这两部分的中间(称为该记录归位),这个过程称作一趟快速排序。
稳定性:由于每次都需要和中轴元素交换,因此原来的顺序就可能被打乱。如序列为 5 3 3 4 3 8 9 10 11会将3的顺序打乱。所以说,快速排序是不稳定的!
复杂度
平均时间:O(nlgn)
最坏情况:有序 (正序和逆序)的情况下,基本退化为冒泡排序。O(n*n)
正序:每次一分为二时,中枢都是第一个元素,只有一个部分(右)进入下一个循环。
逆序:一分为二后,左右两个部分交替轮流进行下一次循环,(不是同时进行,)其本质是第一个和最后一个交换
代码

void quickSort(int A[],int s,int e){
if(s>e) return;
int i=s;
int j=e;
int m=(s+e)/2;
int key=A[s];
while(i<j){
    while(i<j && A[j]>=key) 
        j--;
    A[i]=A[j];
    while(i<j && A[i]<=key)
        ++i;
    A[j]=A[i];
}
A[i]=key;
quickSort(A,s,i-1);
quickSort(A,i+1,e);
}

7.归并排序
思想:其采用分治的思想,先将问题拆解成相同小问题。本质是将多个有序表合并成一个有序表
稳定性:因为其不会有跨越式交换位置,所以为稳定排序
额外空间:需要O(n)的额外空间。
时间复杂度:对长度为n的文件,需进行logN 趟二路归并,每趟归并的时间为O(n),故其时间复杂度无论是在最好情况下还是在最坏情况下均是O(nlgn)。

分治法通常有3步:Divide(分解子问题的步骤) 、 Conquer(递归解决子问题的步骤)、 Combine(子问题解求出来后合并成原问题解的步骤)。
假设Divide需要f(n)时间,Conquer分解为b个子问题,且子问题大小为a,Combine需要g(n)时间,则递归式为:
T(n)=bT(n/a)+f(n)+g(n)

算法导论思考题4-3(参数传递)能够很好的考察对于分治法的理解。

就如归并排序,Divide的步骤为m=(p+q)/2,因此为O(1),Combine步骤为merge()函数,Conquer步骤为分解为2个子问题,子问题大小为n/2,因此:
归并排序的递归式:T(n)=2T(n/2)+O(n)

代码

void mergeSort(int A[],int s,int e){
    if(s>e) return;
    int m=(s+e)/2;
    mergeSort(A,s,m);
    mergeSort(A,m+1,e);
    //合并过程
    int a=m-s+1;
    int b=e-m;//e-(m+1)+1
    int arr1[a]={0};
    int arr2[b]={0};
    for(int i =0;i<a;++i){
        arr1[i]=A[s+i];
    }
    for(int j=0;j<b;++j){
        arr2[j]=A[m+1+j];
    }
    i=j=0;
    for(int k=s;k<e;++k){
        if(i<a && j<b){
            if(arri[i]<arr2[j]){
                A[k]=arr1[i];
                ++i;
            }
            else{
                A[k]=arr2[j];
                ++j;
            }
        }
    }
    while(i<a)
        A[k++]=arr1[i++];
    while(j<b)
        A[k++]=arr2[j++];
}
<think>好的,我现在要处理用户关于Android 12中RKNN开发配置和使用教程的问题。首先,我需要回顾用户提供的引用内容,看看是否有相关的信息。 用户提供了三个引用。引用1提到了RKNN Model Zoo和RKNN Toolkit2的环境配置,包括安装Miniconda3。引用2是关于系统镜像下载,特别是Ubuntu 22.04版本和导出ONNX模型。引用3讨论了RV1808/1126使用RKNN作为MediaPipe推理后端的步骤,包括配置参数如use_rga、use_mmap和data_type的设置,以及反量化的处理。 用户的问题集中在Android 12上,所以需要将引用中的信息与Android系统关联起来。引用3提到了MediaPipe和RKNN的集成,这可能适用于Android开发,但需要确认是否支持Android 12。此外,RKNN Toolkit2的环境配置可能在不同平台上有所不同,用户可能需要针对Android进行调整。 接下来,我需要考虑Android开发中RKNN的一般步骤,比如环境准备、模型转换、集成到Android应用、配置参数和性能优化。结合引用中的信息,模型转换部分可以参考引用2提到的导出ONNX模型,然后使用RKNN Toolkit转换为RKNN格式。引用3中的配置参数(如use_rga、use_mmap、data_type)可能在Android中同样适用,但需要查阅相关文档确认。 可能还需要指导用户如何将RKNN库集成到Android项目中,比如添加依赖项或NDK配置。此外,性能优化方面,引用3提到的反量化处理可能需要特别注意,尤其是在Android设备上的实现。 最后,确保回答结构清晰,分步骤说明,并引用提供的资料。需要验证引用中的信息是否适用于Android 12,例如系统镜像是否支持,或者是否有特定版本的要求。如果某些步骤在引用中没有明确说明,可能需要提示用户参考官方文档或社区资源。</think>### Android 12 RKNN 开发配置与使用教程 #### 1. **环境准备** - **系统镜像与工具链**: 根据引用[2],建议使用Ubuntu 22.04作为开发环境,并下载Orange Pi 5 Pro等支持RKNN的硬件镜像。对于Android 12开发,需确保RKNN Toolkit2适配Android NDK版本(推荐NDK 21+)[^2]。 - **安装RKNN Toolkit2**: 参考引用[1],通过Miniconda3配置Python环境(建议Python 3.8),并安装RKNN Toolkit2以支持模型转换与推理。命令示例: ```bash pip install rknn_toolkit2 ``` #### 2. **模型转换(ONNX转RKNN)** - **导出ONNX模型**: 使用PyTorch或TensorFlow等框架导出模型为ONNX格式(引用[2])。 - **转换为RKNN格式**: 编写RKNN配置文件(`config.yml`),指定输入尺寸、量化方式等参数。示例代码: ```python from rknn.api import RKNN rknn = RKNN() rknn.config(target_platform='rk3568', mean_values=[[127.5]], std_values=[[127.5]]) rknn.load_onnx(model="model.onnx") rknn.build(do_quantization=True, dataset="dataset.txt") rknn.export_rknn("model.rknn") ``` #### 3. **Android项目集成** - **添加RKNN库依赖**: 将编译好的RKNN动态库(`librknnrt.so`)和模型文件(`model.rknn`)放入Android项目的`jniLibs`目录。 - **JNI接口开发**: 通过JNI调用RKNN的C/C++ API实现推理。关键配置参考引用[3]: ```cpp #include <rknn/rknn_runtime.h> rknn_context ctx; int ret = rknn_init(&ctx, model_path, 0, 0, nullptr); ret = rknn_inputs_set(ctx, input_num, input_attrs); ret = rknn_run(ctx, nullptr); ret = rknn_outputs_get(ctx, output_num, output_attrs, nullptr); ``` #### 4. **关键配置参数** - **use_rga与use_mmap**: 根据引用[3],若Android设备不支持硬件加速(如RGA),需将`use_rga`和`use_mmap`设为`false`。 - **数据量化类型**: `data_type`可选`uint8`或`fp32`。若选`uint8`,需手动实现反量化;若选`fp32`,RKNN会自动处理[^3]。 #### 5. **性能优化** - **多线程推理**: 利用Android的`ThreadPoolExecutor`并行处理输入数据。 - **内存优化**: 启用`use_mmap`(若支持)减少内存拷贝开销,或通过零拷贝技术直接操作物理内存。 #### 6. **调试与验证** - **日志输出**: 启用RKNN的调试模式(`RKNN_LOG_LEVEL=DEBUG`)查看详细运行信息。 - **性能分析**: 使用`rknn.query(RKNN_QUERY_PERF_DETAIL)`获取各层耗时,针对性优化模型结构。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值