排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:冒泡排序、选择排序、插入排序、希尔排序、快速排序、堆排序、归并排序、基数排序等。
在开始之前先声明,这是默认为整数排序,都是以从小到大的排列方式进行。
目录
一、冒泡排序
冒泡排序(BubbleSort)它重复地走访要排序的数列,依次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列
的工作是重复地进行直到没有元素再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交
换慢慢“浮”到数列的顶端。
算法步骤:
1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
3)针对所有的元素重复以上的步骤,除了最后一个。
4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现:
void BubbleSort(int *arr,int len)
{
for(int i=0;i<len-1;i++)//趟数,两两交换,五个元素只需四趟
{
for(int j=0;j<len-1-i;j++)
{
int tmp;
if(arr[j]>arr[j+1])
{
tmp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=tmp;
}
}
}
}
void Show(int *arr,int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={12,10,3,8,4};
int len=sizeof(arr)/sizeof(arr[0]);
BubbleSort(arr,len);
Show(arr,len);
return 0;
}
这种算法还可以进行优化,当待排序列是一个已经排好的序列,就不需要每一个元素逐次进行比较,此时就需要定义一个标识符,用来判断元素之间是否进行了互换。如果没有互换,就说明所给序列是一个从小到大的有序序列。
实现代码:
void BubbleSort(int *arr,int len)
{
bool swap=false;
for(int i=0;i<len-1;i++)趟数,两两交换,五个元素只需四趟
{
for(int j=0;j<len-1-i;j++)
{
int tmp;
if(arr[j]>arr[j+1])
{
tmp=arr[j+1];
arr[j+1]=arr[j];
arr[j]=tmp;
swap=true;
}
}
if(!swap)
{
break;
}
}
}
void Show(int *arr,int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={4,6,8,9,10,11,13,23};
int len=sizeof(arr)/sizeof(arr[0]);
BubbleSort(arr,len);
Show(arr,len);
return 0;
}
时间复杂度:O(n^2)
最好时间复杂度:1 2 3 4 5 6 7 8 9 O(n)
最坏时间复杂度: 2 3 4 5 6 1 6 8 2 O(n^2)
空间复杂度:O(1)
稳定性:稳定排序
什么是稳定性?如:14,1,9,7,6,6',8,21;在排序之后==》6'还在6之后,就称之为稳定,否则为不稳定。
二、选择排序
选择排序(SelectSort)从待排序数字后面找到比待排序数字小的数字就交换,一直到整个数列遍历完毕。
算法步骤:
1)首先在未排序序列中选择第一个元素作为待排数字。
2)从待排数字后面找到比待排数字小的数字就交换。
3)重复第二步,直到所有元素均排序完毕。
代码实现:
void SelectSort(int *arr,int len)
{
for(int i=0;i<len;i++)
{
for(int j=i+1;j<len;j++)
{
int tmp;
if(arr[i]>arr[j])
{
tmp=arr[j];
arr[j]=arr[i];
arr[i]=tmp;
}
}
}
}
void Show(int *arr,int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={4,6,8,9,10,11,13,23};
int len=sizeof(arr)/sizeof(arr[0]);
SelectSort(arr,len);
Show(arr,len);
return 0;
}
时间复杂度:O(n^2)
有序:O(n^2)
无序:O(n^2)
空间复杂度:O(1)
稳定性:不稳定排序
三、插入排序
插入排序(InsertSort)就像是玩扑克牌一样,先给你一张牌,下一张比之前给你的那张大,就放在后边;要是比之前给你的那张小,就放在前面。
算法步骤:
1)当元素只有一个的时候,那么它是有序的。所以从第二个元素开始,作为第一张牌。
2)然后开始遍历数组,得到数字与已知的相比较,如果小,放左边(如果左边的数还是比它大,再往左,直到左边最没有比它大的数,即它最小);如果大,放右边(如果右边的数还是比它小,再往右,直到右边最没有比它小的数,即它最大)。
代码实现:
void InsertSort(int *arr,int len)
{
for(int i=1;i<len;i++)
{
int tmp=arr[i];
int j=0;
for(j=i-1;j>=0;j--)
{
if(arr[j]>tmp)
{
arr[j+1]=arr[j];
}
else
{
break;
}
}
arr[j+1]=tmp;//有序排列
}
}
void Show(int *arr,int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={12,10,3,8,4,5};
int len=sizeof(arr)/sizeof(arr[0]);
InsertSort(arr,len);
Show(arr,len);
return 0;
}
当它为有序数列时,时间复杂度会减少。代码中的else语句,使得它不在遍历其余的元素。
注:插入排序,越有序,速度越快。
时间复杂度:O(n^2)
有序:O(n)
无序:O(n^2)
空间复杂度:O(1)
稳定性:稳定排序
四、希尔排序
希尔排序(ShellSort)是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时, 效率高, 即可以达到线性排序的效率
- 但插入排序一般来说是低效的, 因为插入排序每次只能将数据移动一位
希尔排序的基本思想是:(采用分组思想)先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法步骤:
1)选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
2)按增量序列个数n,对序列进行n 趟排序;
3)每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
好的增量序列的共同特征:
1、 最后一个增量必须为1。
2、应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
代码实现:
void Shell(int *arr,int len,int gap)
{
for(int i=gap;i<len;i++)
{
int tmp=arr[i];
int j=0;
for(j=i-gap;j>=0;j=j-gap)
{
if(arr[j]>tmp)
{
arr[j+gap]=arr[j];
}
else
{
break;
}
}
arr[j+gap]=tmp;
}
}
void ShellSort(int *arr,int len)
{
int drr[]={5,3,1};
int lend=sizeof(drr)/sizeof(drr[0]);
for(int i=0;i<lend;i++)
{
Shell(arr,len,drr[i]);
}
}
void Show(int *arr,int len)
{
for(int i=0;i<len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={12,5,9,34,6,8,33,56,89,0,7,4,22,55,77};
int len=sizeof(arr)/sizeof(arr[0]);
ShellSort(arr,len);
Show(arr,len);
return 0;
}
时间复杂度:O(n^1.3)-O(n^1.5)之间
空间复杂度:O(1)
稳定性:不稳定排序
五、快速排序
快速排序(QuickSort) 快速排序是找出一个元素(理论上可以随便找一个)作为基准(pivot), 然后对数组进行分区操作,使基准左边元素的值都不大于基准值,基准右边的元素值 都不小于基准值,如此作为基准的元素调整到排序后的正确位置。
三种排序方式:
1.固定位置选取基准法
2.随机选取基准法
3.三分取中法
算法步骤:
1 )从数列中挑出一个元素,称为 “基准”(pivot),
2 )重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(Partition)操作。
3) 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码实现:1.固定位置选取基准法(递归版本)
//一次划分
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] > tmp)
{
high--;
}
if(high<=low)
{
break;
}
else
{
arr[low]=arr[high];
}
while(low<high && arr[low]<tmp)
{
low++;
}
if(low>=high)
{
break;
}
else
{
arr[high]=arr[low];
}
}
arr[low]=tmp;
return low;
}
void QuickSort(int *arr,int start,int end)
{
int par = Partion(arr,start,end);
if(par > start+1)
{
QuickSort(arr,start,par-1);
}
if(par < end-1)
{
QuickSort(arr,par+1,end);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[] = {20,3,5,0,32,22,11,8,7,9};
int len = sizeof(arr) / sizeof(arr[0]);
QuickSort(arr,0,len-1);
Show(arr,len);
return 0;
}
(非递归版本):将待排序列的下标存放在数组中,依次对下标所对应的序列 进行排序,直到数组为空。
代码实现:
//一次划分
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] > tmp)
{
high--;
}
if(high<=low)
{
break;
}
else
{
arr[low]=arr[high];
}
while(low<high && arr[low]<tmp)
{
low++;
}
if(low>=high)
{
break;
}
else
{
arr[high]=arr[low];
}
}
arr[low]=tmp;
return low;
}
//非递归实现快速排序
void QuickSort2(int *arr,int len)
{
int top=0;//数组下标
int low=0;
int high=len-1;
int size=log((double)len)/log((double)2);//log2n
int *SizeTmp=(int *)malloc(sizeof(int)*size*2);
assert(SizeTmp!=NULL);
int par=Partion(arr,low,high);//第一次找完基准
if(par > low+1)
{
SizeTmp[top++]=low;
SizeTmp[top++]=par-1;
}
if(par < high-1)
{
SizeTmp[top++]=par+1;
SizeTmp[top++]=high;
}
while(top > 0)//栈不为空
{
high=SizeTmp[--top];
low=SizeTmp[--top];
par=Partion(arr,low,high);
if(par > low+1)
{
SizeTmp[top++]=low;
SizeTmp[top++]=par-1;
}
if(par < high-1)
{
SizeTmp[top++]=par+1;
SizeTmp[top++]=high;
}
}
free(SizeTmp);
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[] = {20,3,5,0,32,22,11,8,7,9};
int len = sizeof(arr) / sizeof(arr[0]);
QuickSort2(arr,len);
Show(arr,len);
return 0;
}
2.随机选取基准法:
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
代码实现:
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] >= tmp)
{
high--;
}
if(low >= high)
{
break;
}
else
{
arr[low] = arr[high];
}
while(low < high && arr[low] <= tmp)
{
low++;
}
if(low >= high)
{
break;
}
else
{
arr[high] = arr[low];
}
}
arr[low] = tmp;
return low;
}
void Swap(int *arr,int low,int high)
{
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
void QuickSort(int *arr, int low, int high)
{
//排序方式2:随机选取基准法
srand((unsigned int)time(NULL));
Swap(arr,low,rand()%(high-low)+low);
int par = Partion(arr,low,high);
if(par > low+1)
{
QuickSort(arr,low,par-1);
}
if(par < high-1)
{
QuickSort(arr,par+1,high);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={21,1,4,15,31,10,6,7,19};
int len = sizeof(arr)/sizeof(arr[0]);
QuickSort(arr,0,len-1);
Show(arr,len);
return 0;
}
3.三分取中法:
引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴。
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态是可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。
代码实现:
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] >= tmp)
{
high--;
}
if(low >= high)
{
break;
}
else
{
arr[low] = arr[high];
}
while(low < high && arr[low] <= tmp)
{
low++;
}
if(low >= high)
{
break;
}
else
{
arr[high] = arr[low];
}
}
arr[low] = tmp;
return low;
}
void Swap(int *arr,int low,int high)
{
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
void SelectPivotMedianOfThree(int *arr,int low,int high)
{
int mid = (high-low)/2+low;//(high+low)>>1;
//arr[mid]<=arr[low]<=arr[high];
if(arr[mid] > arr[low])
{
Swap(arr,mid,low);
}//arr[mid] <= arr[low]
if(arr[low] > arr[high])
{
Swap(arr,low,high);
}//arr[low] <= arr[high]
if(arr[mid] > arr[high])
{
Swap(arr,mid,high);
}//arr[mid] <= arr[high]
}
void QuickSort(int *arr, int low, int high)
{
//三分取中法
SelectPivotMedianOfThree(arr,low,high);
int par = Partion(arr,low,high);
if(par > low+1)
{
QuickSort(arr,low,par-1);
}
if(par < high-1)
{
QuickSort(arr,par+1,high);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={21,1,4,15,31,10,6,7,19};
int len = sizeof(arr)/sizeof(arr[0]);
QuickSort(arr,0,len-1);
Show(arr,len);
return 0;
}
两种优化方式:
1.如果待排序序列,在排序过程当中适量变的很小时,直接插入排序
2.聚集相同基准法
1)如果待排序序列,在排序过程当中适量变的很小时,直接插入排序
代码实现:
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] >= tmp)
{
high--;
}
if(low >= high)
{
break;
}
else
{
arr[low] = arr[high];
}
while(low < high && arr[low] <= tmp)
{
low++;
}
if(low >= high)
{
break;
}
else
{
arr[high] = arr[low];
}
}
arr[low] = tmp;
return low;
}
void InsertSort(int *arr,int low,int high)
{
for(int i = low+1;i <= high;i++)
{
int tmp = arr[i];
int j = 0;
for(j = i-1;j >= low;j--)
{
if(arr[j] > tmp)
{
arr[j+1] = arr[j];
}
else
{
break;
}
}
arr[j+1] = tmp;
}
}
void QuickSort(int *arr, int low, int high)
{
//优化方式一
if((high-low)+1 < 100)
{
InsertSort(arr,low,high);
return;
}
int par = Partion(arr,low,high);
if(par > low+1)
{
QuickSort(arr,low,par-1);
}
if(par < high-1)
{
QuickSort(arr,par+1,high);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={21,1,4,15,31,10,6,7,19};
int len = sizeof(arr)/sizeof(arr[0]);
QuickSort(arr,0,len-1);
Show(arr,len);
return 0;
}
2)聚集相同基准法
在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
举例:
待排序序列 1 4 6 7 6 6 7 6 8 6
第一次得到par的值
转换后,待分割序列:6 4 6 7 1 6 7 6 8 6
基准key:6
本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6
下次的两个子序列为:1 4 6 和 7 6 7 6 8 6
本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7
下次的两个子序列为:1 4 和 7 8 7
代码实现:
int Partion(int *arr,int low,int high)
{
int tmp = arr[low];
while(low < high)
{
while(low < high && arr[high] >= tmp)
{
high--;
}
if(low >= high)
{
break;
}
else
{
arr[low] = arr[high];
}
while(low < high && arr[low] <= tmp)
{
low++;
}
if(low >= high)
{
break;
}
else
{
arr[high] = arr[low];
}
}
arr[low] = tmp;
return low;
}
void Swap(int *arr,int low,int high)
{
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
//聚集相同元素基准法
void FocusNumPar(int *arr,int low,int par,int high,int *left, int *right)
{
if(low < high)
{
int parLeft = par-1;
for(int i = par-1;i >= low;i--)
{
if(arr[i] == arr[par])
{
if(i != parLeft)
{
Swap(arr,i,parLeft);
parLeft--;
}
else
{
parLeft--;
}
}
}
*left = parLeft;
int parRight = par+1;
for(int i = par+1;i <= high;i++)
{
if(arr[i] == arr[par])
{
if(i != parRight)
{
Swap(arr,i,parRight);
parRight++;
}
else
{
parRight++;
}
}
}
*right = parRight;
}
}
void QuickSort(int *arr, int low, int high)
{
int par = Partion(arr,low,high);
int left = 0;
int right = 0;
//优化方式2:
FocusNumPar(arr,low,par,high,&left,&right);
if(left >= low+1)//说明左边有两个数据以上
{
QuickSort(arr,low,left);
}
if(right <= high-1)
{
QuickSort(arr,right,high);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[]={21,1,4,15,31,10,6,7,19};
int len = sizeof(arr)/sizeof(arr[0]);
QuickSort(arr,0,len-1);
Show(arr,len);
return 0;
}
时间复杂度:O(nlog2n)
空间复杂度:O(nlog2n)
稳定性:不稳定排序
注:每次可以均匀分割待排序序列,快速排序会越来越快。
六、堆排序
堆排序(HeapSort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个顺序存储的完全二叉树结构。并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
算法步骤:
1)建立大根堆
2)将堆首的元素与最后的元素互换。
3)把堆的尺寸缩小1,然后进行调整(即大根堆)。
4)重复步骤2和3,直至尺寸为1。
代码实现:
void Adjust(int *arr,int start,int end)
{
int tmp=arr[start];
for(int i = start*2+1;i <= end;i = 2*i+1)
{
if(i+1 <= end && arr[i] < arr[i+1])
{
i++;
}
if(arr[i] > arr[start])
{
arr[start] = arr[i];
start=i;
}
else
{
break;
}
arr[start]=tmp;
}
}
void HeapSort(int *arr,int len)
{
for(int i=(len-1)/2;i>=0;i--)
{
Adjust(arr,i,len-1);
}
for(int i = len-1;i > 0;i--)
{
int tmp=arr[i];
arr[i]=arr[0];
arr[0]=tmp;
Adjust(arr,0,i-1);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d\n",arr[i]);
}
}
int main()
{
int arr[]={12,34,45,21,4,6,33,53,15,5};
int len = sizeof(arr) / sizeof(arr[0]);
HeapSort(arr,len);
Show(arr,len);
return 0;
}
时间复杂度:O(nlog2n)
空间复杂度:O(1)
稳定性:不稳定排序
七、归并排序
归并排序(MergeSort)
算法步骤:
1) 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2) 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3) 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4) 重复步骤3直到某一指针达到序列尾
5) 将另一序列剩下的所有元素直接复制到合并序列尾
代码实现:
void Merge(int *arr,int len,int gap)
{
int *brr = (int *)malloc(sizeof(int) * len);
assert(brr != NULL);
int i = 0;//brr的下标
int start1 = 0;
int end1 = start1+gap-1;
int start2 = end1+1;
int end2 = start2+gap-1 < len-1 ? start2+gap-1 : len-1;
//当有两个归并段的时候
while(start2 < len)
{
//当两个归并段还没有比较完的时候
while(start1 <= end1 && start2<=end2)
{
if(arr[start1] <= arr[start2])
{
brr[i++] = arr[start1++];
}
else
{
brr[i++] = arr[start2++];
}
}
while(start1 <= end1)
{
brr[i++] = arr[start1++];
}
while(start2 <= end2)
{
brr[i++] = arr[start2++];
}
//找两个新的归并段
start1 = end2+1;
end1 = start1+gap-1;
start2 = end1+1;
end2 = start2+gap-1 < len-1?start2+gap-1:len-1;
}
while(start1 < len)
{
brr[i++] = arr[start1++];
}
for(int i = 0;i < len;i++)
{
arr[i] = brr[i];
}
}
void MergeSort(int *arr,int len)
{
for(int i = 1;i < len;i *= 2)
{
Merge(arr,len,i);
}
}
void Show(int *arr,int len)
{
for(int i = 0;i < len;i++)
{
printf("%d ",arr[i]);
}
printf("\n");
}
int main()
{
int arr[10000] = {};
int len = sizeof(arr)/sizeof(arr[0]);
srand(time(NULL));
for(int i = 0;i < len;i++)
{
arr[i] = rand()%10000;
}
MergeSort(arr,len);
Show(arr,len);
return 0;
}
时间复杂度:O(nlog2n)
空间复杂度:O(n)
稳定性:稳定排序