排序算法的时间复杂度及其稳定性
排序算法常用分类
1. 冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
#include<iostream>
using namespace std;
// 冒泡排序 最基本的排序算法,核心是交换算法
// 相邻数据依次比较大小,根据大小将相邻元素排序, 获得有序数列
// 冒泡排序的时间复杂:平均复杂度O(n^2) 最坏:O(n^2) 最好情况O(n)
// 空间复杂度: O(1) 是稳定排序 复杂度:简单
// 冒泡排序最少是1趟,最多才是n-1趟,最少比较n-1次,最多才是n(n-1)/2
// 代码实现
void Myprintf(int *arr,int len)
{
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
//输入数列,数列的元素个数
void bubblesort(int *arr,int len)
{
// 此处-1是由于n-1次已经拍好序列
for (int i = 0; i < len-1; i++)
{
for (int j = 0; j < len-i-1; j++) //减1是由于不减1在j=n-1时j+1 =n此时数组的下标越界
{
// 从小到大排序
if (arr[j]>arr[j+1])
{
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
Myprintf(arr,len);
}
}
void main()
{
int arr[100];
int n;
cout<<"请输入数组的元素个数:"<<endl;
cin>>n;
cout<<"请输入元素"<<endl;
for (int i = 0; i < n; i++)
{
cin>>arr[i];
}
bubblesort(arr,n);
}
2. 选择排序
无论什么数据进去都是O(n²)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。通常情况下是普通人第一时间想到的结果。
选择排序,选择最小的一个元素,将其和第一个位置的数交换
接着n-1个元素中再找最小的和第二个
不断重复直到有序
选择排序时间复杂度:最好最坏平均时间都是O(n^2) 空间复杂度O(1),复杂度简单,不稳定排序
快希选一堆// 快速,堆排序,选择排序,希尔排序 不稳定排序
选择排序一定是n-1趟排序,比较的次数永远是n(n-1)/2
前边有序后边无序
#include<iostream>
using namespace std;
void selectsort(int *arr,int len)
{
int add;// 记录最小值的下标
// 排序次数n-1轮
for(int i =0;i<len-1;i++)
{
// 标记为最小值,初始设定为第一个值从小到大
add = i;
// 每次排序都要从i+1到尾比较
for (int j = i+1; j < len; j++)
{
if (arr[add]>= arr[j])
{
add =j;//每轮找出最小的数值
}
}
if (i!=add)
{
int tv = arr[i];
arr[i] = arr[add];
arr[add] = tv;
}
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
}
void main()
{
int arr[] ={100,20,50,80,62,38,65,75,201};
int len = sizeof(arr)/sizeof(arr[0]);
cout<<len<<endl;
selectsort(arr,len);
}
3.插入排序
1.前俩个数据进行从小到大排序
2.将第三个数与拍好顺序的前俩个比较插入合适位置
3.将四个数与前三个比较并插入
4.重复上述步骤至有序
时间: 平均复杂度O(n^2)
最坏:O(n^2)
最好情况O(n)
空间:O(1)
简单复杂度
稳定排序
#include<iostream>
using namespace std;
/*
比较和插入进行实现排序
1.前俩个数据进行从小到大排序
2.将第三个数与拍好顺序的前俩个比较插入合适位置
3.将四个数与前三个比较并插入
4.重复上述步骤至有序
时间: 平均复杂度O(n^2) 最坏:O(n^2) 最好情况O(n)
空间:O(1)
简单复杂度
稳定排序
*/
//从小到大,代码实现
void myprint(int *arr,int len)
{
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
void insertsort(int *arr,int len)
{
for (int i = 1; i < len; i++)
{
int t =arr[i];
int j = i-1;
//判断要插入的位置,找第一个比插入的数小的值,如果比插入的数大就往后移位
//从后往前找插入的位置
while (j>=0&&t<arr[j])
{
arr[j+1]=arr[j];
j--;
}
//如果比插入的数小则插在j指向的位置的下一位
arr[j+1]=t;
myprint(arr,len);
}
}
void main()
{
int arr[8] = {8,7,5,6,2,4,1,3};
insertsort(arr,8);
}
4.希尔排序
缩小增量排序
将n个元素的数组分成n/2个数字序列,第1个数据和第n/2+1个数据为一对,…
每次循环是每一个序列对排好顺序,变成n/4在排序
不断重复,随着序列减少变为一个,完成排序
减少了数据的交换次数
时间复杂度:O(n^1.3)
空间:O(1)
不稳定排序
复杂排序
#include<iostream>
using namespace std;
/*
缩小增量排序
将n个元素的数组分成n/2个数字序列,第1个数据和第n/2+1个数据为一对,...
每次循环是每一个序列对排好顺序,变成n/4在排序
不断重复,随着序列减少变为一个,完成排序
减少了数据的交换次数
时间复杂度:O(n^1.3)
空间:O(1)
不稳定排序
复杂排序
*/
void myprint(int *arr,int len)
{
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
void shellsort(int *arr,int len)
{
int i,j,h;
int r,temp;
// 划分len/2组数据,并对每个小序列排序 第二次n/4组 6个元素为例
for (r=len/2; r>=1; r/=2) // 可以认为这个对应下标的间隔 刚开始是6/2=3 第二轮变为6/4 = 1
{
// 对每组进行插入排序,从后往前排,i = r 6/2=3 ::0-3 1-4 2-5
for ( i = r; i < len; i++)
{
temp =arr[i];// 设定初始值
j =i-r; // 得到下标0 1 2
while (j>=0&&temp<arr[j])//插入排序比较
{
arr[j+r]=arr[j];//交换
j-=r;
}
arr[j+r] =temp;//
}
myprint(arr,len);
}
}
void main()
{
int arr[9]={30,20,10,40,60,50,90,100,80};
shellsort(arr,9);
}
5. 快速排序
改进的冒泡的元素
1.确定分界值,将该分界值分成左右俩部分,其中分界值是先不动的,等排序结束后跟所在位置的元素交换
6 1 2 7 9 3 4 5 10 8
6分界值
6不动
6 1 2 7(比6大)9 3 4 5(第一个比6小的) 10 8 交换位置
6 1 2 5 9(大) 3 4(小) 7 10 8 交换
6 1 2 5 4 3(i==j) 9 7 10 8 结束一次循环
6和3交换
3 1 2 5 4 6 9 7 10 8 第一轮结束
2.大于在右,小于在左
3.左右独立排序,对左右序列进行同样操作
4.重复上述过程,是一个递归的过程,左右排序完时,就是个有序数列
不稳定排序
时间:平均O(nlog2n)最坏O(n^2)最好O(nlog2n)
空间:O(nlog2n)
复杂排序
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
//递归法
void myprint(int *arr,int len)
{
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
//分治的思想,每次实现元素的移动,根据基准数,划分左右
int Partition(int a[],int low,int r){
int i = low,j = r;
int x = a[low];
while(true){
while(a[++i]<x&&i<r);//a[i]比base大
while(a[j]>x)
{
--j;
}
//a[j]比base小
if(i<j){
swap(a[i],a[j]);
}
else{
break;
}
}
//循环之外i=j,交换基准点,将基准点交换到遍历的停止处,即j处
//这样使当前基准值左边都是比他小的数,右边都是比他大的数
a[low] = a[j];
a[j] = x;
return j;
}
void quicksort(int a[],int low,int high){
if(low<high){
int q = Partition(a,low,high);
myprint(a,8);// 打印输出
quicksort(a,low,q-1);
quicksort(a,q+1,high);
}
}
void main()
{
int arr[8] = {69,62,89,37,97,17,28,49};
quicksort(arr,0,7);
myprint(arr,8);
}
6. 堆排序
根据从小到大的顺序则使用大根堆,从大到小排序则使用小根堆,每次都是输出根结点,在数列末尾
即97 最大,为根先输出97,1最小为根先输出 1
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么
1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.左孩子索引:2i+1
3.右孩子索引:2i+2
所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:
大根堆:arr(i)>arr(2i+1) && arr(i)>arr(2i+2)
小根堆:arr(i)<arr(2i+1) && arr(i)<arr(2i+2)
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
即:每插一个数进行一次构造大根堆,即要根结点大于左右孩子结点
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
时间复杂度:
最好:O(n log2 n)
最坏:O(n log2 n)
平均:O(n log2 n)
空间复杂度:O(1)
稳定性:不稳定
#include<iostream>
#include<algorithm>
using namespace std;
// 堆排序
/*
根据从小到大的顺序则使用大根堆,从大到小排序则使用小根堆,每次都是输出根结点,在数列末尾
即97 最大,为根先输出97,1最小为根先输出 1
堆
数组排序就是依次写入完全二叉树中
大根堆 小根堆
9 1
6 8 3 4
5 3 4 7 4 5 8 7
2 1 9 6
9 6 8 5 3 4 7 2 1 1 3 4 4 5 8 7 9 6
数列输出为
....6789 ...54321
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么
1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)
2.左孩子索引:2*i+1
3.右孩子索引:2*i+2
所以上面两个数组可以脑补成堆结构,因为他们满足堆的定义性质:
大根堆:arr(i)>arr(2*i+1) && arr(i)>arr(2*i+2)
小根堆:arr(i)<arr(2*i+1) && arr(i)<arr(2*i+2)
1.首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端
即:每插一个数进行一次构造大根堆,即要根结点大于左右孩子结点
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
将无序数组构造成一个大根堆(升序用大根堆,降序就用小根堆)
时间复杂度:
最好:O(n log2 n)
最坏:O(n log2 n)
平均:O(n log2 n)
空间复杂度:O(1)
稳定性:不稳定
*/
//代码实现
//1、将待排序数组构造成一个大根堆(元素上升)
//2、固定一个最大值,将剩余的数再构造成一个大根堆(元素下降)
#include<iostream>
#include<algorithm>
using namespace std;
void myprint(int *arr,int len)
{
for (int i = 0; i < len; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
void Down(int *arr,int i,int n) // 构建大根堆
{
int parent,child;
parent = i; // 父结点
child = i*2+1;//右孩子结点
while(child < n)
{
//找出孩子结点中最大的一个与父节点比较
if(arr[child]<arr[child+1] &&(child+1)<n )
{
child++;
}
if (arr[parent]<arr[child])//如果父结点小,交换俩个结点
{
swap(arr[parent],arr[child]);
parent = child; // 同时将父结点变为交换的孩子结点判断孩子的结点是不是大根堆结构
}
child = child*2+1; // 将孩子结点同样变到下一行进行判断
}
}
void heapsort(int *arr,int size)
{
for (int i = size/2-1; i >=0; i--)
{
Down(arr,i,size);
}
// 初始化完成后大根堆序列
myprint(arr,size);
for (int i = size-1; i >0; i--)
{
swap(arr[0],arr[i]);
Down(arr,0,i); // 已经是大根堆排序了从上向下进行比较即可
myprint(arr,size); // 输出每次的运行结果
}
}
void main()
{
int array[]={49,38,65,97,76,13,27,49,10};
int size= sizeof(array) / sizeof(int);
myprint(array,size);
heapsort(array,size);
}
7. 归并排序
合并排序归排序
算法思想:用分治策略实现对n个元素进行排序。
将待排序元素分成大小相同的两个子集合,分别对两个子集合进行排序,
刚开始俩个元素一个子序,每个子序排序
排好后四个元素一个子序
…
最终将排好序的子集合合并成所要求的排好序的集合。
时间复杂度:最好最坏平均:O(nlog2n)
空间复杂度:O(n)
稳定排序
较复杂
#include<iostream>
#include<algorithm>
using namespace std;
// 参数 数组,左边界,分界线,右边界,
void merge(int *data, int l, int q, int r)
{
// 存放 左右数组的个数,k
int n1, n2, i, j, k;
n1 = q - l + 1;
n2 = r - q;
int *left = new int[n1];
int *right = new int[n2];
for (i = 0; i<n1; i++) //对左数组赋值
left[i] = data[l + i];
for (j = 0; j<n2; j++) //对右数组赋值
right[j] = data[q + 1 + j];
i = j = 0;
k = l;
while (i<n1 && j<n2) //将数组元素值两两比较,并合并到data数组
{
if (left[i] <= right[j])
data[k++] = left[i++];
else
data[k++] = right[j++];
}
for (i; i<n1; i++) //如果左数组有元素剩余,则将剩余元素合并到data数组
data[k++] = left[i];
for (j; j<n2; j++) //如果右数组有元素剩余,则将剩余元素合并到data数组
data[k++] = right[j];
}
void mergeSort(int *data, int l, int r)
{
int q;
if (l < r) //只有一个或无记录时不须排序
{
// 先进行对半拆分,拆分结束后对每个子序递归排序
q = (int)((l + r) / 2); //将data数组分成两半
mergeSort(data, l, q); //递归拆分左数组
mergeSort(data, q + 1, r); //递归拆分右数组
merge(data, l, q, r); //合并数组
}
}
int main()
{
int n;
int array[]={49,38,65,97,76,13,27,49,10};
n= sizeof(array) / sizeof(int);
mergeSort(array, 0, n - 1); // 数组,左边界,右边界
cout << "合并排序后数组: " << endl;;
for (int i = 0; i<n; ++i)
cout << array[i] << " ";
cout << endl;
return 0;
}
8.计数排序
假设输入序列都是0到k之间的整数,则可使用计数排序。具体操作是这样的:创建一个同类型同等大小的临时数组temp,用于备份输入序列。创建一个整型大小为k的数组count,用于统计序列中各元素出现的次数。接下来只需把备份序列从大到小放回原数组即可。一个示例图:
生成一个长度为K的count数组,每个元素记录是下标所代表的数字出现的次数,
每个下标在数组中的位置由count[i] = count[i]+count[i-1]
最后根据count 数组的元素插入序列中
缺点:
计数排序是一种以空间换时间的排序算法,并且只适用于待排序列中所有的数较为集中时,
比如一组序列中的数据为0 1 2 3 4 999;就得开辟1000个辅助空间。
时间复杂度:
计数排序的时间度理论为O(n+k),其中k为序列中数的范围。
不过当O(k)>O(nlog(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(nlog(n)), 如归并排序,堆排序)
空间复杂度: O(n+k)
稳定排序
#include<iostream>
#include<map>
#include<algorithm>
using namespace std;
//用于定义临时数组大小,可以根据需求改
const int NUMMAX = 1000;
void myprint(int *arr,int n)
{
for (int i = 0; i < n; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
int arrmax(int *arr,int n)
{
int max = arr[0];
for(int i =0;i<n;i++)
{
if (max<arr[i])
{
max =arr[i];
}
}
return max;
}
void countsort(int *arr,int n,int max)
{
map<int,int> p;
int arr1[NUMMAX];
for (int i = 0; i < n; i++)
{
arr1[i] = arr[i];
}
// 插入map组中用来计数
for (int i = 0; i < max; i++)
{
p.insert(make_pair(i,0));
}
//生成一个长度为K的count数组,每个元素记录是下标所代表的数字出现的次数,
for(int i=0 ;i <n;i++)
{
p[arr[i]] +=1;
}
//统计位置每个下标在数组中的位置由p[i] = p[i]+p[i-1] 这里循环最大是max是你创建时的数
for(int i =1;i<=max;i++)
{
p[i] = p[i]+p[i-1];
}
for(map<int,int>::iterator it = p.begin();it !=p.end();it++)
{
cout<<it->first<<" "<<it->second<<endl;
}
//回放序列 map 数组中p[arr1[i]]存放着相应数字的位置 arr[p[arr1[i]]] = arr1[i]的值 对应位置的值
// 如果有重复的数值则继续放入对应位置 --p[arr1[i]] 获得
for (int i = 0; i<n; i++)
{
p[arr1[i]]--;
arr[p[arr1[i]]] = arr1[i];
}
myprint(arr,n);
}
void main()
{
int array[]={2,3,1,5,1,6,4,9};
int size= sizeof(array) / sizeof(int);
myprint(array,size);
int max = arrmax(array,size);
cout<<max<<endl;
countsort(array,size,max);
}
9.基数排序
当序列中元素范围比较大时,就不适合使用计数排序。
针对这种情况,就有了基数排序(Radix Sort),这是一种按位排序。它仍然是以计数排序为基础。
基数排序的基数:十进制数的基数自然是10,二进制的基数自然是2。
通常有两种按位排序策略:
1.高位优先法(most significant digit first,MSD):简单讲就是从高位排起。
2.低位优先法(least significant digit first,LSD):它与高位优先相反,从低位排起。
从排序效果上看,高位优先比较直观,但却涉及到递归的过程,故最常用的还是低位优先法。
以十进制为例,缺少高位补0
#include<iostream>
#include<map>
#include<algorithm>
using namespace std;
void myprint(int *arr,int n)
{
for (int i = 0; i < n; i++)
{
cout<<arr[i]<<" ";
}
cout<<endl;
}
int highest(int *arr,int n)
{
int max,topdigit;
max =arr[0];
topdigit =0; // 最低位
for (int i = 0; i < n; i++)
{
if(arr[i]>max)
max = arr[i];
}
while (max)
{
topdigit++;
max = max/10;
}
return topdigit;
}
void cardinalsort(int *arr,int n)
{
int *temp = new int(n);
int count[10]; // 按位比时候临时计数
int top = highest(arr,n);// 获得最大数字是几位数 排序时需要 从零开始
int div ; // 用来获取每位数字的
for(int i =1;i<=top;i++)
{
// 重置计数数组
memcpy(temp,arr,n*sizeof(int)); // 临时数组存放arr元素
memset(count,0,10*sizeof(int)); // 按位比时候临时计数
// 低位优先
div =1;
int j =1;
while ( j<i)
{
j++;
div *= 10;
}
for (int i = 0; i < n; i++)
// 计数排序
count[(arr[i]/div)%10]++; // 计数
for (int i = 1; i < 10; i++)
// 计算位置
count[i] = count[i]+count[i-1];
for (int i = n-1; i >=0; i--)
{
// 根据低位排序将数据放入指定位置
arr[--count[(temp[i]/div)%10]] = temp[i];
}
}
}
void main()
{
cout << "******基数排序***by David***" << endl;
int array[] = { 123, 234, 45, 111, 6, 128 };
int n = sizeof(array) / sizeof(int);
cout << "原序列" << endl;
myprint(array, n);
cout << "基数排序" << endl;
cardinalsort(array, n);
myprint(array, n);
}
10.桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序
具体算法描述如下:
<1>.设置一个定量的数组当作空桶;
<2>.遍历输入数据,并且把数据一个一个放到对应的桶里去;
<3>.对每个不是空的桶进行排序;
<4>.从不是空的桶里把排好序的数据拼接起来。
#include<iostream>
#include<vector>
using namespace std;
int getRange(vector<int> a, int begin, int end,int &min,int &max) {//获得数组元素范围和极值
for (int i = begin + 1; i <= end; ++i) {
min = a[i] < min ? a[i] : min;
max = a[i] > max ? a[i] : max;
}
return max - min+1;
}
void bucketSort(vector<int>&a, int begin, int end) {//容器做参数时用引用传递来调换元素顺序,
if (end - begin <1)
return;//递归出口
int min, max;min=max = a[0];
int range = getRange(a, begin, end, min, max); //获得元素范围和极值
int bucketNum = 5;//本次定义了5个桶
int gap = range / 5+1;//设定桶区间
vector <vector<int > > bucket(bucketNum);//用二维容器来装桶
for (int i = begin; i <= end; ++i){
cout << "桶编号: " << (a[i] - min) / gap << "放入元素:" << a[i] << endl;
bucket[(a[i]-min )/ gap] .push_back( a[i]);//元素放在不同的桶里
}
for (int i = 0; i < bucketNum; ++i) {
bucketSort( bucket[i],0,bucket[i].size()-1);//对桶里的元素递归调用桶排序
}
for (int i = 0,j=0; i < bucketNum; ++i) {
if (!bucket[i].empty()) {//桶非空判断
for (vector<int>::iterator p = bucket[i].begin(); p!=bucket[i].end(); ++p) {//桶里排序好的元素放回原容器a
a[j++] = *p;
}
}
}
cout << endl;
}
int main() {
vector<int>a;
for (int i = 0,j=100; i<20; ++i) {
a.push_back(j--);
}
bucketSort(a, 0, a.size()-1);
cout << "排序结果:" << endl;
for (int i = 0; i<a.size(); ++i) {
cout << " "<<a[i];
}
return 0;
}