目录
前言:
这是我继上一篇排序所更新的新一篇排序,其中讲解了选择排序的两头并排,基本的冒泡排序,还有堆排序,不过对于堆排序来说,我在之前学习的时候就已经特别为其写下了一篇博客,在本片中我就会当作大家懂得堆的前提下讲解,所以有想要特别了解其本质的可以转到我之前的那一篇博客上去。
一、选择排序
选择排序的思想:
选择排序顾名思义,就是每次从一堆数据里面找到最大或则最小的数据,然后与数组的起始位置或则结束位置的数据进行交换。
上述操作完成之后,我们就将一个数据放到了它正确的位置上去,此时,我们想要找到次大或次小的数据,就需要缩小查找空间,不然又会再次找到原来哪一个数据。看下图操作。
整个查找操作通过遍历数组,然后对比得知。
选选择排序的优化:
通过查看上图我们可以看到,每一次只查找一个数据实在是太没有效率了,所以我们可以在每一次排序都找到当前查找数据的最大和最小值,分别与队尾和队首进行交换,这样就直接提速了一倍。
选择排序代码:
可以看到我们用两个下标表示当前查找数组的长度,注意一点,这里我们的数组是一个闭区间,别把自己坑了。
进入循环之后,每次找到最大值和最小值,与队尾与队头交换,然后区间缩小2,直到区间不存在。
如果每一次只查找一个数据不会出现最大值的下标就是起始位置,然后在最小值与起始值交换之后,最大值的位置就会出现在原来最小值的位置,所以需要判断最小值交换之后是否会影响最大值的选取。
//选择排序
void SelectSort(int* arr, int size)
{
//当次循环的起始值
int Begin_Index = 0;
//当次循环的结束值
int End_Index = size - 1;
//每一次循环的起始值和结束值都会在自己方向缩小一位
for (Begin_Index = 0; Begin_Index <= End_Index; Begin_Index++, End_Index--)
{
//最大值和最小值对应的下标
int Min_Index = Begin_Index;
int Max_Index = Begin_Index;
//遍历查找最大值和最小值下标
for (int j = Begin_Index; j <= End_Index; j++)
{
//小于最小值
if (arr[j] < arr[Min_Index])
{
Min_Index = j;
}
//大于最大值
if (arr[j]>arr[Max_Index])
{
Max_Index = j;
}
}
//交换初始位置与最小位置之间的值
Swap(&arr[Begin_Index], &arr[Min_Index]);
//如果初始位置就是最大值的位置,那么交换之后,最大值对应之前最小值位置
if (Begin_Index == Max_Index)
{
Max_Index = Min_Index;
}
//交换最后位置与最大位置之间的值
Swap(&arr[End_Index], &arr[Max_Index]);
}
}
选择排序的评价:
伙伴们,无论是从代码的书写还是从逻辑的角度,都能看出选择排序无论怎么优化,它永远都是稳定的差劲,可以看到,它每一次最多选取两个数字去到正确的位置,当一个数组的长度足够长,而且该数组本身还是有序的,这个排序还是需要一次一次的查找,然后交换,再查找,整个程序的时间复杂度稳定处于O(n^2),所以虽然该方法很简单,对于实际应用来说,能不用就不用吧,掌握了就好。
二、冒泡排序
冒泡排序的思想:
说到冒泡排序,想必大家一定熟悉得不得了,应该说冒泡排序基本上是我们学习语言最早接触得一类排序手段了。
它的主要思想其实也是每次比较让一个值去到一个正确的位置,例如这里有十个数据[ 10 9 8 7 6 5 4 3 2 1 ],我们想要得到一个升序的数组,那么此时就需要先将10送回家,也就是10与9比,大于,然后交换,10又与8比较,大于,再交换,如此循环操作,直到10到了最后位置,整个过程10比较了9次。
在下一次排序时,就需要将9送回家,此时10已经回到了正确位置,那么他就不再进入比较环节,所以一共有9个数据比较,需要比较8次,以此类推。直到只有1个数据。看下图。
冒泡排序的代码:
可以看到冒泡排序的逻辑十分简单,第一层循环确定比较长度,第二层循环表示需要比较的次数,然后通过升序或则降序规则比较交换比较的两个数据就行。
//冒泡排序
void BubbleSort(int* arr, int size)
{
//size个数,需要比较size-1次
for (int i = 0; i < size - 1; i++)
{
//每次需要size-1-i次交换才能到正确位置
for (int j = 0; j < size - 1 - i; j++)
{
if (arr[j]>arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
}
}
}
}
冒泡排序的评价:
冒泡排序是一个很典型的O(n^2)模型,甚至都没有什么优化空间,比起选择排序来说都要差劲一点,不过对于我第一个接触的排序手段来说,我还是很喜欢用的,因为它写起来很简单,对于一些少量数据的排序,用冒泡排序还是很舒服的,不过数据量较大的时候还是老老实实换成其余几个大哥排序手段吧。
三、堆排序
堆排序的思想:
对于堆排序来说,我们首先拿到一组数据就需要将该组数据整理成为堆。可能大伙对于堆有一点点疑惑,所以我还是简单讲解一下,堆其实就是一个用数组表示的完全二叉树,堆顶的数据一定会比它之后的数据大或小,以此来判断,它是一个大堆或者小堆。
如果我们想要将数据变为一个升序序列,那么就需要建立一个大堆,每一次都将堆顶数据与当前数组的最后一个数据进行交换,然后调整堆,重复此操作。
可以看到,我们将10与1交换之后,10到了正确的位置,所以10就退出了我们的排序序列中,但是1却不是,甚至破坏了堆的结构,所以这个时候需要向下调整1的位置,因为该组数据除了1,其余位置都满足堆的要求,所以直接调整1就行,让它不断向下移动,选出次大位置坐到堆顶位置。
堆排序的向下调整代码:
因为孩子结点与父节点的对应关系为父亲结点下标*2+1或+2,那么此时,就能完成我们不断向下查找的方式,因为我们的最大值被换下去了,所以需要找到次大值,次大值只能在最大值的下一层也就是堆顶的儿子结点,我们不知道谁大,所以需要对两个数据作比较。然后将大的那个值重新作为堆顶。又因为1这个数被交换下来之后可能也不是正确位置,所以我们当作它还在堆顶位置处理,重复刚才操作,与它的孩子结点比较,然后看是否交换,当1到了正确位置,也就是比它的孩子结点大或者,走到叶子结点时就结束,也就重新成为了新的堆。
//向下调整法
void Adjust_Down(int* arr, int size, int Parent)
{
//传入父节点的下标,求出孩子结点的下标
int Child = Parent * 2 + 1;
//当孩子结点还没有超过总结点个数证明还可以继续比较
while (Child < size)
{
//比较左右孩子谁大谁小,前提是在右孩子还存在的情况下
if (Child + 1 < size && arr[Child] < arr[Child + 1])
{
Child++;
}
//当父节点小于孩子结点,交换两个结点的值
if (arr[Parent] < arr[Child])
{
//继续向下传递,为下一次比较做准备
Swap(&arr[Parent], &arr[Child]);
Parent = Child;
Child = Parent * 2 + 1;
}
//因为向下调整的基础为原本就是一个堆,所以当父节点大于了子节点,表示不需要再次向下调整,退出
else
break;
}
}
堆排序代码:
通过向下调整法建堆的方式为,找到最后一个孩子结点的父亲,然后依次向前移动,还记得我们向下调整的基础吗,那就是除了被调整对象,其余的结点满足堆的要求。所以在这样调整完成之后会得到一个完整的堆。具体详解请看我写的关于堆的博客,里面有详细解释。
void HeapSort(int* arr, int size)
{
//向下建堆法是先找到最后一个孩子的父亲,然后向下调整,找到下一个父亲
//直到父亲找到根节点并调整完
int Parent = (size - 1 - 1) / 2;
while (Parent >= 0)
{
Adjust_Down(arr, size, Parent);
Parent--;
}
//因为是升序,所以需要大堆,然后大堆堆顶与最后一个值交换,然后对根向下调整,找到次大值
int i = size-1;
while (i > 0)
{
Swap(&arr[0], &arr[i]);
Adjust_Down(arr, i, 0);
i--;
}
}
堆排序的评价:
堆是我接触的第一个大哥排序,也是我感觉学会之后提升巨大的排序手段,它成功的完成了排序的时间复杂度从O(n^2)变为了O(nlogn),这是一个巨大的突破,让我们对于大数据排序也有了应对手段,不再拘泥于原本的O(n^2)了。但是整体来讲堆排序需要的代码能力与逻辑抽象思想有一点高,他需要控制的量比较多,对于初学者有些困难,所以在大哥排序当中,我并不推荐它,但是它对于我们的能力提升有很大帮助,所以还是需要学会。建堆的手段还有向上调整法建堆,但是它的效率没有向下调整法高,不过理解起来更简单,所以如果不太理解向下调整法,可以先尝试理解向上调整法。
三种排序手段的时间比较:
下面是对冒泡排序,选择排序,还有推排序对于100000个数据的效率比较,可以看到我们的冒泡排序对于十万个数据所消耗的时间甚至到了90秒,选择排序也消耗了5秒左右,但是看到我们的推排序呢,只消耗了36毫秒,这个差距还是很直观的。
以上就是我对于选择排序,堆排序,冒泡排序的全部讲解,希望对你有帮助。