目录
1、归并排序递归版本:
归并的思路在顺序表与链表部分就已经使用过,即,两个升序序列合并之后形成的新序列仍为升序,两个降序序列合并之后形成的新序列仍为降序,合并两个均为
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
//交换函数,传址调用、
void Swap(int*pa, int*pb)
{
assert(pa && pb);
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//打印、
void PrintArray(int*a, int n)
{
assert(a);
for (int i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//归并排序子函数、
//升序、
void _MergeSort(int*a, int begin, int end, int*tmp)
{
assert(a && tmp);
//当某一个区间内只有一个元素或者不存在元素时,可以看成该区间是升序的,也可看成是降序的、
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2; //也可以写成其他形式、
// [begin,mid] [mid+1,end]
//若分割为:[begin,mid-1] [mid,end] 则会出现死循环问题,最好保证均分、
//递归左子树、
_MergeSort(a, begin, mid, tmp);
//递归右子树、
_MergeSort(a, mid + 1, end, tmp);
//归并 [begin,mid] [mid+1,end]、
//printf("归并[%d,%d][%d,%d]\n", begin, mid, mid + 1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2)
{
//当a[begin1] == a[begin2] 进入哪一个语句都是可以的、
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//由于begin1和begin2不是同时走的,故到此,上面while语句中的判断中只有一者不满足要求,要么是begin1所在的数组被遍历完毕,
//要么是begin2所在的数组被遍历完毕,即begin1和begin2所在的数组不可能同时被遍历完毕、
while (begin1 <= end1)
{
//begin2所在的数组被遍历完毕,begin1所在的数组未被遍历完毕、
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
//begin1所在的数组被遍历完毕,begin2所在的数组未被遍历完毕、
tmp[index++] = a[begin2++];
}
//整型int类型的拷贝可以使用memcpy、
memcpy(a + begin, tmp + begin, sizeof(int)*(end - begin + 1));
}
//归并排序、
void MergeSort(int*a, int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
//为了避免每次递归调用都动态开辟n个内存空间,所以要写出一个子函数专门用来进行递归调用、
_MergeSort(a, 0, n - 1, tmp);
//释放、
free(tmp);
tmp = NULL;
}
//归并排序、
void TestMergeSort()
{
int a[] = { 10, 6, 7, 1, 3, 9, 4, 2 };
//归并排序、
MergeSort(a, sizeof(a) / sizeof(int));
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
int main()
{
//归并排序、
TestMergeSort();
return 0;
}
1.1、归并排序递归版本复杂度分析:
1.1.1、时间复杂度分析:
至于归并排序递归版本而言求时间复杂度,最好和最坏情况是一样的,则可以直接套结论求解,又因为此时是要求解递归算法的时间复杂度,并且每一次递归函数
_MergeSort调用中的执行次数和该递归函数的参数是有关系的,则递归算法的时间复杂度就不能再简单的使用递归的次数乘以每次递归中的时间复杂度来求解了,
此时递归算法的时间复杂度等于所有的递归调用中执行次数的累加,必须都列出来,最后再进行求和,所以在此可以把每一次递归函数调用中的执行次数列出来进
行相加,即上图中的每一个长方形的长度进行相加,因为此处所说的执行次数只算循环次数即可,循环次数就看做是区间的长度,也就是上图中的长方形的长度,
为了方便计算也可以从整体来计算,每一层的执行次数均为N次,一共有log(N+1)层,则时间复杂度即为:O(N*logN)、
1.1.2、空间复杂度分析:
至于归并排序递归版本而言求空间复杂度,最好和最坏情况是一样的,则可以直接套结论求解,又因为此时是要求解递归算法的空间复杂度,并且每一次递归函数
_MergeSort调用中额外开辟的内存空间的个数和该递归函数的参数是没有关系的,则递归算法的空间复杂度就等于递归的深度或层数乘以每次递归中的空间复杂
度,此时,递归的深度为log(N+1)层,每一次递归调用中的空间复杂度为O(1),包括代码中开辟的常数个额外的空间,再加上每一次递归调用栈帧中开辟的常数个
额外的空间,两者加起来还是常数个额外的空间,所以此时空间复杂度等于:O(logN),再加上额外开辟的n个内存空间,则空间复杂度为:O(N+logN),省略对结
果影响不大的项,则最终的空间复杂度为:O(N)、
2、归并排序非递归版本:
归并排序非递归版本使用栈不好弄,即,使用栈来存储区间模拟归并排序的递归版本是不好搞的,因为,快速排序的递归版本的思想是前序遍历,而前序遍历使用
栈来存储区间进行模拟快排递归是比较方便的,但是归并排序的递归版本的思想是后序遍历,使用栈来存储区间模拟归并的递归版本是不方便的,,对于归并排序
非递归版本不需要借助栈,直接使用循环就可以解决,比如,给定一个随机数组为:10 6 7 1 3 9 4 2 ,对其使用归并排序非递归版本进行排升序,若使用归并排序
的递归版本排升序的话,该随机数组中的归并的最小单位为1个元素,若使用归并排序非递归版本排升序的话,直接控制该随机数组中的归并的最小单位,即,直接
控制1个元素,因为可以直接控制归并的元素个数,所以先给定一个间距gap,赋值为1,即先让10和6进行归并,再让7和1进行归并,直到把整个原数组中的所有元
素都归并到新数组中,然后把整个新数组中的所有数据都拷贝到原数组中去,然后再赋值gap为2,即让10,6和7,1进行归并,再让3,9和4,2进行归并,直到把整个原
数组中的所有元素都归并到新数组中,然后整个新数组中的所有数据都拷贝到原数组中去,再赋值gap为4,即让10,6,7,1和3,9,4,3进行归并,直到把整个原数组中
的所有元素都归并到新数组中,然后把整个新数组中的所有数据都拷贝到原数组中去,然后再赋值gap为8,此时,gap大于等于原数组中元素的个数,则循环停
止,此时原数组中的数据就已经满足了升序的要求,再把新数组释放即可,总体而言就是先1个1个的进行归并,直到把原数组中所有元素都归并到新数组中去,再
把新数组中的所有元素都拷贝到原数组中,再2个2个的进行归并,直到把原数组中所有元素都归并到新数组中去,再把新数组中的所有元素都拷贝到原数组中,再
4个4个的进行归并,直到把原数组中所有元素都归并到新数组中去,再把新数组中的所有元素都拷贝到原数组中,后一轮中的gap是前一轮中的gap的2倍,当
gap==n/2时,再进行归并的话,归并出来的结果就是整个数组都满足了升序,直到,gap>=n时,循环停止,此时原数组中已经满足了升序的要求,再把动态开辟
出来的新数组进行释放之后,把指针置为空指针即可、
代码为:
//归并排序非递归版本、
void MergeSortNonR(int*a, int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
int gap = 1;
//当gap>=n时,循环结束,当gap==n/2时,进行的是最后一次归并、
while (gap < n)
{
//为什么在gap前面乘一个2,因为是两组数据进行归并,一组数据中有gap个元素,要跳过两组数据,则乘以2、
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;
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
//当a[begin1] == a[begin2] 进入哪一个语句都是可以的、
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//由于begin1和begin2不是同时走的,故到此,上面while语句中的判断中只有一者不满足要求,要么是begin1所在的数组被遍历完毕,
//要么是begin2所在的数组被遍历完毕,即begin1和begin2所在的数组不可能同时被遍历完毕、
while (begin1 <= end1)
{
//begin2所在的数组被遍历完毕,begin1所在的数组未被遍历完毕、
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
//begin1所在的数组被遍历完毕,begin2所在的数组未被遍历完毕、
tmp[index++] = a[begin2++];
}
}
//整型int类型的拷贝可以使用memcpy、
memcpy(a,tmp,sizeof(int)*n);
gap *= 2;
}
//释放、
free(tmp);
tmp = NULL;
}
但是这种写法存在一定的问题,只有当原数组中的元素个数为2的幂次方倍时,才不会出现问题,例如当我们测试6个数时看一下打印的下标:
已知原数组元素个数为6个,下标范围是:[0,5],所以在此就发生了越界,例如当我们测试10个数时看一下打印的下标:
已知原数组元素个数为10个,下标范围是:[0,9],所以在此就发生了越界、
注意,begin1一定不会越界,end1,begin2,end2都有可能会出现越界,并且出现越界的概率从左往右依次增大、
下面将进行 一次修正,将越界的下标强制修改为n-1,修正部分的代码如下所示:
if (end1 >= n)
{
end1 = n - 1;
}
if (begin2 >= n)
{
begin2 = n - 1;
}
if (end2 >= n)
{
end2 = n - 1;
}
修正后打印结果为:
此时程序依旧不对,为什么?因为index会发生越界,当下标为[8,9]和[9,9]的时候,程序会在while(begin1 <= end1 && begin2 <= end2)这个位置进入循环两次,然
后此时index会变成10(最开始的index为8),然后后面因为begin2==end2会再次进入while循环,此时的index就会出现越界访问的现象、
此处进行分析:如果end1越界,我们是可以修正的,如果end2越界,begin2没有越界,我们也是可以修正end2的,如果begin2越界了,那么第二个区间就会直接
不存在,此时需要再对上面的代码进行修改、
//begin1一定不会越界,则end1可能越界,也可能不越界,若end1越界,修改即可、
//end1越界,修正即可、
if (end1 >= n)
{
end1 = n - 1;
}
//如果end1未越界,则begin2可能越界,也有可能不越界,若end1越界,则begin2一定会越界,不管end1是否越界,只要begin2越界,则第二个区间就不存在了、
//begin2越界,则第二个区间就不存在了、
if (begin2 >= n)
{
//保证 begin2 > end2 即代表了该区间不存在,主要是在该种情况下保证不进入下面的while循环语句中、
begin2 = n;
end2 = n-1;
}
//end2越界,修正即可、
//若begin2不越界,则end2可能越界,也可能不越界,若end2越界的话,直接修正即可、
if (begin2 < n && end2 >= n)
{
end2 = n - 1;
}
完整代码如下所示:
//归并排序非递归版本、
//升序、
void MergeSortNonR(int*a, int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
int gap = 1;
//当gap>=n时,循环结束,当gap==n/2时,进行的是最后一次归并、
while (gap < n)
{
//为什么在gap前面乘一个2,因为是两组数据进行归并,一组数据中有gap个元素,要跳过两组数据,则乘以2、
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;
//begin1一定不会越界,则end1可能越界,也可能不越界,若end1越界,修改即可、
//end1越界,修正即可、
if (end1 >= n)
{
end1 = n - 1;
}
//如果end1未越界,则begin2可能越界,也有可能不越界,若end1越界,则begin2一定会越界,不管end1是否越界,只要begin2越界,则第二个区间就不存在了、
//begin2越界,则第二个区间就不存在了、
if (begin2 >= n)
{
//保证 begin2 > end2 即代表了该区间不存在,主要是在该种情况下保证不进入下面的while循环语句中、
begin2 = n; //begin2和end2不一定必须是n和n-1,只要保证begin2 > end2 即可、
end2 = n-1;
}
//end2越界,修正即可、
//若begin2不越界,则end2可能越界,也可能不越界,若end2越界的话,直接修正即可、
if (begin2 < n && end2 >= n)
{
end2 = n - 1;
}
条件断点->用于调试、
//if (begin1 == 8 && end1 == 9 && begin2 == 9 && end2 == 9)
//{
// int x = 0;//随便定义一个变量方便在该条语句上打断点、
//}
打印->用于调试、
//printf("归并[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
//当a[begin1] == a[begin2] 进入哪一个语句都是可以的、
if (a[begin1] < a[begin2]) // 降序 if (a[begin1] > a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//由于begin1和begin2不是同时走的,故到此,上面while语句中的判断中只有一者不满足要求,要么是begin1所在的数组被遍历完毕,
//要么是begin2所在的数组被遍历完毕,即begin1和begin2所在的数组不可能同时被遍历完毕、
while (begin1 <= end1)
{
//begin2所在的数组被遍历完毕,begin1所在的数组未被遍历完毕、
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
//begin1所在的数组被遍历完毕,begin2所在的数组未被遍历完毕、
tmp[index++] = a[begin2++];
}
}
//整型int类型的拷贝可以使用memcpy、
memcpy(a,tmp,sizeof(int)*n);
gap *= 2;
}
//释放、
free(tmp);
tmp = NULL;
}
3、归并排序的外排序:
对于非递归算法,在debug和realase版本下的差距并不是很大,若是递归算法,则在debug和realase版本下的差距就比较大,因为realase版本下对递归算法栈帧建
立的优化比较大,比较明显,只要测性能,debug版本下的参考意义不大、
内部排序:数据元素全部放在内存中的排序,在内存中进行排序,常见排序算法均可以用作内部排序,包括归并排序、
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序,即数据放在磁盘上,归并排序可以用于外部排序,即只有
归并排序能进行外部排序,其他类型的排序均认为不可进行外部排序,故,归并排序既可以做内部排序,也可以做外部排序、
数据放在内存中和磁盘上的区别有:内存访问数据速度快,磁盘访问数据速度慢,不是说所有 在磁盘上的数据都必须要加载到内存才可以进行访问,在某一些地方
上会有缓存,而缓存设在磁盘上,这时需要将磁盘上的数据加载到内存中再进行访问,但这只是某些地方上,并不是所有在磁盘上的数据都必须要加载到内存才可
以访问的,而大多数在磁盘上的数据可以认为能够直接通过磁盘进行访问,并不需要必须加载到内存才可以访问,即,可以通过磁盘直接访问数据,内存是带电存
储,磁盘是不带电存储,程序虽然只能在内存中运行,但是程序可以访问内存中的数据,也可以访问磁盘上的数据,磁盘上的数据的访问形式主要是文件,内存中
的数据的访问形式主要是链表,顺序表,数组等等、
若只考虑内部排序的话,常见的排序算法一般都是通过数组进行存储数据的,因为CPU高速缓存比较快,其次,堆排序需要计算下标,快速排序优化需要计算三数
取中等,都需要在内存里的数组中进行,而数据放在内存和磁盘上的主要区别就是,若进行内部排序,则数据放在内存中,通常都是使用数组进行存储的,则支持
下标随机访问,若进行外部排序,则若数据放在磁盘上,只支持串行访问,大多数情况下,只需要进行内部排序即可满足要求,但是如果当数据量很大,比如10亿
个整型数据时,即大小为4G,就只能进行外部排序了、
例:10亿个整型int数据的文件,只给1G的运行内存,请对文件中的10亿个整型数据进行排升序:
此时只能使用归并排序进行外排序才可以满足要求,要知道,常见的排序算法中,只有归并排序能进行外部排序,其他类型的排序均认为不可进行外部排序,在本
题中,10亿个整数占4G大小的空间,只给1G的运行内存,所以不可以进行内部排序,只能进行外部排序,而进行外部排序只能选择归并排序、
思路一:
此时所指的归并排序的外部排序只是借助于内部归并排序的思想,并不是和内部归并排序一样,把10亿个整型数据都加载到内存中进行排序,而是利用内部归并排
序的思想,直接在磁盘中对10亿个整型数据进行操作,则有:
思路二:
不可以使用堆来选topk方法解决问题,因为,第一次能选出来,但是第二次就选不出来了,是因为直接对磁盘中的数据进行的操作,不能删除文件中的数据、
思路二本质上是对思路一的一种优化,已知在思路一中,若文件file2和文件file3中的各自的5亿个数据均为升序,则可以直接归并这两个文件中的数据到一个新文件
中,然后再把新文件中的数据拷贝到文件file1中,则此时的文件file1中的10亿个数据就满足了升序的要求,但是,不能保证文件file2和file3中的数据都是升序的,那
么此时,能不能,先把文件file2中的数据加载到内存中排升序,等待排好升序后再写入文件file2中,然后再把文件file3中的数据加载到内存中排升序,等待排好升序
后再写入文件file3中呢,这样的话,磁盘中文件file2和file3中的数据均满足了升序的要求,这样是不可以的,因为,文件file2和file3中各自有5亿个数据,所占空间
大小均为2G,而此时运行内存只有1G,故不可以把文件file2和file3中的数据加载到内存中去,所以要再次往下递归划分为file4,file5,file6,file7,在思路二中,
直接将要排序的数据所在的大文件平均分割成若干个可以加载到内存中的小文件,这四个文件中分别有2.5亿个数据,分别占大小为1G的空间,而又因为现在有1G
的运行内存,故可以分别把这四个文件中的数据依次加载到内存中去,然后在内存中进行排序,即先把文件file4中的数据加载到内存中排升序,等待排好升序后再
写入文件file4中,然后再把文件file5中的数据加载到内存中排升序,等待排好升序后再写入文件file5中呢,对于文件file6和file7的操作也是如此,此时,磁盘中的这
四个文件里面的数据均满足升序的要求,再次在磁盘中进行归并,此时已经满足了归并的要求,所以就不再需要像思路一那样等到递归结束时再开始进行归并,不
仅提高了效率,而且还减少了很多文件的使用,但是要注意,将这四个文件中的数据依次加载到内存中去排升序时,不能选择内部归并排序了,因为内部归并排序
空间复杂度是O(N),也就意味着,若把2.5亿个数据加载到内存中去,再使用内部归并排序的话,还需要额外开辟2.5亿个空间,则就需要5亿个内存空间,即2G的
空间大小,但是,运行内存只有1G,所以选择的排序算法的空间复杂度必须是O(1)才可以,所以可以选择快速排序优化,堆排序,希尔排序,这三个效率比较高的
排序算法其中一种方法进行排序,最好使用快速排序优化来进行排序、
注意,直接将要排序的数据所在的大文件平均分割成若干个可以加载到内存中的小文件,而不是递归划分出来的,以上述题目为例,已知原文件中的数据个数为10
亿个,占大小为4G的空间,现在运行内存只有1G,所以直接分为4份,每一份所占空间大小为1G,即分成4个文件,分别为fileD,fileE,fileF,fileG,这四个文件
中分别有2.5亿个数据,分别占大小为1G的空间,而又因为现在有1G的运行内存,故可以分别把这四个文件中的数据依次加载到内存中去,然后在内存中进行排
序,即先把文件fileD中的数据加载到内存中排升序,等待排好升序后再写入文件fileD中,然后再把文件fileE中的数据加载到内存中排升序,等待排好升序后再写入
文件fileE中呢,对于文件fileF和fileG的操作也是如此,此时,磁盘中的这四个文件里面的数据均满足升序的要求,再次在磁盘中进行归并,此时已经满足了归并的
要求,假设文件fileD和fileE中的数据归并到了新文件fileC中,文件fileF和fileG中的数据归并到了新文件fileB中,此时,新文件fileB,fileC中各有5亿个数据,然后文
件fileB,fileC中的数据再归并到一个新的文件fileA中,此时,文件fileA中有10亿个数据,并且满足了升序,然后再把文件fileA中的所有数据拷贝到原文件file中去 ,
那么原文件file中就有10亿个数据,并且都满足了升序,除此之外,文件fileB和fileC中的数据可以直接归并到文件file中,则此时,那么原文件file中就有10亿个数
据,并且都满足了升序,这两种方法都是可以的、
4、非比较排序:
之前所讲述的排序算法均是比较排序,即都是通过比较数据之间的大小进行的排序,此处所谓的非比较排序即指不通过比较数据之间的大小进行的排序,非比较排
序分为:计数排序,基数排序和桶排序,其中,基数排序和桶排序可视为一类,桶排序是基数排序的变形,基数排序和桶排序的时间复杂度还是不错的,但是由于
适用范围比较窄,比如,数据只能是整型数据,不能是负数,浮点数等等,所以在此就不再展开进行阐述了、
4.1、计数排序:
4.1.1、计数排序的思想:
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用, 操作步骤:
假设给定了一个随机数组为:10 6 7 1 6 1 ,现在使用计数排序算法排升序,所谓计数即指,统计相同数据的个数,不进行比较数据大小的操作,首先要开辟一
个额外的内存空间,该额外的空间具体开多大,和原随机数组中最大的数据有关,以上面的随机数组为例,遍历原随机数组得到最大值为10,则要开辟10+1个额外
的内存空间,即所额外开辟的内存空间的下标从0开始,到10结束,即下标范围是:[0,10],一共11个内存空间,然后把额外开辟的内存空间上的元素均初始化为
0,现在要进行统计相同数据的个数,遍历原随机数组,原随机数组首元素为10,则让额外开辟的内存空间下标为10的位置上的元素++,然后再继续遍历原随机数
组,原随机数组第二个元素为6,则让额外开辟的内存空间下标为6的位置上的元素++,重复上述操作,直到当原随机数组遍历完毕后,则额外开辟的内存空间中存
储的元素即为:0 2 0 0 0 0 2 1 0 0 0 , 即,在原随机数组中,若某一个值出现的次数为M次,则在额外开辟的内存空间中,下标为该值的位置上存储的数据的值即
为M,这是因为初始化额外开辟的内存空间中的值为0,这就统计出了在原随机数组中,相同元素出现的次数,然后再进行排序即可,若排升序,则从左往右依次遍
历额外开辟的内存空间,若排降序,则从右往左依次的遍历额外开辟的内存空间,具体见代码即可、
但是上述思路存在一定的问题,假设给定了一个随机数组为:10000 9999 5000 9999 5000 8888 , 现在使用计数排序算法排升序,所谓计数即指,统计相同数
据的个数,不进行比较数据大小的操作,首先要开辟一个额外的内存空间,该额外的空间具体开多大,和原随机数组中最大的数据有关,以上面的随机数组为例,
遍历原随机数组得到最大值为10000,则要开辟10000+1个额外的内存空间,即额外开辟的内存空间的下标从0开始,到10000结束,即下标范围是:[0,10000],
一共10001个内存空间,然后把额外开辟的内存空间上的元素均初始化为0,现在要进行统计相同数据的个数,遍历原随机数组,原随机数组首元素为10000,则让
额外开辟的内存空间下标为10000的位置上的元素++,然后再继续遍历原随机数组,原随机数组第二个元素为9999,则让额外开辟的内存空间下标为9999的位置上
的元素++,重复上述操作,直到当原随机数组遍历完毕后为止,此时就会发现,额外开辟的内存空间中,前5000个内存空间上存储的数据都是0,因为下标为5000
的空间是第5001个空间,那这样的话,前5000个空间都没有val值映射,就会造成浪费,原因是因为我们使用的是绝对映射, 所谓绝对映射即指,在原随机数组中
的某一个数据,以9999为例,则让额外开辟的内存空间下标为9999的位置上的元素++,,这就是绝对映射,使用绝对映射的是不太好的,因为,额外开辟的内存
空间的大小和原随机数组中的最大值有关系,原随机数组中的最大值越大,则额外开辟的内存空间数量就越多,若原随机数组中的元素个数比较少的话,就会造成
额外开辟的内存空间的浪费,除此之外,空间复杂度也会存在浪费,所以,要选择使用相对映射,相对映射是对绝对映射的优化,以上述原随机数组为例,此时,
额外开辟的内存空间中前5000个空间没有val值映射,所以会造成浪费,既然额外开辟的内存空间中前5000个内存空间没有val值映射,就起不到任何作用,那么干
脆,额外开辟的内存空间中前5000个空间就不再进行开辟了,只开辟后5001个内存空间即可,而后5001个内存空间对应的下标范围是:[5000,10000],如果让这
后5001个内存空间的下标从0开始的话,则下标范围应该是:[0,5000],此时,使用相对映射,则额外开辟的内存空间下标为0的位置上所存储的数据代表的就是
在原随机数组中出现最小值5000的次数,则额外开辟的内存空间下标为5000的位置上所存储的数据代表的就是在原随机数组中出现最大值10000的次数,则额外开
辟的内存空间下标为8888-5000,即3888的位置上所存储的数据代表的就是在原随机数组中出现8888的次数,即,在原随机数组中的某一个val值,映射到额外开辟
的内存空间的下标为val-min的位置上,所以,选择相对映射的话,就减少了额外开辟的内存空间的浪费,同时也减少了空间复杂度的浪费、
计数排序算法适用于范围确定且集中的数据,比如26个英文字母,就非常合适使用计数排序算法,即数据不能太离散的情况,在这样的情况下, 计数排序算法可能
比快排优化的效率都高、
1、统计相同元素出现次数、
2、根据统计的结果将序列回收到原随机的序列中、
图示:
代码:
//计数排序、
//时间复杂度的最好最坏情况是一样的,在排序部分中,内外循环嵌套,并且内外循环次数均不固定,与之前所见的循环嵌套存在一定的区别,排序部分中内层while循环总次数就是原随机数组的元素个数n,
//根据思想可知,排序部分的执行次数为:range+n, 总的执行次数为:range+3n ,则时间复杂度即为:O(range+n),若题目中告诉了n远大于range,则时间复杂度即为:O(n)、,空间复杂度为:O(range)、
//根据时间复杂的和空间复杂度也可知,计数排序适用于范围确定且集中的数据、
//此时可以适用于负数,因为使用的是相对映射,若使用绝对映射则不适用于负数,其次,不管是相对映射还是绝对映射,都不适用于浮点数和字符串等其他非整型int类型的数据,
//像其他非整型int类型的数据只能使用比较排序,比较排序才是比较正统的排序,计数排序的适用范围比较小,只能用于一些比较特殊的场景下、
void CountSort(int*a, int n)
{
assert(a);
//求出原随机数组中的最大值和最小值、
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i]>max)
max = a[i];
if (a[i] < min)
min = a[i];
}
//相对映射、
int range = max - min + 1;//计算映射表数组的大小、
//建立映射数组、
//int* countA = (int*)malloc(sizeof(int)*range);
//assert(countA);
初始化、
//memset(countA, 0, sizeof(int)*range);
int* countA = (int*)calloc(range, sizeof(int));//calloc函数会自动初始化内存,单位是字节,即初始化每个字节为0、
assert(countA);
//计数、
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序:直接操作原随机数组、
int j = 0; //j用来记录原随机数组的下标、
//排升序、
for (int i = 0; i < range; i++)//i用来记录映射表数组元素的下标、
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
排降序、
//for (int i = range-1; i >=0; i--)
//{
// while (countA[i]--)
// {
// a[j++] = i + min;
// }
//}
//释放、
free(countA);
countA = NULL;
}
4.1.2、计数排序的复杂度:
时间复杂度:O(N + range)、
空间复杂度:O(range)、
注意:range = max - min +1、
说明:计数排序适用于范围确定且集中的数据,此时可以适用于负数,因为使用的是相对映射,若使用绝对映射则不适用于负数,其次,不管是相对映射还是绝对映
射,都不适用于浮点数和字符串等其他非整型int类型的数据,像其他非整型int类型的数据只能使用比较排序,比较排序才是比较正统的排序,计数排序的适用范围
比较小,只能用于一些比较特殊的场景下、
4.1.3、计数排序的稳定性分析:
计数排序的稳定性不需要进行分析,没有意义,不进行阐述、
4.1.4、计数排序的特性总结:
4.1.5、计数排序其他思路(了解):
5、常见排序算法复杂度及稳定性分析:
图表总结:
6、常见排序算法函数接口实现:
6.1、Test.c文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"
#include"Stack.h"
//插入排序、
void TestInsertSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//插入排序、
InsertSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//冒泡排序、
void TestBubbleSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//冒泡排序、
BubbleSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//希尔排序、
void TestShellSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//希尔排序、
ShellSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//堆排序、
void TestHeapSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//堆排序、
HeapSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//直接选择排序、
void TestSelectSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5};
//直接选择排序、
SelectSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//直接选择排序优化、
void TestNewSelectSort()
{
int arr[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//直接选择排序优化、
NewSelectSort(arr, sizeof(arr) / sizeof(arr[0]));
//打印、
PrintArray(arr, sizeof(arr) / sizeof(arr[0]));
}
//快速排序、
void TestQuickSort()
{
int a[] = { 9, 1, 2, 5, 7, 4, 8, 6, 3, 5 };
//快速排序、
QuickSort(a, 0, sizeof(a) / sizeof(int)-1);
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
//快速排序->非递归、
void TestQuickSortNon_Recursive()
{
int a[] = { 6,1,2,7,9,3,4,5,10,8};
//快速排序->非递归、
QuickSortNon_Recursive(a, 0, sizeof(a) / sizeof(int)-1);
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
//归并排序、
void TestMergeSort()
{
int a[] = {10,6,7,1,3,9,4,2};
//归并排序、
MergeSort(a,sizeof(a) / sizeof(int));
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
//归并排序非递归版本、
void TestMergeSortNonR()
{
int a[] = { 10, 6, 7, 1, 3, 9 ,4 ,2 ,5};
//归并排序非递归版本、
MergeSortNonR(a, sizeof(a) / sizeof(int));
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
//计数排序、
void TestCountSort()
{
int a[] = { 100, -60, 70, 100,65,-60,20,-50,120};
//计数排序、
CountSort(a, sizeof(a) / sizeof(int));
//打印、
PrintArray(a, sizeof(a) / sizeof(int));
}
//测试、
void TestOP()
{
srand(time(0));
//数组元素个数、
const int N = 10000000;
//malloc7个数组,对应7个排序、
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);
int* a7 = (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];
a7[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();
int begin7 = clock();
//BubbleSort(a2, N);
int end7 = 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);
printf("BubbleSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
}
int main()
{
//插入排序、
TestInsertSort();
//冒泡排序优化、
TestBubbleSort();
//希尔排序、
TestShellSort();
//堆排序、
TestHeapSort();
//直接选择排序优化、
TestNewSelectSort();
//快速排序、
TestQuickSort();
//快速排序->非递归、
TestQuickSortNon_Recursive();
//归并排序、
TestMergeSort();
//归并排序非递归版本、
TestMergeSortNonR();
//计数排序、
TestCountSort();
//测试、
TestOP();
return 0;
}
6.2、Sort.c文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Sort.h"
#include"Stack.h"
//打印、
void PrintArray(int*a, int n)
{
assert(a);
int i = 0;
for (i = 0; i < n; i++)
{
printf("%d ", a[i]);
}
printf("\n");
}
//插入排序、
//基本思想:有一个有序区间,插入一个数据后,仍保持有序、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定次数为:N-1,内存while循环次数不固定,则需要列出来具体的执行次数,而总的执行次数为:T(N)=(1+2+3+....+(N-1))、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(1+2+3+....+(N-1)),部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数为:T(N)=N-1个1相加,等于N-1,则部分时间复杂度就是:O(N)、
//时间复杂度看最坏,则还是:O(N^2)、
//若不是最好的情况,即不是顺序的话,但是非常接近于顺序时,则总的执行次数就比最好情况下的执行次数N-1多一部分常数项次,则部分时间复杂度还是:O(N)、
//对于插入排序而言,顺序或者接近于顺序,是比较合适使用的,虽然时间复杂度是:O(N^2),但是若在该种情况下时,其部分时间复杂度是O(N),则很快就可以完成排序,时间效率较高、
//针对于插入排序进行优化,优化后叫做希尔排序,即希尔排序是插入排序的优化、
void InsertSort(int* a, int n)
{
assert(a);
for (int i = 0; i <= n - 2; i++)
{
//写排序时,先把单趟排序写出来,假设要把下标为end+1的元素插入到下标为[0,end]所构成的一个 有序 序列1中,使插入后构成的新的序列2仍是有序序列、
int end=i;
int tmp = a[end + 1];
while (end >= 0)
{
//降序、
//if (tmp > a[end])
//升序、
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
//若在while外部后面加上一条语句后,若是通过break出去的话,就会重复操作,所以可以直接把else里面的这条语句删除即可、
//删除-> a[end + 1] = tmp;
break;
}
}
//若要插入到有序序列1中的数据比该序列1中的所有元素都小的话,此时if中的end--后,则end就等于-1,然后再一次循环就进不去while循环内,则有序序列2
//中第一个位置上是空的,并没有把tmp放在该位置上,所以在while循环外部手动再把tmp放在有序序列2中的第一个位置上、
a[end + 1] = tmp;
}
}
//若要对比插入排序和冒泡排序的话,他们的时间复杂度都是:O(N^2),因为时间复杂度看的是最坏的情况、
//虽然时间复杂度是一样的,但是插入排序还是要比冒泡排序要好,两者在最坏的情况下执行的次数是相等的,但是在最好的情况下,两者的执行次数差的很多,即最好情况下的部分时间复杂度是不一样的,此时插入排序的时间效率要好于冒泡排序,即所用时间较少、
//交换函数,传址调用、
void Swap(int* pa, int* pb)
{
assert(pa && pb);
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
//冒泡排序->本质上是交换排序、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定为N-1次,内层循环不固定,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数仍为:T(N)=(N-1)+(N-2)+..+3+2+1,则部分时间复杂度就是:O(N^2),因为算法中不存在break,当进行升序排序时
//即使数组已经是升序状态了,也不会直接break出来,只是不进入if语句,但两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
//时间复杂度看最坏,则还是:O(N^2)、
方法一:
//void BubbleSort(int* a, int n)
//{
// assert(a);
// 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]);
// }
// }
// }
//}
方法二:
//void BubbleSort(int* a, int n)
//{
// assert(a);
// for (int i = 0; i < n-1; i++)
// {
// for (int j = 1; j < n- i; j++)
// {
// if (a[j-1]>a[j])
// {
// Swap(&a[j-1], &a[j]);
// }
// }
// }
//}
//冒泡排序优化、
//按照自己的方法直接计算时间复杂度则为:O(N^2),外层循环固定为N-1次,内层循环不固定,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1、
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数仍为:T(N)=(N-1),则部分时间复杂度是:O(N),因为,若进行升序,而数组就是升序的话,
//这样的话,外层for循环则只进行了一次,在该次循环中,内层for循环进行了N-1次,然后从break直接出去了外层for循环、
//时间复杂度看最坏,则还是:O(N^2)、
//若不是最好的情况,即不是顺序的话,但是非常接近于顺序时,则总的执行次数就比最好情况下的执行次数N-1多一部分,有可能是T(N)=(N-1)+(N-2),则部分时间复杂度还是:O(N)、
void BubbleSort(int* a, int n)
{
assert(a);
for (int i = 0; i < n - 1; i++)
{
int exchange = 0;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j]>a[j + 1])
{
exchange = 1;
Swap(&a[j], &a[j + 1]);
}
}
//若前一趟冒泡排序若未进行交换,则说明已经满足升序或降序了,则剩余趟数的冒泡排序就不需要再进行了、
if (exchange == 0)
{
//前一趟冒泡排序若未进行交换、
break;
}
}
}
//不是特例的情况下,直接选择排序是最垃圾的、
//插入排序和冒泡排序优化的对比、
//虽然插入排序最坏是N^2,最好是N, 冒泡排序优化最坏是N^2,最好是N,但是两者还是不一样好、
//只有在逆序和顺序,即最坏和最好的情况下,两者是一样的,但是除了这两种情况,还是插入排序比较好,由于时间复杂度看的是最坏的情况,所以即使两者的时间复杂度相同,但是从细节上来说的话,还是插入排序更加合适、
//虽然插入排序在细节上比冒泡排序优化更加合适,但是对于两者而言,由于时间复杂度相同,所以还要看做是同一级别的排序、
//比如:
//数组元素分别为:1 2 3 4 5 6 8 7, ,此时插入排序只计算比较次数,则为:8次, 冒泡排序只计算比较次数则为:7+6=13次,这是因为第一趟冒泡排序比较了7次,交换了7和8,交换完之后
//虽然已经满足了升序,但是此时的exchange==1,所以还要再进入第二趟冒泡排序,第二趟冒泡排序比较次数为6次,但第二趟冒泡排序完之后,exchange仍为0,所以就不再进行第三趟冒泡排序了,
//则总的比较次数为13次,只有在前一趟冒泡排序中未进行交换,才不再进行下一趟冒泡排序,若在前一趟冒泡排序中进行了交换,但交换后满足了升序或者降序,此时是还需要再进行下一次冒泡排序的,这是因为
//前一趟冒泡排序进行了数据的交换,即使前一趟冒泡排序进行交换元素后是升序或者降序了,但是编译器还不知道此时已经是升序或者降序了,只有再进行一趟冒泡排序后未进行数据交换,编译器才知道已经是升序或者降序,就不再进行下一趟了、
//当是顺序时,即最好的情况下,两者一样,但是当部分顺序时,冒泡排序优化中的exchange优化作用不大,而对于插入排序而言,该情况下的那些部分顺序中,也能体现出它的价值,则插入排序更好,其适应性和比较次数都较少、
//当是逆序时,即最坏的情况下,两者一样,但是当部分顺序时,冒泡排序优化中的exchange优化作用不大,而对于插入排序而言,该情况下的那些部分逆序中,也能体现出它的价值,则插入排序更好,其适应性和比较次数都较少、
//所谓的接近顺序指的是接近顺序的程度很大的情况,若接近顺序的程度不大的话,则直接看做是局部顺序即可,当非常接近与顺序时,部分时间复杂度还是O(N),此时的exchange的优化作用很大、
//希尔排序、
//平均时间复杂度为:O(N^1.3)、
//已知,直接插入排序的时间复杂度是:O(N^2),因为看的是最坏的情况,对于插入排序而言,最好的情况下的时间复杂度是:O(N),接近于顺序的情况下时间复杂度也是:O(N),若使用直接插入排序的方法,不考虑它的优化的话
//即使给定的是一个最好的情况,那么该方法的时间复杂度还是O(N^2),因为时间复杂度要看最坏的情况,虽然时间复杂度是O(N^2),但是还是希望是最好的情况或者接近于最好的情况,这样的话,就会节省很多时间,但是如果要求一个序列
//是顺序或者接近顺序,就比较难了,一般来说,要进行排序的序列都是随机的,并不能保证是顺序或者接近顺序,所以在使用插入排序之前能否先对序列进行调整,使之成为顺序或者接近于顺序,然后再进行插入排序就比较节省时间了,该方法
//即指所谓的希尔排序、
void ShellSort(int* a, int n)
{
assert(a);
方法一:
思路:一组间隔为gap的数据进行插入排序完毕后,再进行下一组间隔为gap的数据的插入排序,直到gap组数据都插入排序结束、
假设gap等于3、
int gap = 3;
先写出gap=3的预排序、
gap的值不能使用一个固定值,gap的最合适的值要随着数组元素个数N的不同而变换,gap的值要和数组元素个数N关联起来,N越来,最开始的gap就越大,预排序过程中并不是只能取一次gap值,可以取多次gap值、
先让gap从大开始,让大的数和小的数更快的到后面和前面去,然后gap再越来越小,再让序列越来越接近顺序,最后当gap等于1时,相当于进行插入排序即可,便可得到顺序序列了,即完成了升序排列、
控制gap组数据、
//int gap = n;
//while (gap > 1) 这个地方不能是>=1,因为经过后面的控制后,如果是>=1,就会陷入死循环、
//{
// //gap怎么变并没有规定,只是常使用如下方式:
// //当gap > 1 时,就是预排序的过程、
// //当gap = 1 时,就是直接插入排序的过程、
// //此处加1能保证最后一次一定是1,不管gap是大于3,等于3,还是小于3,最后的gap值一定是1、
// gap = gap / 3 + 1;
// for (int j = 0; j < gap; j++)
// {
// //控制间隔为gap的一组数据进行插入排序、
// for (int i = j; i < n - gap; i += gap) 为什么是n - gap?因为最后一个end是n-1-gap,end+gap一定小于n、
// {
// int end = i;
// int tmp = a[end + gap];
// while (end >= 0)
// {
// if (tmp < a[end])
// {
// a[end + gap] = a[end];
// end -= gap;
// }
// else
// {
// break;
// }
// }
// //到此有两种情况:
// //1、当tmp>=a[end]时,break出来的。
// //2、当end<0时,不进入while循环,从而达到此处、
// a[end + gap] = tmp;
// }
// }
//}
//方法一和方法二的时间复杂度是一样的、
//方法二:
//思路:和方法一的思路不太相同,把第一组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列,先进行第一组数据中第二个数据的插入,把第二组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列
//再进行第二组数据中第二个数据的插入,把第三组数据中的第一个元素看做是一个有序序列,要排升序,则看做是升序序列,再进行第三组数据中第二个数据的插入,则第一组数据中的前两个元素构成了升序序列,再进行第一组数据中第三个数据的插入,
//则第二组数据中的前两个元素构成了升序序列,再进行第二组数据中第三个数据的插入,则第三组数据中的前两个元素构成了升序序列,再进行第三组数据中第三个数据的插入,重复上述操作,直到把gap组数据中剩下的所有元素都插入排序结束、
//假设gap等于3、
//int gap = 3;
//gap的值不能使用一个固定值,gap的最合适的值要随着数组元素个数N的不同而变换,gap的值要和数组元素个数N关联起来,N越来,最开始的gap就越大,预排序过程中并不是只能取一次gap值,可以取多次gap值、
//先让gap从大开始,让大的数和小的数更快的到后面和前面去,然后gap再越来越小,再让序列越来越接近顺序,最后当gap等于1时,相当于进行插入排序即可,便可得到顺序序列了,即完成了升序排列、
int gap = n;
while (gap > 1)//这个地方不能是>=1,因为经过后面的控制后,如果是>=1,就会陷入死循环、
{
//gap怎么变并没有规定,只是常使用如下方式:
//当gap > 1 时,就是预排序的过程、
//当gap = 1 时,就是直接插入排序的过程、
//此处加1能保证最后一次一定是1,不管gap是大于3,等于3,还是小于3,最后的gap值一定是1、
//此处要是除2的话,直接写成:gap = gap/2即可,肯定能够使得gap取到1,除3要写成下面这样,除别的数不一定,但必须保证能够使得gap取到1才可以。
//gap = gap/2;
gap = gap/3+1;
for (int i = 0; i < n - gap; i++) //为什么是n - gap?因为最后一个end是n-1-gap,end+gap一定小于n、
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
//到此有两种情况:
//1、当tmp>=a[end]时,break出来的。
//2、当end<0时,不进入while循环,从而达到此处、
a[end + gap] = tmp;
}
}
}
//如果不对插入排序进行优化的话,直接使用插入排序的话,若是逆序的情况,即最坏情况下,部分时间复杂度就是O(N^2),假设n>1、
//若先进行预排,当gap从大减小到1时,进入for循环就相当于是直接插入排序,而当gap等于1,但未进入for循环的时候,此时序列已经很接近于顺序了,
//就可以认为此时再对该序列进行插入排序操作的话,此时最后一步的直接插入的部分的部分时间复杂度就是O(N),效率非常高,只要预排序叠加最后一次的直接插入排序的部分时间小于O(N^2),则希尔排序就起到了作用、
//当n=1时,gap等于1,不进入while循环,则希尔排序调用函数不起作用,当n>1时,此时把n的值赋给gap,则gap也是大于1的,然后gap再逐渐减小,当gap等于1时,进入for循环就相当于是直接插入排序,
//而当gap等于1,但未进入for循环的时候,此时序列已经很接近于顺序了,就可以认为此时再对该序列进行插入排序操作的话,效率非常高、
//希尔排序的缺点就是,若对数组排升序,本来就是升序的话,或者本来就是很接近于升序的话那么预排序就相当于是白做了,虽然该种情况下,预排序很快就执行完了,但总的来说还是白做了,效果不大,
//但是这种情况很少,多数情况下,所给的数组都是随机的,并不是顺序或者接近顺序的,所以在大部分情况下,希尔排序还是能够起到很大的作用的、
//当顺序或者接近顺序时,不管数据量的大小,此时插入都比希尔要好,因为希尔前面的预排序相当于白做了,当随机,部分顺序,或者逆序时,若数据量较大时,则希尔比插入更加合适,但是如果数据量很小
//的时候,希尔并不一定比插入要好、
//一级指针传参,一级指针接收、
void AdjustDown(int* a, size_t size, size_t root)
{
assert(a);
size_t parent = root;
size_t child = parent * 2 + 1;
while (child < size)
{
//大堆、
//对于大堆而言,要选左右孩子中较大的一个,则改为:
//if (child + 1 < size && a[child + 1] > a[child])
//小堆、
if (child + 1 < size && a[child + 1] > a[child])
{
//左右孩子都存在并且右孩子小于左孩子、
child++;
}
//当执行到此处时,不清道child到底指的是左孩子还是右孩子,但是确定的是,child指向的就是左右两个孩子中较小的一个、
//若孩子大于父亲,则交换、
///大堆、
//if (a[child] > a[parent])
//若孩子小于父亲,则交换、
//小堆、
if (a[child] > a[parent])
{
//拿该较小的孩子与父亲比较, 若比父亲小, 则交换, 则另外一个子树不受影响, 即使第一步中的两个孩子相等, 也不受影响、
Swap(&a[child], &a[parent]);
parent = child;
//再次计算child时,仍默认指向左孩子、
child = parent * 2 + 1;
}
else
{
break;
}
}
}
//堆排序、
//时间复杂度是:O(N*logN)、
//空间复杂度是:O(1)、
void HeapSort(int* a, int n)
{
assert(a);
1、向上调整算法建堆、、
//for (int i = 1; i < n; i++)
//{
// AdjustUp(a, i);
//}
//2、向下调整算法建堆、
int j = 0;
for (j = (n - 1 - 1) / 2; j >= 0; j--)
{
AdjustDown(a, n, j);
}
size_t end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
//直接选择排序优化、
void NewSelectSort(int* a, int n)
{
assert(a);
int left = 0;
int right = n - 1;
while (left < right)
{
//此处若定义成int i = left+1,则把left赋值给mini和maxi,这样就一定不会出现错误,如果在[left+1,right]中找到了比下标为left的值更小的数的话,则更新mini,若找到了比下标为left的值
//更大的数的话,则更新maxi,若在该范围内找不到比下标为mini的值更小的数据的话,比下标为maxi的数更大的数,则此时mini和maxi所指的数,即left所指的数为最小值,则left所指的数即为最大值、
//此处若定义成int i = left+1,则把right赋值给mini的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更小的数的话,则更新mini,此时在范围[left+1,right]中的最小值就是下标为mini的数,
//若在该范围内找不到比mini更小的数的话,则在范围[left+1,right]中的最小的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最小值,该最小值并不一定比left所指的数要小,如果该最小值小于等于left所指的数,则是正确的,
//但若该最小值大于left所指的数,则程序就是错误的,所以尽量不要这样写、
//此处若定义成int i = left+1,则把right赋值给maxi的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更大的数的话,则更新maxi,此时在范围[left+1,right]中的最大值就是下标为maxi的数,
//若在该范围内找不到比maxi更大的数的话,则在范围[left+1,right]中的最大的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最大值,该最大值并不一定比left所指的数要大,如果该最大值大于等于left所指的数,则是正确的,
//但若该最大值小于left所指的数,则程序就是错误的,所以尽量不要这样写、
//所以当定义成int i = left+1的话,一定要把left赋值给mini和maxi,这是一定不会出现错误的,若把right赋值给mini和maxi的话,则有可能会出现错误、
//若定义成定义成int i = left,则令mini和maxi为left或者right都可以,都不会出现错误、
int mini = left;
int maxi = left;
//若定义成int i = left的话,只需要让数组中的任意一个值作为最大值或者最小值都可以,并不是只能让left和right所指的数作为最大值或者最小值,数组中的元素任意一个都是可以的、
//若定义成int i = left+1的话,则只能让left所指的数作为最小值或者最大值、
for (int i = left+1; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
//此时,left和maxi重叠,当执行第一次Swap时,则最大值9所在的位置并不是maxi,被调换了位置,导致出现问题、
//这只是凑巧了,出现了极端情况,在这次示例中出现了错误,可能在其他的示例中并不会出现错误、
Swap(&a[mini], &a[left]);
//如果left和maxi重叠,则修正一下maxi即可、
if (left == maxi)
{
maxi = mini;
}
Swap(&a[maxi], &a[right]);
left++;
right--;
}
}
//若按照自己的方法求时间复杂度,若数组元素个数为偶数,则外层while循环的次数为固定值N/2次,若数组元素个数为奇数,则外层while循环的次数接近为固定值N/2次,内层循环也为固定值是N-1次,则总的执行次数为:T=(N/2)*(N-1),则时间复杂度就是:O(N^2)、
//选择排序优化中不存在break,故不区分最好最坏,两者都是一样的,则
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,若数组元素个数为奇数个,则总的执行次数为:T(N)=N+(N-2)+..+3+1,部分时间复杂度是:O`(N^2),若数组元素个数为偶数个,则总的执行次数为:T(N)=N+(N-2)+..+2+0,则部分时间复杂度还是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,若数组元素个数为奇数个,则总的执行次数为:T(N)=N+(N-2)+..+3+1,部分时间复杂度是:O(N^2),若数组元素个数为偶数个,则总的执行次数为:T(N)=N+(N-2)+..+2+0,则部分时间复杂度还是:O(N^2)、
//因为算法中不存在break,当进行升序排序时即使数组已经是升序状态了,也不会直接break出来,所以两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
//时间复杂度看的是最坏的情况,虽然其时间复杂度和冒泡排序优化的时间复杂度都是一样的,即都是O(N^2),但是具体的执行次数是不一样的,冒泡排序优化最坏情况下的执行次数为:T(N)=(N-1)+(N-2)+..+3+2+1
//而选择排序优化的执行次数不论数组元素个数是奇数还是偶数,都比冒泡排序优化最坏情况下的执行次数少了将近一半,故在测试中,选择排序优化会好于冒泡排序优化,即在一般情况下,随机或者逆序情况下,选择排序优化会好于冒泡排序优化、
//但是如果数组是顺序或者接近顺序时,冒泡排序优化在测试中就会比选择优化要好,是因为,此时冒泡排序优化的部分时间复杂度可看成O(N),而选择优化的部分时间复杂度还是O(N^2),因为选择优化中,不存在break,即使是顺序或者接近顺序,那么所有的循环也必须都要全部执行完毕才可以
//所以该种情况下,冒泡优化会比选择优化要好、
//插入排序,顺序则部分时间复杂度是O(N),接近顺序则部分时间复杂度也是O(N)、
//冒泡排序优化,顺序则部分时间复杂度是O(N),接近顺序则部分时间复杂度也是O(N)、
//选择排序优化,顺序则部分时间复杂度是O(N^2),接近顺序则部分时间复杂度也是O(N^2)、
//选择排序,顺序则部分时间复杂度是O(N^2),接近顺序则部分时间复杂度也是O(N^2)、
//对于整体时间复杂度为:O(N^2)的四个排序,即,插入排序,冒泡优化排序,选择优化排序,选择排序而言:
//若是随机或者逆序时,则最好的是选择排序优化,而其他三者差不多、
//若是顺序的话,则插入和冒泡优化差不多,都算最好,其次是选择优化,最坏的是选择、
//若是接近顺序的话,则插入要稍微好于冒泡优化一点,其次是选择优化,最后是选择、
//部分顺序的话,插入排序也能体现出优势,应是最好,其次是冒泡排序优化,exchange的优化没有插入排序的优势大,冒泡排序优化在部分顺序下和选择优化比较的话,要看部分顺序的程度,两者不能明确得出结果,但是冒泡排序优化和选择优化在部分顺序下一定比选择要好、
//此处所指的好坏只是计算了在某些情况下循环的次数,但是在所用时间测试中不一定是上述结果,是因为时间测试中,也把其他非循环的代码执行也计算了他们的时间,这样时间也和非循环代码的量有关系了、
//虽然在随机或逆序情况下,选择优化会比插入更好一点,但是从整体而言,插入是最合适的,即,要从这四个时间复杂度为O(N^2)的方法中选择的话,优先选择插入排序、
//若顺序或者接近顺序,则选择排序的时间效率低于冒泡排序优化、
//若随机或者逆序时,选择排序和冒泡优化,时间效率差不多、
//直接选择排序,升序、
void SelectSort(int* a, int n)
{
assert(a);
int left = 0;
int right = n - 1;
while (left < right)
{
int mini = left;
//若定义 int i=left,则int mini=left/right 均可,若定义int i=left+1,则必须定义为: int mini =left,否则可能会出问题、
//升序或者降序、
//此处若定义成int i = left+1,则把left赋值给mini和maxi,这样就一定不会出现错误,如果在[left+1,right]中找到了比下标为left的值更小的数的话,则更新mini,若找到了比下标为left的值
//更大的数的话,则更新maxi,若在该范围内找不到比下标为mini的值更小的数据的话,比下标为maxi的数更大的数,则此时mini和maxi所指的数,即left所指的数为最小值,则left所指的数即为最大值、
//此处若定义成int i = left+1,则把right赋值给mini的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更小的数的话,则更新mini,此时在范围[left+1,right]中的最小值就是下标为mini的数,
//若在该范围内找不到比mini更小的数的话,则在范围[left+1,right]中的最小的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最小值,该最小值并不一定比left所指的数要小,如果该最小值小于等于left所指的数,则是正确的,
//但若该最小值大于left所指的数,则程序就是错误的,所以尽量不要这样写、
//此处若定义成int i = left+1,则把right赋值给maxi的话,这样就可能会出现错误,如果在[left+1,right]中找打了比下标为right的值更大的数的话,则更新maxi,此时在范围[left+1,right]中的最大值就是下标为maxi的数,
//若在该范围内找不到比maxi更大的数的话,则在范围[left+1,right]中的最大的数就是下标为right的数,但是这只是找到了在范围[left+1,right]中的最大值,该最大值并不一定比left所指的数要大,如果该最大值大于等于left所指的数,则是正确的,
//但若该最大值小于left所指的数,则程序就是错误的,所以尽量不要这样写、
//所以当定义成int i = left+1的话,一定要把left赋值给mini和maxi,这是一定不会出现错误的,若把right赋值给mini和maxi的话,则有可能会出现错误、
//若定义成定义成int i = left,则令mini和maxi为left或者right都可以,都不会出现错误、
for (int i = left+1 ; i <= right; i++)
{
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[mini], &a[left]);
left++;
}
}
//若按照自己的方法求时间复杂度,则外层while循环的次数为固定值N-1次,内层循环也为固定值是N-1次,则总的执行次数为:T=(N-1)*(N-1),则时间复杂度就是:O(N^2)、
//选择排序中不存在break,故不区分最好最坏,两者都是一样的,则
//最坏:逆序最坏,比如排升序,但现在数组是降序,则是最坏的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+2+1,部分时间复杂度是:O(N^2)、
//最好:顺序最好,比如排升序,现在数组就是升序,则是最好的情况,则总的执行次数为:T(N)=(N-1)+(N-2)+..+2+1,部分时间复杂度是:O(N^2)、
//因为算法中不存在break,当进行升序排序时即使数组已经是升序状态了,也不会直接break出来,所以两层for循环必须都要执行完,则在该种情况下,不区分最好最坏,部分时间复杂度都是O(N^2)、
//三数取中法、
//取出数组首元素,尾元素,中间位置元素,得到这三者中,不是最大,也不是最小的那个值的下标、
int GetMidIndex(int*a, int left, int right)
{
assert(a);
//记录数组中间位置的下标、
//int mid = (left + right) / 2; //如果 left+right 过大可能会存在溢出,最好不采用这种方法、
int mid = left + (right - left) / 2; //避免 left+right 过大导致溢出,也可以使用移位、
//当数组首元素,尾元素,中间位置上的元素,这三个元素至少有两个相等时,则就无法选出来既不是最大,也不是最小的那个值,所以此时把这三个元素中任意一个元素的下标返回出去即可,
//也可以理解为,让这三个元素中任意一个元素作为所谓的既不是最大,也不是最小的元素,则 left mid right 均可视为既不是最大,也不是最小的那个元素的下标、
//故在代码中只需要考虑三个数据两两不相等的情况即可,因为当不是三个数据两两不相等的时候,把这三个数据中任意一个数据的下标返回出去都是可以的、
// left mid right
//方法一:
//分a[left] < a[mid]和a[left] > a[mid] 两种情况
if (a[left] < a[mid]) //a[left] < a[mid]、
{
if (a[mid] < a[right])
{
return mid; //即: a[left] a[mid] a[right] 此时mid所指的元素既不是最大,也不是最小的数据、
}
else if (a[left]>a[right])
{
return left; //即: a[right] a[left] a[mid] 此时left所指的元素既不是最大,也不是最小的数据、
}
else
{
return right; //即: a[left] a[right] a[mid] 此时right所指的元素既不是最大,也不是最小的数据、
}
}
else //a[left] > a[mid]、
{
if (a[mid] > a[right])
{
return mid; //即: a[right] a[mid] a[left] 此时mid所指的元素既不是最大,也不是最小的数据、
}
else if (a[left] > a[right])
{
return right; //即: a[mid] a[right] a[left] 此时right所指的元素既不是最大,也不是最小的数据、
}
else
{
return left; //即: a[mid] a[left] a[right] 此时left所指的元素既不是最大,也不是最小的数据、
}
}
方法二:
//int mid = left + (right - left) / 2;
//if (a[left] < a[right])
//{
// if (a[mid] < a[left])
// return left;
// else if (a[mid] > a[right])
// return right;
// else
// return mid;
//}
//else //a[left] > a[right]
//{
// if (a[mid] < a[right])
// return right;
// else if (a[mid] > a[left])
// return left;
// else
// return mid;
//}
}
//单趟排序->hoare版本、
int PartSortHoare(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
//方法一:
//选取数组首元素作为基准值、
//三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
int midi = GetMidIndex(a, left, right);
//若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
Swap(&a[midi], &a[left]);
//下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int keyi = left; //用keyi记录key的下标、
while (left < right)
{
//升序、
//right先走、
while (left < right && a[right] >= a[keyi])//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
{
right--;
}
//left再走、
while (left < right && a[left] <= a[keyi])//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
{
left++;
}
//上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
//上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组首元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
//交换、
Swap(&a[left], &a[right]);//找到后就交换a[left]和a[right],让小的去左边,大数去右边,降序则让小的去右边,大的去左边、
}
//交换相遇位置和keyi位置上的数据、
Swap(&a[keyi], &a[left]);
//Swap(&a[keyi], &a[right]);
return right;
//return left;
方法二:
选取数组尾元素作为基准值、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[right]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = right; //用keyi记录key的下标、
//while (left < right)
//{
// //升序、
// //left先走、
// while (left < right && a[left] <= a[keyi])//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
// {
// left++;
// }
// //right再走、
// while (left < right && a[right] >= a[keyi])//right找小, 找不到则right--, 降序则right找大, 找不到则right--, 变为<= 、
// {
// right--;
// }
// //上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
// //上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组尾元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
// //交换、
// Swap(&a[left], &a[right]);//找到后就交换a[left]和a[right],让小的去左边,大数去右边,降序则让小的去右边,大的去左边、
//}
交换相遇位置和keyi位置上的数据、
//Swap(&a[keyi], &a[left]);
Swap(&a[keyi], &a[right]);
return right;
//return left;
}
//单趟排序->挖坑法版本、
int PartSortPit(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
//方法一:
//选取数组首元素作为基准值key,则数组中第一个位置即为坑位、
//三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
int midi = GetMidIndex(a, left, right);
//若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
Swap(&a[midi], &a[left]);
//下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int key = a[left];
//记录坑位的下标、
int pit = left;
while (left < right)
{
//升序、
//right先走、
while (left < right && a[right] >= key)//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
{
right--;
}
//找到比key小的值后放入坑pit中、
a[pit] = a[right];
//更新坑位的位置、
pit = right;
//left再走、
while (left < right && a[left] <= key)//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
{
left++;
}
//找到比key大的值后放入坑pit中、
a[pit] = a[left];
//更新坑位的位置、
pit = left;
//上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
//上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组首元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
}
//当left和right相遇时,该相遇位置一定是坑位,再把key中的值放到该相遇位置的这个坑位上、
a[pit] = key;
//返回坑的下标就是key最后的落脚点、
return pit;
方法二:
选取数组尾元素作为基准值key,则数组中最后一个位置即为坑位、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[right]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int key = a[right];
记录坑位的下标、
//int pit = right;
//while (left < right)
//{
// //升序、
// //left先走、
// while (left < right && a[left] <= key)//left找大,找不到则left++,降序则left找小,找不到则left++,变为>=、
// {
// left++;
// }
// //找到比key大的值后放入坑pit中、
// a[pit] = a[left];
// //更新坑位的位置、
// pit = left;
// //right再走、
// while (left < right && a[right] >= key)//right找小,找不到则right--,降序则right找大,找不到则right--,变为<=、
// {
// right--;
// }
// //找到比key小的值后放入坑pit中、
// a[pit] = a[right];
// //更新坑位的位置、
// pit = right;
// //上面的>=和<=中的=,一是表明与基准值相等的数据在基准值的左边和右边都是就可以的,二是避免死循环,比如: 5 5 2 3 5,若两者不加等于的话,就会造成死循环、
// //上面两个while中的 left < right 主要是为了防止越界访问的,若选择数组尾元素作为基准值排升序和排降序时都可能会越界访问,比如1 2 3 4 5、
//}
当left和right相遇时,该相遇位置一定是坑位,再把key中的值放到该相遇位置的这个坑位上、
//a[pit] = key;
返回坑的下标就是key最后的落脚点、
//return pit;
}
//单趟排序->前后指针版本、
int PartSortPC(int*a, int left, int right)//一级指针传参,一级指针接收、
{
assert(a);
方法一:
选取数组首元素作为基准值key,使用变量keyi来记录基准值key的下标、
在该版本内,不适合定义key,分析在下面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
可以使用left在定义keyi的情况下代替keyi进行工作、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[left]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = left; //可省略、
//int prev = left;
//int cur = left + 1;
当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义两个变量prev和cur时,下面再使用这两个下标的时候,不会造成越界访问、
//while (cur <= right)
//{
// //升序,cur找小,当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中、
// if (a[cur] < a[keyi]) // if (a[cur] < a[left])
// {
// prev++;
// //此处可以不进行判断,直接交换也行,但是最好判断一下,避免做无用功、
// //此处直接写成 prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// //即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// if (a[prev] != a[cur]) //if (prev != cur)
// {
// Swap(&a[prev], &a[cur]);
// }
// }
// //此处会有两种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
// //二是当cur找到的数据的数值小于key时,进入if语句,让prev++,再判断一下是否需要进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
// cur++;
// 降序,cur找大,当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中、
// //if (a[cur] > a[keyi]) //if (a[cur] > a[left])
// //{
// // prev++;
// // //此处可以不进行判断,直接交换也行,但是最好判断一下,避免做无用功、
// // //此处直接写成 prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// // //即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// // if (a[prev] != a[cur]) //if (prev != cur)
// // {
// // Swap(&a[prev], &a[cur]);
// // }
// //}
// 此处会有两种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
// 二是当cur找到的数据的数值大于key时,进入if语句,让prev++,再判断一下是否需要进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
// //cur++;
//}
prev所指向的元素就是比基准值key小的值,此时再交换prev和keyi所指向的元素、
prev所指向的元素就是比基准值key大的值,此时再交换prev和keyi所指向的元素、
//Swap(&a[prev], &a[keyi]); //Swap(&a[prev], &a[left]);
//return prev;
//方法二 -> 方法一的简化:
//选取数组首元素作为基准值key,使用变量keyi来记录基准值key的下标、
//在该版本内,不适合定义key,分析在下面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
//可以使用left在定义keyi的情况下代替keyi进行工作、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组首元素作为基准值key,则应为:Swap(&a[midi], &a[left]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[left]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
int keyi = left; //可省略、
int prev = left;
int cur = left + 1;
//当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
//否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义两个变量prev和cur时,下面再使用这两个下标的时候,不会造成越界访问、
while (cur <= right)
{
//默认当变量prev和cur指向同一个数据时,不需要进行交换、
//升序,cur找小,若cur找到了小,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了大于等于key的值,则更不进入if语句、
//只有当cur找到了小,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
//此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
//即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
if (a[cur] < a[keyi] && a[++prev] != a[cur]) //if (a[cur] < a[left] && a[++prev] != a[cur]) //if (a[cur] < a[keyi] && ++prev != cur) if (a[cur] < a[left] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
//此处会有三种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
//二是当cur找到的数据的数值小于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
//三是当cur找到的数据的数值小于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找小、
cur++;
// 降序,cur找大,若cur找到了大,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了小于等于key的值,则更不进入if语句、
// 只有当cur找到了大,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
// 此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// 即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// //if (a[cur] > a[keyi] && a[++prev] != a[cur]) //if (a[cur] > a[left] && a[++prev] != a[cur]) if (a[cur] > a[keyi] && ++prev != cur) if (a[cur] > a[left] && ++prev != cur)
// //{
// // Swap(&a[prev], &a[cur]);
// //}
// 此处会有三种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
// 二是当cur找到的数据的数值大于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
// 三是当cur找到的数据的数值大于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找大、
// //cur++;
}
prev所指向的元素就是比基准值key小的值,此时再交换prev和keyi所指向的元素、
prev所指向的元素就是比基准值key大的值,此时再交换prev和keyi所指向的元素、
Swap(&a[prev], &a[keyi]); //Swap(&a[prev], &a[left]);
return prev;
//对于方法一二中,为什么不能使用变量key来直接记录基准值呢,即定义int key = a[left],在最后再进行Swap(&a[prev], &key);
//这一定是不对的,因为,最后一步的交换是为了交换数组中的元素,即,prev指向的元素与数组首元素,就是所选取的基准值进行的交换,如果按照上述方法的话,因为key是在调用函数
//内部定义的,是一个局部变量,该局部变量中存储的是数组首元素的值,如果与它进行交换的话,确实能够把基准值放到prev所指的位置上,这是因为key中存储的就是所选取的基准值,
//此时,局部变量key中存储的就是交换前prev所指的元素,但是不要忘记,此时数组首元素的值还是最初选定的基准值,原本我们是想把数组首元素的值,即选定的基准值放到变量prev
//所指的位置上,并把prev所指的元素放到数组的首位置上成为数组的首元素,若按照上述方法进行定义的话,就满足不了我们的条件了,所以不可以直接把基准值保存到变量key中,
//所以在此还是要定义变量keyi来记录基准值key的下标、
//在该前后指针版本内,不适合定义key,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组首元素作为基准值key时
//可以使用left在定义keyi的情况下代替keyi进行工作、
//hoare版本和前后指针版本是进行的元素的交换,挖坑法进行的则是元素的覆盖,基于此原因,则hoare版本和前后指针版本适合定义变量keyi来记录基准值key的下标,而对于挖坑法则适合直接定义变量key来保存选取的基准值即可、
//若在前后指针方法中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,也可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,
//并不是left和right,所以,当选取数组首元素作为基准值key时可以使用left在定义keyi的情况下代替keyi进行工作,当选取数组尾元素作为基准值key时可以使用right在定义keyi的情况下代替keyi进行工作、
//若在hoar版本中定义变量key的话,则最会一步的交换还没有办法使用变量key,所以不定义变量key,直接定义keyi就可以解决这个问题,不可以key和keyi都不定义,因为该方法和前后指针方法不一样,该方法中的
//left和right在过程中是在不断地发生变化的,若想要记录每一个单趟排序序列中的首元素和尾元素的话,不能使用left和right,他们都在变化,所以还是避免不了使用额外的变量来记录每一个单趟排序序列中的首元素和尾元素作为基准值
//而且在该方法中不适合定义key,则直接定义keyi即可、
//若在挖坑法版本中定义变量keyi的话,由于是元素的覆盖,所以就可能导致找不到原来的基准值了,所以不定义变量keyi,直接定义变量key就可以解决问题,不可以key和keyi都不定义,即在选取数组首元素作为基准值时,不能直接使用left
//在选取数组尾元素作为基准值时,不能直接使用right,因为挖坑法是进行元素的覆盖,这样还是会导致丢失原来的基准值,所以必须定义key、
方法三:
选取数组尾元素作为基准值key,使用变量keyi来记录基准值key的下标、
在该版本内,不适合定义key,分析在上面,最好定义keyi,除此之外,可以既不定义key,也不定义keyi,因为,前后指针版本在不断改变的指针是prev和cur,并不是left和right,所以,当选取数组尾元素作为基准值key时
可以使用right在定义keyi的情况下代替keyi进行工作、
三数取中法,取出数组首元素,尾元素,中间位置元素,选择这三者中,不是最大,也不是最小的那个值的下标、
//int midi = GetMidIndex(a, left, right);
若选择数组尾元素作为基准值key,则应为:Swap(&a[midi], &a[right]); 这样就让数组首元素,尾元素,中间位置元素,三者中,不是最大,也不是最小的那个值作为了基准值key、
//Swap(&a[midi], &a[right]);
下面的代码不需要进行改变,在单趟排序中,不管使用何种方法,仍选取数组首元素或者尾元素作为基准值key、
//int keyi = right; //可省略、
//int prev = left-1;
//int cur = left;
当定义上面的变量prev和cur时,是不会造成越界访问的,因为,当在递归中凡是能执行到该PartSortPC函数时,则该函数的形参部分中的left一定小于right
否则根本就执行不到该调用函数,所以,本次单趟排序中,至少存在两个数据,所以当定义变量cur时,下面再使用这个下标的时候,不会造成越界访问,当定义变量prev时,也不会造成越界,
因为,是++prev,先++后使用,所以不会造成越界访问、
//while (cur < right) //while (cur <= right-1)
//{
// //默认当变量prev和cur指向同一个数据时,不需要进行交换、
// //升序,cur找小,若cur找到了小,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了大于等于key的值,则更不进入if语句、
// //只有当cur找到了小,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
// //此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// ///即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// if (a[cur] < a[keyi] && a[++prev] != a[cur]) //if (a[cur] < a[right] && a[++prev] != a[cur]) if (a[cur] < a[keyi] && ++prev != cur) if (a[cur] < a[right] && ++prev != cur)
// {
// Swap(&a[prev], &a[cur]);
// }
// //此处会有三种情况,一是当cur找到的数据的数值大于等于key时,则cur继续找小,即不进入下面的if语句中,cur继续++找小、
// //二是当cur找到的数据的数值小于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找小、
// //三是当cur找到的数据的数值小于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找小、
// cur++;
// 降序,cur找大,若cur找到了大,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,若cur找到了小于等于key的值,则更不进入if语句、
// 只有当cur找到了大,并且,++prev后和cur不是指向了同一个数据时,才进入if语句进行交换、
// //此处直接写成 ++prev != cur 也是可以的,只有当prev==cur时,两者指向的数据的数值才是相等的,除此之外,不会存在两者指向不同的数据,但是两个数据的值是相等的情况
// ///即不会存在当prev不等于cur时,但他们所指元素的值是相等的情况,所以当prev不等于cur时,两者指向的元素的数值一定不相等、当prev等于cur时,两者指向的元素的数值一定相等、
// //if (a[cur] > a[keyi] && a[++prev] != a[cur]) //if (a[cur] > a[right] && a[++prev] != a[cur]) if (a[cur] > a[keyi] && ++prev != cur) if (a[cur] > a[right] && ++prev != cur)
// //{
// // Swap(&a[prev], &a[cur]);
// //}
// 此处会有三种情况,一是当cur找到的数据的数值小于等于key时,则cur继续找大,即不进入下面的if语句中,cur继续++找大、
// 二是当cur找到的数据的数值大于key时,并且++prev后和cur不是指向了同一个数据时,则进入if语句进行交换,等if语句出来后,cur仍要进行++,继续往后找大、
// 三是当cur找到的数据的数值大于key时,但是++prev后和cur指向了同一个数据,则不进入if语句,即不进行交换,而cur仍要进行++,继续往后找大、
// //cur++;
//}
prev所指向的元素就是比基准值key小的值,此时不可以直接交换prev和keyi所指向的元素,由于cur找小,则prev+1到right-1的所有元素都是大于等于基准值key的数据,左闭右闭,直接再交换prev+1和keyi所指的元素即可、
prev所指向的元素就是比基准值key大的值,此时不可以直接交换prev和keyi所指向的元素,由于cur找大,则prev+1到right-1的所有元素都是小于等于基准值key的数据,左闭右闭,直接再交换prev+1和keyi所指的元素即可、
//Swap(&a[++prev], &a[keyi]); //Swap(&a[++prev], &a[right]);
//return prev;
Swap(&a[prev + 1], &a[keyi]); //Swap(&a[prev + 1], &a[right]);
//return prev+1;
}
//快速排序、
void QuickSort(int* a, int begin, int end)
{
assert(a);
//如果子区间中只有一个值或者子区间不存在时,即为最小规模的子问题、
//即如果子区间中只有一个数据或者不存在数据时,就是最小规模的子问题,不再进行递归,只有一个元素或者没有元素,也可看成是有序的,可以是升序,也可以是降序、
if (begin >= end)
{
return;
}
else
{
if (end - begin+1 <= 10) //此处的+1主要是为了和下面同步,一般选取13、
{
//小区间优化,直接采用插入排序来代替递归调用进行排序、
//此处要写成a+begin,否则每次都是对数组的前end-begin+1个元素进行的插入排序,不符合题意、
InsertSort(a+begin, end - begin + 1);//begin和end是左闭右闭,所以直接end-begin会导致少算一个元素,则要加上1、
//要注意,若使用了小区间优化,若排升序,不仅要在单趟排序中改为排升序,还要在直接插入排序中改为排升序,同理,排降序两边都要改、
}
else
{
int keyi = PartSortPC(a, begin, end);
//单趟排完后,key已经放在了应该在的正确的位置,如果key的左边满足升序,右边也满足升序,那么则整个数组都是升序的,如果key的左边满足降序,右边也满足降序,那么则整个数组都是降序的、
//采用分治的思想去使得key的左边和右边都满足升序或者降序即可,分治解决子问题、
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
}
//快速排序->非递归版本->栈实现、
//非递归若使用栈实现,可以模拟递归的未优化过程,也可以模拟递归的优化过程,所谓模拟递归的未优化过程即指while循环中的单趟排序不加三数取中,所谓模拟递归的优化过程即指while循环中的单趟排序加上三数取中,此处以非递归模拟递归版本未优化为例、
//时间复杂度:通过思路进行分析,由于是在模拟递归未优化的过程,所以和递归未优化的时间复杂度是一样的,即最好情况下的部分时间复杂度是:O(N*logN),最坏情况下的部分时间复杂度是:O(N^2),则时间复杂度就是:O(N^2)、
//空间复杂度:通过思路进行分析,由于是在模拟递归未优化的过程,所以和递归未优化的空间复杂度是一样的,即最好情况下的部分空间复杂度是:O(logN),最坏情况下的部分空间复杂度是:O(N),则空间复杂度就是:O(N)、
//若非递归模拟递归版本优化过程则有:
//时间复杂度:通过思路进行分析,由于是在模拟递归优化的过程,所以和递归优化的时间复杂度是一样的,即最好情况下的部分时间复杂度是:O(N*logN),最坏情况下的部分时间复杂度是:O(N*logN),则时间复杂度就是:O(N*logN)、
//空间复杂度:通过思路进行分析,由于是在模拟递归优化的过程,所以和递归优化的空间复杂度是一样的,即最好情况下的部分空间复杂度是:O(logN),最坏情况下的部分空间复杂度是:O(logN),则空间复杂度就是:O(logN)、
//快速排序的非递归和递归思路是一样的,无非就是外层调用函数存在差异,非递归是用栈取下标划分小区间,递归就是调用自己来划分小区间,所以由于递归和非递归的思路是一样的,则两者的时间复杂度和空间复杂度也是一样的,未优化的快排的递归和非递归版本的
//时间复杂度都是O(N^2),优化后的快排的递归和非递归版本的时间复杂度都是O(NlogN),未优化的快排的递归和非递归版本的空间复杂度都是O(N),优化后的快排的递归和非递归版本的空间复杂度都是O(logN)、
void QuickSortNon_Recursive(int* a, int begin, int end)
{
assert(a);
//定义一个栈st、
ST st;
//初始化栈、
StackInit(&st);
//入栈第一个要处理的区间,入栈是先左后右,则出栈就是先右后左,因为栈的性质是先进后出、
StackPush(&st, begin);
StackPush(&st, end);
//当栈为空时,停止、
while (!StackEmpty(&st))
{
//入栈是先左后右,则出栈就是先右后左、
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
//以前后指针法为例->单趟排序、
int keyi = PartSortPC(a, left, right);
方法一:
根->右子树->左子树、
当子序列中只有一个或者不存在数据时,则不需要再进行排序、
//if (left < keyi - 1)
//{
// //与上面保持一致,先左后右入栈、
// StackPush(&st, left);
// StackPush(&st, keyi-1);
//}
当子序列中只有一个或者不存在数据时,则不需要再进行排序、
//if (keyi+1 < right)
//{
// //与上面保持一致,先左后右入栈、
// StackPush(&st, keyi + 1);
// StackPush(&st, right);
//}
//方法二:
//根->左子树->右子树---->前序、
//当子序列中只有一个或者不存在数据时,则不需要再进行排序、
if (keyi + 1 < right)
{
//与上面保持一致,先左后右入栈、
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
//当子序列中只有一个或者不存在数据时,则不需要再进行排序、
if (left < keyi - 1)
{
//与上面保持一致,先左后右入栈、
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
}
//销毁栈、
StackDestory(&st);
}
//归并排序、
void MergeSort(int*a, int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
//为了避免每次递归调用都动态开辟n个内存空间,所以要写出一个子函数专门用来进行递归调用、
_MergeSort(a, 0, n - 1, tmp);
//释放、
free(tmp);
tmp = NULL;
}
//归并排序非递归版本->升序、
//时间复杂度为:O(N*logN),空间复杂度为:O(N)、
//对于非递归算法求空间复杂度则不分最好最坏的情况,不考虑栈帧的问题,只数一下算法代码中定义的变量的个数就可以了、
//对于时间复杂度而言,最好最坏的情况是一样的,while (gap < n)循环的内部整体的执行次数为N次,并且while (gap < n)的循环次数为logN次,故内外层循环的次数都是固定值,则时间复杂度为:O(N*logN)、
void MergeSortNonR(int*a, int n)
{
assert(a);
int* tmp = (int*)malloc(sizeof(int)*n);
assert(tmp);
int gap = 1;
//当gap>=n时,循环结束,当gap==n/2时,进行的是最后一次归并、
while (gap < n)
{
//为什么在gap前面乘一个2,因为是两组数据进行归并,一组数据中有gap个元素,要跳过两组数据,则乘以2、
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;
//begin1一定不会越界,则end1可能越界,也可能不越界,若end1越界,修改即可、
//end1越界,修正即可、
if (end1 >= n)
{
end1 = n - 1;
}
//如果end1未越界,则begin2可能越界,也有可能不越界,若end1越界,则begin2一定会越界,不管end1是否越界,只要begin2越界,则第二个区间就不存在了、
//begin2越界,则第二个区间就不存在了、
if (begin2 >= n)
{
//保证 begin2 > end2 即代表了该区间不存在,主要是在该种情况下保证不进入下面的while循环语句中、
begin2 = n; //begin2和end2不一定必须是n和n-1,只要保证begin2 > end2 即可、
end2 = n-1;
}
//end2越界,修正即可、
//若begin2不越界,则end2可能越界,也可能不越界,若end2越界的话,直接修正即可、
if (begin2 < n && end2 >= n)
{
end2 = n - 1;
}
条件断点->用于调试、
//if (begin1 == 8 && end1 == 9 && begin2 == 9 && end2 == 9)
//{
// int x = 0;//随便定义一个变量方便在该条语句上打断点、
//}
打印->用于调试、
printf("归并[%d,%d][%d,%d] -- gap=%d\n", begin1, end1, begin2, end2,gap);
int index = i;
while (begin1 <= end1 && begin2 <= end2)
{
//当a[begin1] == a[begin2] 进入哪一个语句都是可以的、
if (a[begin1] < a[begin2]) // 降序 if (a[begin1] > a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//由于begin1和begin2不是同时走的,故到此,上面while语句中的判断中只有一者不满足要求,要么是begin1所在的数组被遍历完毕,
//要么是begin2所在的数组被遍历完毕,即begin1和begin2所在的数组不可能同时被遍历完毕、
while (begin1 <= end1)
{
//begin2所在的数组被遍历完毕,begin1所在的数组未被遍历完毕、
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
//begin1所在的数组被遍历完毕,begin2所在的数组未被遍历完毕、
tmp[index++] = a[begin2++];
}
}
//整型int类型的拷贝可以使用memcpy、
memcpy(a,tmp,sizeof(int)*n);
gap *= 2;
}
//释放、
free(tmp);
tmp = NULL;
}
//计数排序、
//时间复杂度的最好最坏情况是一样的,在排序部分中,内外循环嵌套,并且内外循环次数均不固定,与之前所见的循环嵌套存在一定的区别,排序部分中内层while循环总次数就是原随机数组的元素个数n,
//根据思想可知,排序部分的执行次数为:range+n, 总的执行次数为:range+3n ,则时间复杂度即为:O(range+n),若题目中告诉了n远大于range,则时间复杂度即为:O(n)、,空间复杂度为:O(range)、
//根据时间复杂的和空间复杂度也可知,计数排序适用于范围确定且集中的数据、
//此时可以适用于负数,因为使用的是相对映射,若使用绝对映射则不适用于负数,其次,不管是相对映射还是绝对映射,都不适用于浮点数和字符串等其他非整型int类型的数据,
//像其他非整型int类型的数据只能使用比较排序,比较排序才是比较正统的排序,计数排序的适用范围比较小,只能用于一些比较特殊的场景下、
void CountSort(int*a, int n)
{
assert(a);
//求出原随机数组中的最大值和最小值、
int min = a[0], max = a[0];
for (int i = 1; i < n; i++)
{
if (a[i]>max)
max = a[i];
if (a[i] < min)
min = a[i];
}
//相对映射、
int range = max - min + 1;//计算映射表数组的大小、
//建立映射数组、
//int* countA = (int*)malloc(sizeof(int)*range);
//assert(countA);
初始化、
//memset(countA, 0, sizeof(int)*range);
int* countA = (int*)calloc(range, sizeof(int));//calloc函数会自动初始化内存,单位是字节,即初始化每个字节为0、
assert(countA);
//计数、
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//排序:直接操作原随机数组、
int j = 0; //j用来记录原随机数组的下标、
//排升序、
for (int i = 0; i < range; i++)//i用来记录映射表数组元素的下标、
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
排降序、
//for (int i = range-1; i >=0; i--)
//{
// while (countA[i]--)
// {
// a[j++] = i + min;
// }
//}
//释放、
free(countA);
countA = NULL;
}
6.3、Sort.h文件:
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
//插入排序、
void InsertSort(int* a, int n);
//希尔排序、
void ShellSort(int* a, int n);
//冒泡排序、
void BubbleSort(int* a, int n);
//打印、
void PrintArray(int*a, int n);
//直接选择排序优化、
void NewSelectSort(int* a, int n);
//直接选择排序、
void SelectSort(int* a, int n);
//堆排序、
void HeapSort(int* a, int n);
//快速排序、
void QuickSort(int* a, int begin, int end);
//快速排序->非递归、
void QuickSortNon_Recursive(int* a, int begin, int end);
//归并排序、
void MergeSort(int*a, int n);
//归并排序非递归版本、
void MergeSortNonR(int*a, int n);
//计数排序、
void CountSort(int*a, int n);
6.4、Stack.c文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include"Stack.h"
//初始化、
void StackInit(ST* ps)
{
//此处如果传值调用的话,形参是实参的一份临时拷贝,操作的就是形参中的结构体,而不能改变实参中的结构体,所以采用传址调用、
assert(ps);
ps->a = NULL;
ps->top = 0;
//在数组栈/顺序表栈中,top的初始化的值不同,则表示的意义不同、
//若top初始化为0,则表示的是top指向了栈顶元素的下一个元素,该方法是先向top所指的位置放数据,然后top再++、
//若top初始化为-1,则表示的是top指向了栈顶元素,该方法是top先++,然后再向top所指的位置放数据、
//在此我们选择前者,即将top初始化为0这种方法、
ps->capacity = 0;
//在之前写顺序表时,单独把扩容写成一个函数,是因为有尾插,头插,任意位置插,这些都需要先判断一下是否扩容,所以封装成一个函数比较方便,
//但是对于栈这个数据结构而言,只有一个尾插,所以不需要单独把扩容封装成一个函数,直接写到尾插,即入栈调用函数里面即可、
}
//销毁栈、
void StackDestory(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->top = 0;
}
//入栈、
void StackPush(ST* ps, STDataType x)
{
assert(ps);
//判断是否需要扩容、
if (ps->top == ps->capacity)
{
//需要扩容、
size_t newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newCapacity*sizeof(STDataType));
if (tmp == NULL)
{
//由于 ps->a 初始化的结果是NULL,再使用realloc函数的话,相当于是动态开辟内存空间,而不是增容、
//增容失败、
printf("realloc fail\n");
return;
}
else
{
//增容成功
ps->a = tmp;
ps->capacity = newCapacity;
}
}
ps->a[ps->top] = x;
ps->top++;
//ps->a[ps->top++] = x;
}
//出栈、
void StackPop(ST* ps)
{
assert(ps);
//确保栈中还存在数据,若不存在数据则不能再进行出栈操作、
//确保栈不为空、
assert(ps->top > 0);
ps->top--;
}
//判断是否栈为空、
//如果为空返回非零结果,如果不为空返回0、
bool StackEmpty(ST* ps)
{
assert(ps);
方法一:
//if (ps->top > 0)
//{
// //栈不为空、
// return false;
//}
//else
//{
// //栈为空、
// return true;
//}
//方法二:
return ps->top == 0;
//若栈为空,则上面为真,VS默认返回1,表示栈为空,若栈不为空,则上面为假,VS返回0,表示栈不为空、
}
//访问栈顶的数据、
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[(ps->top) - 1];
}
//记录栈内数据的个数、
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
6.5、Stack.h文件:
#pragma once //防止头文件被重复包含、
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
//实现数组(顺序表)栈、
//静态、
//下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈、
//#define N 100
//typedef int STDataType;
//typedef struct Stack
//{
// STDataType a[N];
// int top;//记录栈顶的位置、
//}Stack;
//动态、
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; //记录栈顶的位置、
int capacity; //容量的大小、
}ST;
//初始化、
void StackInit(ST* ps);
//销毁栈、
void StackDestory(ST* ps);
//入栈、
void StackPush(ST* ps, STDataType x);
//出栈、
void StackPop(ST* ps);
//判断是否栈为空、
bool StackEmpty(ST* ps);
//访问栈顶的数据、
STDataType StackTop(ST* ps);
//记录栈内数据的个数、
int StackSize(ST* ps);
7、常见排序算法例题:
7.1、例题1:
7.2、例题2:
7.3、例题3:
7.4、例题4:
关于常见排序算法的讲解到此为止,希望大家一键三连,谢谢大家!