基本的排序算法分为内排序和外排序,内排序:数据在内存中存储。外排序:数据在外存中存储。在本篇博客中,我们重点来细说下内排序。
内排序分为五大类,基本的有八种:
一、插入排序。
1)、直接插入排序。
算法:将待排序的数组分为两大部分,已排序部分和待排序部分,每次从待排序部分拿出一个元素,在已排序部分找到合适的位置插入元素。
以如下数组为例{12,15,9,20,6,31,24},红线左边为已排序序列,右边为待排序序列,蓝色方框标识从待排序序列拿出来的元素。
代码实现如下:
void Insertsort(int *arr,int len)
{
int tmp=0;
int j=0;
for(int i=1;i<len ;i++)
{
tmp=arr[i];
for(j=i-1;j>=0&&arr[j]>tmp;j--)
{
arr[j+1]=arr[j];
}
arr[j+1]=tmp;
}
}
时间复杂度分析:
(1)顺序排列时,只需比较(n-1)次,插入排序时间复杂度为O(n);
(2)逆序排序时,需比较n(n-1)/2次,插入排序时间复杂度为O(n^2);
(3)当原始序列杂乱无序时,平均时间复杂度为O(n^2)。
空间复杂度分析:
插入排序过程中,需要一个临时变量temp存储待排序元素,因此空间复杂度为O(1)。
算法稳定性分析:
插入排序是一种稳定的排序算法。
2)、希尔(shell)排序。
算法:先将 整个待排序的元素序列分割成若干子序列,分别进行直接插入排序待整个序列中的元素“基本有序”后,再对全体元素进行直接插入排序。
以如下数组为例{232,4,25,16,67,87,1,54,34}:
(1)、先将数组按间隔4个元素分为若干子序列,每种颜色代表一个子序列。
(2)、对每个子序列进行直接插入排序,再按间隔为3个元素分成若干子序列。
(3)、对每个子序列进行直接插入排序,再按间隔为2个元素分成若干个子序列。
(4)、对每个子序列进行直接插入排序,再按间隔为1个元素分成若干个子序列。
(5)、最后再对整个序列进行直接插入排序。
代码如下:
void shell(int *arr,int len,int dk)
{
int i=dk;
int j=i-dk;
int tmp=0;
for(i;i<len;i++)
{
tmp=arr[i];
for(j=i-dk;j>=0&&arr[j]>tmp;j-=dk)
{
arr[j+dk]=arr[j];
}
arr[j+dk]=tmp;
}
}
void shellsort(int *arr,int len,int *dk,int dlen)
{
for(int i=0;i<dlen;i++)
{
Hill(arr,len,dk[i]);
}
}
int main()
{
int arr[]={232,4,25,16,67,87,1,54,34};
int len = sizeof(arr)/sizeof(arr[0]);
int dk[]={5,3,1};
int dlen=sizeof(dk)/sizeof(dk[0]);
shellsort(arr,len,dk,dlen);
return 0;
}
时间复杂度分析:
1)、增量序列的选择
shell排序的执行时间依赖于增量序列。
好的增量序列的共同特征:
① 最后一个增量必须为1;
② 应该尽量避免序列中的值(尤其是相邻的值)互为倍数的情况。
有人通过大量的实验,给出了较好的结果:当n较大时,比较和移动的次数约在nl.25到1.6n1.25之间。
2)、Shell排序的时间性能优于直接插入排序
希尔排序的时间性能优于直接插入排序的原因:
①当文件初态基本有序时直接插入排序所需的比较和移动次数均较少。
②当n值较小时,n和 n^2 的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度O(n^2)差别不大。
③在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。
因此,希尔排序在效率上较直接插入排序有较大的改进。
shell排序是不稳定的。
二、选择排序。
1)、简单选择排序。
算法:每次从待排序元素中找到最大(最小)元素与待排序数组的第一个元素交换。
以如下数组为例{232,4,25,16,67,87,1,54,34}:每趟排序找最小元素与前面元素进行交换。
代码实现如下:
void SimpleSelect(int *arr,int len)
{
int min=0;
int tmp=0;
for(int i=0;i<len-1;i++)
{
min=i;
for(int j=i+1;j<len;j++)
{
if(arr[j]<arr[min])
{
min=j;
}
}
tmp=arr[i];
arr[i]=arr[min];
arr[min]=tmp;
}
}
时间复杂度分析:
简单选择排序的比较次数与序列的初始排序无关。 假设待排序的序列有 n 个元素,则比较次数总数为n(n-1)/2.而移动次数与序列的初始排序有关。当序列正序时,移动次数最少,为 0.当序列反序时,移动次数最多,为3n(n-1)/2.
所以,综合以上,简单排序的时间复杂度为O(n^2)。
空间复杂度分析:
简单选择排序需要占用1个临时空间,在交换数值时使用。空间复杂度为0(1).
稳定性:不稳定。
2)、堆排序
算法:先将待排序序列排成一个大顶堆(小顶堆),每次取出堆顶元素与最后一个叶子节点进行交换,将len--;(len为堆中元素个数)。
以如下数组为例{232,4,25,16,67,87,1,54,34}:先排大顶堆。(大顶堆:跟结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大根堆,又称最大堆(大顶堆)。大根堆要求根节点的关键字既大于或等于左子树的关键字值,又大于或等于右子树的关键字值。),再将堆顶元素与最后一个叶子节点进行交换,缩小大顶堆,再次调整大顶堆,重复以上操作。
大顶堆的排序方法:
1)、先排一个大顶堆。
2)、将堆顶元素与最后一个叶子节点进行交换,缩小堆元素的个数(len--);
3)、再次对大顶堆进行调整。
4)、重复2到3的操作,直到堆中剩余一个元素为止。
代码如下所示:
void heap(int *arr,int len,int i)
{
int tmp=0;
int j=2*i+1;
if(j<len-1&&arr[j]<arr[j+1])j++;
if(j>len-1)
{
return;
}
if(arr[j]>arr[i])
{
tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
if(2*j+1<=len-1)
{
heap(arr,len,j);
}
}
void Heapsort(int *arr,int len)
{
int tmp=0;
int j=len-1;
for(int i=len/2-1;i>=0;i--)
{
heap(arr,len,i);
}
while(j>=0)
{
tmp=arr[0];
arr[0]=arr[j];
arr[j--]=tmp;
heap(arr,j,0);
}
}
时间复杂度分析
堆的存储表示是顺序的。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。当想得到一个序列中的k个最小的元素之前的部分排序序列,最好采用堆排序。因为堆排序的时间复杂度是O(n+k*log2n),若k≤n/log2n,则可得到的时间复杂度为O(n)。
平均情况下堆排序的时间复杂度为O(nlog2n),空间复杂度为O(1)。
稳定性:不稳定。因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。
三、交换排序
1)、冒泡排序
算法:冒泡排序也称为沉石排序,重复地遍历过要排序的元素列,依次比较两个相邻的元素,如果当前元素大于其后元素就把他们交换过来。遍历元素的工作是重复地进行,直到没有相邻元素需要交换,也就是说该元素已经排序完成。
以如下数组为例{232,4,25,16,67,87,1,54,34}:每一趟将最大的元素交换至最后。其排序结果如下:
代码分析如下:
void BubblingSort(int *arr,int len)
{
if(arr == NULL||len<=0)
{
return ;
}
for(int i = 0;i<len-1;i++)
{
for(int j = 1;j<len-i;j++)
{
if(arr[j]<arr[j-1])
{
int tmp = arr[j];
arr[j] = arr[j-1];
arr[j-1] = tmp;
}
}
}
}
时间复杂度分析:
冒泡排序需要多次循环遍历排序序列,没找到一个最大的元素,就需要进行一次循环,如果有n个元素,其需要进行n次循环,所以其时间复杂度为O(n)。
稳定性:冒泡排序并没有跨越式的交换元素,不会将前后相同的元素位置颠倒,所以冒泡排序是一种稳定的排序算法。
2)、快速排序
算法:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
以如下数组为例{232,4,25,16,67,87,1,54,34}:
代码分析:
void Fast(int *arr,int left,int right)
{
if(arr == NULL||left >= right)
{
return ;
}
int base = arr[left];
int i = left;
int j = right;
while(i<j)
{
while(i<j&&arr[j]>base)
j--;
arr[i] = arr[j];
while(i<j&&arr[i]<base)
i++;
arr[j] = arr[i];
}
arr[i] = base;
Fast(arr,left,i-1);
Fast(arr,i+1,right);
}
void FastSort(int *arr,int len)
{
Fast(arr,0,len-1);
}
时间复杂度分析:
快速排序的最坏情况基于每次划分对基准元素的选择。基本的快速排序选取第一个元素作为基准元素。这样在数组已经有序的情况下,每次划分将得到最坏的结果。一种比较常见的优化方法是随机化算法,即随机选取一个元素作为基准。这种情况下虽然最坏情况仍然是O(n^2),但最坏情况不再依赖于输入数据,而是由于随机函数取值不佳。实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
随机化快速排序的唯一缺点在于,一旦输入数据中有很多的相同数据,随机化的效果将直接减弱。对于极限情况,即对于n个相同的数排序,随机化快速排序的时间复杂度将毫无疑问的降低到O(n^2)。解决方法是用一种方法进行扫描,使没有交换的情况下基准元素保留在原位置。
空间复杂度为O(1).
稳定性:快速排序并会产生跨越式的交换元素,会将前后相同的元素位置颠倒,所以快速排序是一种不稳定的排序算法。
四、归并排序。
算法:1). 从下往上的归并排序:将待排序的数列分成若干个长度为1的子数列,然后将这些数列两两合并;得到若干个长度为2的有序数列,再将这些数列两两合并;得到若干个长度为4的有序数列,再将它们两两合并;直接合并成一个数列为止。这样就得到了我们想要的排序结果。(参考下面的图片)
2). 从上往下的归并排序:它与"从下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 将当前区间一分为二,即求分裂点 mid = (low + high)/2;
② 求解 -- 递归地对两个子区间a[low...mid] 和 a[mid+1...high]进行归并排序。递归的终结条件是子区间长度为1。
③ 合并 -- 将已排序的两个子区间a[low...mid]和 a[mid+1...high]归并为一个有序的区间a[low...high]。
以如下数组为例{232,4,25,16,67,87,1,54,34}:
代码分析如下:
void Merge(int *arr,int start,int mid ,int end,int *tmp)
{
int i=start;
int j=mid+1;
int k=0;
while(i<=mid&&j<=end)
{
tmp[k++]=arr[i]<arr[j]?arr[i++]:arr[j++];
}
while(i<=mid)
{
tmp[k++]=arr[i++];
}
while(j<=end)
{
tmp[k++]=arr[j++];
}
for(int i=0;i<k;i++)
{
arr[start+i]=tmp[i];
}
}
void Divide(int *arr,int start,int end,int *tmp)
{
if(start==end)
{
return ;
}
int mid=(start+end)/2;
Divide(arr,start,mid,tmp);
Divide(arr,mid+1,end,tmp);
Merge(arr,start,mid,end,tmp);
}
void Mergesort(int *arr,int len)
{
int *tmp=(int *)malloc(sizeof(int)*len);
Divide(arr,0,len-1,tmp);
free(tmp);
}
时间复杂度分析:
从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
空间复杂的分析:
因为归并排序需要申请一个与原排序数组相同的小的数组来存储排序的元素,所以其空间复杂度为O(n).
稳定性分析:归并排序并不会跨越式的交换数据,所以不会将后面出现的相同元素交换到前面去。所以归并排序是一个稳定的排序算法。
五、基数排序。
算法:基数排序又称“桶排序”,属于分配式排序。以十进制的数字元素为例,将根据整数的最右边数字将其扔进相应的0~9号的篮子里,对于相同的数字要保持其原来的相对顺序(确保排序算法的稳定性),然后将篮子里的数按照先后入桶的顺序串起来,然后再进行第二趟的收集(按照第二位的数字进行收集),就这样不断的反复,当没有更多的位时,串起来的数字就是排好序的数字。
以如下数组为例{232,4,25,16,67,87,1,54,34}:
代码如下:
//得到数字i的第d位数
int get_place_number(int i,int d)
{
while((d--)-1)
{
i/=10;
}
return i%10;
}
void Base(int *arr,int len,int max_digit)
{
//创建10个空桶,用0初始化
int **bucket = (int **)malloc(10*sizeof(int *));
for(int i = 0;i<10;i++)
{
bucket[i] = (int *)calloc(4,len);
}
int bucket_number[10] = {0}; //保存桶中的元素个数
for(int digit = 1;digit <= max_digit;digit++)
{
//将数组元素依次入桶
for(int i = 0;i<len;i++)
{
int which_bucket = get_place_number(arr[i],digit);
bucket[which_bucket][bucket_number[which_bucket]++] = arr[i];
}
//将桶中元素重新放入数组中
int arr_index = 0;
for(int i = 0;i<10;i++)
{
for(int j = 0;j<bucket_number[i];j++)
{
arr[arr_index++] = bucket[i][j];
}
}
//将桶置空
for(int i = 0;i<10;i++)
{
bucket_number[i] = 0;
for(int j = 0;j<len;j++)
{
bucket[i][j] = 0;
}
}
}
//释放资源
for(int i = 0;i<10;i++)
{
free(bucket[i]);
}
free(bucket);
}
void Basesort(int *arr,int len)
{
if(arr == NULL || len<1)
{
return;
}
int max_value = arr[0];
for(int i = 1;i<len;i++)
{
if(arr[i]>max_value)
{
max_value = arr[i];
}
}
int max_digit = 1;
while(max_value/=10)max_digit++;
Base(arr,len,max_digit);
时间复杂度分析:
基数排序的循环趟数与最大数字的位数(digit)有关,其时间复杂度为O(n*digit).
空间复杂度分析:
基数排序需要开辟桶空间来存放数据,每个桶的大小与待排序序列的元素个数有关,所以其空间复杂度为O(n)。
稳定性分析:
基数排序并没有跨越式的交换数据,入桶的顺序也是依次进行,不会将后出现的相同元素交换至前面,所以基数排序是一种稳定的排序算法。