dataStructure_交换排序(冒泡排序bubbleSort/快速排序QuickSort)
文章目录
交换排序(冒泡排序bubbleSort/快速排序QuickSort)
refs
- python_分治算法举例(在互不相同的数列中找到第i小的数)(随机化性能为线性性能)的算法_Randomized divide-and- conquer algorithm_xuchaoxin1375的博客-CSDN博客
- python/C_快速排序(randomize_quick_sort())_xuchaoxin1375的博客-CSDN博客
冒泡排序
基本概念
最终有序位置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区
- 为了确定第一个元素的
- 初始时,全部元素都处在B区
- 相应的,已经确定全局最终位置的元素(称处于A区)的数量+1
- 这一点和插入排序很不一样
- 强调全局是为了和插入排序做区分
- 插入排序在排序过程中维护的一个有序序列是局部的,这些相对有序的元素所在的位置只有在最优一趟排序(最后一个元素插入到有序区才能够确定所有元素的最终位置)
- 这一点和插入排序很不一样
- 因此,冒泡排序可以认为是反复确定:最小值/最大值的排序过程
- 每趟排序,就会使得待排序序列(称为B区)的长度-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区中的元素为 { a [ i ] , a [ i + 1 ] , ⋯ , a [ n − 1 ] } \set{a[i],a[i+1],\cdots,a[n-1]} {a[i],a[i+1],⋯,a[n−1]}
-
假设数组下标从0开始计算
-
再设一个比较指针j来指示现在比较到哪里了
-
不妨将指针的含义约定为比较表达式的右侧
-
指针j的每个取值对应的比较表达式形如 a [ j − 1 ] > a [ j ] a[j-1]>a[j] a[j−1]>a[j]
- 如果比较结果为True,那么执行swap(j-1,j)来交换数组中的两个元素
- 那么,对于从后往前比较的方式,指针取值范围(顺序依次为): n − 1 , n − 2 , ⋯ , i + 1 n-1,n-2,\cdots,i+1 n−1,n−2,⋯,i+1
- 总之要保持/使得指针所指的元素是B区中的尽可能小/大元素(根据是逆序排序还是顺序排序确定)
- 当本趟排序结束后,位置j-1(也就是i)上的元素的元素是B区中的最值,也就是B区的第一个元素被算法置为B区的最小值
-
注意到,我们的指针j不会直接B取的第一个元素位置等于i
-
因为j=i+1的时候,j-1可以取到i
-
-
基本冒泡(c指针版)
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
//在此下方插入自定义函数对的声明:
void bubble_int_sort(int *p,int n)
{
void swap(int*a,int*b);
/* 冒泡https://img02.sogoucdn.com/app/a/100520146/2ebb85e6d696706cd231a745c593b1dd */
/*冒泡法不需要设立最值flag. */
for(int i = 0;i < n-1;i++)
{
for(int j = 0;j<=n-2-i;j++)
{
if(*(p+j) < *(p+j+1))/* 通过监视*(p+j)和*(p+j+1)可以知道当前(第j组)相邻量的值的情况 */
{
swap(p+j,p+j+1);
}
}
}
}
void swap(int*a,int*b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
//主函数main
int main()
{
int *p;
p = (int*)malloc(10*sizeof(int));
for(int i = 0;i<10;i++)
{
scanf("%d",p+i);
}
bubble_int_sort(p,10);
for(int i = 0;i < 10;i++)
{
printf("%d\n",*(p+i));
}
return 0;
}
性能与改进
-
冒泡排序在**最坏**的情况下,需要排序n-1趟
- 下面我们基于最坏的情况(待排序列为逆序)
- i = 1 , 2 , . . . ⋯ , n − 1 i=1,2,...\cdots, n-1 i=1,2,...⋯,n−1
- 第i趟排序需要进行n-i次关键字比较
- 比较次数 C = ∑ i = 1 n − 1 ( n − i ) = ( n − 1 ) + ( n − 2 ) + ⋯ + 1 = n ( n − 1 ) 2 C=\sum\limits_{i=1}^{n-1}(n-i)=(n-1)+(n-2)+\cdots+1=\frac{n(n-1)}{2} C=i=1∑n−1(n−i)=(n−1)+(n−2)+⋯+1=2n(n−1)
-
交换元素位置:
- 相当于移动三次元素(三次赋值操作)
- 那么对于逆序的待排序列(最坏情况):
- 每次比较都需要伴随一次交换,即移动次数是交换次数的3倍情况下
- M = ∑ i = 1 n − 1 3 ( n − i ) = 3 C M=\sum\limits_{i=1}^{n-1}3(n-i)=3C M=i=1∑n−13(n−i)=3C
-
在非最坏情况下,可以借助标记位,可以提前判断出某趟排序是否已经得到全局有序的序列
- 具体表现为:
- 如果在某一趟排序中,发现没有做任何的交换,说明任意两个相邻的元素被判断出来是有序的
- 降序或者升序
- 可以在每趟排序前设定一个标记值
- 在该趟排序执行完毕后比较是否发生交换,如果没有交换,则说明有序
- 注意这个标记值每趟排序前都要重置一次,以免受到上一次排序的影响
- 如果在某一趟排序中,发现没有做任何的交换,说明任意两个相邻的元素被判断出来是有序的
- 具体表现为:
-
对于标记位改进的冒泡法,最好的情况下也是待排序列有序
- 需要n-1次比较,且无序任意交换操作
- 时间复杂度为O(n)
参考代码python
import random as rand
l = list(range(10))
rand.shuffle(l)
rl = l #random number list
print(rl)
def swap(l,i,j):
#python支持成组赋值
l[i],l[j] = l[j],l[i]
#传统写法如下:
# tmp = rl[j]
# rl[j] = rl[j - 1]
# rl[j - 1] = tmp
def bubble_sort(rl):
n = len(rl)
for i in range(n - 1):
#在本趟排序前,设立一个标记
flag = True #标记:假设本轮排序前是有序的
for j in range(n - 1, i, -1):
if (rl[j] < rl[j - 1]):
flag = False#若发现逆序,修改标记
#交换序列中的两个元素
swap(rl,j,j-1)
#本趟排序结束后,检查标记是否被更改,来判定是否已经得到一个有序序列
if flag:#如果本趟排序没有发现逆序(交换),则已经可以认定序列是有序的了,可以结束排序
break
return rl
if __name__ == "__main__":
bubble_sort(rl)
print(rl)
快速排序QuickSort
-
快速排序属于交换排序的范畴
-
和基本的交换排序(冒泡排序)的基本特征一样,也会提到
最终有序位置
-
qsort还应用了
分治
( divide-and- conquer algorithm)的思想
枢轴(Pivot)
- 枢轴一般取待排序序列(区间)中的某个元素
- 通常是首元素
- 但是也不总是这样,有时为了更好的对抗最坏的情况,会采取一些取枢轴的策略
- qsort通过枢轴pivot来对待排序列进行划分(partition)(体现分治)
划分操作partition
- 划分是根据前面提到的枢轴为依据,进行一定次数的比较,将待排序列划分为两个独立的部分
-
不妨记根据枢轴
p
(
i
)
划分出来的两个区间分别为
A
(
p
(
i
)
)
,
B
(
p
(
i
)
)
不妨记根据枢轴p(i)划分出来的两个区间分别为A(p(i)),B(p(i))
不妨记根据枢轴p(i)划分出来的两个区间分别为A(p(i)),B(p(i))
- 如果不特地指明具体的划分,我们将分别简称为:枢轴p,区间A,区间B
- case1:将小于枢轴p的元素调到A
- 记 A = L [ 1 , 2 , ⋯ , k − 1 ] 记A=L[1,2,\cdots,k-1] 记A=L[1,2,⋯,k−1]
- case2:将大于枢轴p的元素调到B
- 记 B = L [ k + 1 , k + 2 , ⋯ , n ] 记B=L[k+1,k+2,\cdots,n] 记B=L[k+1,k+2,⋯,n]
- 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中递归调用
-
不妨记根据枢轴
p
(
i
)
划分出来的两个区间分别为
A
(
p
(
i
)
)
,
B
(
p
(
i
)
)
不妨记根据枢轴p(i)划分出来的两个区间分别为A(p(i)),B(p(i))
不妨记根据枢轴p(i)划分出来的两个区间分别为A(p(i)),B(p(i))
代码参考py
def partion(l, low=0, high=0, pivot=0):
#简单的指定枢轴为待划分区间的第一个元素 (将这个元素备份到pivot变量中保存)
pivot = l[low]
high=len(l)-1
while (low < high):
#操作连个区间的指针
while (low < high and l[high] >= pivot):
high -= 1
#离开循环的时候说明high指针所指的元素比pivot小,
# 需要把它移到low所指的地方(此时l[low]可以被安全覆盖而不会丢失必要数据)
l[low] = l[high]
#轮到另一个指针运动,做类似的比较和覆盖操作
while (low < high and l[low] < pivot):
low += 1
l[high] = l[low]
#覆盖掉可以被覆盖的元素(第一个是区间内的第一个元素原来的位置)
#直到区间内的元素被划分完毕
# 最后将枢轴pivot中保存的元素插入到序列中的正确位置,k=low=high
pivot_postion = low #low==high
l[pivot_postion] = pivot
return pivot_postion
调整操作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是单调递减的
- 对于指针i,在遇到大于p的元素前,不断+1右移步进
- 初始A,B区间内的元素为空
-
由于引入了枢轴变量p,我们可以将被选为枢轴的元素(比如第一个元素L[0]备份到p)
-
这样待排序列中就有一个空位L[0]可以被覆盖而不比担心数据丢失
- 这时候,调整可以认为是交替填充
-
现在,可以根据两个指针的位置关系来判断某轮划分是否已经结束:
- 当
i==j
的时候划分结束
- 当
快速排序qsort
- 对上述划分得到的每个独立区间重新执行qsort
- 也就是递归操作重复
- 直到所有元素都被确定下来FA,结束算法
- 每部分都只有0/1个元素的时候,停止算法
- 可以看到,整个序列最终会被划分成二叉查找树的形式
- 划分形式上看类似于二分查找,这是这里的枢轴不一定是中间位置的元素
参考代码
def quick_sort(l,low=0,high=0):
#快速排序是递归实现的
#首先编写递归出口逻辑:
#或者说递归继续深入的条件(包含了出口的意思)
if(low<high):
#首先对传入的区间片段做一个partition
pivot_position=partion(l,low,high)
quick_sort(l,low,pivot_position-1)
quick_sort(l,pivot_position+1,high)
调用者函数
def generate_by_shuffle(n=30):
#随机序列生成函数
l=list(range(n))
random.shuffle(l)
return l
if __name__ == "__main__":
# l = [2,3,5,7,1,4,6,15,5,2,7,9,10,15,9,17,12]
l = generate()
print(l)
len_l = len(l)
high = len_l - 1
#测试函数功能
## 测试partition()
# print(quick_sort_poor(l))
# p = partion(l)
##测试quicksort()
quick_sort(l,low=0,high=len_l-1)
## 打印结果
# print("p=%d;l[p]=%d" %(p,l[p]))
print("🎈排序结果:")
print(l)
性能分析
- 快速排序由于其平均性能优秀,而且是原地排序,应用广泛
- 它的最坏情况下的性能是 O ( n 2 ) 它的最坏情况下的性能是O(n^2) 它的最坏情况下的性能是O(n2)
- 平均性能和最优性能都是
O
(
n
log
2
n
)
O(n\log_2n)
O(nlog2n)
- 最理想的情况是,每次取的枢轴都是能够将序列分为元素数量大致相当的两部分A/B
- 新能分析可以转换为类似于BST的高度的高度分析
- 由于partition操作对元素的位置调整,可能导致算法不稳定
- 例如对3,2,2进行partition,去pivot=3,那么partition会将第二个2覆盖掉3的位置,最终将pivot插入到序列中,得到2,2,3