数据排序
信息处理方法有多种,通常有数据的排序、查找、插入、删除、归并等操作。
下面重点介绍几种排序的方法:
1. 选择排序
(1)基本思想:每一趟从待排序的数据元素中选出最小(或最大)的一个元素,顺序放在待排序的数列的最前,直到全部待排序的数据元素排完。</br>
(2)排序过程:</br>
初始关键字 \[49 38 65 97 76 13 27 49] </br>
第一趟排序后 13 \[38 65 97 76 49 27 49] </br>
第二趟排序后 13 27 \[65 97 76 49 38 49] </br>
第三趟排序后 13 27 38 \[97 76 49 65 49] </br>
第四越排序后 13 27 38 49 \[76 97 65 49] </br>
第五趟排序后 13 27 38 49 49 \[97 65 76] </br>
第六趟排序后 13 27 38 49 49 65 \[97 76] </br>
第七趟排序后 13 27 38 49 49 65 76 \[97] </br>
最后排序结果 13 27 38 49 49 65 76 97 </br>
案例1. 输入n个数,将n个数按从小到大的顺序输出(n<= 10000)。
【输入样例】:
8
49 38 65 97 76 13 27 49
【输出样例】:
13 27 38 49 49 65 76 97
#include<iostream>
using namespace std;
int main(){
int n,k;
float temp,a[1005];
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){ //i 控制当前序列中最小值存放的数据位置
k=i;
for(int j=i+1;j<=n;j++){ //在当前无序区a[i..n]中选最小的元素a[k]
if(a[k]>a[j]) k=j;
if(k!=i){ //交换 a[i]和 a[k],将当前最小值放到 a[i]位置
temp=a[i];
a[i]=a[k];
a[k]=temp;
}
}
}
for(int i=1;i<=n;i++) cout<<a[i]<<" ";
return 0;
}
2.冒泡排序
冒泡排序的思想:以n个人站队为例,从第1个开始,依次比较相邻的两个是否逆序对(高在前 ,矮在后),若逆序就交换这两人,即第1个和第2个比,若逆序就交换两人,接着第2个和第3个比,若逆序就交换两人,接着第3个和第4个比,若逆序就交换两人 , …… ,直到n-1和n比较,经过一轮比较后,则把最高的人排到最后,即将最高的人像冒泡一样逐步冒到相应的位置。 原n个人的排序问题,转换为n-1个人的排序问题。</br>
第二轮从第1个开 始,依次比较相邻的两个人是否逆序对,若逆序就交换两人,直到n-2和n-1比较。如此,进行n-1轮后,队列为有序的队列。</br>
从上述分析中可以看出,每进行一轮 的比较之后 ,n个数的排序规模就转化为n-1个数的排序规模。</br>
【步骤】:
①读入数据存放在a数组中。
②比较相邻的前后两个数据,如果前面数据大于后面的数据,就将两个数据交换。
③对数组的第0个数据到n-1个数据进行一次遍历后,最大的一个数据就“冒”到数组第n-1个位置。
④n= n-1,如果n不为0就重复前面二步,否则排序完成。
程序实现方法:用两层循环完成算法,外层循环i控制每轮要进行多少次的比较,第1轮比较n-1次,第2轮比较n-2次,……,最后一轮比较1次。 内层循环j控制每轮i次比较相邻
两个元素是否逆序,若逆序就交换这两个元素。
程序如下:
#include<iostream>
using namespace std;
int a[1005];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n-1;i++){ //进行n-1轮冒泡
for(int j=1;j<=n-i;j++){
if(a[j]>a[j+1]) //相邻两个元素比较,若逆序就交换
swap(a[j],a[j+1]);
}
}
for(int i=1;i<=n;i++) cout<<a[i]<<" ";
return 0;
}
改进的冒泡排序:
对于有些数据.我们发现,不一定要n-1次才能排完。 例如1 5 2 3 4 6,我们发现只需一趟排序就可以将整个序列排完,于是,我们可以设置一个布尔变量,判断是否有
进行交换,如果没有交换,说明已经排序完成,进而减少几趟排序。
bool ok;
for(int i=1;i<=n-1;i++){
ok=true; //判断是否有交换
for(int j=1;j<=n-i;j++){
if(a[j]>a[j+1]){
swap(a[j],a[j+1]);
ok=false;
}
}
if(ok==true) break; //没有交换就退出
}
3. 插入排序
插入排序思想:回忆一下打牌时抓牌的情景,为了方便打牌,抓牌时一般一边抓牌一按花色和大小插入恰当的位置,当抓完所有的牌时,手中的牌便是有序的,这种排序方法即 插入排序。</br>
当读入一个元素时,在已经排好序的序列中,搜寻它的正确位置,再放入读入的元素。但不该忽略一个重要的问题:在插入这个元素前,应当先将它后面的所有元素后移一位,以保证插入位置的原元素不被覆盖。</br>
假设:设n=8,数组a中8个元素是:36 25 48 12 65 43 20 58,执行插入排序程序后,其数据变动情况:</br>
第0步:[36] 25 48 12 65 43 20 58 </br>
第1步:[++25++ 36] 48 12 65 43 20 58 </br>
第2步:[25 36 ++48++] 12 65 43 20 58 </br>
第3步:[++12++ 25 36 48] 65 43 20 58 </br>
第4步:[12 25 36 48 ++65++] 43 20 58 </br>
第5步:[12 25 36 ++43++ 48 65] 20 58 </br>
第6步:[12 ++20++ 25 36 43 48 65] 58 </br>
第7步:[12 20 25 36 43 48 ++58++ 65]</br> </br>
程序如下:
#include<iostream>
using namespace std;
int a[1005];
int main(){
int n,i,j,k;
cin>>n;
for(i=1;i<=n;i++) cin>>a[i];
for(i=2;i<=n;i++){
k=a[i];
for(j=i-1;j>=1;j--){ //在前面有序区间中为 a[i]找合适的插入位置
if(a[j] > k) a[j+1]=a[j]; //找到比a[i]小
else break;
}
a[j+1]=k;
}
for(i=1;i<=n;i++) cout<<a[i]<<" ";
return 0;
}
4.快速排序
快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小。则可分别对这两部分记录继续进行排序,以达到整个序列有序。</br>
假设待排序的序列为{a\[l],a\[l+1],a\[l+2],… ,a\[r]},首先任意选取一个记录(通常可选中间一个记作为为枢轴或支点),然后重新排列其余记录,将所有关键字小于它的记录都放在左子序列中,所有关键字大于它的记录都放在右子序列中。 由此可以将该“支点 ”记录所在的位置mid作分界线,将序列分割成两个子序列和。这个过程称作一趟快速排序(或一次划分)。</br>
【分析】一趟快速排序的具体做法是:附设两个指针i和j,它们的初值分别为l和r,设枢轴记录取mid,则首先从j所指位置起向前搜索找到第一个关键字小于mid的记录,
然后从i所指位置起向后搜索,找到第一个关键字大于mid的记录,将它们互相交换,重复这两步直至i>j为止。
程序如下:
#include<iostream>
using namespace std;
int a[1005];
void qsort(int, int);
int main(){
int n,i,j,k;
cin>>n;
for(i=1;i<=n;i++) cin>>a[i];
qsort(1,n);
for(i=1;i<=n;i++) cout<<a[i]<<" ";
return 0;
}
void qsort(int l, int r){
int i,j,mid,p;
i=l; j=r;
mid=a[(i+j)/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);
}
快速排序的时间的复杂性是O(nlog2n),速度快,但它是不稳定的排序方法。就平均时间而言,快速排序是目前被认为是最好的一种内部排序方法。
由以上讨论可知,从时间上看,快速排序的平均性能优于前面讨论过的各种排序方法。快速排序需一个栈空间来实现递归。若每一趟排序都将记录序列均匀地分割成长度
相接近的两个子序列,则栈的最大深度为log(n+l)。
5.归并排序
归并排序是建立在归井操作上的一种有效一的排序算法,该算法是采用分治法( Divide and ConQuer)的一个非常典型的应用。将已有序的子序列合井,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。 若将两个有序表合并成一个有序表,称为二路归并。</br>
例如有8个数据需要排序:10 4 6 3 8 2 5 7 </br>
归并排序主要分两大步:分解、合并。</br>
![image](https://note.youdao.com/yws/public/resource/02b7c5aa7d03a32f1d64ee55c43336d5/119093C954C84F6BA4B749EA1D64CADA/BA17469114A44022B3A65BE0CD8EE62E)
合并过程为:比较a[i]和a[j]的大小,若a[i]<=a[j],则将第一个有序表中的元素a[i]复制到r[k]中,并令1和k分别加上1;否则将第二个有序表中的元素a[j]复制到r[k]中,
并令j和k分别加上1,如此循环下去,直到其中一个有序表取完,然后再将另一个有序表中剩余的元素复制到r中从下标k到下标t的单元。 归并排序的算法我们通常用递归实现,
先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s, t]。
程序如下:
#include<iostream>
using namespace std;
int a[1005], r[1005];
void msort(int, int);
int main(){
int n,i,j,k;
cin>>n;
for(i=1;i<=n;i++) cin>>a[i];
msort(1,n);
for(i=1;i<=n;i++) cout<<a[i]<<" ";
return 0;
}
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]; k++; i++;
}else{
r[k]=a[j]; k++; j++;
}
}
while(i<=mid){ //复制左边子序列剩余
r[k]=a[i]; k++; i++;
}
while(j<=t){ //复制右边子序列剩余
r[k]=a[j]; k++; j++;
}
for(int i=s; i<=t; i++)
a[i]=r[i];
}
6.桶排序
桶排序的思想是若待排序的值在-个明显有限范围内(整型)时,可设计有限个有序桶, 待排序的值装人对应的桶(当然也可以装入若干个值),桶号就是待排序的值,顺序输出各桶的值,将得到有序的序列。</br> </br>
例:输入n个0到100之间的整数,由小到大排序输出。
程序如下:
#include<iostream>
using namespace std;
int a[1005];
int main(){
int n,i,j,k;
cin>>n;
for(i=1;i<=n;i++) { //将等于 k的值全部装入第k桶中
cin>>k;
a[k]++;
}
for(i=0;i<=100;i++){
while(a[i]>0){ //相同的整数,要重复输出
cout<<i<<" "; //输出一个整数后,个数减1
a[i]--;
}
}
return 0;
}
各种排序算法的比较
(1).稳定性比较
插入排序、冒泡排序、二叉树排序、二路归并排序及其他线性排序是稳定的。
选择排序、希尔排序、快速排序、堆排序是不稳定的。即有跨度的交换都会导致不稳定。
(2).时间复杂性比较
插入排序、冒泡排序、选择排序的时间复杂性为O(n²);快速排序、堆排序、归并排序的时间复杂性为O(nlog₂n);桶排序的时间复杂性为O(n) ;
若从最好情况考虑,则直接插入排序和冒泡排序的时间复杂度最好,为O(n),其他算法 的最好情况同平均情况相同;若从最坏情况考虑,则快速排序的时间复杂度为O(n²),直接插入排序和冒泡排序虽然平均情况相同,但系数大约增加一倍,所以运行速度将降低一半,最坏情况对直接选择排序、堆排序和归并排序影响不大。