目录
一、概念
1.评价
(1)稳定性
排序前后相同元素的相对位置是否改变
(2)Tn、Sn
2.分类
(1)内部排序
关注算法的Sn、Tn
(2)外部排序
还关注硬盘的读写次数尽量少
二、插入排序
1.直接插入排序(InsertSort)
(1)思路
遍历i个eles,第一个ele当作有序,从第二个ele开始,依次与前面ele比较,if <,交换
(2)实现
//不使用哨兵
void InsertSort(SqList &L,int n){
ElemType tmp;//记录待交换结点
for (int i = 1; i < n; ++i) {
tmp = L->data[i];
int j;
for ( j = i-1 ; j >=0 && L->data[j] > tmp; j--) {
L->data[j+1] = L->data[j];
}
L->data[j] = tmp;
}
}
//有哨兵
void InsertSort2(SqList &L,int n){
int i,j;
for (i = 2; i < n; ++i) {
if(L->data[i] < L->data[i-1]){
L->data[0] = L->data[i];
}
for (j = i-1; L->data[0] < L->data[j]; --j) {
L->data[j+1] = L->data[j];
}
L->data[j+1] = L->data[0];
}
L->len -=1;
}
(3)性能分析
1)Tn
对n个eles,so需要执行n-1次,主要Tn为找位置和交换,so Tn = O(n-1 * n) ~ O(n)
best:全部有序,仅查找即可不用交换 soTn = O(n)
由此可知,在数组有序or大概有序时,直接插入排序效率较高
worst:全部逆序,查找n-1次,每次交换i-1次,so O(n^2)
avg:O(n^2)
2)Sn
O(1)
3)稳定性
当相等时设置不会交换,so稳定
4)适用性
线性表,顺序表和链表均可
2.折半插入排序
(1)思路
利用二分查找查找目标ele应该插入的pos,pos前面的ele前移
(2)实现
//定一个元素位置操作
//挖坑法
int partition(ElemType A[],int low,int high){
ElemType pivot=A[low];//存分割值,最左边的元素
//∵用pivot存储了定位的值,so可以直接覆盖,不交换了,effective
//high 大 high--, 小 A[low] = A[high] low++ high--
//间隔着换,low换完high换,high换完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;
}
void QuickSort(ElemType A[],int low,int high){
//low最左边元素,high最右边元素
if(low <high) {//至少2个元素
//partition->O(n)分成两半while->O(logn)
//O(n)O(logn)
//最差O(n^2)
// reason:有序的数组,递归n次,递归一次O(n)
int pivot_pos = partition(A, low, high);//找定哪个位置
//pivot:n.支点,中心点
//之后再递归前一半
QuickSort(A, low, pivot_pos - 1);
//后一半
QuickSort(A, pivot_pos + 1, high);
}
}
(3)性能分析
1)Tn
折半查找仅在查找时做了优化,但是在交换时O(n),总共需要n-1轮,so Tn = O(n^2)
best:O(n^2)
worst:O(n^2)
avg:O(n^2)
2)Sn
O(1)
3)稳定性
查找时可能会发生交换,so不稳定
4)适用性
二分查找适用于顺序表且有序的,so链式表不行
3.希尔排序(ShellSort)(手算)
(1)背景
因为直接插入排序在有序or大致有序时效率较高,so在希尔提出将数组分成若干了子数组,那么在各个子数组中就是大致有序的
(2)思路
依次减少增量d,以距离为d的子ele作为一个子列,在子列中进行插入排序
d一般为数组长度依次/2 ,直到1为止
(3)实现
颜色分组
(4)性能分析
1)Tn
当d直接为1时,直接退化为直接插入排序,O(n^2)
当d在某些范围内,可达到O(n^1.3)
2)Sn
O(1)
3)稳定性
分成不同组的时候可能会交换相对位置,so不稳定,上例可知
4)适用性
因为要求随机存取,so只适用于顺序表
三、交换排序
1.冒泡排序(BubbleSort)
(1)思路
从后->前 or 从前->后,两两交换,大的往后走,一轮可以定一个,so之后就不用比较了,if在一轮中无ele交换,则说明已经有序,排序停止(*考趟数)
(2)实现
void BubbleSort(SSTable A,int n){
//排序往往用两层循环,优先写内层,之后写外层
//内层循环控制比较交换,外层控制有序数的个数
int i,j;
bool flag;//添加哨兵,才能实现最高时间复杂度O(n)
for (int i=0;i<n-1;i++) {
flag = false;
//一轮比较下来没有交换的,说明已经有序了
//每一轮就定一个,因此j比较次数是动的,用外层控制
for (int j = n-1; j > i ; j--) {
if(A.elem[j] < A.elem[j-1]){
swap(A.elem[j],A.elem[j-1]);
flag = true;
}
}
if(false == flag ){//未进行交换
return;
}
}
}
void swap(int &a,int &b){
int tmp;
tmp = a;
a = b;
b =tmp;
}
(3)性能分析
1)Tn
best:都是有序的,只用进行n-1轮比较,O(n)
worst:都是逆序,比较n-1轮,每轮交换i-1次,O(n^2)
avg:O(n^2)
2)Sn
O(1)
3)稳定性
当相同时设置不交换,so稳定
4)适用性
顺序表和链表均可
2.快速排序(QuickSort)
(1)思路
以第一个ele作为枢轴,依次与后面eles进行比较,大的放右面,小的放左面,这样每一轮就可以确定一个ele,然后左右分治递归即可
(2)实现
void QuickSort(ElemType A[],int low,int high){
//low最左边元素,high最右边元素
if(low <high) {//至少2个元素
//partition->O(n)分成两半while->O(logn)
//O(n)O(logn)
//最差O(n^2)
// reason:有序的数组,递归n次,递归一次O(n)
int pivot_pos = partition(A, low, high);//找定哪个位置
//pivot:n.支点,中心点
//之后再递归前一半
QuickSort(A, low, pivot_pos - 1);
//后一半
QuickSort(A, pivot_pos + 1, high);
}
}
//定一个元素位置操作
//挖坑法
int partition(ElemType A[],int low,int high){
ElemType pivot=A[low];//存分割值,最左边的元素
//∵用pivot存储了定位的值,so可以直接覆盖,不交换了,effective
//high 大 high--, 小 A[low] = A[high] low++ high--
//间隔着换,low换完high换,high换完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;
}
(3)性能分析
1)Tn
主要取决于递归的次数,相当于二叉树
best:每次枢轴可以将左右平分 O(nlogn)
worst:有序or大致有序 O(n^2)
avg: 整体效率接近 O(nlogn)
if每次次枢轴可以将左右平分,那么就可以提高效率
way1:取头部中间尾部,比较大小,取中间
way2:随机取
2)Sn
递归栈的层数(二叉树的高度)O(logn)
3)稳定性
不稳定
4)适用性
顺序表链表均可,但是顺序表效率高
(4)一次划分 vs 一趟排序
408要求一次划分只能定一个ele,一趟排序可以定多个,相当于QuickSort的一层
四、选择排序
每次选择一个定下来最终位置
1.简单选择排序
(1)思想
遍历arr,选择min与第一个交换
(2)实现
void SelectionSort(int A[], int n) {
for (int i = 0; i < n - 1; ++i) {
int min = i;
for (int j = i + 1; j < n; ++j) {
if (A[j] < A[min]) min = j;
}
if (min != i) swap(A[i], A[min]);
}
}
2.堆排序(以大根堆为eg)
(1)思想
初始化堆,建立大根堆
选择根节点,之后重复建堆
(2)实现
void HeapAdjust(int A[],int k,int len){
A[0] = A[k];
for (int i = 2*k; i <=len ; i*=2) {
if(i<len&&A[i]<A[i+1]) i++;
if(A[0] >= A[i]) break;
else{
A[k] = A[i];
k=i;
}
}
A[k] = A[0];
}
void BuildMaxHeap(int A[],int len){
for (int i = len/2; i >0 ; i--) {
HeapAdjust(A,i,len);
}
}
void HeapSort(int A[],int len){
BuildMaxHeap(A,len);
for (int i = len; i >1 ; --i) {
swap(A[i],A[1]);
HeapAdjust(A,i,i-1);
}
}
五、归并排序(MergeSort)
1.基本概念
归并:将k个子数列归并成一个子数列,同时变为升序
2.思路
归并排序一般是二路归并,即把两个arr变成一个arr
so先把A[]分成一个个子arr,然后依次进行merge即可
attn:合并时每组需要排序,此处省略
3.实现
int *B = (int *) malloc(7 * sizeof(int));//辅助空间
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++];
else
A[k] = B[j++];
}
//有一个子arr为null
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;
//先递归只剩1个ele
MergeSort(A, low, mid);
MergeSort(A, mid + 1, high);
//依次merge
Merge(A, low, mid, high);
}
}
4.性能分析
(1)Tn
MergeSort可以视作倒的二叉树,so高度为logn
每次归并Tn= O(n)
so O(nlogn)
(2)Sn
一个辅助空间 O(n)
(3)稳定性
稳定,当子arr中有相同ele,要求先复制前面arr中ele
六、基数排序(RadixSort)
1.手算
eg:520、211、438、888、007、111、985、666、996、233、168
第一次分配收集
第二次分配收集
第三次分配收集
2.思路
(1)初始化
先确定分成几类(几趟),即d,之后根据每一类的最大最小值,确定r(辅助队列的范围)
(2)分配
根据每一位上数字大小分配到第几个队列上
(3)收集
从大到小排序
3.效率分析
(1)Tn
d趟O(d),一趟O(r+n) so O(d(n+r))
(2)Sn
辅助队列O(r)
(3)稳定性
稳定
4.application
三个条件同时满足
(1)可以分成的类d较少
(2)每个位置的取值r范围较小
(3)总数n较大
eg:10000个学生的年龄排序,分成年份、月份、日
5个人的身份证号排序 x
10000个人的身份证号排序 √ d = 18 r = 10
七、外部排序
1.准备
因为数据量过大,so不能将所有数据放入内存,只能放在磁盘中
so排序前需要将data读入内存,排序之后写入磁盘
2.思路
读写+归并排序
step1:构建初始归并段,根据输入缓冲区的大小确定
step2:读入内存,进行归并排序
step3:写入磁盘
3.k路归并
eg:
step1:因为输入缓冲区只有两块,so只能放入前两块
第一轮之后:(仅以前两块为例)
再分归并段:(2块1段)
ATTn:当归并段第一块读空,需要先补充第二块,再归并排序
reason:防止乱序
第二轮之后:
以此类推最后全部有序
4.Tn
总花费时间= 读写时间+归并时间+排序时间
ATTn:读写占主要时间
5.优化
(1)多路归并
同时将多块数据读入内存,从而可以减少读写的次数
(2)败者树
1)background
if仅使用多路平衡归并,则排序占的时间增多,败者树用来提高排序的效率
2)说明
当第一次构建败者树的时候需要比较n-1次,之后比较仅需log2n up次(树高)
因为在排序之前需要初始归并段,用bn表示,so叶节点表示的不是数的大小,而是第几个归并段
3)构建
小的晋级,大的留下
解释:圆形结点表示是哪个归并段的data,线上的是晋级的,so下次比较仅需沿着path依次比较即可
(3)置换-选择排序(手算)
1)background
选择合适归并段的个数可以提高效率,置换选择排序是用来确定几个归并段
置换表示每次确定ele之后换新的,选择表示选择min
2)思想
输入缓冲区的大小是固定的,so将待排序data依次放入输入缓冲区中,依次选出最小的ele放入归并段,并记录该归并段最大数,当缓冲区中所有ele都小于该归并段的最大数,停止,之后进行下一个归并段的构建
3)构建
eg:4 6 9 7 13 11 16 14 10 22 30 2 3 19 20 17 1 23 5 36 12 18 21 39 输入缓冲区容量为3
此时输入缓冲区中的所有data都<Max,so第一个归并段确定
此时第二个归并段确定
最后确定
(4)最佳归并树
1)background
当经过置换-选择排序之后,归并段的长度不同,那么如何选择哪两个归并段进行归并排序的效率最高呢?即使用最佳归并树,本质就是k叉的哈夫曼树
2)计算
I/O次数=2*WPL
3)构建(3路平衡归并树)
eg: 2 3 6 9 12 17 24 18
WPL = (9+12+17+18+24)*2+(2+3+6)*3=193
if按正常步骤去构建,可知该树并非严格3叉树
so 需要添加一个为0的虚结点
WPL = 24*1+(6+9+12+17+18)*2+(2+3)*3=163
4)确定虚结点个数
已知待排序结点个数,严格k叉树度为k的结点数nk,度为0的结点数n0,总结点数n
so n = nk+n0
n = k*nk+1
nk = (n0-1) / (k-1)
so 要求可以整除
即 n0-1 % k-1 =0
if == 0 不需要虚节点
else if n0-1 % k-1 = u ,则需要 k-1-u个
reason:多了u个,so补到k-1个