常见的有以下八中排序算法:
七大算法之间的时间复杂度、空间复杂度、稳定性的比较:
选择排序算法准则:
每种排序算法都各有优缺点。
影响排序的因素有很多,平均时间复杂度低的算法并不一定就是最优的。相反,有时平均时间复杂度高的算法可能更适合某些特殊情况。同时,选择算法时还得考虑它的可读性,以利于软件的维护。一般而言,需要考虑的因素有以下四点:
- 1.待排序的记录数目n的大小;
- 2.记录本身数据量的大小,也就是记录中除关键字外的其他信息量的大小;
- 3.关键字的结构及其分布情况;
- 4.对排序稳定性的要求。
设待排序元素的个数为n:
1)当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
- 快速排序:是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
- 堆排序 : 如果内存空间允许且要求稳定性的,
- 归并排序:它有一定数量的数据移动,所以我们可能过与插入排序组合,先获得一定长度的序列,然后再合并,在效率上将有所提高。
2) 当n较大,内存空间允许,且要求稳定性 ——归并排序
3)当n较小,可采用直接插入或直接选择排序。
- 直接插入排序:当元素分布有序,直接插入排序将大大减少比较次数和移动记录的次数。
- 直接选择排序 :元素分布有序,如果不要求稳定性,选择直接选择排序
5)一般不使用或不直接使用传统的冒泡排序。
6)基数排序:
它是一种稳定的排序算法,但有一定的局限性:1、关键字可分解。2、记录的关键字位数较少,如果密集更好3、如果是数字时,最好是无符号的,否则将增加相应的映射复杂度,可先将其正负分开排序。
七大排序算法
1.选择排序
2.插入排序
3.快速排序
4.堆排序
5.冒泡排序
6.希尔排序
7.归并排序
1.选择排序
思想:遍历数组,每次遍历都在未排序的部分找到最小元素的下标,在此次遍历结束后将最小元素放到遍历开始的位置
性能:时间复杂度为O(n2),算法比较次数与初始序列状态无关,性能在所有排序算法中最差。
动态演示图:
c++代码:
void select_Sort(int* A, int len){
for(int i=0;i<len;i++)
{
int k = i;
for(int j=i;j<len;j++)
if(A[j]<A[k])
k = j;
if(k != i){
int t = A[i];
A[i] = A[k];
A[k] = t;
}
}
}
2.插入排序
思想:将数组中的所有元素依次和前面的已经排好序的元素相比较(依次),如果选择的元素比已排序的元素小,则交换,直到全部元素都比较过。
实现:利用end为指向已经排好序的最后一个元素,temp声明为待排序的数,在以排好序的数列中寻找可以插入的位置,在寻找的过程中是从后向前寻找,发现比temp大的就向后移动,直到发现比temp小的终止寻找,并将temp放入该位置,进入到下一个循环。
动态演示图:
3.快速排序 :
参考质料链接
思路:与归并排序类似,也使用分治思想,选择一个元素值(一般选择最后一个元素),将比它小的放在左边部分,比它大的放在右边,然后对两部分分别进行上述操作知道递归结束,关键步骤在于元素的分类,且只占用O(1)的额外存储空间。
步骤(小到大):选择最左边的数为基准数,利用双边循环法,右边先向前搜索,大于等于基准数就继续前进,否则停止,开始移动左边指针向前,小于基准数就后退,否则停止,此时交换左右指针指向的数值再次前进后退,知道左右二个指针相遇。相遇后与基准数交换数值。此时将原数组分成了二个部分,一部分大于基准数,一部分小于基准数。利用递归方法继续分裂,变成最简单的排序数列。
性能:时间复杂度为O(nlgn),与归并排序不同,该算法占用常数级额外存储,在大规模序列排序应用中性能较好。
c++代码:
//快速排序(从小到大)
void quickSort(int left, int right, vector<int>& arr)
{
if(left >= right)
return;
int i, j, base, temp;
i = left, j = right; //左右二个指针
base = arr[left]; //取最左边的数为基准数
while (i < j)
{
while (arr[j] >= base && i < j) j--; //移动右指针
while (arr[i] <= base && i < j) i++; //移动左指针
if(i < j){ //二个指针停止时 交换指向的数值
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
arr[left] = arr[i]; //基准数归位
arr[i] = base;
//开始递归分裂 分治法体现在这里
quickSort(left, i - 1, arr);//递归左边
quickSort(i + 1, right, arr);//递归右边
}
4.堆排序 :
参考质料
思想:使用堆数据结构进行排序,堆是一种用数组存储的二叉树,根据父节点和子节点的大小关系分为最大堆和最小堆,这里使用最大堆进行排序。将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
算法的步骤:
- 把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大 到小排序,则构建成最小堆。
- 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
性能:
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。空间复杂度:O(1).
动态演示图:
c++代码:
#include <stdio.h>
void swap(int array[],int x,int y){
int key;
key=array[x];
array[x]=array[y];
array[y]=key;
}
//从大到小排序
//void Down(int array[],int i,int n){
// int child=2*i+1;
// int key=array[i];
// while (child<n){
// if (array[child]>array[child+1] && child+1<n) {
// child++;
// }
// if (key>array[child]){
// swap(array, i, child);
// i=child;
// }
// else{
// break;
// }
// child=child*2+1;
// }
//}
//从小到大排序 使得i节点下沉满足堆对节点的要求
void Down(int array[],int i,int n){ //最后结果就是大顶堆
int parent=i; //父节点下标
int child=2*i+1; //子节点下标
while (child<n){
if (array[child]<array[child+1] && child+1<n) { //判断子节点那个大,大的与父节点比较
child++;
}
if (array[parent]<array[child]){ //判断父节点是否小于子节点
swap(array, parent, child); //交换父节点和子节点
parent=child; //子节点下标 赋给 父节点下标
}
child=child*2+1; //换行,比较下面的父节点和子节点
}
}
void BuildHeap(int array[],int size)
{
int i=0; //倒数第二排开始 从右到左 从小到上
for(i=size/2-1;i>=0;i--){ //创建大顶堆,必须从下往上比较
Down(array,i,size); //否则有的不符合大顶堆定义
}
}
void heapSort(int array[],int size){
BuildHeap(array,size); //初始化堆(使非叶子节点都下沉)
printf("初始化数组:");
for(int i=0;i<size;i++){
printf("%d ",array[i]);
}
printf("\n");
for(int i=size-1;i>0;i--)
{ //交换最后一个节点与堆顶的节点,然后将堆顶节点做下沉处理
swap(array,0,i); //交换顶点和第 i 个数据
//因为只有array[0]改变,其它都符合大顶堆的定义,所以可以从上往下重新建立
Down(array,0,i); //重新建立大顶堆
printf("排序的数组:");
for(int i=0;i<size;i++){
printf("%d ",array[i]);
}
printf("\n");
}
}
int main(){
int array[]={49,38,65,97,76,13,27,49,10};
int size= sizeof(array) / sizeof(int);
printf("%d \n",size);
printf("排序前数组:");
for(int i=0;i<size;i++){
printf("%d ",array[i]);
}
printf("\n");
heapSort(array,size);
return 0;
}
运行结果图:
5.冒泡排序
思想:从左往右遍历,比较相邻两个元素的大小,将大的一个放在后面,每遍历一趟,可找到一个最大值放置在最后,经过n-1趟遍历即可。
性能:比较次数为:(n-1) + (n-2) + … + 1 = n*(n-1) / 2,因此冒泡排序的时间复杂度为O(n^2)。
步骤:
1.比较相邻的元素,前一个比后一个大(或者前一个比后一个小)调换位置
2.每一对相邻的元素进行重复的工作,从开始对一直到结尾对,这步完成后,结尾为做大或最小的数.
3.针对除了最后一个元素重复进行上面的步骤。
4.重复1-3步骤直到完成排序
动态演示图:
c++代码:
#include<stdio.h>
void BuddleSort(int a[],int n){
for(int i=0;i<n-1;i++){
for( int j=0;j<n-i-1;j++){
if(a[j]>a[j+1]){
int swap=a[j];
a[j]=a[j+1];
a[j+1]=swap;
}
}
}
}
int main(){
int a[]={6, 9,8,4,5,2,1,3,7};
int n=sizeof(a)/sizeof(int);
BuddleSort(a,n);
printf("冒泡排序结果:");
for (int i = 0; i < n; i++){
printf("%d ", a[i]);
}
return 0;
}
6.希尔排序(缩小增量排序 )
参考质料链接
简介:是简单插入排序的改进版,是将整个无序列分割成若干小的子序列分别进行插入排序的方法。它与插入排序的不同之处在于,它会优先比较距离较远的元素。简单插入排序对小规模数据数据或者基本有序时十分高效,数据有序程度越高,越高效。使用希尔排序对较大规模并且无序的数据会非常有效率。
算法步骤:
首先它把较大的数据集合按照n/2的增量分割成若干个小组(逻辑上分组),然后对每一个小组分别进行插入排序,此时,插入排序所作用的数据量比较小(每一个小组),插入的效率比较高。每个分组进行插入排序后,各个分组就变成了有序的了(整体不一定序)。接着将增量再次缩小一倍,重复上面的操作,直到增量为1.
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列个数k,对序列进行k 趟排序;
- 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
性能:希尔排序的性能无法准确量化,跟输入的数据有很大关系。在实际应用中也不会用它,因为十分不稳定,虽然比传统的插入排序快,但比快速排序等慢。其时间复杂度介于O(nlogn) 到 O(n^2) 之间
动画演示:
c++代码:
void shellsort(int arr[],int len) { //数组作为参数时,将长度代入会比较好,不然通过sizeof无法知道数组长度
int n = len; //数组长度
for (int gap = n / 2; gap > 0; gap /= 2) { //改变增量gap 进行插入排序
for (int i = gap; i < n; i++) { //对各分组的每个元素进行插入排序
int key = arr[i];
int j; //插入到合适的位置
for (j = i - gap; j >= 0 && key < arr[j]; j -= gap) {
arr[j + gap] = arr[j]; //排好的序列移位
}
arr[j + gap] = key; //将待插入的数插入到合适的位置
}
}
}
int main() {
int sum[] = { 4,2,3,1,5,8};
int len = sizeof(sum) / sizeof(sum[0]);
shellsort(sum,len);
int n = len;
while (n)
{
cout << sum[len - n] << " ";
n--;
}
return 0;
}
7.归并排序
简介:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
注:分治思想的代表——首先把原问题分解为两个或多个子问题,然后求解子问题的解,最后使用子问题的解来构造出原问题的解。
算法描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
性能分析:
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
动画演示:
c++代码:
const int N=1001;
int a[N];
int n;
//左右表有序表进行合并
void Merge(int a[],int low,int mid,int high){
int i,j,k;
int b[N]; //复制a中数据
for(int z=low;z<=high;z++)
b[z]=a[z];
i=low; //以mid为中心i遍历左边的表,j遍历右边的表,找出最小值放入a中
j=mid+1;
k=i;
while(i<=mid&&j<=high){ //二表开始比较大小排入原数组,当左右序列任一个读完时结束
if(b[i]<=b[j])
a[k]=b[i++];
else
a[k]=b[j++];
k++;
}
//左右序列有一个没读完,继续读不用比较
while(i<=mid) a[k++]=b[i++];
while(j<=high) a[k++]=b[j++];
}
void MergeSort(int a[],int low,int high){
if(low<high){
int mid=(low+high)/2; //在此是该序列一分为二,直到1-1,然后将1-1归并成有序的2
MergeSort(a,low,mid);
MergeSort(a,mid+1,high);
Merge(a,low,mid,high); //合并前面二个序列
}//else 是low=high时
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
MergeSort(a,1,n);
for(int i=1;i<=n;i++)
cout<<a[i]<<" ";
return 0;
}