本节目录
概念
- 算法稳定性:相同元素在排序后相对位置没有变化,则称算法是稳定的。
- 内部排序:数据全放入内存中再进行排序。
- 外部排序:数据太多,无法全部放入内存中进行排序。
- 稳定算法:插入排序、冒泡排序、并归排序、(桶、计数)基数排序
插入排序
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度: 最好 : O ( n ) 最坏 : O ( n 2 ) 平均时间复杂度 : O ( n 2 ) 最好:O(n)~~~~最坏:O(n^2)~~~~平均时间复杂度:O(n^2) 最好:O(n) 最坏:O(n2) 平均时间复杂度:O(n2)
- 稳定性:稳定
- 优化:采用二分法
c
复制代码
int a[10]={2,42,42,1,64,35,6,32,4354,78};
void InsertSort(int *suzu){ //插入排序
for (int i=1;i<10;i++){
int a=suzu[i],j=i;
if (suzu[i]<suzu[i-1]){
for (;j>=0 && j-1>=0;j--){
if (a>=suzu[j-1])
break;
if (a<suzu[j-1])
suzu[j]=suzu[j-1];
}
suzu[j]=a;
}
}
}
希尔排序
- 步骤:将表划分为若干子表,每个子表执行插入排序。建议第一次选择 ⌊ 总元素数 / 2 ⌋ \lfloor总元素数/2\rfloor ⌊总元素数/2⌋ 来作为增量,以后每一次都/2作为增量。(可以缩短增量 d)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度:
- 最坏情况: O ( n 2 ) ( 直接退化成插入排序 ) 当 n 在某个范围内 : O ( n 1.3 ) O(n^2)~~_{(直接退化成插入排序)}~~~当n在某个范围内:O(n^{1.3}) O(n2) (直接退化成插入排序) 当n在某个范围内:O(n1.3)
- 最优情况: O ( N log 2 N ) O(N\log_2N) O(Nlog2N)
- 稳定性:不稳定
- 适用性:适用于顺序表,不适用于链表
c
复制代码
void ShellSort(int *suzu){ //希尔排序
for (int k=10/2;k>=1;k/=2) {
for (int i=k;i<10;i++){
int a=suzu[i],j=i;
if (suzu[i]<suzu[i-k]){
for (;j>=0 && j-k>=0;j-=k){
if (a>=suzu[j-k])
break;
if (a<suzu[j-k])
suzu[j]=suzu[j-k];
}
suzu[j]=a;
}
}
}
}
冒泡排序
- 空间复杂度: O ( 1 ) O(1) O(1)
- 时间复杂度: 最好 : O ( n ) 最坏 : O ( n 2 ) 平均 : O ( n 2 ) 最好:O(n)~~~最坏:O(n^2)~~~~平均:O(n^2) 最好:O(n) 最坏:O(n2) 平均:O(n2)
- 稳定性:稳定
- 适用:顺序表、链表
c
复制代码
void BubbleSort( int *suzu){ //冒泡排序
for (int i=0;i<10-1;i++){
int sign=false;
for (int j=0;j<10-1-i;j++){
if (suzu[j]>suzu[j+1]){
int a=suzu[j];
suzu[j]=suzu[j+1];
suzu[j+1]=a;
sign=true;
}
}
if (sign==false)
return;
}
}
//鸡尾酒排序算法
void Swap(int *a,int *b){
int c=*a;
*a=*b;
*b=c;
}
void Logic(int *array){
int len=8;
int front=0,rear=len-1;
_Bool tag=false; //false代表未完成排序
while (front<rear && tag==false) {
tag=true;
for (int i = front; i < rear; i++) {
if (array[i] > array[i + 1]) {
Swap(&array[i], &array[i + 1]);
tag = false;
}
}
rear--;
for (int i = rear; i > front; i--) {
if (array[i] < array[i - 1]) {
Swap(&array[i], &array[i - 1]);
tag = false;
}
}
front++;
}
}
快速排序
- 算法步骤:( "画成" 二叉排序树)
- 从数列中挑出一个元素,称为 "基准"(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区操作;
- 递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
- 空间复杂度: O ( 递归深度 ) O(递归深度) O(递归深度) 最好: O ( log 2 h ) O(\log_2h) O(log2h) 最坏: O ( h ) ( 树最高 ) O(h)~~~_{(树最高)} O(h) (树最高)
- 时间复杂度: O ( n ∗ 递归深度 ) = > 最好、平均 : O ( n log 2 n ) 最坏 : O ( n 2 ) O(n*递归深度)=>~~最好、平均:O(n\log_2n)~~最坏:O(n^2) O(n∗递归深度)=> 最好、平均:O(nlog2n) 最坏:O(n2)
- 有序序列使用快速排序性能最差,因为树最高,递归深度最大。
- 稳定性:不稳定
c
复制代码
int QSort(int *array,int head,int tail){ //快速排序
int mid=array[head];
while (head<tail){
while (array[tail]>=mid && head<tail){
tail--;
}
array[head]=array[tail];
while (array[head]<=mid && head<tail){
head++;
}
array[tail]=array[head];
}
array[head]=mid;
return head;
}
void QS2(int *array,int head,int tail){ //快速排序递归排序每一个分表
if (head<tail){
int a=QSort(array,head,tail);
QS2(array,head,a-1);
QS2(array,a+1,tail);
}
}
void QuickSort( int *array){
int head=0,tail=9;
QS2(array,head,tail);
}
直接选择排序
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 稳定性:不稳定
- 适用:顺序表、链表
c
复制代码
void SimpleSelect( int *array){ //直接快速排序
for (int i=0;i<10-1;i++){
for (int j=i+1;j<10;j++){
if (array[i]>array[j]){
int a=array[i];
array[i]=array[j];
array[j]=a;
}
}
}
}
简单选择排序
- 时间复杂度: O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 稳定性:不稳定
- 适用:顺序表、链表
c
复制代码
void swap(int *a,int *b){
int temp = *a;
*a = *b;
*b = temp;
}
void selection_sort(int arr[], int len){
int i,j;
for (i = 0 ; i < len - 1 ; i++){
int min = i;
for (j = i + 1; j < len; j++){
if (arr[j] < arr[min])
min = j;
swap(&arr[min], &arr[i]);
}
}
}
堆排序(选择排序 Pro)
- 堆:(利用完全二叉树)
- 大顶堆:所有节点元素都 大于 它的左右子树节点的值。
- 小顶堆:所有节点元素都 小于 它的左右子树节点的值。
- 详情:https://www.runoob.com/w3cnote/heap-sort.html
- 建堆过程:关键字对比次数不超过 4 n 4n 4n,建堆时间复杂度: O ( n ) O(n) O(n)
- 时间复杂度: O ( n log 2 n ) O(n\log_2n) O(nlog2n)
- 空间复杂度: O ( 1 ) O(1) O(1)
- 稳定性:不稳定
- 堆插入元素:将新插入元素放到堆尾,然后调整成大(小)顶堆。
- 堆删除元素:用堆低元素替代被删除元素,然后调整成大(小)顶堆。
- 每上升一次对比一次关键字,每下降一次可能对比一次、可能对比两次关键字。
c
复制代码
void BigHeapSort( int *array){ //堆排序
for (int k=(10-1);k>=0;k--){
for (int i = k / 2; i >= 0; i--) {
int max = array[i];
for (int j= i*2;j<= k;j*=2) {
if (array[j] < array[j + 1] && j < k) {
j++;
}
if (array[i] < array[j]) {
array[i] = array[j];
i = j;
} else
break;
}
array[i] = max;
}
int a=array[0];
array[0]=array[k];
array[k]=a;
}
}
归并排序
- 将多个有序数组进行合并。
- 归并树:归并排序形态上是倒立的 k 叉树。二叉树第 h 层最多有 k h − 1 k^{h-1} kh−1个节点,若树高为 h ,则 n ≤ k h − 1 n\le k^{h-1} n≤kh−1,归并总次数是 ⌊ log k n ⌋ + 1 \lfloor\log_kn\rfloor+1 ⌊logkn⌋+1 (k为归并路数,n为元素个数)。
- 每次归并时间复杂度: O ( n ) O(n) O(n)
- 时间复杂度: O ( n log k n ) ( 每趟归并时间为 n ) O(n\log_kn)~~~_{(每趟归并时间为n)} O(nlogkn) (每趟归并时间为n)
- 空间复杂度: O ( n ) O(n) O(n)
- 稳定性:稳定
c
复制代码
void TwoMergeSort(int *array1,int *array2){ //二路归并排序
BubbleSort(array1);
BubbleSort(array2);
int len1=5,len2=5;
int target[10];
int tar_len=0,point2=0;
for (int i=0;i<len1;i++){ //array1长度为5
for (int j=point2;j<len2;j++){ //array2长度为5
if (array1[i]<=array2[j]){
target[tar_len++]=array1[i];
break;
}
if (array1[i]>array2[j]){
target[tar_len++]=array2[j];
point2++;
}
}
if (point2==len2)
target[tar_len++]=array1[i];
}
while (point2<len2)
target[tar_len++]=array2[point2++];
}
(桶)基数排序
- 桶类型排序区别:
- 基数排序:根据键值的每位数字来分配桶;
- 计数排序:每个桶只存储单一键值;
- 桶排序:每个桶存储一定范围的数值。
- 步骤:
- 初始化队列个数(r):有多少种类型的数字就初始化多少个队列,一般初始化10个;
- 每一轮依次按照"个十百..."位的大小将元素加入相应队列的队尾;(一趟耗时 n)
- 加入完成后依次从每个队头取出所有元素组成新的序列;(一趟耗时 r)
- 重复 b。
- 分配次数(排序次数):依据整个序列中最大位的元素。
- 空间复杂度: O ( r ) O(r) O(r)
- 时间复杂度: O ( d ∗ ( n + r ) ) ( d 为排序次数, n 为元素个数, r 为队列个数 ) O(d*(n+r))~~~~~~_{(d为排序次数,n为元素个数,r为队列个数)} O(d∗(n+r)) (d为排序次数,n为元素个数,r为队列个数)
- 稳定性:稳定
- 适用于:基数关键字可以很方便的拆分成多个小关键字(d较小);每组关键字取值范围不是很多(r较小);需要排序的数据基数很大(n很大)。
外部排序
- 进行 k 路并归排序,需要在内存中开辟 k 个输入缓冲区和 1 个输出缓冲区。(缓冲区大小等于磁盘block大小)(采用多路归并可以减少并归趟数,进而减少磁盘IO次数)
- 步骤:(总归并次数: ⌊ log k n ⌋ + 1 \lfloor\log_kn\rfloor+1 ⌊logkn⌋+1)
- 先将每个片段进行内部排序,生成 r 个初始并归片段;
- 进行 k 路并归排序。
- 优化:
- 增加并归路数 k ;(代价:需要增加相应缓冲区;增加了多路并归排序的关键字对比次数)
- 减少并归片段 r 数量。(代价:增加了内部归并时间)
- 外部排序时间开销:读写外存时间(主要) + 内部排序时间 + 内部并归时间
- 什么是 k 路平衡归并:
- 最多只能有 k 段归并为一个;
- 每趟归并,若有 m 个段参与归并,则经过一趟归并后得到 ⌈ m + k ⌉ \lceil m+k\rceil ⌈m+k⌉ 个新的归并段。
- 败者树:(可以减少多路归并中关键字对比次数)(第一次构造败者树需要进行 n-1 次对比)
- 对于 n 路并归,最多需要对比关键词次数: ⌈ log 2 n ⌉ \lceil\log_2n\rceil ⌈log2n⌉
置换-选择排序
- 步骤:
- 从文件记录中输入 n 个记录到工作区;(工作区长度为 n)
- 找出工作区中最小关键词(MIX)输入到输出缓冲区,并递增式记录当前最小值MIX;
- 若当前元素中全部元素都小于MIX,则重新开辟新的并归段,执行 b。
- 继续加入文件记录中的待排序元素,执行 b,直到工作区为空。
最佳归并树(哈夫曼树)
- 归并中磁盘 I / O = k 路归并树 W P L ∗ 2 ( W P L 为归并树树的带权路径长度之和 ) 归并中磁盘I/O=k路归并树WPL * 2~~~~_{(WPL为归并树树的带权路径长度之和)} 归并中磁盘I/O=k路归并树WPL∗2 (WPL为归并树树的带权路径长度之和)
- 归并排序:
- 优化:
- 减少磁盘IO:构造哈夫曼树以获取最优WPL。(若无法严格构成 k 叉归并树,则需补充 0 补全(虚段,长度为 0,占位用))
- k 叉最佳并归树一定是一个严格的 k 叉并归树,设度为 k 的节点有 a 个,度为 0 的节点有 b 个,总的节点数为 c,则:(添加虚段的数量)