dataStructure_交换排序(冒泡排序bubbleSort/快速排序QuickSort)

交换排序(冒泡排序bubbleSort/快速排序QuickSort)

refs

冒泡排序

基本概念

最终有序位置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区中的元素为 {   a [ i ] , a [ i + 1 ] , ⋯   , a [ n − 1 ]   } \set{a[i],a[i+1],\cdots,a[n-1]} {a[i],a[i+1],,a[n1]}

    • 假设数组下标从0开始计算

    • 再设一个比较指针j来指示现在比较到哪里了

      • 不妨将指针的含义约定为比较表达式的右侧

      • 指针j的每个取值对应的比较表达式形如 a [ j − 1 ] > a [ j ] a[j-1]>a[j] a[j1]>a[j]

        • 如果比较结果为True,那么执行swap(j-1,j)来交换数组中的两个元素
        • 那么,对于从后往前比较的方式,指针取值范围(顺序依次为): n − 1 , n − 2 , ⋯   , i + 1 n-1,n-2,\cdots,i+1 n1,n2,,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,...,n1
    • 第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=1n1(ni)=(n1)+(n2)++1=2n(n1)
  • 交换元素位置:

    • 相当于移动三次元素(三次赋值操作)
    • 那么对于逆序的待排序列(最坏情况):
      • 每次比较都需要伴随一次交换,即移动次数是交换次数的3倍情况下
      • M = ∑ i = 1 n − 1 3 ( n − i ) = 3 C M=\sum\limits_{i=1}^{n-1}3(n-i)=3C M=i=1n13(ni)=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,,k1]
    • 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中递归调用
代码参考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是单调递减的
  • 由于引入了枢轴变量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
posted @ 2024-10-23 10:21  xuchaoxin1375  阅读(4)  评论(0编辑  收藏  举报  来源