排序
信息获取后通常需要进行处理,处理后的信息其目的是便于人们的应用。信息处理方法有多种,通常有数据的排序,查找,插入,删除,归并等操作。读者已经接触了一些这方面的知识,本章重点介绍数据排序的几种方法。
1. 选择排序
基本思想:每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在待排序的数列的最前,直到全部待排序的数据元素排完。
【程序实现】
for (i=1;i<=n;i++) //i控制当前序列中最小值存放的数据位置
{
for (j=i+1;j<=n;j++) //在当前无序区a[i..n]中选最小的元素a[i]
if (a[j]<a[i]) swap(a[i],a[j]);
交换a[i]和a[j],将当前最小值放到a[i]位置
}
2. 冒泡排序
冒泡排序的思想:以n个人站队为例,从第1个开始,依次比较相邻的两个是否逆序对(高在前,矮在后),若逆序就交换这两人,直到n-1和n比较,经过一轮比较后,则把最高的人排到最后,即将最高的人像冒泡一样逐步冒到相应的位置。如此,进行n-1轮后,队列为有序的队列。
【程序实现】
for(i=n-1; i>=1; i--) 进行n-1轮冒泡
for(j=1; j<=i; j++) 每轮进行i次的比较
if(a[j]>a[j+1]) 相邻两个元素比较,若逆序就交换
swap(a[j], a[j+1]); 交换
3. 插入排序
插入排序思想:当读入一个元素时,在已经排序好的序列中,搜寻它正确的位置,再放入读入的元素。
【程序实现】
for(i=1; i<=n; i++)
{
Scanf(“%d”,&p);
for(j=1; j<i; j++) 在前面有序区间中为a[i]找合适的插入位置
if (p>a[J]) break; 找到比a[i]小的位置就退出,插入其后 for(k=I+1;k>=J;k--) 将比a[i]大的数据向后移
a[k]=a[k-1]; 将a[i]放在正确位置上
A[J]=P;
}
4. 桶排序
桶排序的思想是若待排序的值在一个明显有限范围内(整型)时,可设计有限个有序桶,待排序的值装入对应的桶(当然也可以装入若干个值),桶号就是待排序的值,顺序输出各桶的值,将得到有序的序列。
【程序实现】
for (i=1;i<=n;i++)
cin>>k; b[k]++; //将等于k的值全部装入第k桶中
for (i=0;i<=100;i++) //输出排序结果
{ while (b[i]) cout<<i<<""; b[i]--; }
5.快速排序
快速排序是对冒泡排序的一种改进。它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
假设待排序的序列为{a[L],a[L+1],a[L+2],……,a[R]},首先任意选取一个记录(通常可选中间一个记作为枢轴或支点),然后重新排列其余记录,将所有关键字小于它的记录都放在左子序列中,所有关键字大于它的记录都放在右子序列中。由此可以将该“支点”记录所在的位置mid作分界线,将序列分割成两个子序列和。这个过程称作一趟快速排序(或一次划分)。
一趟快速排序的具体做法是:附设两个指针i和j,它们的初值分别为L和R,设枢轴记录取mid,则首先从j所指位置起向前搜索找到第一个关键字小于的mid的记录,然后从i所指位置起向后搜索,找到第一个关键字大于mid的记录,将它们互相交换,重复这两步直至i>j为止。
快速排序的时间的复杂性是O(nlog2n),速度快,但它是不稳定的排序方法。就平均时间而言,快速排序是目前被认为是最好的一种内部排序方法
由以上讨论可知,从时间上看,快速排序的平均性能优于前面讨论过的各种排序方法,但快速排序需一个栈空间来实现递归。若每一趟排序都将记录序列均匀地分割成长度相接近的两个子序列,则栈的最大深度为log(n+1)。
【程序实现】
void qsort(int l,int r)
{
int i,j,mid,p;
i=l; j=r;
mid=a[(l+r) / 2]; 将当前序列在中间位置的数定义为分隔数
do
{
while (a[i]<mid) i++; 在左半部分寻找比中间数大的数
while (a[j]>mid) j--; 在右半部分寻找比中间数小的数
if (i<=j)
Swap(a[i],a[j]),i++,j--;
若找到一组与排序目标不一致的数对则交换它们,继续找
}while (i<=j);
if (l<j) qsort(l,j); 注意这里不能有等号
if (i<r) qsort(i,r); 若未到两个数的边界,则递归搜索左右区间
}
6.归并排序
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
合并过程为:比较a[i]和a[j]的大小,若a[i]≤a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令i和k分别加上1;否则将第二个有序表中的元素a[j]复制到r[k]中,并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。
【程序实现】
void msort(int s,int t)
{
if(s==t) return; 如果只有一个数字则返回,无须排序
int mid=(s+t)/2;
msort(s,mid),msort(mid+1,t); 分解左右序列
int i=s, j=mid+1, k=s; 接下来合并
while(i<=mid && j<=t)
{
if(a[i]<=a[j]) r[k++]=a[i++];
Else r[k++]=a[j++],ans+=mid-i+1; 统计产生逆序对的数量
}
while(i<=mid) r[k++]=a[i++];
while(j<=t) r[k++]=a[j++]; 复制左右边子序列剩余
for(int i=s; i<=t; i++) a[i]=r[i];
}
归并排序的时间复杂度是O(nlogn),速度快。同时,归并排序是稳定的排序。即相等的元素的顺序不会改变。
7.各种排序算法的比较
1.稳定性比较
插入排序、冒泡排序、二叉树排序、二路归并排序及其他线形排序是稳定的。选择排序、希尔排序、快速排序、堆排序是不稳定的。
2.时间复杂性比较
插入排序、冒泡排序、选择排序的时间复杂性为O(n2);快速排序、堆排序、归并排序的时间复杂性为O(nlog2n);桶排序的时间复杂性为O(n);若从最好情况考虑,则直接插入排序和冒泡排序的时间复杂度最好,为O(n),其它算法的最好情况同平均情况相同;若从最坏情况考虑,则快速排序的时间复杂度为O(n2),直接插入排序和冒泡排序虽然平均情况相同,但系数大约增加一倍,所以运行速度将降低一半,最坏情况对直接选择排序、堆排序和归并排序影响不大。
由此可知,在最好情况下,直接插入排序和冒泡排序最快;在平均情况下,快速排序最快;在最坏情况下,堆排序和归并排序最快。
3.辅助空间的比较
桶排序、二路归并排序的辅助空间为O(n),快速排序的辅助空间为O(log2n),最坏情况为O(n),其它排序的辅助空间为O(1);
4.其它比较
插入、冒泡排序的速度较慢,但参加排序的序列局部或整体有序时,这种排序能达到较快的速度。反而在这种情况下,快速排序反而慢了。
当n较小时,对稳定性不作要求时宜用选择排序,对稳定性有要求时宜用插入或冒泡排序。
若待排序的记录的关键字在一个明显有限范围内时,且空间允许是用桶排序。
当n较大时,关键字元素比较随机,对稳定性没要求宜用快速排序。
当n较大时,关键字元素可能出现本身是有序的,对稳定性没有要求时宜用堆排序
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。
【总结】:
- 考试尽量用STL的快速排序
- 归并排序需要掌握,解决部分问题
- 归并排序注意最后的数组间a,b的赋值
- 计算逆序对的时候只需要进行nlogn的归并排序即可
- 对于原本已经是有序的两个集合,使用归并
- 桶排序的时候,非常小心时空复杂度
感谢各位与信奥一本通的鼎力相助!