排序
排序的概念及其运用
1.1排序的概念
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
常见的排序算法
- 要掌握的点: 各个排序算法的原理&代码实现&时间复杂度&空间复杂度&稳定性&应用场景
插入类排序
插入排序
基本思想
- 直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列
- 实际中我们玩扑克牌时,就用了插入排序的思想
- 插入排序的工作方式像许多人排序一手扑克牌。开始时,我们的左手为空并且桌子上的牌面向下。然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置。为了找到一张牌的正确位置,我们从右到左将它与已在手中的每张牌进行比较。拿在左手上的牌总是排序好的,原来这些牌是桌子上牌堆中顶部的牌
- 插入排序是指在待排序的元素中,假设前面n-1(其中n>=2)个数已经是排好顺序的,现将第n个数插到前面已经排好的序列中,然后找到合适自己的位置,使得插入第n个数的这个序列也是排好顺序的。按照此法对所有元素进行插入,直到整个序列排为有序的过程,称为插入排序
- 根据插入排序的思想大致可以写出如下的插入排序的代码
插入排序的代码如下所示:
void InsertSort(int array[], int size)
{
for (int i = 1; i < size; i++)
{
int key = array[i];
int end = i - 1;
//去找待插入元素在数组中的位置
while (end >= 0 && key < array[end])
{
array[end + 1] = array[end];
end--;
}
//插入元素
array[end + 1] = key;
}
}
插入排序复杂度的问题
- 时间复杂度—O(N^2)
- 空间复杂度—O(N)
插入排序稳定性的问题
- 插入排序是稳定的排序
插入排序的适用场景
- 插入排序适用于原先就已经有序的序列
- 插入排序还适用于数据量比较小得序列
- 总之就是想办法去减少数据搬移得次数,就可以尽可能得最大程度降低插入排序的时间复杂度
- 插入排序适用于已经有部分数据已经排好,并且排好的部分越大越好。一般在输入规模大于1000的场合下不建议使用插入排序
总结
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
现实情况中
- 现实的情况是,可能需要我们去排序的数字很多,也就是说数据量会比较大一些,而且,可能需要我们去进行排序的数据序列也并不是有序的,所以说我们现在需要对已有的数据序列进行改进,使得其尽可能的适用于直接插入排序的引用场景
- 那么,问题来了,是把凌乱的序列改变成有序的序列方便一些,还是说把数据量大的数据序列变成数据量小的数据序列更加的方便呢
- 当然是把数据量大的序列的数据改编成数据量小的序列更为方便,那么我们现在就需要对数据量大的数据序列去进行改变,把他变成数据量小的数据序列
- 那么我们就需要对所给出的序列进行平均分组的操作
希尔排序( 缩小增量排序 )
- 希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
- 希尔排序是基于插入排序的以下两点性质而提出改进方法的:(1)插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。(2)但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序的基本思想
- 先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量…
- 该方法实质上是一种分组插入方法—比较相隔较远距离(称为增量)的数,使得数移动时能跨过多个元素,则进行一次比较就可能消除多个元素交换。D.L.shell于1959年在以他名字命名的排序算法中实现了这一思想。算法先将要排序的一组数按某个增量d分成若干组,每组中记录的下标相差d.对每组中全部元素进行排序,然后再用一个较小的增量对它进行,在每组中再进行排序。当增量减到1时,整个要排序的数被分成一组,排序完成。
- 一般的初次取序列的一半为增量,以后每次减半,直到增量为1。
希尔排序的概念
- 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
- 那么,上述的过程在经历了一次分组之后,内部使用了插入排序之后,形成了上述这样的结果,那么问题来啦,上述的序列算不算是已经基本有序的队列了呢
- 问题来了,什么才算是基本有序的情况呢?
- 所以说在经历了上面的平均分组以及内部进行插入排序之后,所得到的序列仍然不是基本有序的,所以我们现在需要对我们之前所给出的分组方式进行一定的调整
- 我们修改之后,就要将我们所给出的元素序列按照一定的间隔进行分组
- 将数组下标模上一个三,然后将他们放在同一个组(当然,这个地方的3是随机给出的,只会还是会进行调整的
- 分组分好了之后,然后再在每一个分组的内部进行插入排序
- 对蓝色的分组内部进行插入排序可以得到如下的结果
- 按照同样的方式,对每个分组的内部进行插入排序,得到如下的结果:
- 对每个分组的内部使用插入排序就会导致每个分组的内部就已经是有序的序列了,但是,整个序列其实还并不是有序的序列,那么我们继续对现在的这个序列进行分组的操作
- 我们刚才是按照数组下标为3的方式来进行分组的,那么我们现在按照数组下标为2的方式继续对其进行分组操作
- 对上述的序列,进行处理之后,得到如下的序列
- 经过分组为2的排序之后,我们就可以看的出来,待排序的序列已经基本是有序的序列了,只是其中有一些个别元素的顺序可能还存在有一定的问题,大部分的元素已经是基本有序了,然后我们现在去进行第三次的划分,这次,我们把划分的间隔给成1 (间隔指的是下标之间的间隔,并不是说是数字之间的间隔),然后就可以得到像下面这样的序列
- 上面的排序过程就是希尔排序
- 希尔排序其实并不是将一个分组排完了再去排另一个分组的,而是将三个分组放在一起同时进行排序的,是三个分组交替来进行排序的
- 那么既然这样的话,希尔排序其实就是隔着分组来进行元素的搬移的,所以说希尔排序在本质上是不稳定的排序
- 希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
- 在此我们选择增量 gap=length/2,缩小增量以 gap = gap/2 的方式,用序列 {n/2,(n/2)/2…1} 来表示。
- 希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成若干个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。 这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
希尔排序的代码
- 代码如下所示:
void ShellSort(int array[], int size)
{
int gap = 3;
while (gap >= 1)
{
for (int i = gap; i < size; i++)
{
int key = array[i];
int end = i - gap;
//去找待插入元素在数组中的位置
while (end >= 0 && key < array[end])
{
array[end + gap] = array[end];
end -= gap;
}
//插入元素
array[end + gap] = key;
}
gap--;
}
}
- 但是,现在写出这样的代码的话,就会有一个问题,因为上述的代码中的gap的值是随便给出来的,那么,其实问题就是gap的值到底如何确定
- 那么,再有人经历了大量的实现之后,得出了如下去确认gap的值的方法
void ShellSort(int array[], int size)
{
int gap = size;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = gap; i < size; i++)
{
int key = array[i];
int end = i - gap;
//去找待插入元素在数组中的位置
while (end >= 0 && key < array[end])
{
array[end + gap] = array[end];
end -= gap;
}
//插入元素
array[end + gap] = key;
}
}
}
void ShellSortIII(int arr[], int sz)
{
int gap = sz/2;
while (gap > 1)
{
gap >>= 1;
for (int i = gap; i < sz; i++)
{
int key = arr[i];
int end = i - gap;
while (end >= 0 && key < arr[end])
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = key;
}
gap--;
}
}
- 那么,既然按照上面那种情况所给出的希尔排序的代码之后,就可以看出来,其实希尔排序的时间复杂度就不是很好求了
- 有人在经历了大量的代码操作之后,得出了如下的结论,就是因为gap的取值不一样,所以可能会导致算法的时间复杂度不一样
希尔排序的应用场景
- 希尔排序适用于数据量很大的情况
- 希尔排序适用于待排序序列无序的情况
选择类排序
选择排序
基本思想:
- 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
- 选择排序是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法
- 选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了
直接选择排
- 在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
- 重复的去进行上述的步骤,直到最终排成的序列是有序的序列就可以了
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
void SelectSort(int array[], int size)
{
for (int i = 0; i < size - 1; i++)
{
int maxPos = 0;
for (int j = 1; j < size - i; j++)
{
if (array[j] > array[maxPos])
{
maxPos = j;
}
}
if (maxPos != size - i - 1)
{
Swap(&array[maxPos], &array[size - i - 1]);
}
}
}
优化之后的选择排序
//优化的选择排序
void SelectSortOP(int array[], int size)
{
int begin = 0;
int end = size - 1;
while (begin < end)
{
int minPos = begin;
int maxPos = begin;
int index = begin + 1;
while (index <= end)
{
if (array[index] > array[maxPos])
maxPos = index;
if (array[index] < array[minPos])
minPos = index;
++index;
}
//注意:最右侧位置可能存储的是当前的最小值
if (maxPos != end)
{
Swap(&array[maxPos], &array[end]);
}
//如果最右侧的位置存储的是当前的最小值,经过上面的交换之后
//最小值的元素的位置就已经发生了变化
//所以我们紧接着就要去更新minPos的位置
if (minPos == end)
{
minPos = maxPos;
}
if (minPos != begin)
{
Swap(&array[minPos], &array[begin]);
}
++begin;
--end;
}
}
直接选择排序的特性总结:
- 稳定性:不稳定
- 选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法
复杂度问题
- 时间复杂度—>选择排序的交换操作介于 0 和 (n - 1)次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+…+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
- 空间复杂度为O(1)
堆排序
- 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆排序的基本思想
- 堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
- 首先需要进行建堆的操作
- 当大顶堆或者小顶堆创建好的时候,我们再去利用堆删除的原理去进行排序
void HeapAdjust(int* array, int size, int parent)
{
int child = parent * 2 + 1;
while (child < size)
{
if (child + 1 < size && array[child + 1] > array[child])
{
child += 1;
}
if (array[child] > array[parent])
{
Swap(&array[child], &array[parent]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
void HeapSort(int array[], int size)
{
int end = size - 1;
//首先,第一步需要进行的操作就是建堆的操作
for (int root = (size - 2) / 2; root >= 0; root--)
{
//然后进行向下调整的思路
HeapAdjust(array, size, root);
}
//然后利用堆删除的思想来进行排序
while (end)
{
Swap(&array[0], &array[end]);
HeapAdjust(array, end, 0);
end--;
}
}
复杂度的分析
- 时间复杂度为:O(NlogN)
- 空间复杂度为:O(1)
- 关于稳定性的问题:堆排序是不稳定的排序
交换排序
- 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
冒泡排序
void BubbleSort(int array[], int size)
{
//外层循环控制冒泡的趟数,也就是说控制的是需要冒泡多少躺
for (int i = 0; i < size - 1; i++)
{
//具体冒泡的方式:用相邻的两个元素进行比较
//把大的元素往后放
//j表示的是数组的下标
for (int j = 0; j < size - i - 1; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
- 一趟冒泡排序只会讲最大的或者最小的元素放在它应该在的位置上,也就是说一次冒泡只会筛选出来一个元素
- 对冒泡排序进行优化的操作—如何对冒泡排序进行优化,对冒泡排序进行优化的话,就是给出一个标记就可以了,标记用来代表是否有序,如果已经是有序了的话,那么, 就不用再去交换了
void BubbleSortOP(int array[], int size)
{
//外层循环控制冒泡的趟数,也就是说控制的是需要冒泡多少躺
for (int i = 0; i < size - 1; i++)
{
int flag = 1;
//具体冒泡的方式:用响铃的两个元素进行比较
//把大的元素往后放
//j表示的是数组的下标
for (int j = 0; j < size - i - 1; j++)
{
if (array[j] > array[j + 1])
{
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag = 0;
}
}
if (flag == 1)
break;
}
}
冒泡排序的总结
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
- 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
- 快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序
- 快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
将区间按照基准值划分为左右两半部分的常见方式有:
- (1)hoare版本
- (2)挖坑法
- (3)前后指针版本
- 对区间进行划分的方式一共有三种
- 采用hoare版本,最后记得要把基准值和begin位置所处的元素进行交换
int Partion(int* array, int left, int right)
{
int begin = left;
int end = right - 1;
int key = array[end];
while (begin < end)
{
//让begin从前往后找比基准值大的元素,找到就停止
//但是从前往后找的时候,需要注意一点就是begin不可以越界
while (begin < end && array[begin] <= key)
{
begin++;
}
//让end从后往前找比基准值小的元素,找到就停止
while (begin < end && array[end] >= key)
{
end--;
}
if (begin < end)
{
Swap(&array[begin], &array[end]);
}
}
if (begin != end - 1)
{
Swap(&array[begin], &array[right - 1]);
}
return begin;
}
void QuickSort(int array[], int left,int right)
{
if (right - left > 1)
{
int div = Partion(array, left, right);
QuickSort(array, left, div);
QuickSort(array, div + 1, right);
}
}
- 采用挖坑法的方式对区间进行划分
- 首先,我们需要向hoare版本的方法那样,给出一个最左边的指针,同时,给出一个最右边的指针,如下图所示:
- 然后,我们现在需要做的就是,还是需要去取一个基准值,我们仍然取区间最右侧的数值为基准值,这个时候,相当于我们已经把区间最右侧的值保存起来了,既然我们已经把区间右侧得知保存到key里面去了,那么我们现在就可以在区间最右侧处的位置放置新的元素了,因为这个时候就不用担心我们把区间最右侧的那个元素5覆盖了,因为我们现在相当于是已经把5保存起来了。那么,其实就是相当于最开始的时候,5的位置其实就相当于是一个坑了,已经被我们挖走了,就已经形成了一个坑了,那么,接下来,我们就需要让begin从头的位置开始寻找,寻找比基准值大的元素,如果找到了,让begin停下来就可以了,当begin来到8的位置的时候,8其实是比基准值大的,按照上一种思路,现在end需要从后往前走了,但是挖坑法其实是不一样的,既然我已经从前面找到了一个比基准值大的元素,那么这个元素是肯定要往后放的,那么我们这个时候,就可以把这个比基准值大的元素,放在基准值的位置,因为基准值一开始所处的位置就相当于已经是一个坑了,那么我们去把那个坑一填其实就可以了,填完坑之后,让end向前走一下
- 那么,我刚才既然把8那个元素挖走了,那么8那个元素所处在的位置其实也就相当于是一个坑了,那么,现在我们就需要让end从后往前去寻找比基准值小的元素,利用同样的方法, 把原先8所处在的位置的那个坑填上就好了,之后,都是同样的思路
- 最终的效果如下所示:
int PartionII(int* array, int left, int right)
{
int begin = left;
int end = right - 1;
int key = array[end];
while (begin < end)
{
//让begin从前往后找比基准值大的元素
while (begin < end && array[begin] <= key)
{
begin++;
}
//让begin位置的元素填end位置的坑
if (begin < end)
{
array[end--] = array[begin];
//填完坑之后还需要end--
}
//现在begin位置形成了一个新的坑
//那么,我们现在就需要让end从后往前去寻找
//寻找比基准值小的元素,去填begin的坑
while (begin < end && array[end] >= key)
{
end--;
}
//现在end找到了一个比基准值小的元素
//让end位置的元素去填begin位置的坑
if (begin < end)
{
array[begin++] = array[end];
//填完坑之后还需要begin++
//先填坑,然后再往后走
}
}
array[begin] = key;
return begin;
}
- 那么,现在,前两种方法已经看完了,现在我们来看一看第三种前后指针的方法是如何操作的
- 采用前后指针的方法
- 这种前后指针的方式仍然是需要我们去取一个基准值的,我们取区间最左侧的基准值可以,当然,我们也可以去取区间最右侧的基准值,当然,我们还是去取区间最右侧的数值成为基准值
- 然后,我们现在需要给出两个指针,一个指针指向待排序序列的第一个元素,再取一个指针,作为当前指针的前驱指针
- 取好了基准值之后,我们就从cur开始向后进行遍历的操作,如果cur<right,就说明这个遍历操作没有结束,仍然可以继续向后行遍历的操作,那么现在,如果cur所处位置的元素小于我们所取得基准值,就让我们得prev向后走一步,然后去看prev和cur是否处在同一个位置上,如果prev和cur处在同一个位置上,我们就让cur向后继续走
int PartionIII(int* array, int left, int right)
{
int cur = left;
int prev = cur - 1;
int key = array[right-1];
while (cur < right)
{
if (array[cur] < key && ++prev != cur)
{
Swap(&array[prev], &array[cur]);
}
cur++;
}
if (++prev != right - 1)
{
Swap(&array[prev], &array[right - 1]);
}
return prev; //基准值所处的位置
}
问题
- 在涉及到快排之前,为什么要把5放置在区间的最后侧?
- 那下面给出两种不同的序列的方式:
- 那么,我们现在要使用三种划分方式的一种,来对基准值来进行划分,会得到如下的两种结果:
- 右侧的情况是快排最差的情况—>就是说每次划分数据都集中在一侧,就由理想情况下的平衡树退化成了单支树
- 下面的这两种情况其实就是快排的最优情况和最差的情况
最优情况下和最坏情况下算法时间复杂度的问题
- 但是按照一般递归算法的时间复杂度求解的方法来说的话,对于快排的时间复杂度来说,求解的放大不是那么简单,所以对于快排的时间复杂度来说,不适合用传统的递归算法的时间复杂度来进行求解,下面来说一种别的求快排时间复杂度的方法,我们需要从快速排序算法的原理入手进行复杂度的求解问题
- 第一次对乱序的序列进行划分,不管是用上面三种方式的哪一种方式都是相当于把整个待排序的序列完整的便利了一次,那么,时间复杂度其实就是O(N),接下来,完成了第一次划分之后,我们需要对划分的结果再次进行划分,需要对左侧进行划分,同时也需要对右侧进行划分,左侧是N/2,右侧也是N/2,那么,第二层,左半部分和右半部分,加起来其实也是O(N)的时间复杂度,下面的层数其实都是一样的思路,那么最终有多少个O(N),就看这棵树有多少层,有多少层,就有多少个O(N)
- 最优秀的情况下:
- 最差的情况下:O(N^2)
时间复杂度一般都是取算法的最差的时间复杂度,那么快排的最坏的时间复杂度为O(N^2),那么为啥书上给出的快排的时间复杂度为O(NlogN)
- 原因在于
- 那么,我们如何避免取到的基准值是极大值或者极小值的情况呢,那么我们就要对基准值的划分方式取进行优化的操作,就是说,不要单纯的去取区间的最左侧的值和区间的最右侧的值,因为假如说待排序的序列是有序的,那么我们取最左侧的元素或者最右侧的元素就会取到极值,这样划分得话,就容易让数据全部都集中在一侧。
- 那么如何进行划分呢,就需要进行划分元素为三者取中
int GetIndexOfMid(int* array, int left, int right)
{
int mid = left + (right - left) / 2;
if (array[left] < array[right - 1])
{
if (array[mid] < array[left])
return left;
else if (array[mid] > array[right - 1])
return right - 1;
else
return mid;
}
else
{
if (array[mid] > array[left])
return left;
else if (array[mid] < array[right - 1])
return right - 1;
else
return mid;
}
}
int Partion(int* array, int left, int right)
{
int begin = left;
int end = right - 1;
int key;
int mid = GetIndexOfMid(array, left, right);
Swap(&array[mid], &array[end]);
key = array[end];
while (begin < end)
{
//让begin从前往后找比基准值大的元素,找到就停止
//但是从前往后找的时候,需要注意一点就是begin不可以越界
while (begin < end && array[begin] <= key)
{
begin++;
}
//让end从后往前找比基准值小的元素,找到就停止
while (begin < end && array[end] >= key)
{
end--;
}
if (begin < end)
{
Swap(&array[begin], &array[end]);
}
}
if (begin != end - 1)
{
Swap(&array[begin], &array[right - 1]);
}
return begin;
}
int PartionII(int* array, int left, int right)
{
int begin = left;
int end = right - 1;
int key;
int mid = GetIndexOfMid(array, left, right);
Swap(&array[mid], &array[end]);
key = array[end];
while (begin < end)
{
//让begin从前往后找比基准值大的元素
while (begin < end && array[begin] <= key)
{
begin++;
}
//让begin位置的元素填end位置的坑
if (begin < end)
{
array[end--] = array[begin];
//填完坑之后还需要end--
}
//现在begin位置形成了一个新的坑
//那么,我们现在就需要让end从后往前去寻找
//寻找比基准值小的元素,去填begin的坑
while (begin < end && array[end] >= key)
{
end--;
}
//现在end找到了一个比基准值小的元素
//让end位置的元素去填begin位置的坑
if (begin < end)
{
array[begin++] = array[end];
//填完坑之后还需要begin++
//先填坑,然后再往后走
}
}
array[begin] = key;
return begin;
}
int PartionIII(int* array, int left, int right)
{
int cur = left;
int prev = cur - 1;
int key;
int mid = GetIndexOfMid(array, left, right);
Swap(&array[mid], &array[right-1]);
key = array[right - 1];
while (cur < right)
{
if (array[cur] < key && ++prev != cur)
{
Swap(&array[prev], &array[cur]);
}
cur++;
}
if (++prev != right - 1)
{
Swap(&array[prev], &array[right - 1]);
}
return prev; //基准值所处的位置
}
void QuickSort(int array[], int left,int right)
{
if (right - left < 16)
InsertSort(array + left, right - left);
else
{
int div = PartionIII(array, left, right);
QuickSort(array, left, div);
QuickSort(array, div + 1, right);
}
}
快排得空间复杂度问题
快排的非递归形式
void QuickSort(int array[], int left,int right)
{
if (right - left < 16)
InsertSort(array + left, right - left);
else
{
int div = PartionIII(array, left, right);
QuickSort(array, left, div);
QuickSort(array, div + 1, right);
}
}
快排的非递归形式的书写
//快排的非递归的形式
void QuickSortNor(int array[],int size)
{
Stack s;
StackInit(&s);
//将数据整体区间入栈
StackPush(&s, size);
StackPush(&s, 0);
int left = 0;
int right = 0;
while (!StackEmpty(&s))
{
left = StackTop(&s);
StackPop(&s);
right = StackTop(&s);
StackPop(&s);
if (right - left > 1)
{
int div = Partion(array, left, right);
//div是基准值的位置
//基准值的左侧[left,div)
//基准值的右侧[div+1,right)
StackPush(&s, right);
StackPush(&s, div+1);
StackPush(&s, div);
StackPush(&s, left);
}
}
}
归并排序
归并排序的原理
- 归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
- 归并排序的划分与快速排序的划分不同,快速排序的划分是选取一个基准值从而进行元素的划分,但是,归并排序的划分是每次都进行均分的操作,而且只有每个数组都已经是有序的数组了,才能对两个有序的数组进行合并的操作
//将两组有效的数据进行合并
void MergeData(int* array, int left, int mid, int right, int* temp)
{
int begin1 = left, end1 = mid;
int begin2 = mid, end2 = right;
int index = left;
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
{
temp[index++] = array[begin1++];
}
else
{
temp[index++] = array[begin2++];
}
}
while (begin1 < end1)
{
temp[index++] = array[begin1++];
}
while (begin2 < end2)
{
temp[index++] = array[begin2++];
}
}
void _MergeSort(int* array, int left,int right, int* temp)
{
if (right - left > 1)
{
int mid = left + (right - left) / 2;
_MergeSort(array, left, mid,temp);
_MergeSort(array, mid, right, temp);
MergeData(array, left, mid, right, temp);
memcpy(array + left, temp + left, (right - left)*sizeof(array[0]));
}
}
void MergeSort(int* array, int size)
{
int* temp = (int*)malloc(sizeof(array[0]) * size);
if (NULL == temp)
{
assert(0);
return;
}
_MergeSort(array, 0, size, temp);
free(temp);
}
归并排序的稳定性问题
- 归并排序是稳定的排序
归并排序的复杂度问题
归并排序的循环方式
//写循环方式的归并排序
void MergeSortNor(int* array, int size)
{
int gap = 1;
int* temp = (int*)malloc(sizeof(array[0]) * size);
if (NULL == temp)
{
assert(0);
return;
}
while (gap < size)
{
for (int i = 0; i < size; i += 2 * gap)
{
int left = i;
int mid = left + gap;
int right = mid + gap;
//防止越界
if (mid > size)
mid = size;
if (right > size)
right = size;
MergeData(array, left, mid, right, temp);
}
memcpy(array, temp, size * sizeof(array[0]));
gap <<= 1;
}
free(temp);
}
非比较排序
- 计数排序(也称为鸽巢原理)
- 但是如果数据的范围不知道的话,那么首先就是需要去确定数据的范围
void CountSort(int array[], int size)
{
//首先需要进行统计数据的范围,但是这一步其实并不是必须的
//如果用户告诉了我们数据的范围的话,我们就不用去统计数据的范围了
int minValue = array[0], maxValue = array[0];
for (int i = 1; i < size; i++)
{
if (array[i] < minValue)
minValue = array[i];
if (array[i] > maxValue)
maxValue = array[i];
}
//第二步,申请计数的空间
int range = maxValue - minValue + 1;
int* ArrayCount = (int*)malloc(sizeof(int) * range);
if (NULL == ArrayCount)
{
assert(0);
return;
}
//进行值的设置
memset(ArrayCount, 0, sizeof(int) * range);
//第三步,统计每个元素出现的次数
for (int i = 0; i < size; i++)
{
ArrayCount[array[i] - minValue]++;
}
//第四步,对数据进行回收
int index = 0;
for (int i = 0; i < range; i++)
{
while (ArrayCount[i]--)
{
array[index++] = i + minValue;
}
}
free(ArrayCount);
}
复杂度问题
- 时间复杂度O(N)-----N表示的是元素的个数
- 时间复杂度O(M)-----M表示的是数据范围中不同数据的个数
- 应用场景----数据密集的集中在某个范围
- 稳定性问题----是一种稳定排序
基数排序(多关键码排序)
- 低关键码优先(用循环处理)
- 高关键码优先(用递归处理)
低关键码优先
- 就是先去比较个位数字,然会去比较十位数字,最会再去比较百位数字
- 那么,现在我们给出十个桶,桶的下标分别为0~9,数字的各位数字是几,就把这个数字放到下标为几的桶中
- 接下来要对每个桶中的数据进行回收,注意:按照桶的编号从小到大进行回收,如果桶中有多个数据,按照先放先回收的规则来进行回收
- 然后按照相同的规则,对十位上的数字进行相同的处理,然后放入桶内
- 然后对百位进行处理
- 然后就最终得到了有序的序列
复杂度问题
- 稳定性排序-----这是一种稳定的排序
总结