目录
前言
一、常见的排序方法
二、插入排序
1.插入排序的思想
2、直接插入排序
2.1直接插入排序的实现
2.2插入排序特点?
3、希尔排序
3.1希尔排序介绍
?3.2希尔排序实现
三、选择排序?
1、基本思想
2、直接选择排序
?3、堆排序
四、交换排序
1、冒泡排序
2、快速排序
2.1快速排序单趟排序的实现方法
2.2快速排序优化
2.3快递排序的非递归写法
五、归并排序
1、归并排序概念
2、归并排序的递归实现?
3、归并排序的非递归实现
?六、非比较排序
七、排序算法复杂度和稳定性分析?
总结
前言
哈喽,小伙伴们大家好。排序一直是算法中的一个重要内容,对于计算机初学者有一定的挑战。相信很多刚刚接触排序的小伙伴除了冒泡排序掌握的非常熟练之外,对其它的排序方法都是一知半解(反正我刚开始学的是这样hhh)。那么今天我将把几种常用的排序进行汇总,对原理以及实现方法进行讲解,希望能给大家带来帮助。
一、常见的排序方法
排序概念:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
常见的排序方法:
二、插入排序
1.插入排序的思想
相信大家都应该玩过扑克牌吧,在我们玩斗地主的时候,为了方便看牌,常常会一边摸牌一边把牌按大小顺序排列起来,在这个过程中其实就用到了插入排序的思想。我们不妨一起回忆一下平常在摸牌的时候是怎样进行排序的。
首先,我们摸了一张5放到手里。然后又摸到了一张7,用7和5去比较,7比5大所以我们把它放到了5的右边。然后又摸到了一张6,6和7比较,6比7小,6继续和5比较,6比5大,放到5的右边……在这之后我们每摸一张牌都和手中原来的牌从右向左依次比较,直到找到比摸到的牌小的牌,然后把摸到的牌插入到它的右边,这个过程就是插入排序的思想。
2、直接插入排序
2.1直接插入排序的实现
按照我们上面描述的思想,我们可以进行实现,代码如下:
insert_sort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
//[0,end]有序,把a[end+1]插入进去
int end = i;
int temp = a[end + 1];
while (end >= 0)
{
if (a[end]>temp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = temp;
}
}
2.2插入排序特点?
- 时间复杂度为O(N^2)
- 元素集合越接近有序,插入排序时间效率越高。在有序的情况下时间复杂度为O(N)
- 空间复杂度:O(1)
3、希尔排序
3.1希尔排序介绍
简介:希尔排序是对直接插入排序的优化。通过上面的介绍我们知道,在越接近有序的情况下,插入排序的效率越高。既然如此,就有人想到,在进行排序之前,能不能先进行一下预排序,使元素集合处在接近有序的状态来提高效率呢?于是希尔排序就诞生了。
思想:希尔排序又称缩小增量法,基本思想是把一个元素集合分为gap组,每个组之间元素的距离为gap,然后分别对几个组进行排序。排序完成后再缩小gap,再重新分组进行排序,重复此过程。直到gap为1,预排序完成,再进行最后的插入排序。
?3.2希尔排序实现
代码如下:
shell_sort(int* a,int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//保证最后一次gap是1
for (int i = 0; i < n - gap; i++)
{
int end = i;
int temp = a[end + gap];
while (end >= 0)
{
if (a[end] > temp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = temp;
}
}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
- 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,我们可以参考一下书中给出的。
三、选择排序?
1、基本思想
选择排序的思想非常简单粗暴,即每次从要排序的集合中选出一个最大或最小的数放在序列起始位置,重复此操作,直到序列中的全部元素被排完。
2、直接选择排序
根据选择排序的原理,我们不难把代码实现出来。
void select_sort(int* a, int n)
{
int begin = 0;
int end = n - 1;
//最小的放到队头,最大的放到队末
while (begin < end)
{
int max = begin, min = begin;
for (int i = begin; i <=end ; i++)
{
if (a[i] > a[max])
{
max = i;
}
if (a[i] < a[min])
{
min = i;
}
}
swap(&a[min], &a[begin]);
if (max == begin)//如果最大的数在队头,交换之后它会跑到min的位置,所以要把max指向min
{
max = min;
}
swap(&a[max], &a[end]);
begin++;
end--;
}
}
直接选择排序的特性总结
- ?直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- ?时间复杂度:O(N^2)
- ?空间复杂度:O(1)
?3、堆排序
堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。我在二叉树的文章中介绍过该排序,这里不再过多介绍,感兴趣的小伙伴可以去看一下我之前的文章。
四、交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
1、冒泡排序
代码实现如下:
void bubble_sort(int* a, int n)
{
int flag = 0;
for (int i = 0; i < n-1; i++)
{
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] > a[j + 1])
{
swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0) //算法优化,如果flag为0,说明没有发生交换,排序已经完成,直接退出即可
{
break;
}
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- ?时间复杂度:O(N^2)
- 空间复杂度:O(1)
2、快速排序
基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。然后队左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
2.1快速排序单趟排序的实现方法
(1)hoare版本
把左边第一个数设为基准值,先从右往左找,找到比基准值小的就停下来,在从左往右找,找到比基准值大的停下来,交换两个位置的数。然后一直重复该过程,直到左右两个指针相遇,把相遇点的值与基准值交换。
int part_sort1(int* a, int begin, int end)
{
int key = begin;
while (begin < end)//左右相遇时停下来
{
//右边先走,找小
while (begin < end&&a[end] >=a[key])
{
end--;
}
//左边再走,找大
while (begin < end && a[begin] <= a[key])
{
begin++;
}
swap(&a[begin], &a[end]);
}
swap(&a[begin], &a[key]);
key = begin;
return key;
}
(2)挖坑法
把左边第一个位置的值定为基准值存起来,并把该位置设为坑。从右往左找比基准值小的数,找到之后扔到坑里,然后把该数所在的位置设成新坑。再从左往右找比基准值大的数,找到之后扔到坑里,该数所在的位置设成新坑。重复此过程,直到左右指针相遇,把相遇位置的值设成基准值。
int part_sort2(int* a, int begin, int end)
{
//在第一个位置挖坑
int hole = begin;
int temp = a[hole];
while (begin<end)
{
//右边先走,找小,往坑里填
while (begin < end && a[end] >= temp)
{
end--;
}
a[hole] = a[end];
hole = end;
//左边再走,找大,往坑里填
while (begin < end && a[begin] <= temp)
{
begin++;
}
a[hole] = a[begin];
hole = begin;
}
//把最开始坑里的数填到相遇处
a[begin] = temp;
hole = begin;
return hole;
}
(3)前后指针法
把左边第一个位置的值定为基准值,令前指针指向序列第一个位置,令后指针指向序列第二个位置。后指针开始向后移动,每次遇到比基准值小的值,前指针就向前移动一次,再把两个指针指向的值进行交换。重复该过程,直到前指针走到头停下来,把此刻后指针指向的值与基准值也就是开头位置的值进行交换。
int part_sort3(int* a, int begin, int end)
{
int key = begin;
int pre = begin;
int cur = begin + 1;
while (cur <= end)
{
if (a[cur] < a[key])
{
pre++;
swap(a[cur], a[pre]);
}
cur++;
}
//当cur指针走到头时跳出循环,把pre此刻指向的值与key位置的进行交换
swap(&a[pre], &a[key]);
key = pre;
return key;
}
2.2快速排序优化
有了上面的单趟排序,想要实现完整的快排就变得十分简单,只要分别对基准值的左右两个区间进行递归就ok了。但是这样写出的快排还存在一定问题,我们需要进一步优化。
(1)三数取中法选基准值
快排这种通过选基准值分割序列进行排序的思想非常巧妙,如果每次取的值的大小都恰好是序列中间的值,那递归的深度只需要达到logN,就能全部递归完。但是我们在实际取基准值的时候采用的方法是直接把第一个数作为基准值,这样很难保证每次这个数的大小都在序列中间,也就不能使排序效率达到理想的效果。如果序列原本的顺序是接近顺序或接近逆序的话,那么排序效率甚至会达到O(N^2)。这时候我们就会发现一个恐怖的问题,快排居然不快了!这时候通常有两个解决办法,一是给快速排序换个名字,如果不叫快排的话那么排的慢当然是情有可原的啦。二是进行算法优化,不知道大家选择第几种呢?hhh反正我选第二种。那么这时就要介绍一种思想,三数取中法。
三数取中法:三数取中法是指每次排序前,都把序列的开头值结尾值和中间值进行比较,取三个数里大小在中间的那个做基准值,这样做就可以避免取的基准值过大或过小。
//三数取中
int get_mid_indx(int* a, int begin, int end)
{
int mid = (end - begin) / 2;
if (a[begin] > a[end])
{
if (a[mid] > a[begin])
{
return a[begin];
}
else if (a[mid] > a[end])
{
return a[mid];
}
else
{
return a[end];
}
}
else
{
if (a[mid] > a[end])
{
return a[end];
}
else if (a[mid] < a[begin])
{
return a[begin];
}
else
{
return a[mid];
}
}
}
(2)递归到较小的区间时,可以使用插入排序
我们都知道,每次递归都需要开栈帧,这个过程是比较消耗时间的。在快速排序过程中,往往当区间越小时,进行递归是很不划算的,为了排十来个数进行一次递归有点杀鸡用牛刀的感觉。排这种较短序列时,用插入排序更为合适。而且要注意一点,越是小区间在整个递归过程中占的比例越多,因为每递归一层,序列长度都缩减二分之一,序列个数都翻倍,区间越小,翻得倍数越多。所以优化小区间其实能减少大量的递归次数。
快速排序最终代码实现:
结合以上两点优化,最后的快排代码如下:
void quick_sort(int* a, int begin, int end)
{
//当区间为一个值或者不存在时返回
if (begin >= end)
{
return;
}
int midindx = get_mid_indx(a, begin, end);
swap(&a[midindx], &a[begin]);
if (end - begin > 10)//如果区间大于10,采用递归
{
int key = part_sort3(a, begin, end);
quick_sort(a, begin, key - 1);//递归左区间
quick_sort(a, key + 1, end);//递归右区间
}
else //区间小于10,采用插入排序
{
insert_sort(a + begin, end - begin + 1);//注意传a+begin才是当前序列的地址
}
}
2.3快递排序的非递归写法
非递归写法可以借用栈完美模仿递归过程。每取出一个区间排完序后,就再插入两个新区间(先插右再插左),这样就实现了先序遍历过程,和递归过程相同。
//快速排序非递归
void quick_sort_nonr(int* a,int begin,int end)
{
ST st;
StackInit(&st);
//注意先插区间右值,再插左值
StackPush(&st, end);
StackPush(&st, begin);
while (!StackEmpty(&st))
{
//取出一段区间后进行排序
int begin1 = StackTop(&st);
StackPob(&st);
int end1 = StackTop(&st);
StackPob(&st);
int key = part_sort3(a, begin1, end1);
//插入两段新区间
if (key + 1 < end1)
{
StackPush(&st, end1);
StackPush(&st, key+1);
}
if (begin1<key-1)
{
StackPush(&st, key-1);
StackPush(&st, begin1);
}
}
}
五、归并排序
1、归并排序概念
基本思想:归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列。想要进行归并操作,必须保证两个子序列有序,而想要对子序列进行归并操作,必须保证子序列的子序列有序,所以归并排序是典型的后序遍历。
2、归并排序的递归实现?
进行归并操作时,需要建立一个新序列,把原序列的值归并到新序列,再把新序列的值拷贝回原序列。
代码如下:
void _merge_sort(int* a, int begin, int end, int* temp)
{
//区间为1或不存在时返回
if (begin >= end)
{
return;
}
int begin1 = begin;
int end1 = (begin + end) / 2;
int begin2 = end1 + 1;
int end2 = end;
//递归
_merge_sort(a,begin1,end1,temp);
_merge_sort(a,begin2,end2,temp);
//开始归并
int i = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
temp[begin + i] = a[begin1];
i++;
begin1++;
}
else
{
temp[begin + i] = a[begin2];
i++;
begin2++;
}
}
while (begin1 <= end1)
{
temp[begin + i] = a[begin1];
i++;
begin1++;
}
while (begin2<= end2)
{
temp[begin + i] = a[begin2];
i++;
begin2++;
}
//把临时数组的值拷贝到原序列中
for (int i = begin; i <= end; i++)
{
a[i] = temp[i];
}
}
//归并排序
void merge_sort(int* a, int begin, int end)
{
int* temp = (int*)malloc(sizeof(int) * (end - begin + 1));
_merge_sort(a, begin, end, temp);
free(temp);
}
3、归并排序的非递归实现
归并排序的非递归实现起来比较复杂,主要利用分组循环的思想进行实现,先一个数为区间进行递归,再两个数为区间进行递归,依次增加,直到一个区间包含所有数为止。要注意,代码实现时很容易出现越界,要注意边界的修正。
//归并排序非递归
void merge_sort_nonr(int* a, int n)
{
int gap = 0;
int* temp = (int*)malloc(sizeof(int) * n);
//分组循环递归
while (gap < n)
{
gap += 1;
for (int j = 0; j < n; j += 2 * gap)
{
int begin1 = j;
int end1 = j + gap - 1;
int begin2 = j + gap;
int end2 = j + 2 * gap - 1;
//边界修正
if (end1 >= n || begin2 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
//归并
int i = 0;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
temp[j + i] = a[begin1];
i++;
begin1++;
}
else
{
temp[j + i] = a[begin2];
i++;
begin2++;
}
}
while (begin1 <= end1)
{
temp[j + i] = a[begin1];
i++;
begin1++;
}
while (begin2 <= end2)
{
temp[j + i] = a[begin2];
i++;
begin2++;
}
//把临时数组的值拷贝到原序列中
for (int i = j; i <=end2 ; i++)
{
a[i] = temp[i];
}
}
}
}
?六、非比较排序
基本思想:非比较排序里我们主要介绍一个计数排序。计数排序是采用映射的思想,建一个空序列,然后根据原序列每个数的大小与新序列的下标形成映射关系,在新序列中进行计数。再进行反向映射,根据计的个数来实现排序。
//计数排序
void count_sort(int* a, int n)
{
//找最小数和最大数
int min = a[0];
int max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] >max)
{
max = a[i];
}
}
//计算有多少个不同的值,开相应个数的数组
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
memset(count, 0, sizeof(int) * range);//把序列值清零
//计数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//把计数数组中的值回写回去
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i] = j + min;
i++;
}
}
}
?计数排序的特性总结:
- 计数排序在数集中时,效率很高
- 时间复杂度O(max(N,范围))
- 空间复杂度O(范围)
七、排序算法复杂度和稳定性分析?
稳定性:假设在一个序列中r[i]=r[j]且r[i]在r[j]前面,经过多次排序后,r[i]仍在r[j]前面,则成为该排序是稳定的。
稳定性的意义:稳定性在单种类型的数据排序中是体现不出来的,在多种数据的组合,比如结构体中才能体现出来。例如高考报志愿在总分相同的条件下,优先录取语文分高的,这时候我们就要用稳定的排序来排。先根据语文分数排一次,再根据总分排,就能实现总分相同语文分高的在前面。
?从图中可以看出,只要与非相邻数据发生交换的排序一般都不稳定。?
总结
本文主要介绍了最为常用的几种排序方法,希望能给小伙伴们带来帮助。如果觉得写的还不错的话不妨点个赞支持一下博主,你们的支持就是我创作的最大动力。感谢阅读,来日方长,期待再次相见。
|