要求:算法思想,排序过程(手动模拟),特征(初态的影响,时空复杂度,稳定性,适用性等)
算法的稳定性:存在相同关键字时,排序后二者的相对位置是否发生改变。如果改变了,则为不稳定的排序,否则,则为稳定的排序算法。
内部排序:在排序期间元素全部存放在内存中的排序。
外部排序:是指在排序期间元素无法全部同时存放在内存中,必须在排序过程中根据要求不断的在内外存之间移动的排序。
一般情况下,内部排序算法执行过程中都要进行两种操作:比较和移动。通过比较两个关键字,确定对应元素的前后关系,然后通过移动元素以达到有序。
【注】有些例外,基数排序就不是基于比较的。[h1]
插入排序
【思想】 每次将一个待排序的记录,按其关键字大小,插入到前面已经排好序的子序列中(每趟排序后左边有序,右边保持原始状态),直到全部记录插入完成。
由插入排序思想可以引申出三个重要的排序算法:直接插入排序,折半查找排序和希尔排序。
一. 直接插入排序
【算法步骤】
假设待排序表L[ 1..n ]在某次排序过程中的某一时刻状态如下:为了实现将元素L(i)插入到已有序子序列L[ 1....i-1 ]中,需要执行一下操作:
1) 查找出L(i)在L[ 1...i-1 ]中插入位置k
2) 将L[ k..i-1 ]中所有元素全部后移一个位置。
3) 将L(i)复制到L(k)
【代码】
Void InsertSort(ElemType A[],int n)
{
Int i,j;
For(i=2;i<=n;i++) //依次将A[2....n]往前插入
If(A[i].key<A[i-1].key)
A[0]=A[i]; //哨兵,
For(j=i-1;A[0].key<A[j].key;--j) //从后往前查找插入位置
A[j+1]=A[j]; //向后挪位,边比较,边移动
A[j+1]=A[0]; // A[ j ].key恰好小于A[ i ].key
}
}
【性能分析】
空间效率:只使用了一个记录的辅助空间,因而空间复杂度为O(1)
时间效率:比较次数和移动次数取决于待排序表的初始状态
1. 最好情况,表中元素已有序,此时每插入一个元素,都只需比较依次而不用移动元素,比较次数为n-1,故时间复杂度为O(n)
2. 最坏情况,表中元素为逆序,总比较次数为2+3+......+n (每次和哨兵重复比较了一次) (n+2)(n-1)/2 ,记录总移动的次数为3+4+......+n+1(哨兵+挪位+复制)
(n+4)(n-1)/2。
3. 平均情况,考虑待排表元素是随机的,可以取上述最好最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数和总的移动次数都约为n^2/4
由此,直接插入排序算法时间复杂度为O(n^2)
稳定性:由于每次插入元素总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变换的情况,即直接插入排序是一个稳定的排序算法。
适用性:直接插入排序算法适用顺序存储和链式存储的线性表,当为链式存储时,可以从前往后查找指定元素的位置(由于前面已有序,所以只需按顺序查找,找到 后按链表的插入操作,无序整体移动元素)。
【注】大部分排序算法都仅适用于顺序存储的线性表。
二 折半插入排序
【引】 直接插入算法中,总是边比较,边移动元素。在折半插入算法中,将比较和移动操作分离,即先折半查找出元素的待插入位置(折半查找仅限于顺序存储的有序线性表,由此可见该算法也仅适用于顺序存储的线性表),然后统一地移动待插入位置之后的所有元素。
【代码】
Void InsertSort(ElemType A[],int n)
{
Inti,j,low,high,mid;
For(i=2;i<=n;i++)
{
A[0]=A[i]; //并不是充当哨兵,将A[i]暂存到A[0]
Low=1; //折半查找的是有序子表
High=i-1;
While(low<=high) //必须low>high时才跳出循环
{
Mid=(low+high)/2;
If(A[mid].key>A[0].key) //中间元素大于A[i]
High=mid-1;
Else
Log=mid+1;
}
For(j=i-1;j>=high+1;--j)
A[j+1]=A[j];
A[high+1]=A[0];
}
}
【查找实例】
三 希尔排序
【引】直接插入排序算法适用于基本有序的排序表和数据量不大的排序表。O(n^2)
【基本思想】先将待排表分割成若干个形如L[ i,i+d,i+2d, ......i+kd ]的”特殊“子表(若干个 相同步长的组),分 别 进行直接插入排序,当整个表中元素已呈“基本有序”时,再 对全体记录进行一次直接插入排序。
【代码】
Void ShellSort(ElemType A[],int n)
{
For(dk=n/2;dk>=1;dk=dk/2) //步长变换
{
For(i=dk+1;i<=n;++i) //dk+1表示从每组的第二个记录开始比对
If(A[i].key<A[i-dk].key)
{
A[0]=A[i];
For(j=i-dk;j>0&&A[0].key<A[j].key;j-=dk)
A[j+dk]=A[j];
A[j+dk]=A[0];
}
}
}
【图例】
【注】所谓的在各组中进行插入排序:在程序执行过程中,每一趟依次定位到某个记录准备插入时,按其所在组进行直接插入排序。(而不是第一组都完成直接插入排序后,再第二组执行直接插入排序,依次等等。各组的插入排序一定是交替进行的。)
【性能分析】
空间效率: 仅使用了一个暂存单元,空间复杂度为O(1)
时间效率: 由于希尔排序的时间复杂度依赖于增量序列的函数,然而这个函数目前是未知的。当n在某个特定范围内,希尔排序的时间复杂度为 ,在最坏的情况下为O(n^2)
稳定性: 只有当相同关键字的记录被划分到不同的子表中,才可能会改变他们的相对顺序。故希尔排序是不稳定排序。
适用性: 希尔排序仅适用于当线性表为顺序存储的情况。(why?要实现在各组内快速跨步长的直接插入排序,必须得顺序存储,索引才会更高效,就如折半查找一样。)
四. 交换排序
【交换】 就是根据序列中两个元素关键字的比较结果来对换这两个记录在序列中的位置。
A. 冒泡排序
【算法思想】 假设待排序表长为n,从后往前(或从前往后)两两比较相邻元素的值,若为逆序,则交换他们,直达序列比较完成。我们称它为一趟冒泡。下一趟冒泡时,前一趟确定的最小元素(或者是最大元素)不再参与比较,待排序列减少一个元素,每趟冒泡的结果把在排序列中的最小元素(或者是最大元素)放到了序列的最终位置。
【代码】
Void BubbleSort(ElemType A[],intn)
{
For(i=0;i<n-1;i++)
{
Flag=false; //表示本趟冒泡是否发生交换
For(j=n-1;j>I;j--) //一趟冒泡(从后往前,也可从前往后)
If(A[j-1].key>A[j].key) // 若为逆序
{
Swap(A[j-1],A[j]);
Flag=true;
}
If(flag==false)
Return ; //本趟遍历后没有发生交换,说明表已有序
}
}
【性能分析】
空间效率: 使用了常数个辅助单元(交换标志,交换时的暂存单元),空间复杂度为O(1)
时间效率:最好情况(即有序的时候),比较次数为n-1,移动次数为0,从而时间复杂度为O(n);最坏情况(即为逆序的时候),需要进行n-1趟排序,第i趟排序要进行n-i次关键字比较(比如说最后一次即n-1趟时,进行n-(n-1)也就是1次比较),而且每次比较都必须移动元素3次来交换元素位置。因此:
比较次数= 移动次数=
从而最坏情况下时间复杂度为O(n^2)。其平均时间复杂度也为 O(n^2)(O(O(n^2)+O(n)/2)
稳定性:由于当i>j 且 A[i].key=A[j].key)时不会交换元素(由定义可知,只有严格逆序时才发生交换) 故冒泡排序是一个稳定的排序方法。
【注】冒泡排序的特点:每一趟排序都会将一个元素放置到其最终位置。
B. 快速排序
【算法思想】 基于分治法:在待排序表L[1….n]中任取一个元素pivot作为基准,通过一趟排序将待排序表划分为独立的两部分L[1…k-1]和L[k+1…n],使得L[1…..k-1]中所有元素小于pivot,L[k+1…n]所有元素大于或者等于pivot,则pivot放在了其最终位置L(k)上(也就是说每一趟快速排序中,都有一个元素[就是pivot]放置在了其最终位置,每趟必有一划分嘛)。这一过程称作为一趟快速排序。而后分别递归的对两个子表重复上述过程,直至每部分只有一个元素或为空为止,即所有元素都放在了其最终位置。
一趟快速排序的具体做法:附设两个指针low和high,他们的初值氛围为low和high,设枢轴记录的关键字为pivotkey,则首先从high所指位置起向前搜索找到第一个关键字小于pivotkey的记录和枢轴记录对换,然后从low所指位置起向后搜索,找到第一个关键字大于pivotkey的记录和枢轴记录相互对换,重复这两步直至low=high为止。
【代码】
Void QuickSort(ElemType A[],int low,inthigh)
{
If(low<high)
{
//partition()才是划分的关键,将表划分为两个相对有序的子表
Intpivotpos=Partition(A,low,high);
QuickSort(A,low,pivotpos-1);
QuickSort(A,pivotpos+1,high);
}
}
Int Partition(ElemTYpe A[],int low,inthigh)
【性能分析】
时间效率:快速排序的运行时间与划分时候对称有关,而后者又与具体使用的划分算法有关。
最坏情况:每次划分产生分别产生n-1个元素和1个0元素,由于划分的时间代价为O(n)(只是数量级是线性的,原因在于这是一遍历比较的过程,至多为n-1) ,
T(n)=T(n-1)+T(0)+O(n) =T(n-1)+O(n)
可以求得,T(n)=O(n^2)
【证1】:
T(n) =T(n-1)+O(n)
T(n-1)=T(n-2)+O(n)
....
T(2) =T(1)+O(n)
以上各式相加:
T(n)=T(1)+nO(n)=O(
【证2】 也可用二叉树的思想来理解,深度为n-1即O(n),同时每次划分代价(即每层)为O(n) 故总时间复杂度为O(
最好情况:每次划分都产生 [n/2] 和 个子问题,
T(n)<= 2T(n/2)+O(n)
证明:由主定理(见算法导论)得: a=2,b=2, =O(n) 选择情况2,故T(n)=
为原来的1/2规模,则二叉树深度为O( ,又因为每次划分时间代价为O(n)所以 总的时间复杂度为 O(nlogn)
平均情况:类比最好情况理解,比如说1:9划分时,
T(n)<=T(9n/10)+T(n/10)+cn
每一次划分时间代价是O(n),而每一层有多个划分,故某一层上划分时间代价为cn,c为某一常数 。直到深度 (n )处达到边界条件为止,在该层之下,各层代价至多也为cn,递归于深度 处终止, 故时间复杂度也为O(nlogn),由此可见,任何一种按常数比例进行的划分,都会产生深度为O(logn)的递归树,其中每一层的代价为O(n),因而每次按照常数比例来划分时,总运行时间都是O(nlogn)
稳定性:是不稳定的排序方法。L={3 22} 一趟排序后 { 2 2 3} 最终也为 {2 23} 排序前后可见相对顺序发生了变化