数据结构王道笔记⑦排序

78 阅读13分钟

一、基本概念

  1. 若对任意的数据元素序列,使用某个排序方法,对它按关键字进行排序:若相同关键字元素间的位置关系,排序前与排序后保持一致,称此排序方法是稳定的;而不能保持一致的排序方法则称为不稳定的
  2. 内部排序:在排序期间元素全部存放在内存中的排序;外部排序:在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断在内外存之间移动的排序
  3. 大部分内部排序算法更适用于顺序存储的线性表
  4. 内部排序算法:平均性能最优的是快速排序

二、插入排序

(一)直接插入排序

  • 每次把该元素插入到前面已排好序的序列中
  • n个数据元素的待排序序列,插入操作要进行n-1趟
  • 空间O(1)
  • 时间最好全部有序O(n),最坏全部逆序O(n^2),平均O(n^2)
  • 稳定
  • 适用于顺序存储和链式存储的线性表,采用链式存储时无需移动元素
//直接插入排序
void InsertSort(int Al],int n){
    int i,j,temp;
    for(i=1;i<n;i++) //将各元素插入已排好序的序列中
        if(A[i]<A[i-1]){//若A[i]小于前驱
            temp=A[i];//用temp暂存A[i]
            for(j=i-1;j>=0 && A[j]>temp;--j) //检查所有前面已排好序的元素
                A[j+1]=A[j];//所有大于temp的元素都向后挪位
            A[j+1]=temp;//复制到插入位置
}

//带哨兵
    void InsertSort(ElemType A[], int n) {
        int i, j;
        for (i = 2; i <= n; i++) //依次将A[2]A[n]插入到前面 已排好的序列
            if (A[i] < A[i - 1]) { //若A[i]小于其前驱,需将A[i]插入有序表
                A[0] = A[i]; //复制为哨兵,A[0]不存放元素
                for (j = i - 1; A[0] < A[j]; --j) // 从后往前查找待插入位置
                    A[j+1] = A[j]; //大于A[0]的向后挪位,等于的不移位因此稳定
                A[j + 1] = A[0]; //复制到插入位置
            }
    }
    

(二)折半插入排序

  • 时间O(n^2)
  • 稳定
  • 仅适用于顺序存储的线性表
void InsertSort(ElemType A[], int n) {
    int i, j, low, high, mid;
    for (i = 2; i <= n; i++) { //依次将A[2]A[n]插入到 前面已排序序列
        A[0] = A[i]; //将A[i]暂存到A[0]
        low = 1; high = i - 1; //设置折半查找的范围
        while (low <= high) { //折半查找(默认递增有序)
            mid = (low + high) / 2; //取中间点
            if (A[mid] > A[0]) high = mid - 1; // 查找左子表
            else low = mid + 1; //查找右子表
        }
        for (j = i - 1; j >= high + 1; --j)
            A[j + 1] = A[j]; //统一后移元素,空出插入位置
            A[high + 1] = A[0]; //插入操作
    }
}

(三)希尔排序(缩小增量排序)

  • 先取一个小于n的增量d,把表中的全部记录分成d₁组,所有距离为d₁的倍数的记录放在同一组,在各组内进行直接插入排序;然后取第二个增量d₂<d,重复上述过程,直到所取到的d₁=1,即所有记录已放在同一组中,再进行直接插入排序
  • 空间O(1)
  • 时间未知,优于直接插入
  • 不稳定
  • 仅适用于是顺序存储的线性表
  • d=1就是直接插入排序
void ShellSort(ElemType A[], int n) {
//1.前后记录的增量是dk,不是1
//2.A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
    int i, j, dk; //dk:步长
    for (dk = n / 2; dk >= 1; dk = dk / 2) //步长变化(无统一规定) 
        for (i = dk + 1; i <= n; ++i)
            if (A[i] < A[i - dk]) { //需将A[i]插 入有序增量子表
                A[0] = A[i]; //暂存在A[0]
                for (j = i - dk; j > 0 && A[0] < A[j]; j -= dk)
                    A[j + dk] = A[j]; //记录后移,查找插入 的位置
                A[j + dk] = A[0]; //插入 
            }//if
}

三、交换排序

(一)冒泡排序

  • 从后往前或从前往后两两比较相邻元素,若为逆序(前大后小)则将两个元素交换。下一趟,前一趟确定的元素不参与冒泡。
  • 对n个元素排序最多需要n-1趟冒泡排序
  • 空间O(1)
  • 最好情况:n个数据元素,1趟冒泡排序,0次数据移动,n-1次比较(初始的待排序序列恰好是递增有序,R1≤R2 ≤…… ≤Rn),时间O(n)
  • 最坏情况: n个数据元素, n-1趟冒泡排序。第1趟比较n-1次,移动3(n-1)次,……总计n-1趟,比较n(n-1)/2次,移动3n(n-1)/2次(初始的待排序序列恰好是逆序 ,R1≥R2 ≥ …… ≥ Rn),时间O(n^2)
  • 平均时间复杂度O(n^2)
  • 稳定
  • 适用于顺序存储和链式存储的线性表
void BubbleSort(ElemType A[], int n) {//从小到大排列
    bool flag;
    for (int i = 0; i < n - 1; i++) {
        flag = false; //表示本趟冒泡是否发生交换的标志  
        for (int j = n - 1; j > i; j--) //一趟冒泡过程
            if (A[j - 1] > A[j]) { //若为逆序 
                swap(A[j - 1], A[j]); //交换
                flag = true;
            }
        if (flag == true)
            return; //本趟遍历后没有发生交换,说明表已经有序 }
    }
}

//每次交换移动元素3次
void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

(二)快速排序

  • 在待排序表L[1.…n]中任取一个元素pivot作为枢轴(或称基准,通常取首元素),通过一趟排序将待排序表划分为独立的两部分L[1.….k-1]和L[k+1.…n],使得L[1….k-1]中的所有元素小于pivot,L[k+1…n]中的所有元素大于或等于pivot,则pivot放在了其最终位置工(k)上,这个过程称为一次划分。然后分别递归地对两个子表重复上述过程,直至每部分内只有一个元素或为空为止,即所有元素放在了其最终位置上。
  • 空间效率:=O(递归层数),最好O(log₂n);最坏O(n)
  • 时间效率:O(n*递归层数),最坏情况为在两个区域分别包含n-1个元素和0个元素时,即对应于初始排序表有序或逆序时,时间为O(n²);最好情况每次划分平均,时间O(nlog₂n);平均O(nlog₂n)
  • 提高算法的效率:一种方法是尽量选取一个可以将数据中分的枢轴元素,如从序列的头尾及中间选取三个元素,再取这三个元素的中间值作为最终的枢轴元素;或者随机地从当前表中选取枢轴元素,这样做可使得最坏情况在实际排序中几乎不会发生。
  • 不稳定
  • 仅适用于顺序存储的线性表
  • 一次划分确定一个元素的最终位置(一次partition),一趟排序可能确定多个元素的最终位置(对未排好序的所有元素进行一次完整处理)
void QuickSort(ElemType A[], int low, int high) {
    if (low < high) { //递归跳出的条件,最左和右元素下标
    //Partition()是划分操作,将表A[low...high]划分为满 足上述条件的两个子表
        int pivotpos = Partition(A, low, high); //划分
        QuickSort(A, low, pivotpos - 1); //依次对两个子表进行递归排序
        QuickSort(A, pivotpos + 1, high);
    }
}
//时间O(n)
int Partition(ElemType A[], int low, int high) {//一趟排序过程
    ElemType pivot = A[low]; //将当前表中第一个元素设为枢 轴值,对表进行划分
    while (low < high) { //循环跳出条件	
	while (low < high && A[high] >= pivot) --high;
        A[low] = A[high]; //将比枢轴值小的元素移动到左端
        while (low < high && A[low] <= pivot) ++low;
        A[high] = A[low]; //将比枢轴值大的元素移动到右端
    }
    A[low] = pivot; //枢轴元素存放到最终位置
    return low; //返回存放枢轴元素的位置	
}


1744033439526.png 1744033472599.png

四、选择排序

(一)简单选择排序

  • n个数据元素必须总共进行n-1趟扫描
  • 第1趟扫描:进行n-1次比较,选出n个数据元素中关键字值最小的数据元素,与第1个数据元素交换;第i趟扫描:进行(n-i)次比较,选出剩下的n-i+1个数据元素中关键字值最小的数据元素,并与第i个数据元素交换
  • 空间O(1)
  • 时间O(n^2)
  • 不稳定
  • 适用于顺序存储和链式存储的线性表,以及关键字较少的情况
void SelectSort(ElemType A[], int n) {
//对表A进行简单的选择排序,A[]0开始存放元素
    for (int i = 0; i < n - 1; i++) { //一共进行n-1趟
        int min = i; //记录最小元素位置
        for (int j = i + 1; j < n; j++) //在A[1...n-1]中 选择最小的元素
            if (A[j] < A[min]) min = j; //更新最小元素的位 置
        if (min != i) swap(A[i], A[min]); //与第i个位置互 换
    }
}

//每次交换移动元素3次
void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

(二)堆排序

  • n个关键字序列L[1…n]称为堆,当且仅当该序列满足: ①L(i)≥L(2i)且L(i)≥L(2i+1)或 ②L(i)≤L(2i)且L(i)≤L(2i+1)(1≤i≤Ln/2」)
  • 可以将堆视为一棵完全二叉树,满足条件①的堆称为大根堆(大顶堆),大根堆的最大元素存放在根结点,且其任意一个非根结点的值小于或等于其双亲结点值。满足条件②的堆称为小根堆(小顶堆),小根堆的定义刚好相反,根结点是最小元素
  • 结论:一个结点,每“下坠”一层,最多只需对比关键字2次若树高为h,某结点在第i层,则将这个结点向下调整最多只需要“下坠”h-i层,关键字对比次数不超过 2(h-i)
  • 基于大根堆的堆排序得到递增序列,基于小根堆的堆排序得到递减序列, 1. 构造初始堆1744102262364.png 2. 删除和调整:输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将 09 和左右孩子的较大者 78 交换,交换后破坏了 L(3)子树的堆,继续对 L(3)子树向下筛选,将 09 和左右孩子的较大者 65 交换,交换后得到了新堆
  • 调整的时间O(h),h为树高
  • 建立的时间O(n)
  • 堆排序的空间O(1),时间O(nlog(2,n))
  • 不稳定
  • 仅适用于顺序存储的线性表 1744102393419.png
//建立大根堆
void BuildMaxHeap(ElemType A[], int len) {
    for (int i = len / 2; i > 0; i--) //从i=[n/2]1,反 复调整堆
        AdjustDown(A, i, len);
}

void HeadAdjust(ElemType A[], int k, int len) {
//函数HeadAdjust对以k为根的子树进行调整 调整为大根堆
    A[0] = A[k]; //A[0]暂存子树的根节点
    for (int i = 2 * k; i <= len; i *= 2) { //沿key较大 的子结点向下筛选
        if (i < len && A[i] < A[i + 1])
            i++; //取key较大的子结点的下标   
        if (A[0] >= A[i]) break; //筛选结束 
        else {
            A[k] = A[i]; //将A[i]调整到双亲结点上 
            k = i; //修改k值,以便继续向下筛选
        }
    }//for
    A[k] = A[0]; //被筛选结点的值放入最终位置 
}

//堆排序算法
void HeapSort(ElemType A[], int len) {
    BuildMaxHeap(A, len); //建立初始堆
    for (int i = len; i > 1; i--) { //n-1趟的交换和建堆 过程
        swap(A[i], A[1]); //输出堆顶的元素(和堆底元素交 换)
        HeadAdjust(A, 1, i - 1); //整理,把剩余的i-1个元 素整理成堆
    }
}

3. 插入 1744102858446.png

五、归并排序

1744103512817.png

ElemType *B=(ElemType *)malloc((n+1)*sizeof(ElemType));//辅助数组B

void Merge(ElemType A[],int low,int mid,int high){
//表A的两端A[low...mid]和A[mid+1...high]各自有序(相邻的两表),将 它们合并成一个有序表
    for(int k=low;k<=high;k++)
        B[k]=A[k]; //将A中所有元素复制到B中
    for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++){
        if(B[i]<=B[j]) //比较B的左右两段中的元素 
            A[k]=B[i++]; //将较小值复制到A中
        else
            A[k]=B[j++]; 
    }//for
    while(i<=mid) A[k++]=B[i++]; //若第一个表未检测完,复制
    while(j<=high) A[k++]=B[j++]; //若第二个表未检测完,复制
}

void MergeSort(ElemType A[],int low,int high){
if(low < high){
int mid=(low+high)/2; //从中间划分两个子序列 MergeSort(A,low,mid); //对左侧子序列进行递归排序
MergeSort(A,mid+1,high);//对右侧子序列进行递归排 序
Merge(A,low,mid,high); //归并 }//if
}
  • 递归形式的二路归并排序算法是基于分治的,其过程如下。分解:将含有n个元素的待排序表分成各含 n/2 个元素的子表,采用二路归并排序算法对两个子表递归地进行排序;合并:合并两个已排序的子表得到排序结果。
  • 空间O(n)
  • 时间O(nlog(2,n))
  • 稳定
  • 适用于顺序存储和链式存储的线性表
void Mergesort(ElemType All,int low,int high) {
    if(low<high){
        int mid = (low+high)/2;//从中间划分两个子序列
        MergeSort(A,low,mid);//对左侧子序列进行递归排序
        MergeSort(A,mid+1,high);//对右侧子序列进行递归排序
        MergeSort(A,low,mid,high);//归并
    }
}       

六、基数排序(几乎只考手算,不考代码)

  • 空间O(r)(r个队列)。个位0~9就是r=10
  • 时间O(d(n+r))(d趟分配O(n)和收集O(r)操作)
  • 稳定
  • 适用于顺序存储和链式存储的线性表,通常为链式 image.png

image.png 若要递增序列,则收集相反

1744275471114.png

七、计数排序

书未看

八、内部排序算法比较

折半插入排序、希尔排序、快速排序和堆排序适用于顺序存储。直接插入排序、冒泡排序、简单选择排序、归并排序和基数排序既适用于顺序存储,又适用于链式存储。 1744104624467.png

438cd1362318e9020d7dfd5752c2440.png f4cc26f13acb11deece15cadae1dfc2.png

九、外部排序

  • 需要将待排序的记录存储在外存上,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间的交换。这种排序算法就称为外部排序。
  • 外部排序用归并排序算法,

1744293554793.png 增加初始归并段的长度,可以减小初始归并段数量r
但k增加会导致内部归并的时间增加;
1744293858739.png 按照本节介绍的方法生成的初始归并段,若共N个记录,内存工作区可以容纳L个记录,则初始归并段数量r=N/L

1744294164354.png

1.败者树(考的少,考手算,不考代码)

  • 完全二叉树,k个叶结点分别存放k个归并段在归并过程中当前参加比较的元素,内部结点用来记忆左右子树中的“失败者”,而让胜利者往上继续进行比较,一直到根结点。若比较两个数,大的为失败者、小的为胜利者,则根结点指向的数为最小数。
  • 可以减少关键字对比次数

1744296934965.png

2.置换-选择排序

  • 产生更长的初始归并段,减少初始归并段数量
  • 使用置换-选择排序,可以让每个初始归并段的长度超越内存工作区大小的限制

1744297507122.png

3.最佳归并树

  • 经过置换-选择排序后,得到长度不等的初始归并段。将这些段的归并过程的磁盘I/O次数=归并树的WPL*2,要让IO次数最少,就要让WPL最小,即哈夫曼树
  • 求最佳归并树(二路、多路),与求哈夫曼树方法相同

1744298450737.png

1744298618831.png

1744298689863.png