排序
(1)基本思想:
每一趟从待排序的数据元素中选出最小(或最大)的一个元素
顺序放在待排序的数列的最前
直到全部待排序的数据元素排完。
(2)排序过程:
【示例】:
初 始 关键字 [49 38 65 97 76 13 27 49]
第一趟排序后 13[38 65 97 76 49 27 49]
第二趟排序后 13 27[65 97 76 49 38 49]
第三趟排序后 13 27 38 [97 76 49 65 49]
第四趟排序后 13 27 38 49 [76 97 65 49]
第五趟排序后 13 27 38 49 49 [97 65 76]
第六趟排序后 13 27 38 49 49 65 [97 76]
第七趟排序后 13 27 38 49 49 65 76 [97]
最后排序结果 13 27 38 49 49 65 76 97
//选择排序
#include<cstdio>
using namespace std;
const int maxn = 100005;
int a[maxn],n,cnt;
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i++)
scanf("%d",&a[i]);
for(int i = 1;i <= n;i++)
{
cnt = i;
for(int j = i + 1;j <= n;j++)
if(a[j] < a[cnt])
cnt = j;
if(cnt != i)
{
int tmp = a[i];
a[i] = a[cnt];
a[cnt] = tmp;
}
}
for(int i = 1;i <= n;i++)
printf("%d ",a[i]);
return 0;
}
二、冒泡排序
(1)基本思想:
以n个人站队为例
从第1个开始,依次比较相邻的两个是否逆序对(高在前,矮在后)
若逆序就交换这两人,即第1个和第2个比,若逆序就交换两人,接着第2个和第3个比,若逆序就交换两人,接着第3个和第4个比,若逆序就交换两人,……
直到n-1和n比较,经过一轮比较后,则把最高的人排到最后,即将最高的人像冒泡一样逐步冒到相应的位置。
原n个人的排序问题,转换为n-1个人的排序问题。
第二轮从第1个开始,依次比较相邻的两个人是否逆序对,若逆序就交换两人,直到n-2和n-1比较。
如此,进行n-1轮后,队列为有序的队列。
(2)排序过程
例如有6个元素需要排序:
6 5 3 4 1 2
1.第一趟排序
2.第二趟排序
3.第三趟排序
4.第四趟排序
5.第五趟排序
#include<cstdio> #include<algorithm> using namespace std; const int maxn = 100005; int n,a[maxn]; int main() { scanf("%d",&n); for(int i = 0;i < n;i++) scanf("%d",&a[i]); for(int i = n-1;i >= 1;i--) { for(int j = 0;j < i;j++) { if(a[j] > a[j + 1]) swap(a[j],a[j + 1]); } } for(int i = 0;i < n;i++) printf("%d ",a[i]); return 0; }
冒泡的优化
对于有些数据,我们发现,不一定要n-1次才能排完。
例如1 5 2 3 4 6,我们发现只需一趟排序就可以将整个序列排完
于是,我们可以设置一个布尔变量,判断是否有进行交换,如果没有交换,说明已经排序完成,进而减少几趟排序。
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100005;
int n,a[maxn];
bool opt;
int main()
{
scanf("%d",&n);
for(int i = 0;i < n;i++)
scanf("%d",&a[i]);
for(int i = n - 1;i >= 1;i--)
{
opt = true;
for(int j = 0;j < i;j++)
{
if(a[j] >= a[j + 1])
{
swap(a[j],a[j + 1]);
opt = false;
}
}
if(opt == true)
break;
}
for(int i = 0;i < n;i++)
printf("%d ",a[i]);
return 0;
}
三、插入排序
(1)基本思想:
回忆一下打牌时抓牌的情景,为了方便打牌,抓牌时一般一边抓牌一边按花色和大小插入恰当的位置,当抓完所有的牌时,手中的牌便是有序的,这排序方法即插入排序。
当读入一个元素时,在已经排序好的序列中,搜寻它正确的位置,再放入读入的元素。
但不该忽略一个重要的问题:在插入这个元素前,应当先将将它后面的所有元素后移一位,以保证插入位置的原元素不被覆盖。
(2)排序过程:
例如:设n=8,数组a中8个元素是: 36,25,48,12,65,43,20,58,执行插入排序程序后,其数据变动情况:
第0步:[36] 25 48 12 65 43 20 58
第1步:[25 36] 48 12 65 43 20 58
第2步:[25 36 48] 12 65 43 20 58
第3步:[12 25 36 48] 65 43 20 58
第4步:[12 25 36 48 65] 43 20 58
第5步:[12 25 36 43 48 65] 20 58
第6步:[12 20 25 36 43 48 65] 58
第7步:[12 20 25 36 43 48 58 65]
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 10005;
int n,a[maxn];
int main()
{
int i,j,k;
scanf("%d",&n);
for(i = 0; i < n; i++)
scanf("%d",&a[i]);
for(i = 0; i < n; i++)
{
for(j = i - 1; j >= 0; j--)
if(a[j] < a[i])
break;
if(j != i - 1)
{
int tmp = a[i];
for(k = i - 1;k > j;k--)
a[k + 1] = a[k];
a[k + 1] = tmp;
}
}
for(int i = 0;i < n;i++)
printf("%d ",a[i]);
return 0;
}
四、桶排序
(1)基本思想:
桶排序的思想是若待排序的值在一个明显有限范围内(整型)时,可设计有限个有序桶,待排序的值装入对应的桶(当然也可以装入若干个值),桶号就是待排序的值,顺序输出各桶的值,将得到有序的序列。
#include<cstdio>
#include<algorithm>
using namespace std;
int b[101],n,k;
int main()
{
scanf("%d",&n);
for(int i = 1;i <= n;i++)
{
scanf("%d",&k);
b[k]++;
}
for(int i = 0;i <= 100;i++)
while(b[i])
{
printf("%d ",i);
b[i]--;
}
return 0;
}
五、快速排序
(1)基本思想:
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
(2)排序过程:
假设待排序的序列为{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;
mmid = 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);
}
六、归并排序
(1)基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;
即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。
(2)排序过程:
例如有8个数据需要排序:10 4 6 3 8 2 5 7
归并排序主要分两大步:分解、合并。
合并过程为:比较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]。
#include<cstdio>
#define ll long long
using namespace std;
const int maxn=5e5+5;
int a[maxn],r[maxn],n;
ll ans=0;
void msort(int s,int t)
{
if(s==t)
return;
int mid=s+t>>1;
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+=(ll)mid-i+1;
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];
}
inline int read()
{
char ch=getchar();
int sum=0,k=1;
while(ch<'0'||ch>'9')
{
if(ch=='-')
k=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
sum=sum*10+(ch^48),ch=getchar();
return sum*k;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
a[i]=read();
msort(1,n);
printf("%lld\n",ans);
return 0;
}
各种排序算法的比较
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较大时,关键字元素可能出现本身是有序的,对稳定性没有要求时宜用堆排序
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。