一.插入排序
1.插入排序基本思想
插入排序的基本思想就是通过构建有序序列,对于未排序数据在已排序序列从后向前扫描找到相应的位置并插入。通俗一点的解释就是和打扑克摸牌类似。首先你手中的牌一定是有序的,这时候如果你新抓了一张牌,你想让其有序就得从前向后或者从后向前找到它合适的位置。
而我们的插入排序采用的是从后向前查找新数据插入的位置 ,比如上面的图片,7要进入这个数组里面,从后向前查找,10比7大,那么就将10向后移动,7继续和前面的数据比较,5比7小那么就将7放在5的后面。
1.1动图演示
2.单趟代码实现及解释
下面我们来完成单趟代码
首先我们定义已经排好序的数组a的末尾是end,我们要从头开始去排序(因为插入一个新数据必须要保证之前的数据是有序的,我们没有办法直接在数组的末尾进行操作) 。这样我们摸得第一个数字是4,4只有一个数前面没有数据所以他就是有序的,所以就让他来做第一趟排序的end;然后我们去摸第二个数据,将他放在tmp中。显然它比4大那么它就直接放在end后面即可。
而后end++到下一个位置,然后我们再抓下一个数字1,将它放到tmp中,而后和end对比end=7,tmp=1,这时只要将a[end+1] = a[end]; 然后--end;就可以将7挪到1的位置并且将end前移为下一组4和1比较做准备。而4也大于1, 所以4被换到a[1]的位置,这时候end已经来到数组的-1。最后我们要将tmp的数组放在已经挪好的位置中。
接下来我们根据上边的思路完成单趟插入的代码
//这里使用i来控制end向后移动 i初始为0;
int end = i;
int tmp = a[i+1];
while (end >= 0)//单趟插入循环条件
{
if (a[end] > tmp) //将比tmp大的数向后挪
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;//将要插入的数字放到相应位置
现在已经完成了单趟排序,我们只要控制end从前向后走即可完成排序;这样我们就完成了插入排序。如下是插入排序完整代码:
void InsertSort(int* a, int n)
{
// [0,end] 有序 ,插入tmp依旧保持有序;
// 从后向前比较 比不到了 比所有值都小走到-1停下;
for (int i = 1; i < n; i++)
{
int end = i-1;
int tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
二.希尔排序
1.希尔排序基本思想
希尔排序是插入排序的一种,它也被称为缩小增量排序。通俗一点来说希尔排序的基本原理还是插入排序,希尔排序比插入排序多的一点就是它可以对数据进行预排序,使数据接近有序,最后在进行一遍插入排序,这样插入排序的效率会高出许多。因为当数据越有序时插入排序所需要挪动的数据就变少了很多,所以希尔排序比直接插入排序要快很多。
希尔排序的主要思路是将数据等距分组,然后对不同的组进行插入排序,即分组插入排序。我们假设间隔gap = 3的数据分为一组。例如下图的分组:
图2.1
如图我们将这组数据分成了三组也就是gap组,这些被分割的数组的数据分别为4986,732,145;而我们接下来需要对这些组数分别进行插入排序;首先我们要排序第一组数据,通过插入排序的逻辑我们很容易得到这一组数据变成了4689;而数组的整体序列就变为4716348259 。
图2.2
按照这个逻辑我们将剩余两组数据通过插入排序排后整体的序列为:4216348759
图2.3
这样就达到了我们想要的预排结果,大数被换到了后面,小数换到前面。 这是当gap=3时进行的预排序,当gap减小时我们会发现整个数组越有序,当gap=1时,他就是我们上面的插入排序。下面用动态图片演示希尔排序的整个过程。
当gap = 3时;因为gif只支持60s,最后一趟排序没有加上。但是最后一组是有序的。
图2.4
当gap = 2时;
当gap = 1时;此时为插入排序。排完一趟数据就变得有序了。
2.单趟代码实现及解释
根据上面我们的思想我们首先得定义一个gap来确定数据的间隔,也就是数据的总组数。这里我们以gap = 3来做代码实现。用变量end来确定数据的尾。用tmp来表示下一个要插入的数据。
这里end是我们第一个数据,tmp是待插入数据,如此根据图示我们很轻易就可以写出单组的排序。
int gap = 3;
for(int i = 0; i< n-gap;i+=gap)//控制end持续向后移动,这里n是数据总个数
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;//找到end前面的数继续和tmp比较;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
这就是单组排序对应上面绿色的那一组end从零开始完成一组排序。再此基础上我们只要再加一组循环,将分出来的三组数据都进行排序即可完成对gap = 3 的排序了。代码如下。
int gap = 3;
for (int k = 0; k < gap; k++)//一组一组排序
{
for (int i = k; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;//找到end前面的数继续和tmp比较;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
以上为第一种多组排序方法,下面是第二种
int gap = 3;
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;//找到end前面的数继续和tmp比较;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
这是gap = 3时对数组分组进行的排序我们画图发现,此次排序达到了预排序的效果。如图2.3及图2.4演示的一样。大数都被排到了后边小数跑到了前面。但是他还不是有序的,所以我们要调整gap的大小,直至gap==1时此时单组排序就变成了插入排序,经过此次排序即gap==1时数组变得有序。因此我们要逐渐调整gap的大小直至gap==1。--- 所以我们要在此基础上再套一层循环来控制gap。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 0)
{
gap = gap / 3 + 1;///这里加一防止gap直接被整除变成0;
for (int k = 0; k < gap; k++)
{
for (int i = k; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;//找到end前面的数继续和tmp比较;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
}
以上就是希尔排序的函数实现即思路。
三.选择排序
1.选择排序基本思想
选择排序的基本思路是从头开始查找最小的数,然后让第一个数据和找到的最小数据交换,然后继续查找次小的数和第二个位置的数交换。如此往复直到数组有序。
3.1(图片来自菜鸟教程)
2.选择排序代码实现
我们实现的是查找两个数,上面的思路是从头查找小的数,这里增加了从后向前查找最大的数和最后面的数交换的逻辑。
首先我们完成的是一趟排序的代码。这里我们用begin来指向数组的第一个数。end指向数组的第二个数。
int begin = 0;
int end = n - 1;
int mini = begin, maxi = end;
for (int i = begin; i <= end; i++)//循环查找
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);//这个函数用来交换数据
if (begin == maxi)///如果最大的数就是begin那么、begin就被换到mini的位置上了;所以要做交换
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
接下来我们只要加一个循环即可完成选择排序。
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
//如果maxi和begin重叠
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
四.堆排序
1.堆排序的基本思想
如果有一个关键码的集合K = { , , ,…, },把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足: = 且 >= ) i = 0,1, 2…,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。 堆的性质: 堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树。
通俗来讲堆是一颗完全二叉树,这里以小堆为例,它的特点是它的所有父节点都大于等于它的孩子节点。如图所示:
图3.1
堆的逻辑结构是一颗树,但是其物理结构是一个数组。父节点和子节点的关系为parent = (child-1)/2; 现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整 成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。所以我们从最后一个孩子节点来向下调整,因为一个叶子节点就可以看作是一个小堆。我们通过最后一个节点找到他的父节点,就可以对以这个父节点为根的树进行向下调整,因为它的孩子都是叶子节点。
而利用堆来排序的原理是堆的顶永远是最大/最小的数。而将堆顶的数据挪到最后一个节点,此时我们就找到了最大的数,接下来我们只要控制让尾部的数据不参与这个堆,然后从最上面的根节点进行一次向下调整,那么就可以很快的找到次大的数据了。重复这个过程我们就可以得到一组升序序列。
图3.2(图片来源于菜鸟教程)
2.堆排序代码实现
首先我们要完成向下调整算法。我们用parent代表父亲节点,用child代表孩子节点。n表示数组数据个数。这里我们以建立一个大堆为例。首先我们先找到最后一个节点的父节点,我们需要比较次父节点和两个子节点之间的大小(子节点等于父节点*2+1),让后进行交换。然后找到下一个父节点在进行向下调整算法;下一个父节点就是前面父节点的下标大小减1。这样我们就可以将数组建成一个大堆了。
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)//向下调整结束条件
{
if ((child + 1 < n) && (a[child] < a[child + 1]))//这里找出大的child
{
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else//如果孩子比父亲小就不用再调整了
{
break;
}
}
}
接下来我们就要建立大堆再进行排序了。
void HeapSort(int* a, int n)
{
for(int i = (n-2)/2; i >= 0; i--)//向下调整建堆
{
AdjustDown(a, n, i);
{
int end = n-1;
//循环交换堆顶和尾的数据,使数组变有序。
while(end>0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
五.快速排序
1.快速排序的递归实现hoare法
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右 子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
总的来说快速排序是先找到基准值(我们在这里将基准值定义为key,key永远在最左边),然后依据此基准值将数据分为两半,我们以升序为例,那么此基准值的左边的数都是比key小的数,而右边都是比key大的数。接下来我们要继续持续这个过程,对key左边和右边的值再次进行快速排序。直到只剩下一个数据时停止。因为key的左边的数字比key小,右边的所有数都比key大,所以一次快速排序后基准值就到达了其最终位置。
hoare法代码实现基本思路,这里我们用left代表最左边的数据,right代表最右边的数据。首先right先走找到比key小的值,然后left向后走,找到比key大的值,接下来交换left和right的值right继续寻找比key小的值,重复这个过程直到right遇到left停止。当right与left相遇时交换left和key的值就完成了第一个大区间排序--(这里存在三种停止情况,第一种right没有找到比key小的值一直向前走直到和left相遇,而left在最左边,说明key就是最小的数。第二种right找到了比left小的数,left找到了比key大的数,left和right交换后,right继续走没有找到比key小的数和left相遇,这时left的数刚好比key小,交换left和key完成此次排序。第三种是第二种的相反,right找到了比key小的数,但是left没有找到比key大的数,left和right相遇直接交换left和key的数据即可完成此次排序)。此时的key的位置就是最终排序完成的位置。最后我们要返回left,也就是key值所在的位置作为下次左右区间执行排序提供区间的值。动图演示如下:
图5.1
接下来我们完成单趟快速排序的代码。
int ParkSort(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
while (left < right && a[right] >= a[keyi])//right找小
{
--right;
}
while (left < right && a[left] <= a[keyi])//left找大
{
++left;
}
Swap(&a[right], &a[left]);//交换
}
Swap(&a[keyi], &a[left]);
return left;
}
这样我们就完成了一次排序,将小于key的都挪到了它的左边,大于key的挪到了右边,接下来我们需要对0到key-1这个区间,和key+1到right这个区间分别进行排序,然后再次分割直到只剩下一个数据为止。代码如下:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = ParkSort(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
图示如下:
图5.2
2.快速排序的非递归实现使用栈
当递归的深度非常大时,递归情况下会导致栈溢出问题;所以这里我们实现一下快速排序的非递归方法。
这里我们实现非递归是使用的栈(这里的栈是数据结构的栈,并不是计算机的物理结构);这里我们实现的具体思路是将left和right存入栈中,调用排序函数时我们先在堆中取到left和right的值,然后传到ParkSort函数中,当此次排序完成,返回了keyi,然后我们将left,keyi-1,right,keyi+1压入栈中,循环读取栈中区间的数据,即可模拟实现递归结构。这个前提需要我们有一个栈Stack。下面是代码实现:
void QuickSortNonR1(int* a, int begin, int end)
{
Stack s;
StackInit(&s);//初始化栈
StackPush(&s, end);
StackPush(&s, begin);
while (!StackEmpty(&s))
{
int left = StackTop(&s);
StackPop(&s);
int right = StackTop(&s);
StackPop(&s);
int keyi = PartSort(a, left, right);
if (keyi + 1 < right)
{
StackPush(&s, right);
StackPush(&s, keyi + 1);
}
if (keyi - 1 > left)
{
StackPush(&s, keyi - 1);//循环上去后第二个读取的数据
StackPush(&s, left);//循环上去后第一个读取的数据
}
}
StackDestroy(&st);//销毁栈
}
这里增加了一些对快速排序的简单优化,第一个优化为三数取中,使key的值尽可能排在数组中间。这里我们用的是伪随机取法。
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + (rand() % (right - left))) / 2;//注意rand的使用需要srand生成随机数种子
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else
{
if (a[left] < a[right])
{
return left;
}
else if (a[mid] > a[right])
{
return mid;
}
else
{
return right;
}
}
return left;
}
六.归并排序
1. 归并排序的递归版本实现
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
归并排序将数据分割成两半,直至将数据切割为1个,然后合并两个有序区间。这里我们用mid来代表数据中间位置的下标,两个区间的开始和结束分别为,begin1,end1,begin2,end2; 我们将两个区间切割完后开始归并,归并的思路是在两个有序区间中(这里以升序为例),两个序列的到一个数比较,找到最小的数插入到另一个临时数组tmp中,然后再次比较插入。如果一个数组没有数据了,就将另一个数组剩下的数据拷到临时数组tmp中。最后将数据拷贝回最初的数组中。如下动图:
图6.1
接下来我们完成归并排序的代码。
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
{
return;
}
int mid = (begin + end) / 2;
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = 0;
while (begin1 >= end1 && begin2 >= end2)
{
if (a[begin1] >= a[begin2])
{
tmp[i++] = a[begin2++];
}
else
{
tmp[i++] = a[begin1++];
}
}
//将剩余数据拷贝至tmp
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)//归并排序函数
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a, 0, n - 1, tmp);//子函数
free(tmp);
}
2.归并排序的非递归实现循环
这里我们使用循环模拟递归分割数组的过程,首先我们要将数据分割为一个,然后两个数据合并完毕之后,控制begin跳到第三个数的位置,然后对第三四个数据进行归并,重复此步骤我们就完成了最开始的一趟合并。接下来,我们的数据就变成两个两个一组了,我们需要控制begin一次跳4个数据来进行排序。重复这个过程直至最后剩下两个区间,合并完成就是有序的数组。
代码实现,这里我们定义一个gap来控制begin移动的大小,每次合并完更换合并区间时只需要更改gap*=2即可。非归并排序会出现各种各样的越界问题,因此我们在这里对越界进行详细分析当 end1 越界时,我们要将end1拉回区间,让end1 = n-1(n为数组大小);同时我们要将begin2和end2设置为不存在区间防止其越界。 当end1没有越界begin2越界时我们需要将begin2和end2设置为不存在区间。当只有end2越界时只需要将end2拉回区间即可,即让end2 = n-1 。
代码实现如下:
//非递归
void MergeSortNonr(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);//开tmp 数组来临时存储归并好的数据
if (tmp == NULL)
{
perror("malloc false");
exit(-1);
}
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
//每组数据的合并;
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
if (end1 >= n)//越界处理 -- 1
{
end1 = n - 1;
//不存在区间
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)//end2复位//防止越界
{
//不存在区间
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)//开始归并
{
if (a[begin1] < a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)//归并后剩余数据拷贝
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//解决越界办法 解决不存在区间
memcpy(a, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}