常用排序算法总结
本文包括:选择排序、简单插入排序、折半插入排序、希尔排序、冒泡排序、快速排序、归并排序、基数排序、堆排序
一、选择排序
选择排序是最简单的一种排序算法,它的思想很简单,即:第一趟遍历整个无序的序列,然后将最小的放首位;第二趟继续遍历后面无序的序列,将第二小的放在第二位…故而时间复杂度是O(n^2)
所以代码直接模拟即可
//简单选择
void select_sort()
{
for(int i = 0;i < n;i++){
int min = arr[i], min_index = i;
for(int j = i + 1;j < n;j++){
if(arr[j] < min){
min = arr[j];
min_index = j;
}
}
int temp = arr[i];
arr[i] = min;
arr[min_index] = temp;
show();
}
}
通过运行结果看一看排序过程:
二、简单插入
也是一个非常简单的排序算法。它实现的前提是要保证第i个元素前面的所有元素都要保持有序,因此思路就是,从数组第一个元素开始遍历,先排好第一个元素;再遍历第二个元素,然后排好前两个元素的顺序;再遍历第三个元素,排好前三个元素的顺序…一直到第n个,就排好了。
代码:
//直接插入排序
void insert_sort(){
for(int i = 1;i < n;i++){
int val = arr[i], j;
for(j = i - 1;j >= 0;j--){
if(val < arr[j])
arr[j + 1] = arr[j];
else
break;
}
arr[j + 1] = val;
}
}
运行结果:
第一趟,将4和6排好了
第二趟,将1、4、6,前三个元素排好顺序了。1和4、6比较,只要发现6比1大,6就往后面移动;4也比1大,那么4也往后面移动一格;最后剩下的位置就是1了。
…
一直到最后
三、折半插入排序
这是在简单插入排序算法上进行一点小小的修改。
因为插入排序每一趟,前i个元素肯定都是有序的,所以我们没有必要一个一个地比较大小,可以运用《查找》学到的知识,有序序列下,用折半查找效率更高。
因此,折半插入没有什么新鲜东西,就是折半查找和插入排序的结合,数据量大的时候还是能大大提升查找效率的。
代码:
//折半插入排序
void half_insert_sort(){
for(int i = 1;i < n;i++){
int val = arr[i];
//折半查找
int low = 0, high = i - 1;
while(low <= high){
int mid = (low + high) / 2;
if(arr[mid] < arr[i])
low = mid + 1;
else
high = mid - 1;
}
//位置移动
for(int j = i - 1;j >= high + 1;j--){
arr[j + 1] = arr[j];
}
arr[high + 1] = val; //注意,这里的下标一定是high+1
}
}
折半查找最重要的是搞清楚边界条件,最后元素要放到high + 1的下标上去
四、希尔排序
希尔排序是一种改良的插入排序。
希尔排序开始需要设定一个步长d,根据这个步长我们可以把整个数组分成几个小组,然后每一个小组内部按照直接插入排序来排就可以了。步长要求递减,等到步长小于1时,整个排序算法就完成了(步长 = 1时就是直接插入排序)
以上文字可能不太好理解,给个图:
第一趟排序时,步长为5,故针对这个长度为10的数组,我们可以分为以下五个小组。每个小组内部按照插入排序来进行
第一趟排序后的结果:
和你的想法想法是否一样呢?
总体排序流程:
大家可以手动模拟验证一下
代码:
void shell_sort(int d)
{
//分组
while(d > 0){
//每一组内部还是用插入排序的思想来做
for(int i = d;i < n;i++){
int val = arr[i], j;
//和前面组内的元素对比
for(j = i - d;j >= 0;j -= d){
if(arr[j] > val)
arr[j + d] = arr[j];
else
break;
}
arr[j + d] = val;
}
d -= 2;
show();
}
}
五、冒泡排序
这可能是知名度最高的一种排序算法,同时也很简单。它属于交换排序的一种。
核心思想是:从头开始遍历,每次遍历前后相邻的两个元素,如果前面的元素大于后面的元素,那么将两者交换…所以第一轮下来,可以把最大的元素放最后,第二轮可以把第二大的元素放到倒数第二个位置…最后完成
代码:
//冒泡
void bubble_sort(){
for(int i = 0;i < n - 1;i++){
for(int j = 0;j < n - 1 - i;j++){
if(arr[j] > arr[j + 1]){
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
排序流程:
六、快速排序
被誉为 “效率最高” 的排序算法。虽然快速排序在理论上比堆排序、归并排序慢,但是实际情况下,尤其是处理大量随机无序的数据时,快速排序的效率往往是最高的!因此,快速排序的使用非常广泛。注:快速排序是一种不稳定的排序算法。关于稳定性的问题,我会在文末进行总结。
//快排
void quick_sort(int low, int high){
if(low >= high) return ;
int i = low, j = high;
int srd = arr[low]; //每次取第一个数作为快排的基准数
while(i < j){
while(arr[j] > srd && i < j){ //先从右边开始
j--;
}
while(arr[i] <= srd && i < j){ //因为i一开始是从low出发的,所以是小于等于
i++;
}
//i, j交换
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
show();
//基准数归位。此时srd这个值的位置已经确定下来了。
arr[low] = arr[i]; //此时i==j,填i或者填j都可以
arr[i] = srd;
quick_sort(low, i - 1);
quick_sort(i + 1, high);
}
过程:
快排是递归的,所以这个过程图可能不那么直观
七、归并排序
归并排序我觉得是排序算法中相对难一点的算法。它和快排一样,也有分治递归的思想,但实现起来却没有那么容易。
将整个序列拆分成若干个(分治,对应代码中的merge_sort)
将两个有序的序列合成一个有序的序列(归并:对应代码中的merge)
代码:
//归并排序
//左右有序的时候就能排了
void merge(int arr1[], int L, int M, int R){
int LEFT_SIZE = M - L;
int RIGHT_SIZE = R - M + 1;
int left[LEFT_SIZE];
int right[RIGHT_SIZE];
//fill left
for(int i = L;i < M;i++){
left[i - L] = arr1[i];
}
//fill right
for(int i = M;i <= R;i++){
right[i - M] = arr1[i];
}
//merge into orignal array
int i = 0;int j = 0;int k = L;
while(i < LEFT_SIZE && j < RIGHT_SIZE){
if(left[i] < right[j]){
arr1[k] = left[i];
i++;k++;
}else{
arr1[k] = right[j];
k++;j++;
}
}
while(i < LEFT_SIZE){
arr1[k] = left[i];
i++;k++;
}
while(j < RIGHT_SIZE){
arr1[k] = right[j];
j++;k++;
}
}
//分治
void merge_sort(int arr1[], int L, int R)
{
if(L == R) return ;
//将长数组看成两段
int M = (L + R) / 2;
merge_sort(arr1, L, M);
merge_sort(arr1, M + 1, R);
merge(arr1, L, M + 1, R);
}
大体上的思路就是,先把左边弄成有序的,右边再弄成有序的,最后通过merge方法,将左右两边merge起来,就是一个有序的整体了。
八、基数排序
第三趟做完之后,我们惊讶的发现,数据已经排好了。
基数排序就是这么简单,第一趟按个位分配,收集回来后再按照十位分配到十个捅里面,第三趟再按照百位分配到十个桶里面,因此也叫做桶排序
因为基数排序的这个特点,很容易让人想到用邻接链表去实现。
这就意味着我们要先手动实现以下邻接表这个数据结构,所以代码会比较长一点。
代码:
#include<stdio.h>
#include<stdlib.h>
#define MAX 10005
typedef struct Node{
int val;
int len; //链接链表的长度
Node *next = NULL;
} Node;
int nums[10] = {102,31,24,55,13,201,307,901,100,2};
//对邻接表进行头插
/*void insert(int key, int val, Node* arr){
Node *p = (Node*)malloc(sizeof(Node));
p->val = val;
p->next = arr[key].next;
arr[key].next = p;
}*/
//基数排序需要用尾插法
void insert_tail(int key, int val, Node* arr){
Node *p = (Node*)malloc(sizeof(Node));
p->val = val;
//拿到当前链表的长度
Node* temp = &arr[key];
int len = arr[key].len;
for(int i = 1;i <= len;i++){
temp = temp->next;
}
temp->next = p;
p->next = NULL;
//有一插入了,len++
arr[key].len++;
}
//销毁邻接表
void destroy(int n, Node* arr){
for(int i = 0;i < n;i++){
Node *p = arr[i].next;
while(p != NULL){
Node *temp = p->next;
printf("%d的节点已经删除...\n", p->val);
free(p);
p = temp;
}
}
}
//打印一行
void showLine(int i, Node* p){
printf("当前是第%d行:", i);
while(p!=NULL){
printf("%d ", p->val);
p = p->next;
}
printf("\n");
}
//展示邻接表
void show(Node* arr, int len){
for(int i = 0;i < len;i++){
showLine(i, arr[i].next);
}
}
//show数组
void showNums(){
printf("===当前轮次下的nums数组为:");
for(int i = 0;i < 10;i++){
printf("%d ", nums[i]);
}
printf("\n");
}
//radix = 1:表示按个位建邻接表;radix = 2--->十位
void sort(Node* arr, int radix){
//十个数
for(int i = 0;i < 10;i++){
//取它的关键值
int val = nums[i];
for(int j = 1;j < radix;j++){
val /= 10;
}
int key = val % 10;
//将这个节点数据插在key上面的邻接表
insert_tail(key, nums[i], arr);
}
//将链表上的数据收回来
int idx = 0;
for(int i = 0;i < 10;i++){
Node *p = arr[i].next;
while(p != NULL){
int val = p->val;
//填回到nums数组里去
nums[idx] = val;
idx++;
p = p->next;
}
}
//showNums
showNums();
//show表
show(arr, 10);
}
/***
基数排序
***/
void radix_sort(){
//初始化
Node *arr = (Node*)malloc(10 * sizeof(Node));
int radix = 3;
//个位、十位、百位。循环放桶
for(int i = 1;i <= radix;i++){
//初始化表
for(int j = 0;j <= 9;j++){
arr[j].next = NULL;
arr[j].val = 0;
arr[j].len = 0;
}
//建表,填数据
sort(arr, i);
//销毁
destroy(10, arr);
}
free(arr);
}
int main()
{
radix_sort();
return 0;
}
排序结果:
下面是按照百位排序的结果:
可以看到,按照百位排好之后,nums数组里面的数据就已经有序了,相当神奇吧!
九、堆排序
构造一个堆的过程:
因为每一趟下来,会把最大值放到完全二叉树的最后节点,然后将其无情删去,这样破坏了堆的性质。因此,每一趟之后要经历一个筛选调整的过程。而每一次筛选调整就是用前面的二叉树再建一个新堆的过程。
可以看到,堆排序的特点是,每一趟都可以找到一个最大值或最小值,这一点上和选择排序、冒泡排序有相似之处。
堆排序是不稳定的。
各种排序算法总结:
希尔排序的时间复杂度还没有证明出来
注意四种不稳定的排序算法。简单选择和希尔也是不稳定的。