目录
- 第八章 排序
- 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. 概念
- 排序:重新排列表中的元素,使表中元素满足按关键字有序的过程(关键字可以相同)
- 排序算法的评价指标:时间复杂度、空间复杂度;
- 排序算法的稳定性:关键字相同的元素在排序之后相对位置不变,称为稳定的;(选择题考查)
Q: 稳定的排序算法一定比不稳定的好?
A: 不一定,要看实际需求;
2. 排序算法的分类
- 内部排序: 数据都在内存——关注如何使时间、空间复杂度更低;
- 外部排序: 数据太多,无法全部放入内存——关注如何使时间、空间复杂度更低,如何使读/写磁盘次数更少;
之所以有这种分类是因为磁盘的容量一般远大于内存,而运算速度又远不如内存。因此排序算法不仅要关注时间和空间复杂度,有时考虑到内存容量的问题还要关注如何使读/写磁盘次数更少
8.2 插入排序
8.2.1 直接插入排序
-
算法思想: 每次将一个待排序的记录按其关键字大小,插入(依次对比、移动)到前面已经排好序的子序列中,直到全部记录插入完成
-
代码实现:
不带“哨兵”
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
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²)
总结
8.2.3 希尔排序
思路: 先追求表中元素的部分有序,再逐渐逼近全局有序;
更适用于基本有序的排序表和数据量不大的排序表,仅适用于线性表为顺序存储的情况
代码实现
void ShellSort(ElemType A[], int n){//A[0]为暂存单元for(dk=n/2; dk>=1; dk=dk/2) //步长递减(看题目要求,一般是1/2for(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冒泡排序
第一趟排序使关键字值最小的一个元素“冒”到最前面(其最终位置)—— 每趟冒泡的结果是把序列中最小元素放到序列的最终位置,这样最多做n-1趟冒泡就能把所有元素排好序;
为保证稳定性,关键字相同的元素不交换;
代码实现
//交换
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²)
- 交换次数和移动次数不是一个概念
冒泡排序可以用于链表、顺序表
8.3.2快速排序(了解栈的过程)
每一趟排序都可使一个中间元素确定其最终位置
用一个元素(不一定是第一个)把待排序序列“划分”为两个部分,左边更小,右边更大,该元素的最终位置已确认
算法实现(重点)
//快速排序
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);
-
若每次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最小,算法效率最高
-
若每次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低
-
若初始序列有序或逆序,则快速排序的性能最差(因为每次选择的都是最靠边的元素)
8.4 选择排序
每一趟结束就会有一个元素的左边是全小于等于右边是全大于等于
8.4.1 简单选择排序
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次}
}
算法性能分析
8.4.2 堆排序
堆的定义
建立大根堆
大根堆:根≥ 左 右
-
思路:把所有非终端结点都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整
-
检查当前结点是否满足根≥ \ge≥左,右。若不满足,将当前结点与更大的一个孩子互换
-
若元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)
代码实现
//建立大根堆
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] //将被筛选结点的值放入最终位置
}
堆排序的完整逻辑
//建立大根堆
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); //把剩余的待排序元素整理成堆}
}
算法效率分析
堆排序是不稳定的
8.4.3 堆的插入删除
-
在堆中插入新元素
对于小根堆,新元素放到表尾,与父节点对比,若新元素比父节点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止 -
在堆中删除元素
被删除的元素用堆顶元素替代,然后让该元素不断“下坠”,直到无法下坠为止
8.5 归并排序(Merge Sort)内外部排序
Merge(归并/合并)
- 归并:把两个或多个已经有序的序列合并成一个
“2路”归并 —— 每选出一个小元素就需对比关键字1次。“4路”归并 —— 每选出一个小元素就需对比关键字3次。故m路归并,每选出一个元素需要对比关键字m − 1次
归并排序(手算模拟)
在内部排序中一般采用2路归并
核心操作:把数组内的两个有序序列归并为一个
代码实现
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中elseA[k]=B[j++];}//forwhile(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
}
算法效率分析
而递归栈的空间复杂度是 l o g 2 n log_2^n log2n,选取最大的就是辅助数组带来的空间复杂度
两个元素相等时,优先使用靠前的那个。归并排序是稳定的
8.6 基数排序(Radix Sort)
基数排序
可见基数排序不是基于“比较”的排序算法
排序方式
递减
递增
算法效率分析
基数排序通常基于链式存储实现
基数排序的应用
各种内部排序算法的比较及应用
内部排序算法的应用
8.7 外部排序
8.7.1 归并排序的实现
内存、外存之间的数据交换
外部排序原理
时间开销分析
读写都是一块一块
优化
多路归并
减少初始归并段数量
什么是多路平衡归并?
图没错,定义错了
图错的例子,如下
8.7.2 败者树
败者树的构造
败者树的使用
败者树在多路平衡归并中的应用
败者树的实现思路
从不同位置插入元素,会经过不同层数,也就是不同的对比次数
8.7.3 置换-选择排序
8.7.4 最佳归并树
归并树的性质
构造2路归并的最佳归并树
多路归并的情况