数据结构 排序

目录

  • 第八章 排序
  • 8.1排序的基本概念
    • 1. 概念
    • 2. 排序算法的分类
  • 8.2 插入排序
    • 8.2.1 直接插入排序
    • 8.2.2 算法效率分析
    • 8.2.2 折半插入排序
    • 总结
    • 8.2.3 希尔排序
  • 8.3 交换排序
    • 8.3.1冒泡排序
    • 8.3.2快速排序(了解栈的过程)
  • 8.4 选择排序
    • 8.4.1 简单选择排序
    • 8.4.2 堆排序
    • 8.4.3 堆的插入删除
  • 8.5 归并排序(Merge Sort)内外部排序
  • 8.6 基数排序(Radix Sort)
      • 排序方式
      • 递减
      • 递增
      • 算法效率分析
      • 基数排序的应用
  • 各种内部排序算法的比较及应用
  • 内部排序算法的应用
  • 8.7 外部排序
  • 8.7.1 归并排序的实现
    • 内存、外存之间的数据交换
    • 外部排序原理
    • 时间开销分析
    • 优化
      • 多路归并
      • 减少初始归并段数量
    • 什么是多路平衡归并?
  • 8.7.2 败者树
    • 败者树的构造
    • 败者树的使用
    • 败者树在多路平衡归并中的应用
    • 败者树的实现思路
  • 8.7.3 置换-选择排序
  • 8.7.4 最佳归并树
    • 归并树的性质
    • 构造2路归并的最佳归并树
    • 多路归并的情况
    • 添加虚段的数量
  • 数据结构结束

第八章 排序

8.1排序的基本概念

1. 概念

数据结构 排序_第1张图片

  • 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程(关键字可以相同)
  • 排序算法的评价指标:时间复杂度、空间复杂度;
  • 排序算法的稳定性:关键字相同的元素在排序之后相对位置不变,称为稳定的;(选择题考查)

Q: 稳定的排序算法一定比不稳定的好?
A: 不一定,要看实际需求;

2. 排序算法的分类

数据结构 排序_第2张图片

  • 内部排序: 数据都在内存——关注如何使时间、空间复杂度更低;
  • 外部排序: 数据太多,无法全部放入内存——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少;

之所以有这种分类是因为磁盘的容量一般远大于内存,而运算速度又远不如内存。因此排序算法不仅要关注时间和空间复杂度,有时考虑到内存容量的问题还要关注如何使读/写磁盘次数更少

8.2 插入排序

8.2.1 直接插入排序

  • 算法思想: 每次将一个待排序的记录按其关键字大小,插入(依次对比、移动)到前面已经排好序的子序列中,直到全部记录插入完成

  • 代码实现:
    数据结构 排序_第3张图片

不带“哨兵”

void InsertSort(int A[], int n){    //A中共n个数据元素
    int i,j,temp;
    for(i=1; i<n; i++)
        if(A[i]<A[i-1]){    //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;         //复制到插入位置
        }
}


带“哨兵” ,优点:不用每轮循环都判断j>=0
数据结构 排序_第4张图片

void InsertSort(int A[], int n){    //A中从1开始存储,0放哨兵
    int i,j;
    for(i=1; i<n; i++)
        if(A[i]<A[i-1]){    
            A[0] = A[i];     //复制为哨兵
            for(j=i-1; A[0] < A[j]; --j)  //从后往前查找待插入位置
                A[j+1] = A[j];     //向后挪动
            A[j+1] = A[0];          //复制到插入位置
        }
}


8.2.2 算法效率分析

  • 空间复杂度:O(1)

  • 时间复杂度:主要来自于对比关键字、移动关键字,若有n个元素,则需要n-1次处理

  • 最好情况: 原本为有序,共n-1趟处理,每一趟都只需要对比1次关键字,不需要移动元素,共对比n-1次 —— O(n)

  • 最差情况: 原本为逆序 —— O(n²)

  • 平均情况:O(n²)

  • 算法稳定性:稳定

8.2.2 折半插入排序

思路: 先用折半查找找到应该插入的位置,再移动元素;

  • 为了保证稳定性,当查找到和插入元素关键字一样的元素时,应该继续在这个元素的右半部分继续查找以确认位置; 即当 A[mid] == A[0] 时,应继续在mid所指位置右边寻找插入位置

  • 当low>high时,折半查找停止,应将[low,i-1]or[high+1,i-1]内的元素全部右移,并将A[0]复制到low所指的位置

代码实现

void InsertSort(int A[], int n){ 
    int i,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){               //折半查找
            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]
    }
}


  • 与直接插入排序相比,比较关键字的次数减少了,但是移动元素的次数没有变,时间复杂度仍然是O(n²)

  • 对链表进行插入排序
    移动元素的次数变少了,因为只需要修改指针,不需要依次右移
    但是关键字对比的次数依然是O(n²)数量级,因此整体看来时间复杂度仍然是O(n²)

总结

数据结构 排序_第5张图片

8.2.3 希尔排序

数据结构 排序_第6张图片
在这里插入图片描述

思路: 先追求表中元素的部分有序,再逐渐逼近全局有序;

更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况

代码实现

void ShellSort(ElemType A[], int n){
    //A[0]为暂存单元
    for(dk=n/2; dk>=1; dk=dk/2)   //步长递减(看题目要求,一般是1/2
        for(i=dk+1; i<=n; ++i)
            if(A[i]<A[i-dk]){
                A[0]=A[i];
                for(j=i-dk; j>0&&A[0]<A[j];j-=dk)
                    A[j+dk]=A[j];         //记录后移,查找插入的位置
                A[j+dk]=A[0;]             //插入
            }
}

i++会切换着处理每个子表

算法效率分析

  • 空间效率:空间复杂度=O(1)
  • 时间效率: 最坏情况下时间复杂度=O(n²)
  • 稳定性:希尔排序是一种不稳定的排序方法
  • 在这里插入图片描述

8.3 交换排序

基于“交换”的排序根据序列中两个元素关键字的比较结果来对换这两个记录再序列中的位置;

8.3.1冒泡排序

数据结构 排序_第7张图片

第一趟排序使关键字值最小的一个元素“冒”到最前面(其最终位置)—— 每趟冒泡的结果是把序列中最小元素放到序列的最终位置,这样最多做n-1趟冒泡就能把所有元素排好序;

为保证稳定性,关键字相同的元素不交换;
数据结构 排序_第8张图片

代码实现

//交换
void swap(int &a,int&b){
    int temp=a;
    a=b;
    b=temp;
}

//冒泡排序
void BubbleSort(int A[],int n){
    for(int i=0;i<n-1;i++){            
        bool 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==false)
            return;                    //本趟遍历后没有发生交换,说明表已经有序
    }
}

算法效率分析

  • 空间复杂度:O(1)
  • 时间复杂度
  • 最好情况 (有序) :只需要一趟排序,比较次数=n-1,交换次数=0,最好时间复杂度=O(n)
  • 最坏情况 (逆序) :比较次数 = (n-1)+(n-2)+…+1 = n(n-1)/2 = 交换次数,最坏时间复杂度 = O(n²),平均时间复杂度 = O(n²)
  • 交换次数和移动次数不是一个概念数据结构 排序_第9张图片在这里插入图片描述
    冒泡排序可以用于链表、顺序表

8.3.2快速排序(了解栈的过程)

每一趟排序都可使一个中间元素确定其最终位置
用一个元素(不一定是第一个)把待排序序列“划分”为两个部分,左边更小,右边更大,该元素的最终位置已确认

数据结构 排序_第10张图片
数据结构 排序_第11张图片

算法实现(重点)

//快速排序
void QuickSort(int A[],int low,int high){
    if(low<high){   //递归跳出的条件
        int pivotpos=Partition(A,low,high); //划分
        QuickSort(A,low,pivotpos-1);        //划分左子表
        QuickSort(A,pivotpos+1,high);       //划分右子表
    }
}

//用第一个元素将待排序序列划分为左右两个部分
int Partition(int A[],int low,int high){
    int 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;             //返回存放枢轴的最终位置
}

算法效率分析

每一层的QuickSort只需要处理剩余的待排序元素,时间复杂度不超过O(n);
数据结构 排序_第12张图片
数据结构 排序_第13张图片
数据结构 排序_第14张图片
数据结构 排序_第15张图片

  • 若每次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高

  • 若每次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低

  • 若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)

数据结构 排序_第16张图片
在这里插入图片描述

8.4 选择排序

每一趟结束就会有一个元素的左边是全小于等于右边是全大于等于
数据结构 排序_第17张图片

8.4.1 简单选择排序

数据结构 排序_第18张图片

n个元素的简单选择排序需要n − 1趟处理

最后剩一个不用再处理(最后一个一定是最值)

算法实现

//交换
void swap(int &a,int&b){
    int temp=a;
    a=b;
    b=temp;
}

//简单选择排序
void SelectSort(int A[],int n){
    for(int i=0;i<n-1;i++){            //一共进行n-1趟
        int min=i;                     //记录最小元素位置
        for(int j=i+1;j<n;j++)         //在A[i...n-1]中选择最小的元素
            if(A[j]<A[min]) min=j;     //更新最小元素位置
        if(min!=i) swap(A[i],A[min]);  //封装的swap()函数共移动元素3次
    }
}

算法性能分析
数据结构 排序_第19张图片
数据结构 排序_第20张图片

8.4.2 堆排序

数据结构 排序_第21张图片

堆的定义
数据结构 排序_第22张图片
数据结构 排序_第23张图片
建立大根堆
大根堆:根≥ 左 右

  • 思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整

  • 检查当前结点是否满足根≥ \ge≥左,右。若不满足,将当前结点与更大的一个孩子互换

  • 若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)

数据结构 排序_第24张图片
数据结构 排序_第25张图片


数据结构 排序_第26张图片
数据结构 排序_第27张图片


代码实现

//建立大根堆
void BuildMaxHeap(int A[],int len){
    for(int i=len/2;i>0;i--){      //从后往前调整所有非终端结点
        HeadAdjust(A,i,len);
    }
}

//将以 k 为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len){
    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值,以便继续向下筛选
        }
    }
    A[k]=A[0]                      //将被筛选结点的值放入最终位置
}

堆排序的完整逻辑
数据结构 排序_第28张图片

//建立大根堆
void BuildMaxHeap(int A[],int len)
    
//将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len)
    
//堆排序的完整逻辑
void HeapSort(int 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);     //把剩余的待排序元素整理成堆
    }
}

算法效率分析
数据结构 排序_第29张图片
数据结构 排序_第30张图片
数据结构 排序_第31张图片
数据结构 排序_第32张图片
数据结构 排序_第33张图片
堆排序是不稳定的

8.4.3 堆的插入删除

数据结构 排序_第34张图片

  • 在堆中插入新元素
    对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止

  • 在堆中删除元素
    被删除的元素用堆顶元素替代,然后让该元素不断“下坠”,直到无法下坠为止

8.5 归并排序(Merge Sort)内外部排序

在这里插入图片描述

数据结构 排序_第35张图片

Merge(归并/合并)

  • 归并:把两个或多个已经有序的序列合并成一个

“2路”归并 —— 每选出一个小元素就需对比关键字1次。“4路”归并 —— 每选出一个小元素就需对比关键字3次。故m路归并,每选出一个元素需要对比关键字m − 1次

归并排序(手算模拟)
在内部排序中一般采用2路归并

核心操作:把数组内的两个有序序列归并为一个
数据结构 排序_第36张图片

代码实现

int *B=(int *)malloc(n*sizeof(int)); //辅助数组B

//A[low...mid]和A[mid+1...high]各自有序,将两个部分归并
void Merge(int A[],int low,int mid,int high){
    int i,j,k;
    for(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])    
            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(int 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
}

数据结构 排序_第37张图片
算法效率分析
数据结构 排序_第38张图片
而递归栈的空间复杂度是 l o g 2 n log_2^n log2n,选取最大的就是辅助数组带来的空间复杂度
两个元素相等时,优先使用靠前的那个。归并排序是稳定的

8.6 基数排序(Radix Sort)

数据结构 排序_第39张图片

基数排序
可见基数排序不是基于“比较”的排序算法

排序方式

数据结构 排序_第40张图片
数据结构 排序_第41张图片
数据结构 排序_第42张图片
数据结构 排序_第43张图片
数据结构 排序_第44张图片
数据结构 排序_第45张图片
数据结构 排序_第46张图片
数据结构 排序_第47张图片

递减

数据结构 排序_第48张图片

递增

数据结构 排序_第49张图片

算法效率分析

基数排序通常基于链式存储实现
数据结构 排序_第50张图片
数据结构 排序_第51张图片
数据结构 排序_第52张图片

基数排序的应用

数据结构 排序_第53张图片
数据结构 排序_第54张图片

各种内部排序算法的比较及应用

数据结构 排序_第55张图片

数据结构 排序_第56张图片

内部排序算法的应用

数据结构 排序_第57张图片

8.7 外部排序

8.7.1 归并排序的实现

内存、外存之间的数据交换

数据结构 排序_第58张图片

外部排序原理

数据结构 排序_第59张图片
数据结构 排序_第60张图片
数据结构 排序_第61张图片
数据结构 排序_第62张图片
数据结构 排序_第63张图片
数据结构 排序_第64张图片
数据结构 排序_第65张图片
数据结构 排序_第66张图片
数据结构 排序_第67张图片
数据结构 排序_第68张图片

时间开销分析

读写都是一块一块
数据结构 排序_第69张图片

优化

数据结构 排序_第70张图片

多路归并

数据结构 排序_第71张图片

减少初始归并段数量

数据结构 排序_第72张图片
数据结构 排序_第73张图片
数据结构 排序_第74张图片
数据结构 排序_第75张图片

什么是多路平衡归并?

数据结构 排序_第76张图片
图没错,定义错了

图错的例子,如下
数据结构 排序_第77张图片

8.7.2 败者树

败者树的构造


败者树的使用


败者树在多路平衡归并中的应用

数据结构 排序_第78张图片
数据结构 排序_第79张图片

败者树的实现思路

从不同位置插入元素,会经过不同层数,也就是不同的对比次数
数据结构 排序_第80张图片

8.7.3 置换-选择排序

数据结构 排序_第81张图片

数据结构 排序_第82张图片
数据结构 排序_第83张图片
数据结构 排序_第84张图片
数据结构 排序_第85张图片

8.7.4 最佳归并树

数据结构 排序_第86张图片

归并树的性质

数据结构 排序_第87张图片

构造2路归并的最佳归并树

数据结构 排序_第88张图片

多路归并的情况

数据结构 排序_第89张图片
数据结构 排序_第90张图片
数据结构 排序_第91张图片
数据结构 排序_第92张图片
数据结构 排序_第93张图片

添加虚段的数量

数据结构 排序_第94张图片

数据结构结束

数据结构 排序_第95张图片

你可能感兴趣的:(#,数据结构,考研,数据结构,算法,学习,笔记,考研)