文章目录
- 基本概念
- 算法描述
- 例子
- 待排序区的比较规律
- 性能与改进
- 性能分析
交换排序(冒泡排序bubbleSort/快速排序QuickSort)
冒泡排序
基本概念
最终有序位置FA
- 最终有序位置FA:元素(记录Record)x的最终有序位置A(x)是指:元素在待排序列完全排完序后所处的位置是A(x)
- FA(x):FinalAddress(of x in the sequence)
- 在序列的某一趟排序后,确定下来x的位置是A(x)
- 在整个序列全部排完序之后的元素x的位置仍然在A(x)
- 则称A[x]是x的最终有序位置
算法描述
- 冒泡排序的没一趟排序,都会全局地确认出一个一个元素的最终位置
- 这些已经被确认位置的元素不再参与后续比较
- 可以理解为:
- 每趟排序,就会使得待排序序列(称为B区)的长度-1
- 初始时,全部元素都处在B区
- 为了确定第一个元素的
- 相应的,已经确定全局最终位置的元素(称处于A区)的数量+1
- 这一点和插入排序很不一样
- 强调全局是为了和插入排序做区分
- 插入排序在排序过程中维护的一个有序序列是局部的,这些相对有序的元素所在的位置只有在最优一趟排序(最后一个元素插入到有序区才能够确定所有元素的最终位置)
- 因此,冒泡排序可以认为是反复确定:最小值/最大值的排序过程
- 最坏情况下,算需要做完n-1趟排序才能够使得整个序列有序
- 比如,对于升序冒泡,且被排序序列是一个逆序序列
例子
- 为了抽象出算法的一般流程,对一个具体的例子进行算法处理
- 对L=3,2,1进行升序冒泡排序
- 假设我们没一趟排序都从后面往前比较
- 第一趟排序:
- swap(2,1):312
- swap(3,1):132
- 其中有序区A中加入了一个元素1,待排序区B中减少了一个元素
- 下一趟排序只会操作B区中剩余的元素(2个)
- 第二趟排序:
- swap(3,2):2,3
- 现在确定了2要加入A区,B区中的待排序列长度剩下1
- 当待排序列剩下1的时候,算法可以结束,因为,前n-1个已经是都处于最终有序位置FA,剩下一个元素必然也是位于最终有序位置
- 因此,可以将B区中的唯一元素直接加入到A区
- 现在总结过程,A区中的元素序列为1,2,3
- 排序总共进行了两趟(n-1)趟
- 第一趟所做的比较的次数是做多
- 第二趟比第一趟少比较一次
- 后面一次类推,第n-1趟比价只需要比较一次,就可以将剩余的两个元素都加入到有序区A,结束算法
待排序区的比较规律
- 假设算法:从后往前比较,将待排序列排成升序
- 假设B区中的元素为
- 假设数组下标从0开始计算
- 再设一个比较指针j来指示现在比较到哪里了
- 不妨将指针的含义约定为比较表达式的右侧
- 指针j的每个取值对应的比较表达式形如
- 如果比较结果为True,那么执行swap(j-1,j)来交换数组中的两个元素
- 那么,对于从后往前比较的方式,指针取值范围(顺序依次为):
- 总之要保持/使得指针所指的元素是B区中的尽可能小/大元素(根据是逆序排序还是顺序排序确定)
- 当本趟排序结束后,位置j-1(也就是i)上的元素的元素是B区中的最值,也就是B区的第一个元素被算法置为B区的最小值
- 注意到,我们的指针j不会直接B取的第一个元素位置等于i
- 因为j=i+1的时候,j-1可以取到i
基本冒泡(c指针版)
性能与改进
- 冒泡排序在**最坏**的情况下,需要排序n-1趟
- 下面我们基于最坏的情况(待排序列为逆序)
- 第i趟排序需要进行n-i次关键字比较
- 比较次数
- 交换元素位置:
- 相当于移动三次元素(三次赋值操作)
- 那么对于逆序的待排序列(最坏情况):
- 每次比较都需要伴随一次交换,即移动次数是交换次数的3倍情况下
- 在非最坏情况下,可以借助标记位,可以提前判断出某趟排序是否已经得到全局有序的序列
- 具体表现为:
- 如果在某一趟排序中,发现没有做任何的交换,说明任意两个相邻的元素被判断出来是有序的
- 降序或者升序
- 可以在每趟排序前设定一个标记值
- 在该趟排序执行完毕后比较是否发生交换,如果没有交换,则说明有序
- 注意这个标记值每趟排序前都要重置一次,以免受到上一次排序的影响
- 对于标记位改进的冒泡法,最好的情况下也是待排序列有序
- 需要n-1次比较,且无序任意交换操作
- 时间复杂度为O(n)
参考代码python
快速排序QuickSort
- 快速排序属于交换排序的范畴
- 和基本的交换排序(冒泡排序)的基本特征一样,也会提到
最终有序位置
- qsort还应用了
分治
( divide-and- conquer algorithm)的思想
枢轴(Pivot)
- 枢轴一般取待排序序列(区间)中的某个元素
- 通常是首元素
- 但是也不总是这样,有时为了更好的对抗最坏的情况,会采取一些取枢轴的策略
- qsort通过枢轴pivot来对待排序列进行划分(partition)(体现分治)
划分操作partition
- 划分是根据前面提到的枢轴为依据,进行一定次数的比较,将待排序列划分为两个独立的部分
- 如果不特地指明具体的划分,我们将分别简称为:枢轴p,区间A,区间B
- case1:将小于枢轴p的元素调到A
- case2:将大于枢轴p的元素调到B
- case3:而等于枢轴p的元素选定一个区间,比如B,那么所有和p相等的元素之后就都调整到B
- 下面假设把这种情况和case2合并
- cases称为:将大于或等于枢轴p的元素调到B
-
if(L[x]>=q){L[j] in parttion_B}
- 关于
调整
操作的实现的好坏可以进一步调率
- 一般调整也指的是交换操作(q_swap)
- 当所有元素都被调整到对应的区间A和B,并将枢轴p赋值给L[k],那么这一趟的排序就算结束
- 其中L[k]上的元素今后的后续排序中不在发生改变(p已经处于它的最终有序位置,FA§=k)
- partion调用返回枢轴最终插入的位置,以便于qsort中递归调用
代码参考py
调整操作qswap
- 调整操作qswap根据具体的实现,有时也可理解为交替填充
- 前面提到partition操作需要调用调整操作qswap
- qswap的一种比较好的实现是:
- 设置两个辅助指针i,j,它们分别代表区间A,B
- 初始A,B区间内的元素为空
- i指针将元素加入到A
- j指针将元素加入到B
- 初始状态
- 指针i作为指向区间首元的指针
- 指针j指向最后一个元素
- 运动规则:
- 对于指针i,在遇到大于p的元素前,不断+1右移步进
- 否则暂停变化(
if(L[i]>=p)
) -
while(i<j&&L[i]<p){++i}
- 对于指针j,在遇到小于p的元素前,不断-1向左步进
- 否则暂停变化(
if(L[j]<p)
) -
while(i<j&&L[j]>=p){--j}
- 当两个指针都遇到暂停的时候,说明指针i,j遇到来来自本该属于对方区间的元素
- 将这对元素交换,达到一举两得的效果
- 交换完元素后,i,j继续按照原来的规则变化1次
- 🎈i是单调递增的,j是单调递减的
- 由于引入了枢轴变量p,我们可以将被选为枢轴的元素(比如第一个元素L[0]备份到p)
- 这样待排序列中就有一个空位L[0]可以被覆盖而不比担心数据丢失
- 这时候,调整可以认为是交替填充
- 现在,可以根据两个指针的位置关系来判断某轮划分是否已经结束:
- 当
i==j
的时候划分结束
快速排序qsort
- 对上述划分得到的每个独立区间重新执行qsort
- 也就是递归操作重复
- 直到所有元素都被确定下来FA,结束算法
- 每部分都只有0/1个元素的时候,停止算法
- 可以看到,整个序列最终会被划分成二叉查找树的形式
- 划分形式上看类似于二分查找,这是这里的枢轴不一定是中间位置的元素
参考代码
调用者函数
性能分析
- 快速排序由于其平均性能优秀,而且是原地排序,应用广泛
- 平均性能和最优性能都是
- 最理想的情况是,每次取的枢轴都是能够将序列分为元素数量大致相当的两部分A/B
- 新能分析可以转换为类似于BST的高度的高度分析
- 由于partition操作对元素的位置调整,可能导致算法不稳定
- 例如对3,2,2进行partition,去pivot=3,那么partition会将第二个2覆盖掉3的位置,最终将pivot插入到序列中,得到2,2,3