王道C语言笔记NOTE-中级阶段Note7-排序算法
一、排序
1、分类:排序算法分为交换类排序,插入排序,选择排序,归并排序
2、交换排序
①、冒泡排序
②、快速排序
二、冒泡排序
1、基本思想:从后往前(或从前 往后)两两比较相邻元素的值,(若A[j-1]>A[j]) ,则交换它们,直到序列比较完。称为第一趟冒泡,结果是将最小的元素交换到待排序列的第一个位置。关键字最小的元素如气泡一般逐渐往上“漂浮”直至“水面”。下一趟冒泡时,前一趟确定的最小元素不再参与比较,每趟冒泡的结果是把序列中的最小元素放到了序列的最终位置....这样最多做n-1趟冒泡就能把所有元素排好序。
2、冒泡过程图示
3、代码实战
①、步骤:首先通过随机数生成10个元素,多次测试排序算法是否正确。先打印随机生成后的元素,后通过冒泡排序对元素进行排序,再次打印排序后的元素顺序。
②、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<time.h> 4 //顺序表数据结构定义 5 typedef struct{ 6 int *elem;//存储元素的起始地址 7 int len;//元素个数 8 }SSTable; 9 10 //初始化顺序表 11 void Init_ST(SSTable &ST,int len) 12 { 13 ST.elem=(int*)malloc(sizeof(int)*len); 14 ST.len=len; 15 //随机数生成 16 srand(time(NULL)); 17 int i; 18 for(i=0;i<ST.len;i++) 19 { 20 ST.elem[i]=rand()%100;//生成的随机数是0-99之间 21 } 22 } 23 24 //打印数组中的元素 25 void Print_ST(SSTable ST) 26 { 27 int i=0; 28 for(i=0;i<ST.len;i++) 29 { 30 printf("%3d",ST.elem[i]); 31 } 32 printf("\n"); 33 } 34 35 //交换两个元素 36 void swap(int &a,int &b) 37 { 38 int tmp=a; 39 a=b; 40 b=tmp; 41 } 42 43 //冒泡排序 44 void Bubble_Sort(int *A,int len) 45 { 46 int i,j; 47 for(i=0;i<len-1;i++) 48 { 49 for(j=len-1;j>i;j--) 50 { 51 if(A[j]<A[j-1]) 52 { 53 swap(A[j],A[j-1]); 54 } 55 } 56 } 57 } 58 59 60 //主函数 61 int main() 62 { 63 SSTable ST; 64 Init_ST(ST,10); 65 Print_ST(ST);//打印排序前的顺序表 66 Bubble_Sort(ST.elem,10); 67 printf("冒泡排序后的结果是:\n"); 68 Print_ST(ST); 69 return 0; 70 }
③、结果
4、复杂度分析
①、时间复杂度是O(n2):内外层循环相乘。
②、空间复杂度是O(1):未使用额外空间。
三、快速排序
1、基本思想:假设是按从小到大排序,先找到数组中的一个分割值,把比分割值小的数都放在数组左边,把比分割值大的数都放在数组右边,这样分割值的位置就确定了。数组一分为二,只需要排前一半数组和后一半数组,复杂度直接减半。采用这种思想,不断的进行递归,最终分割得只剩下一个元素时,整个序列就自然是有序的。
2、代码实战
①、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<time.h> 4 //顺序表数据结构定义 5 typedef struct{ 6 int *elem;//存储元素的起始地址 7 int len;//元素个数 8 }SSTable; 9 10 //初始化顺序表 11 void Init_ST(SSTable &ST,int len) 12 { 13 ST.elem=(int*)malloc(sizeof(int)*len); 14 ST.len=len; 15 //随机数生成 16 srand(time(NULL)); 17 int i; 18 for(i=0;i<ST.len;i++) 19 { 20 ST.elem[i]=rand()%100;//生成的随机数是0-99之间 21 } 22 } 23 24 //打印数组中的元素 25 void Print_ST(SSTable ST) 26 { 27 int i=0; 28 for(i=0;i<ST.len;i++) 29 { 30 printf("%3d",ST.elem[i]); 31 } 32 printf("\n"); 33 } 34 35 //交换两个元素 36 void swap(int &a,int &b) 37 { 38 int tmp=a; 39 a=b; 40 b=tmp; 41 } 42 43 //分割 44 int Partition(int *A,int low,int high) 45 { 46 int pivot=A[low];//首先使用左边变量存储值 47 while(low<high) 48 { 49 while(low<high&&A[high]>=pivot) 50 { 51 high--; 52 } 53 A[low]=A[high]; 54 while(low<high&&A[low]<=pivot) 55 { 56 low++; 57 } 58 A[high]=A[low]; 59 } 60 A[low]=pivot; 61 return low; 62 } 63 64 65 //递归实现 66 void Quick_Sort(int *A,int low,int high) 67 { 68 if(low<high) 69 { 70 int pivotpos=Partition(A,low,high); 71 Quick_Sort(A,low,pivotpos-1); 72 Quick_Sort(A,pivotpos+1,high); 73 } 74 } 75 76 //主函数 77 int main() 78 { 79 SSTable ST; 80 Init_ST(ST,10); 81 Print_ST(ST);//打印排序前的顺序表 82 printf("快速排序后的结果是:\n"); 83 Quick_Sort(ST.elem,0,9);//快速排序 84 Print_ST(ST);//打印排序后的顺序表 85 return 0; 86 }
②、结果
3、复杂度分析
①、分析:每次快速排序数组被平均的一分为二,Quick_Sort递归的次数是log2n,第一次partition遍历次数是n,分成两个数组后,每个数组遍历n/2次,加起来还是n,因此时间复杂度是O(nlog2n);如果数组本身是从大到小有序时,每次仍然采用最左边的数作为分割值,那么每次数组都不会二分,导致递归n次,最坏时间复杂度是O(n2)。
解决方法:首先随机选择一个下标,先将对应下标的值和最左边的元素进行交换,在进行分割操作,从而降低出现最坏时间复杂度的概率,但是并不能完全避免。
②、结论
a.时间复杂度
b.空间复杂度:O(log2n)
因为递归次数是log2n,而每次递归的形参都是需要占用空间的。
四、插入排序
插入排序分类
①、直接插入排序
②、折半插入排序
③、希尔排序
五、直接插入
1、直接插入排序原理:如果一个序列只有一个数,那么该序列是自然有序的。插入排序首先将第一个数视为有序序列,然后把后面的数视为要依次插入的序列。
2、图示
3、代码实战
①、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<time.h> 4 //顺序表数据结构定义 5 typedef struct{ 6 int *elem;//存储元素的起始地址 7 int len;//元素个数 8 }SSTable; 9 10 //初始化顺序表 11 void Init_ST(SSTable &ST,int len) 12 { 13 ST.elem=(int*)malloc(sizeof(int)*len); 14 ST.len=len; 15 //随机数生成 16 srand(time(NULL)); 17 int i; 18 for(i=0;i<ST.len;i++) 19 { 20 ST.elem[i]=rand()%100;//生成的随机数是0-99之间 21 } 22 } 23 24 //打印数组中的元素 25 void Print_ST(SSTable ST) 26 { 27 int i=0; 28 for(i=0;i<ST.len;i++) 29 { 30 printf("%3d",ST.elem[i]); 31 } 32 printf("\n"); 33 } 34 35 //插入排序 36 void Insert_Sort(int *A,int len) 37 { 38 int i,j,insertVal; 39 for(i=1;i<len;i++)//控制要插入的数 40 { 41 insertVal=A[i];//先保存要插入的值 42 //内存控制比较j,要大于等于0,同时A[j]大于insertVal时,arr[j]位置元素往后覆盖 43 for(j=i-1;j>=0&&A[j]>insertVal;j--) 44 { 45 A[j+1]=A[j]; 46 } 47 A[j+1]=insertVal; 48 } 49 } 50 51 52 //主函数 53 int main() 54 { 55 SSTable ST; 56 Init_ST(ST,10); 57 Print_ST(ST);//打印排序前的顺序表 58 Insert_Sort(ST.elem,10); 59 printf("插入排序后的结果是:\n"); 60 Print_ST(ST); 61 return 0; 62 }
②、结果
4、复杂度分析
随着有序序列的不断增加,插入排序比较次数也会增加,插入排序的执行次数也是从1增加到N-1,总运行次数N(N-1)/2,时间复杂度依旧是O(n2)。因为没有使用额外的空间(额外空间必须与输入元素N有关),所以空间复杂度是O(1)。如果数组本身有序,那么就是最好的时间复杂度O(n)。
六、选择排序
选择排序分类
①、简单选择排序
②、堆排序
七、简单选择排序
1、原理:假设排序表L[1,……,n],第i趟排序即从L[i…,n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过n-1趟排序就可以使得整个排序表有序。
2、图示
3、代码实战
①、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<time.h> 4 //顺序表数据结构定义 5 typedef struct{ 6 int *elem;//存储元素的起始地址 7 int len;//元素个数 8 }SSTable; 9 10 //初始化顺序表 11 void Init_ST(SSTable &ST,int len) 12 { 13 ST.elem=(int*)malloc(sizeof(int)*len); 14 ST.len=len; 15 //随机数生成 16 srand(time(NULL)); 17 int i; 18 for(i=0;i<ST.len;i++) 19 { 20 ST.elem[i]=rand()%100;//生成的随机数是0-99之间 21 } 22 } 23 24 //打印数组中的元素 25 void Print_ST(SSTable ST) 26 { 27 int i=0; 28 for(i=0;i<ST.len;i++) 29 { 30 printf("%3d",ST.elem[i]); 31 } 32 printf("\n"); 33 } 34 35 //交换两个元素 36 void swap(int &a,int &b) 37 { 38 int tmp=a; 39 a=b; 40 b=tmp; 41 } 42 43 //选择排序 44 void Selete_Sort(int *A,int len) 45 { 46 int i,j,min;//min记录最小元素的下标 47 for(i=0;i<len-1;i++)//最多可以为8 48 { 49 min=i;//认为第i个元素最小 50 for(j=i+1;j<len;j++)//最多可以为9 51 { 52 if(A[j]<A[min]) 53 { 54 min=j; 55 } 56 } 57 //遍历完毕找到最小值的位置后,与A[i]交换,这样最小值就到了最前面 58 if(i!=min) 59 { 60 swap(A[i],A[min]); 61 } 62 } 63 } 64 65 66 //主函数 67 int main() 68 { 69 SSTable ST; 70 Init_ST(ST,10); 71 Print_ST(ST);//打印排序前的顺序表 72 Selete_Sort(ST.elem,10); 73 printf("选择排序后的结果是:\n"); 74 Print_ST(ST); 75 return 0; 76 }
②、结果
4、复杂度分析
①、分析:选择排序虽然减少了交换次数,但是循环比较次数依旧和冒泡排序的数量是一样的,都是从1增加到N-1,总运行次数为N(N-1)/2。时间复杂度仍旧是O(n2),因为未使用额外的空间(额外空间必须与输入元的个数N有关),所以空间复杂度是O(1)。
②、总结
八、堆排序
1、原理
①、基本概念:堆(Heap)是计算机科学中一种特殊的树状数据结构。若满足以下特性,则可以被称作堆
a.给定堆中任意结点P和C,若P是C的父节点,则P的值小于等于(或者大于等于)C的值。
b.若父节点的值恒小于等于子节点的值,则称该堆为最小堆;反之,若父节点的值恒大于等于子节点的值,则该堆称为最大堆。
c.堆中最顶端的那个结点称为根结点。在工作中,一般把最小堆称作小根堆或者小顶堆,把最大堆称为大根堆或者大顶堆。
2、图示
3、代码实战
①、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<time.h> 4 //顺序表数据结构定义 5 typedef struct{ 6 int *elem;//存储元素的起始地址 7 int len;//元素个数 8 }SSTable; 9 10 //初始化顺序表 11 void Init_ST(SSTable &ST,int len) 12 { 13 ST.elem=(int*)malloc(sizeof(int)*len); 14 ST.len=len; 15 //随机数生成 16 srand(time(NULL)); 17 int i; 18 for(i=0;i<ST.len;i++) 19 { 20 ST.elem[i]=rand()%100;//生成的随机数是0-99之间 21 } 22 } 23 24 //打印数组中的元素 25 void Print_ST(SSTable ST) 26 { 27 int i=0; 28 for(i=0;i<ST.len;i++) 29 { 30 printf("%3d",ST.elem[i]); 31 } 32 printf("\n"); 33 } 34 35 //交换两个元素 36 void swap(int &a,int &b) 37 { 38 int tmp=a; 39 a=b; 40 b=tmp; 41 } 42 43 //调整子树 44 void AdjustDown(int *A,int k,int len) 45 { 46 int dad=k; 47 int son=2*dad+1;//左孩子下标 48 while(son<=len) 49 { 50 if(son+1<=len&&A[son]<A[son+1])//判断有没有右孩子,比较左右孩子选大的 51 { 52 son++; 53 } 54 if(A[son]>A[dad])//比较孩子和父亲,如果孩子大于父亲,那么进行交换 55 { 56 swap(A[son],A[dad]); 57 dad=son;//孩子重新作为父亲,判断下一棵子树 58 son=dad*2+1; 59 } 60 else 61 { 62 break; 63 } 64 } 65 } 66 67 68 69 70 //用数组取表示树,类似于层次建树 71 void Heap_Sort(int *A,int len) 72 { 73 int i; 74 //建立大根堆 75 for(i=len/2;i>=0;i--) 76 { 77 AdjustDown(A,i,len); 78 } 79 swap(A[0],A[len]);//交换顶部和数组最后一个元素 80 //方法是:不断调整剩余元素为大根堆,因为根部最大,所以再次和A[i]交换(相当于放到数组后面),循环往复 81 for(i=len-1;i>0;i--) 82 { 83 AdjustDown(A,0,i);//剩余元素调整为大根堆 84 swap(A[0],A[i]); 85 } 86 } 87 88 89 //主函数 90 int main() 91 { 92 SSTable ST; 93 Init_ST(ST,10); 94 Print_ST(ST);//打印排序前的顺序表 95 Heap_Sort(ST.elem,10);//所有元素参与排序 96 printf("堆排序后的结果是:\n"); 97 Print_ST(ST); 98 return 0; 99 }
②、结果
4、复杂度分析
①、AdjustDown函数循环的次数是log2n,Heap_Sort函数的第一个for循环了n/2次,第二个for循环循环了n次,总计是3/2nlog2n次,因此时间复杂度是O(nlog2n)。
②、堆排序的最好、最坏、平均时间复杂度都是O(nlog2n)。
③、堆排的空间复杂度是O(1),因为没有使用与n相关的额外空间。
九、归并排序
1、原理解析
如上图所示,把每两个元素归为一组,进行小组内排序,然后再次把两个有序小组合并为一个有序小组,不断进行,最终合并成一个有序数组。
2、代码实战
①、代码
1 #include<stdio.h> 2 #include<stdlib.h> 3 #define N 7 4 //合并操作 5 void Merge(int *A,int low,int mid,int high) 6 { 7 static int B[N];//加static的目的是无论递归调用多少次,都只有一个B[N] 8 int i,j,k; 9 for(k=low;k<=high;k++)//复制元素到B中 10 { 11 B[k]=A[k]; 12 } 13 for(i=low,j=mid+1,k=i;i<=mid&&j<=high;k++)//合并两个有序数组 14 { 15 if(B[i]<B[j]) 16 { 17 A[k]=B[i++]; 18 } 19 else 20 { 21 A[k]=B[j++]; 22 } 23 } 24 while(i<=mid)//如果有剩余元素,接着放入即可 25 { 26 A[k++]=B[i++]; 27 } 28 while(j<=high) 29 { 30 A[k++]=B[j++];//后一半有剩余元素放入 31 } 32 } 33 34 //归并排序不限制是两两归并,此处使用两两归并 35 void MergeSort(int *A,int low,int high) 36 { 37 if(low<high) 38 { 39 int mid=(low+high)/2; 40 MergeSort(A,low,mid);//排序好前一半 41 MergeSort(A,mid+1,high);//排序好后一半 42 Merge(A,low,mid,high);//将两个有序数组合并 43 } 44 } 45 46 //打印数组 47 void print(int *a) 48 { 49 int i; 50 for(i=0;i<7;i++) 51 { 52 printf("%3d",a[i]); 53 } 54 printf("\n"); 55 } 56 57 58 //主函数 59 int main() 60 { 61 int A[7]={49,38,65,97,76,13,27};//数组,共有7个元素 62 MergeSort(A,0,6); 63 print(A); 64 return 0; 65 }
②、结果
4、复杂度分析
①、分析:MergeSort函数的递归次数是log2n,Merge函数循环了n次,因此时间复杂度是O(nlog2n)。
②、总结:
归并排序的最好、最坏、平均时间复杂度都是O(nlog2n)。
归并排序的空间复杂度是O(n),因为使用了数组B,它的大小和A一样,占用了n个元素空间。
十、所有排序算法时间与空间复杂度汇总
1、图示
2、注意
①、稳定性是指排序前后,相等元素位置是否会被交换。
②、复杂性是指代码编写的难度。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· DeepSeek在M芯片Mac上本地化部署