理解常用的八个排序
Posted aaaaaaaWoLan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解常用的八个排序相关的知识,希望对你有一定的参考价值。
文章目录
排序的概念及其运用
排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次
序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排
序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
排序运用
常见的排序算法
// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n)
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
// 归并排序递归实现
void MergeSort(int* a, int n)
// 归并排序非递归实现
void MergeSortNonR(int* a, int n)
// 计数排序
void CountSort(int* a, int n)
// 测试排序的性能对比
void TestOP()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int)*N);
int* a2 = (int*)malloc(sizeof(int)*N);
int* a3 = (int*)malloc(sizeof(int)*N);
int* a4 = (int*)malloc(sizeof(int)*N);
int* a5 = (int*)malloc(sizeof(int)*N);
int* a6 = (int*)malloc(sizeof(int)*N);
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, 0, N-1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
printf("InsertSort:%d\\n", end1 - begin1);
printf("ShellSort:%d\\n", end2 - begin2);
printf("SelectSort:%d\\n", end3 - begin3);
printf("HeapSort:%d\\n", end4 - begin4);
printf("QuickSort:%d\\n", end5 - begin5);
printf("MergeSort:%d\\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
排序OJ(可使用各种排序跑这个OJ) OJ链接 (插入、选择、冒泡、前后指针法超时)
插入排序
插入排序的思想就像我们打扑克牌,摸完所有牌之后,要将牌进行排序,通常都是将牌插在对应的位置
比如一个数组a,从a[0],a[1]…a[k],a[k+1]…一直到a[n],使用插入排序的话,就是先将a[0],a[1]…a[k]看成一个有序数列,然后将a[k+1]与a[k]比较,如果比a[k]小,则继续向前比较,直到a[k]大于其中某个数,就将a[k]插在该数的后面
整体流程:
- 将a[0]看成有序数列,a[1]向前比较,再插入
- 将a[0],a[1]看成有序数列,a[2]向前比较,再插入
- 将a[0],a[1],a[2]看成有序数列,a[3]向前比较,再插入
- 将a[0],a[1],a[2],a[3]看成有序数列,a[4]向前比较,再插入
- …
- 将a[0],a[1],a[2],a[3]…a[k]看成有序数列,a[k + 1]向前比较,再插入
- …
- 将a[0],a[1],a[2],a[3]…a[k],a[k+1]…a[n-1]看成有序数列,a[n]向前比较,再插入
思路: 使用一个end来表示每次插入位置,每次需要插入的元素就在a[end]之后,也就是a[end]是小于需要插入的元素的
代码:
//插入排序 时间复杂度O(N^2)
void InsertSort(int* a, int n)
{
if (n < 2)
return;
for (int i = 0; i < n - 1; ++i)
{
int end = i;
int tmp = a[i + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
break;
}
//两种情况
if (end < 0)//元素插入到数组的第一位
a[0] = tmp;
else//元素插入到end之后
a[end + 1] = tmp;
//实际上,不加判断,直接写成a[end + 1]也是一样的,因为end<0时,也就是end == 1,end+1就是数组的第一个位置了
}
}
插入排序的时间复杂度:最坏情况下:O(N^2 ),如果我们的插入排序是要排成升序,则完全是降序的情况下时间复杂度为O(N^2)
最好情况:O(N),即已经是有序的情况下,就只需要遍历数组,时间复杂度O(N)
希尔排序
希尔排序本质上是插入排序的变种,我们来看机组几组例子:
我们观察到这两个例子都是接近有序的,所以只移动了几次,时间复杂度近似为O(N)
希尔排序就是利用这个特点,先将数组排成近似有序的,再进行一次插入排序。
大致分为两个步骤:
- 预排序
- 插入排序
那么如何实现预排序呢?
可以利用一个间隙gap,每隔gap个元素分成一组,再对每组元素进行排序,最后即可得到一个接近有序的数组。
将数组分为gap部分:
然后蓝色的线为一组,红色的线为一组,绿色的线为一组,分别对三组数据进行插入排序,即为预排序过程。
预排序好后,就变成了下图的样子:
此时的数组就是一个接近有序的数组,再次对数组整体进行插入排序,得到有序数组。
实际上,当gap为1时,就可以将预处理过程看成是插入排序了。
基于以上分析,我们可以将gap设置为数组长度n的三分之一、四分之一或者五分之一都行,经过一轮预处理后,再对gap进行缩小,直到最后gap等于1,就可以看作是插入排序了。
关于代码里有几个问题需要大家回答:
- 为什么gap要以gap = (gap / 3 + 1)的方式缩小而不是gap = gap / 3
- 为什么for循环里循环条件要写成i < n - gap而不是i < n,更新条件是++i而不是i += gap呢?
代码:
void ShellSort(int* a, int n)
{
int gap = n;//先初始化gap
while (gap > 1)
{
gap = (gap / 3 + 1);//gap大约为数组长度的三分之一
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)//查找插入的位置
{
if (a[end] > tmp)
{
a[end + gap] = a[end];/
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
问题一:如果使用gap = gap / 3的方式缩小gap的话,有可能就不能使gap缩小到1。比如n = 6时,一开始gap = 2,随后根据gap = gap / 3,gap就变成0了,就没有完成整体的插入排序,所以要使用gap = (gap / 3 + 1)的形式
问题二:因为我们的tmp是设置为a[end + gap],而end == i,当tmp小于前面的元素时,就向前插入,所以只需要将i控制在n - gap就行。那么为什么更新条件是++i而不是i+=gap呢?如果是i+=gap的话,以上面的图为例,就只能对蓝色小组进行预排序,之后就结束循环了。
而++i的方式则是挨个处理蓝红绿三个小组,可以确保每个小组都被处理到。
选择排序
我们可以观察到,选择排序是遍历数组,选择一个最小的数,然后与数组开头元素交换,以此循环下去。
时间复杂度O(N^2)。
我们可以用一种更快的方式,但并不能改变时间复杂度:在找最小数的同时找最大数,将最大数放在数组末尾,最小数放在数组开头。我们是以下标来标记对应最大值最小值的位置的。
但是这样写有一个需要注意的地方,即当数组的开头就是最大数时,达不到我们想要的效果。比如[4,2,1,3],最小值为1,最大值为4,我们先将最小值与数组开头元素交换,得到[1,2,4,3],接下来我们还想将最大值与数组末尾元素交换,但此时,原本最大元素的位置以及不是最大值了,而是1,此时就需要更新最大值的位置:maxindex = minindex(新的4的位置)
既然当最大值在数组开头时需要处理,那当最小值在数组末尾时需要处理吗?以[2,4,3,1]为例,最小值为1,最大值为4,先将1和2交换,得到[1,4,3,2],此时maxindex位置处的元素仍然是4,直接交换4,2即可,不需要进行特殊处理。
代码:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int maxindex = left;//一开始初始化最大最小值均为数组开头元素
int minindex = left;
for (int i = left; i <= right; ++i)
{
if (a[maxindex] < a[i])//查找最大值
maxindex = i;
if (a[minindex] > a[i])//查找最小值
minindex = i;
}
//将最大值最小值归位
Swap(&a[left], &a[minindex]);
if (left == maxindex)
maxindex = minindex;
Swap(&a[right], &a[maxindex]);
++left;
--right;
}
}
堆排序
在了解堆排序之前需要先了解一下什么是堆,堆就类似一棵二叉树。不过堆是相对更有序的二叉树,有大堆和小堆之分。
可以先了解一下作者的上一篇文章:堆的实现及应用
了解了堆后,如果我们想要实现排升序,就要先建大堆。
为什么建大堆而不是小堆呢?
我们来看建小堆的话是怎么一个情况:
以[43,12,3,77,14,2,10,56]为例
建好小堆后:[2,12,3,56,14,43,10,77]
堆顶最小的元素,所以我们需要拿出第二小的数,就要将2从堆中分离出来,也就是剩下的元素从成组成堆。2是根节点,将2脱离出来后,意味着又要重新建堆,时间复杂度O(N),如果反复这样的话,整个排序的时间复杂度就为O(N^2),堆的优点就没有被体现出来。
所以,要想排升序的话需要建大堆(排降序是小堆)
我们建好大堆后,堆顶就是最大的数了,将最大的数与堆底的数交换,将堆的大小减1,再进行向下调整,如此往复,当堆只剩下一个元素时,排序就完成了。
也许大家会觉得,那建小堆的话,也将堆顶与堆底的数据进行交换不就可以了。这么做确实可以完成排序,不过完成的是降序,因为最小值在数组末尾,然后依次类推…
整体过程如下:
代码:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//向下调整算法
void AddJustDown(int*a, int n, int parent)
{
int child = 2 * parent + 1;
while (child < n)//控制孩子节点不能越界
{
//将左右孩子中较小的与父亲比较
//右孩子不能越界
if ((child + 1) < n && a[child] < a[child + 1])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
//堆排序——s
void HeapSort(int* a, int n)
{
//建大堆
//时间复杂度O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AddJustDown(a, n, i);
}
int end = n - 1;
//总体时间复杂度O(n*logn)
while (end > 0)
{
Swap(&a[0], &a[end]);
AddJustDown(a, end, 0);
--end;
}
}
冒泡排序
冒泡排序的思想类似于选择排序,冒泡是对数组里的元素逐一比较,如果前一个元素大于后一个元素,则调换两者的位置,以此类推,可以将数组中的最大值放到数组末尾,下一轮排序时数组末尾就不用再进行比较了。经过n-1轮调换,就可以将数组排好序。
两者的时间复杂度都为O(N^2),但在数组接近有序的情况下,冒泡的效率是要高于选择排序的。因为冒泡可以对数组有序进行判断,如果有序就不再接着排序了。而选择排序是无法判断数组是否有序的,也就是需要无条件遍历数组。
代码:
void BubbleSort(int* a, int n)
{
int i = 0;
int j = 0;
int flag = 0;//用来判断数组是否有序
for (i = 0; i < n - 1; ++i)
{
flag = 0;
for (j = 0; j < n - i - 1; ++j)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
快速排序
快速排序有四种实现方法:
- 左右指针法
- 挖坑法
- 前后指针法
- 非递归实现
左右指针法
我们先解释左右指针法。
大概思路是选出一个key,key一般是最左边或者最右边的元素,将key放到正确的位置上去,比如key是第六大的数,就将key放在数组的第六个位置,而key左边的数小于key,右边的数大于key,依次再分别对左右两边数组进行递归,当数组缩小到只有1个或0个元素时,就停止递归。
所以我们可以定义一个左指针left,一个右指针right,right先从右向左查找小于key的元素,找到了就停下来,left就开始从左向右查找大于key的元素,找到了也停下来,再交换right、left处的元素。由此再继续right和left的查找,直到left与right相遇,此时将相遇处的元素与key交换,再进行递归。
我们必须要注意:相遇处的元素是一定小于key的,因为是right先走,如果是left先走就不是了。
观察一个示例:
right先走,找到比key(6)小的数字,也就是1,left再查找比key大的数字,也就是7
交换7和1的位置,再继续进行查找
再查找的话,right就指向了4,left再向右查找,此时left还未找到比key大的数字就与right相遇了,相遇元素就是4,4<6,交换4、6的位置。而此时,6的位置就被找好了,再对6左边的数组与右边的数组进行相同的处理即可完成排序。
我们看到,相遇元素4是小于key的,这不是巧合,因为是right先走,所以相遇的元素一定是right先找到的。
相遇时有两种情况:
-
右遇左:
a.数组有序
b.左右位置的值刚进行交换,所以左指针指向的值小于key,而右指针向左没有找到小于key的元素,遇到左指针,所以相遇值小于key
-
左遇右:右指针已经找到小于key的元素,左指针向右没有找到大于key的元素,遇见了右指针,所以相遇值小于key
但是,如果是left先走,相遇元素就一定大于key了,并且key要是最右边的元素。
由于有三种方法,我们将每种方法单独写成一个函数SingleWaySorting,再共用一个函数QuickSort包装进行递归。
看下面的代码,大家思考一下为什么这里的循环需要加上left < right的限制呢?
while (left < right && a[right] >= a[keyi])//查找小于key的元素
{
--right;
}
while (left < right && a[left] <= a[keyi])//查找大于key的元素
{
++left;
}
代码:
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//单趟排序——左右指针法
int SingleWaySorting1(int* a, int begin, int end)
{
int left = begin + 1;
int right = end;
int keyi = begin;
while (left < right)
{
while (以上是关于理解常用的八个排序的主要内容,如果未能解决你的问题,请参考以下文章