数据结构:排序算法
禁止码迷,布布扣,豌豆代理,码农教程,爱码网等第三方爬虫网站爬取!
希尔排序
希尔排序又称缩小增量排序,输入插入类排序。当排序的记录个数较少且待排序序列的关键字基本有序时,算法的效率较高,它从“减少记录个数”和“序列基本有序”两个方面进行了改进。
算法思想
希尔排序实质上是采用分組插入的方法,先将整个待排序记录序列分割成 n 组,从而减少参与直接插人排序的数据量,对每组分别进行直接插入排序。然后增加每组的数据量重新分组,这样经过几次分组排序后,整个序列中的记录“基本有序”时再吋全体记录进行一次直接插入排序。希尔排序对记录的分组,不是简单地“逐段分割”,而是将相隔某个“增量”的记录分成一组。
排序流程
已知待排序记录的关键字序列为 {49,38,65,97,76,13,27,49 ,55,04},增量为 5,3,1。
代码实现
public static void shell_sort(int array[],int lenth)
{
int temp = 0;
int incre = lench; //增量
while(1)
{
incre = incre / 2;
/*根据增量分为若干个子序列*/
for(int k = 0; k < incre; k++)
{
/*对每组子序列进行插入排序*/
for(int i = k + incre; i < lenth; i += incre)
{
/*单个子序列插入排序*/
for(int j = i; j > k; j -= incre)
{
if(array[j] < array[j - incre])
{
temp = array[j - incre];
array[j - incre] = array[j];
array[j] = temp;
}
else
{
break;
}
}
}
}
/*增量为 1,即对整个序列进行插入排序后*/
if(incre == 1)
{
break;
}
}
}
算法分析
时空复杂度
当增量大于 1 时,关键字较小的记录就不是一步一步地挪动,而是跳跃式地移动,从而使得在进行最后一趟增量为 1 的插入排序中,序列已基本有序,只要做记录的少量比较和移动即可完成排序,因此希尔排序的时间复杂度较直接插入排序低。但要具体进行分析,则是一个复杂的问题,因为希尔排序的时间复杂度是所取 “增量” 序列的函数,这涉及一些数学上尚未解决的难题。
从空间来看, 希尔排序和前面两种排序方法一样,也只需要一个辅助空间 r[O], 空间复杂度为 O(1)。
算法特点
- 记录跳跃式地移动导致排序方法是不稳定的。
- 只能用于顺序结构,不能用于链式结构。
- 增量序列可以有各种取法,但应该使增量序列中的值没有除 l 之外的公因子,并且最后一个增量值必须等于 1。
- 记录总的比较次数和移动次数都比直接插入排序要少,n 越大时效果越明显,所以适合初始记录无序、n 较大时的情况。
快速排序
在冒泡排序过程中,只对相邻的两个记录进行比较,因此每次交换两个相邻记录时只能消除一个逆序。如果能通过两个(不相邻)记录的一次交换,消除多个逆序,则会大大加快排序的速度。**快速排序 (Quick Sort) **是由冒泡排序改进而得的,快速排序方法中的一次交换可能消除多个逆序。
算法思想
在待排序的 n 个记录中任取一个记录(通常取第一个记录)作为枢轴(或支点),设其关键字为 pivotkey。经过一趟排序后,把所有关键字小于 pivotkey 的记录交换到前面,把所有关键字大于 pivotkey 的记录交换到后面,结果将待排序记录分成两个子表,最后将枢轴放置在分界处的位置。然后分别对左、右子表重复上述过程,直至每一子表只有一个记录时,排序完成。
排序流程
待排序记录的关键字序列为 {49,38,6 5,97,76,13,27,49},首先关注一趟排序。选择待排序表中的第一个记录作为枢轴,将枢轴记录暂存,附设两个指针 low 和 high, 初始时分别指向表的下界和上界。简而言之就是以枢轴为基准,从右边找小的数,从左边找大的数进行数字替换,最后指针交汇的地方填入枢轴记录。
重复对上一趟排序的所有子序列进行快速排序,最终就可以得到有序序列,以下是完整流程。
代码实现
public static void quickSort(int array[],int low,int high)
{
int i = low;
int j = high;
int pivotkey = array[low]; //选择第一个数为 pivotkey
if(low >= high)
{
return;
}
while(i < j)
{
/*从右向左找第一个小于 pivotkey 的值*/
while(i < j && array[j] >= pivotkey)
{
j--;
}
if(i < j)
{
array[i] = array[j];
i++;
}
/*从左向右找第一个大于 pivotkey 的值*/
while(i < j && array[i] < pivotkey)
{
i++;
}
if(i < j)
{
array[j] = array[i];
j--;
}
}
/*此时 i == j*/
array[i] = pivotkey;
quickSort(array, low, i - 1); //递归调用
quickSort(array, i + 1, high); //递归调用
}
算法分析
时空复杂度
从快速排序算法的递归树可知,快速排序的趟数取决于递归树的深度。最好情况为,每一趟排序后都能将记录序列均匀地分割成两个长度大致相等的子表,类似折半查找。最坏情况是,在待排序序列已经排好序的情况下,其递归树成为单支树,每次划分只得到一个比上一次少一个记录的子序列。理论上平均情况下,快速排序的时间复杂度为 O(nlog2 n)。
对于空间复杂度而言,快速排序是递归的,执行时需要有一个栈来存放相应的数据。最大递归调用次数与递归树的深度一致,所以最好情况下的空间复杂度为 O(log2 n),最坏情况下为 O(n)。
算法特点
- 记录非顺次的移动导致排序方法是不稳定的。
- 排序过程中需要定位表的下界和上界,所以适合用于顺序结构,很难用于链式结构。
- 当 n 较大时,在平均情况下快速排序是所有内部排序方法中速度最快的一种,所以其适合初始记录无序、n 较大时的情况。
堆排序
堆排序建立在堆结构的基础上实现,左转博客堆、优先级队列和堆排序。
归并排序
归井排序(Merging Sort) 就是将两个或两个以上的有序表合并成一个有序表,将两个有序表合并成一个有序表的过程称为 2 路归并。
算法思想
假设初始序列含有 n 个记录,则可看成是 n 个有序的子序列,每个子序列的长度为 1 然后两两归并,得到n/2 个长度为 2 或 1 的有序子序列,再进行两两归并,如此重复直至得到一个长度为 n 的有序序列为止。
排序流程
待排序记录的关键字序列为 {49,38,65,97,76,13,27},不断将待排序序列中前后相邻的两个有序序列归并为一个有序序列。
代码实现
public static void merge_sort(int array[],int first,int last)
{
if(first < last)
{
int middle = (first + last) / 2;
merge_sort(array,first,middle); //左半部分排好序
merge_sort(array,middle + 1,last); //右半部分排好序
mergeArray(array,first,middle,last); //合并左右部分
}
}
public static void mergeArray(int array[],int first,int middle,int end)
{
int i = first;
int j = middle + 1;
int k = 0;
int temp[] = new int[array.length];
/*将序列分为两个子序列,二路归并*/
while(i <= m && j <= n)
{
if(array[i] <= array[j])
{
temp[k] = array[i];
k++;
i++;
}
else
{
temp[k] = array[j];
k++;
j++;
}
}
while(i <= middle)
{
temp[k] = array[i];
k++;
i++;
}
while(j <= end)
{
temp[k] = array[j];
k++;
j++;
}
/*将排序后的序列拷贝回 array*/
for(int i = 0;i < k;i++)
{
array[first + i] = temp[i];
}
}
算法分析
时空复杂度
当有 n 个记录时需进行 log2n 趟归并排序,每一趟归并的关键字比较次数不超过 n,元素移动次数都是 n, 因此归并排序的时间复杂度为 O(nlog2n)。用顺序表实现归并排序时,需要和待排序记录个数相等的辅助存储空间,所以空间复杂度为O(n)。
算法特点
- 归并排序是稳定排序。
- 可用于链式结构,且不需要附加存储空间,但递归实现时仍需要开辟相应的递归工作栈。
基数排序
前述各类排序方法都是建立在关键字比较的基础上,而分配类排序不需要比较关键字的大小,它是根据关键字中各位的值, 通过对待排序记录进行若干趟 “ 分配 ” 与 “收集” 来实现排序的,是一种借助于多关键字排序的思想对单关键字排序的方法。
算法思想
基数排序的是借助 “分配” 和 “收集” 两种操作对单逻辑关键字进行排序的一种内部排序方法,有的逻辑关键字可以看成由若干个关键字复合而成的。例如若关键字是数值,且其值都在 O ≤ K ≤ 999 范围内, 则可把每一个十进制数字看,故可以按照 “分配” 和 “收集” 的方法进行排序。
算法流程
算法具体实现时,一般采用链式基数排序。首先以链表存储 n 个待排记录,并令表头指针指向第一个记录,如图然后通过以下三趟 “分配” 和 “收集" 操作来完成排序。
第一趟分配对最低数位关键字(个位数)进行,改变记录的指针值将链表中的记录分配至 10 个链队列中去,每个队列中的记录关键字的个位数相等。
第二趟分配和第二趟收集是对十位数进行的,其过程和个位数相同。
第三趟分配和第三趟收集是对百位数进行的,至此排序完毕。
代码实现
public static void RadixSort(int array[],int n,int k,int r,int cnt[])
{
int temp[] = new int[array.length];
//n:序列的数字个数
//k:最大的位数
//r:基数 10
//cnt:存储 bin[i] 的个数
for(int i = 0, rtok = 1; i < k ; i++, rtok = rtok * r)
{
//初始化
for(int j = 0; j < r; j++)
{
cnt[j] = 0;
}
//计算每个箱子的数字个数
for(int j = 0; j < n; j++)
{
cnt[(array[j] / rtok) % r]++;
}
//cnt[j] 的个数修改为前 j 个箱子一共有几个数字
for(int j = 1; j < r; j++)
{
cnt[j] = cnt[j-1] + cnt[j];
}
for(int j = n - 1; j >= 0; j--)
{
cnt[(array[j] / rtok) % r]--;
temp[cnt[(array[j] / rtok) % r]] = A[j];
}
for(int j = 0; j < n; j++)
{
array[j] = temp[j];
}
}
}
算法分析
时空复杂度
对于 n 个记录(假设每个记录含 d 个关键字,每个关键字的取值范围为 rd 个值)进行链式基数排序时,每一趟分配的时间复杂度为 O(n),每一趟收集的时间复杂度为 O(rd),整个排序需进行 d 趟分配和收集,所以时间复杂度为 O(d(n + rd))。
空间复杂度而言,所需辅助空间为 2rd 个队列指针,另外由于需用链表做存储结构,则相对于其他以顺序结构存储记录的排序方法而言还增加了 n 个指针域的空间,所以空间复杂度为O(n + rd)。
算法特点
- 基数排序是稳定排序。
- 用于链式结构,也可用于顺序结构。
- 时间复杂度可以突破基千关键字比较一类方法的下界 O(nlog2n),达到 O(n)。
- 基数排序使用条件有严格的要求:需要知道各级关键字的主次关系和各级关键字的取值范围。
总结
总的来看,各种排序算法各有优缺点,没有哪一种是绝对最优的。在使用时需根据不同情况适当选用,甚至可将多种方法结合起来使用。一般综合考虑以下因素:
- 待排序的记录个数;
- 记录本身的大小;
- 关键字的结构及初始状态;
- 对排序稳定性的要求;
- 存储结构。
对于先进的排序方法,从平均时间性能而言快速排序最佳,是目前基于比较的排序方法中最好的方法。但在最坏情况下,即当关键字基本有序时,快速排序的递归深度为 n, 时间复杂度为 O(n^2) 空间复杂度为 O(n)。堆排序和归并排序不会出现快速排序的最坏情况,但归并排序的辅助空间较大。当 n 较大时,算法具体选用的原则:
- 当关键字分布随机,稳定性不做要求时,可采用快速排序;
- 当关键字基本有序,稳定性不做要求时,可采用堆排序;
- 当关键字基本有序,内存允许且要求排序稳定时,可采用归并排序。
可以将简单的排序方法和先进的排序方法结合使用。 例如当 n 较大时,可以先将待排序序列划分成若干子序列分别进行直接插入排序。然后再利用归并排序,将有序子序列合并成一个完整的有序序列。或者在快速排序中,当划分子区间的长度小于某值时,可以转而调用直接插入排序算法。
参考资料
菜鸟教程排序算法总结
堆、优先级队列和堆排序
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社