目录
一、排序定义
1 排序定义
排序,就是把一堆数据元素,按照关键字的递增或者递减的关系把它们排列,即经过排序之后,数据元素的关键字要么递增要么递减(即“有序”)。有时候关键字会相同,这时候就会引出算法的稳定性的问题。
2 稳定性
带排序表中关键字相同的元素,其相对次序在排序前后不变,这称这个排序算法是稳定的。算法是否稳定并不能衡量一个算法的优劣。如果带排序表中的关键字均不重复,则排序结果是唯一的,算法的稳定性就无关紧要。
大部分的内部排序都需要执行比较和移动操作。通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。
在基于比较的排序方法中,每次比较两个关键字大小之后,仅出现两种可能的转移,因此可以用一棵二叉树来描述比较判定过程,由此可以得出结论:当文件的 n 个关键字随机分布时,任何借助比较的排序算法,至少需O(nlog 2n)的时间。——这个log 2n是以2为底的对数,全篇都是这么表示。
算法的稳定性总结见十一。
3 排序分类:
(1)插入排序(直接插入排序、折半插入排序、希尔排序)
(2)交换排序(冒泡排序、快速排序)
(3)选择排序(简单选择排序、堆排序)
(4)归并排序(二路归并、多路归并)
(5)基数排序
二、插入排序——直接插入排序
1. 插入排序的描述
插入排序是一种简单直观的排序方法,其基本思想是将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。
插入排序通常采用就地排序,在从后向前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素腾出插入空间。
2. 代码和示例
下面是直接插入排序的代码
#include <stdio.h>
int main()
{
//建立一个原始的乱序的数组
int A[]={3,4,6,2,1};
printf("排序之前: \n");
int m;
for(m=0;m<5;m++){
printf("%d",A[m]);
}
printf("\n");
//核心算法思想。每个数字都和已经排好序的进行比较
int i ,j ,temp,len;
len=sizeof(A)/sizeof(0);//得到数组的长度
for(i=1;i<len;i++){
if(A[i]<A[i-1]){ //如果它比前面一个数字小,就进入排序的算法
temp=A[i]; //先用temp保存它
for(j=i-1;j>=0 && A[j]>temp;j--){ //循环的条件是数值比temp大
A[j+1]=A[j]; //比temp大的数值统统往后移动
}
A[j+1]=temp; //数值没有比temp大的时候,就插入temp
}
}
//检验是否排序成功:
printf("排序之后: \n");
int n;
for(n=0;n<5;n++){
printf("%d",A[n]);
}
return 0;
}
//以下是王道原代码。
void InsertSort(int A[], int n){
int i ,j ,temp;
for(i=1;i<n;i++){//从数组第二个数开始比较,比较n-1次
if(A[i]<A[i-1]){//如果它小于它前面的数,就开始处理
temp=A[i]; //先用temp暂时保存
for(j=i-1;j>=0 && A[j]>temp;j--){ //j指针从i-1处开始往前面移动,如果j所指大于temp,就继续向前移动
A[j+1]=A[j]; //路上的元素往后移动
}
A[j+1]=temp; //找到位置,存好temp的值
}
}
}
代码逻辑:
(1)i指针指向第二个元素,意思是从第二个元素开始处理。循环n-1次。
(2)比较i位置和i-1位置的元素,如果i位置的更小,那么就用temp暂时缓存i的值,然后查找插入的位置。
(3)设立j指针指向i前面的元素,如果j的值比temp大, 那么j的值就向后移动,移动完数值再把j指针往前移动,一直循环,直到j的值小于等于temp的值,然后把temp的值插入到j+1的位置。
运行结果:
3. 空间效率
仅使用了常数个辅助单元,空间复杂度为O ( 1 )
4. 时间效率
排序过程中,向有序子表中逐个插入元素的操作进行了n-1趟;每趟操作都分为比较关键字和移动元素,次数取决于待排序表的初始状态。
在最好情况下,表中元素已经有序,此时每插入一个元素,都只需一次比较而不需要移动元素。时间复杂度为O ( n ) ;
在最坏情况下。表中元素的顺序刚好与排序结果相反(逆序),总的比较次数达到最大为O(n²);
在平均情况下,考虑待排序表中的元素是随机的,此时取最好与最坏情况的平均值作为平均情况的时间复杂度。
5. 稳定性
在每次查找插入位置的时候,是temp的值比前面的值小,才会移动,所以如果是相同的两个数值,查找的指针是不会移动的,所以算法是稳定的。
6. 适用性
直接插入排序算法适用于顺序储存(大部分排序算法仅适用于顺序储存的线性表)和链式储存。当采用链表的时候,查找的次数和顺序表相同,但是移动的次数减少了,因为只需要修改几个指针就可以。
其更适用于基本有序、数据量不大的排序表。但是,它有缺点,也就是每次查找的次数会多很多。下面的算法是对直接插入排序进行的优化。
三、插入排序——折半插入排序
1. 描述
上个直接插入排序当中,因为子表已经排好序了,所以没有必要从头到尾比较来查找,在查找插入点的时候可以采用折半插入,可以减少查找的次数。
2 代码如下:
#include <iostream>
using namespace std;
void InsertSort(int A[],int n){ //n是数组长度
int i,j,low ,high,mid;
for(i =2;i<=n;i++){ //i是从待排序序列中拿出来的数值。 依次将A[2]~A[n]插入前面的已排序序列(注意这里是从2开始,这里是用哨兵的方法)
A[0]=A[i]; //A[0]处暂时存放A[i]
low=1;high=i-1; //设值折半查找的范围(默认递增有序)
//用一个while循坏,用折半查找方法查找插入位置
while(low<=high){
mid=(low+high)/2; //取中间点
if(A[0]<A[mid]) high=mid-1; //查找左半子表(记忆:先左后右)
else low=mid+1; //查找右半子表。
} //重点:最终,high指向了小于等于A[0]的位置。
//把high后的元素统一后移
for(j = i-1;j>=high+1;j--){
A[j+1]=A[j];//统一后移元素,空出插入位置
}
//把A[0]插入high+1处
A[high+1]=A[0];
}
}
int main()
{
//1 建立一个原始的乱序的数组
int A[]={0,7,9,2,1};//0第一个元素作为哨兵没有用处
printf("排序之前: \n");
int len_1=sizeof(A)/sizeof(0);//得到数组的长度
int m;
for(m=1;m<len_1;m++){ //第一个元素(也就是数组下标为0),只是作为哨兵,所以不打印
printf("%d",A[m]);
}
printf("\n");
//2 调用插入排序方法
InsertSort(A,4);
//3 校验是否排序成功
printf("排序之后: \n");
int n;
for(n=1;n<5;n++){
printf("%d",A[n]);
}
return 0;
}
运行 结果:
3.空间效率
和直接插入排序一样,仅使用了常数个辅助单元,空间复杂度为O ( 1 ) 。
4. 时间效率
相对于直接插入排序,折半插入排序仅仅减少了比较元素的次数,没有减少移动的次数,所以时间复杂度仍为O(n²)。
但对于数据量不是很大的排序表,折半插入排序往往表现出很好的性能。
5. 稳定性
和折半插入一样,在移动的过程中, 只有前面一个数大于temp,才会移动,所以如果遇到相等的情况,就不会移动。所以折半插入排序也是一种稳定的排序方法。
6 适用性
和直接插入排序不一样,折半插入只适用于顺序表,不能用于链表!因为链表不支持随机查找,它不能随意定位到low mid high这些点处的数值。