1.插入排序:直接插入排序
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。将未排序的数据插入到已经排好序的数据中,从第二个到最后一个数依次插入,最后整个数组变成有序。
void InsertSort(int* a, int n)
{//插入排序:直接插入排序
assert(a);
for (int i = 1; i < n; i++)
{
for (int j = i - 1; j >= 0;j--)
{
if (a[j+1] < a[j])
{
swap(a + j + 1, a + j);
}
else break;
}
}
}
----实现单次排序举例,将2插入到前面的有序序列:(令 j 指向2的前一个数,大于2则互换,令j--,进行下一次循环比较直到 j 所指向的数小于2)
1 3 4 5 6 2; 1 3 4 5 2 6; 1 3 4 2 5 6; 1 3 2 4 5 6; 1 2 3 4 5 6;
----令 i 指向数组第二个数,遍历完数组给每个数完成单次排序整个数组的排序也完成。
最坏时间复杂度:O(n^2),当数组接近完全逆序;
最好时间复杂度:O(n),但数组接近完全有序
空间复杂度:未开辟新的空间,复杂度为O(1)。
2.插入排序:希尔排序
----预排序:让数组接近有序,把规定间距(gap)的值分为一组,进行插入排序
----直接插入排序
与排序中,gap越大,前面大的数据可以越快到后面,后面小的数,可以越快到前面。gap越小,在排序中会使原数组更加接近有序。
----多组并排:使被gap分成几组的数间错同时开始插入排序
void ShellSort(int* a, int n)
{//插入排序:希尔排序
assert(a);
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{//多组并排!!!!
for (int j = i; j >= 0; j -= gap)
{
if (a[j + gap] < a[j])
{
swap(a + j + gap, a + j);
}
else break;
}
}gap--;
}
}
3. 选择排序:直接选择排序
在元素数组的第i个数到最后一个数中选出最小一个数放在数组首位,再到剩下的数中选出最小的数放在数组的第二位......循环这个操作直至集合剩余一个元素,排序完成。直接选择排序不论数组有没有序都要从头选择,效果并不好。
我们可以做出一点改变,每次可以同时选择出最大和最小的数加快排序,但是这里需要注意的是每次要交换两次,先将最小值min交换到begin时,原begin如果存的最大的数max,此时max指向的值就会被begin交换出去,在这个时候需要进行判断,才能令max和end交换。
void SelectSort(int* a, int n)
{
assert(a);
int begin = 0,end = n - 1;
while (begin<end)
{
int max = begin, min = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] < a[min]) min = i;
if (a[i] > a[max]) max = i;
}
swap(a+begin, a+min);
if (max == begin) max = min;//
swap(a+end, a+max);
begin++;
end--;
}
}
4.选择排序:堆排序
----从最后一个非叶结点开始向根结点遍历用向下调整算法构建大堆(排升序)
----每次选取大堆的根结点和最后一个结点交换,令堆元素减1,再对新的根节点进行向下调整算法,循环直到堆元素剩一个即完成排序。
堆排序的时间复杂度是N倍的以2为底的logN。
void con(int* a,int root, int n)
{//向下调整算法
int parent = root;
int child = parent * 2 + 1;
while(child<n)
{
if (child<n-1 && a[child] < a[child + 1]) child++;
if (a[child] > a[parent]) swap(a + child, a + parent);
else break;
parent = child;
child = parent * 2 + 1;
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
con(a, i, n);//遍历结点构建大堆
for (int i = n - 1; i > 0; i--)
{//交换根节点进行排序
swap(a, a + i);
con(a, 0, i);
}
}
5.冒泡排序
每一趟冒泡排序从第一个开始相邻交换,交换至最后一个就是该轮排序中最大的一个。冒泡排序的时间复杂度是O(n^2),性能也不是很好。
void BubbleSort(int* a, int n)
{
for (; n > 0; n--)
{
int exchange = 0;
for (int j = 0; j < n - 1; j++)
{
if (a[j] > a[j + 1])
{
swap(a + j, a + j + 1);
exchange = 1;
}
}
if (exchange == 0) break;
//如果一趟冒泡排序下来没有发生任何交换,说明已经有序
}
}
6.快速排序
----单趟排序(左右指针法):选定数组第一个值或者数组最后一个值key,遍历数组其余元素,一个begin指针从前到后,一个end指针从后到前,当end指向的数大于key就end--继续比较,直到end指向的数小于key,此时开始走begin,若begin指向的数小于key就begin++继续比较,直到begin指向的数大于key,就与end所指向小于key的数进行交换;完成交换后end--比较继续这个过程直到end等于begin若该处的数大于key就和key进行交换,否则就将它紧邻的下一个数和key交换,此时就完成了单趟排序,key左边都是比它小的数,右边都是比它大的数,虽然不是有序的,但key的位置已经确定。
----左右指针谁先走的问题。在快速排序的单次排序中,有很多需要注意的地方。如果令key取end的话,在一整轮单次排序中最后一次循环有两种情况,一是begin在碰到end之前找到了比key大的数,此时需要end开始找小刚好上回交换结束时end所指的数一定大于key此时begin也小于end,end进入循环自减;自减之后end等于begin,循环结束,此时循环结束它们所指向的值大于key,而key处于数组末尾,直接进行交换即可。第二种情况是begin在碰到end之前一直比key所指的数小直到begin与end相等跳出循环,此时begin与end所指的数仍旧大于key,与key互换即可。所以当key取末尾时,应该让begin先开始找数,因为begin在循环结束指向的数一定大于key所指向的值;如果key取数组首位时,就应当让end先走,它最终会停留在比key小的值处。
int PartSort(int* a, int begin, int end)
{//单次排序
int key = end;
while (begin < end)
{
while (a[begin] <= a[key] && begin < end)//<=的条件
{//找大
begin++;
}
while (a[end] >= a[key] && begin < end)//>=的条件
{//找小
end--;
}//如果循环条件没有等于的话,两边一旦都停到等于key的情况就会陷入死循环
swap(a + begin, a + end);
}
swap(a + end, a + key);
return begin;
}
void QuickSort(int* a, int left, int right)
{
assert(a);
if (left < right)
{
int k = PartSort(a, left, right);
QuickSort(a, left, k - 1);
QuickSort(a, k + 1, right);
}
else return;
}
----优化:三数取中。对于快速排序,当key的选取接近最大或最小时排序效率会下降,当每次选择接近中位数时排序的效率最好。它在数据量较大且有序时测试甚至会栈溢出,可以用三数取中的方法来改善这种情况,在排序开始前,对首尾及中间三个数比较选取中间大的数与key所指的数交换再开始排序。 三数取中,保证不要选到最小或者最大,让有序时变成最优,让最坏的情况不会再出现,时间复杂度不再看最坏,综合而言快排时间复杂度为O(N*logN)。
int GetMidIndex(int* a, int begin, int end)
{//返回中间大小数的下标
int mid = (begin + end) / 2;
int i = begin, j = end, s = mid;
if (a[i] > a[j]) swap(&i, &j);
if (a[j] > a[s]) swap(&j, &s);
return j;
}//后续和key所指向的数交换即可
int k = GetMidIndex(a, begin, end);
swap(a + k, a + end);
int key = end;
-----单趟排序(挖坑法)。坑(表示该位置的数已经被拿走了 可以覆盖)
int PartSort(int* a, int begin, int end)
{
int key = a[end];
//必须用key将末尾值取出来,此后它作为坑值会被覆盖
while (begin < end)
{
while (a[begin] <= key && begin < end)
{//找大
begin++;
}
a[end] = a[begin];//end的值已被取 可以覆盖
while (a[end] >= key && begin < end)
{//找小
end--;
}
a[begin] = a[end];//begin的值已被end取 成为坑 可以被覆盖
}
a[begin] = key;
return begin;
}
----单趟排序(前后指针法)。两个指针cur和prev,cur从左往右找小,cur一找到比key小的就停下来,然后++prev,将cur和prev位置的值互换;在这个过程中确保了cur扫过的区域被分成了左部分小于key,右部分大于key;cur指向遍历过区域的下一个,prev是最后一个小于key的数也就是分界线。cur新指向的数要是大于key,直接跟在区域尾遍历下一个,若是小于key,要同prev++的数交换,确保被遍历过的地方仍旧被划分为比key小的左部分和比key大的右部分。
int PartSort(int* a, int begin, int end)
{
int key = a[end];
int prev = begin;
for (int cur = begin; cur <= end; cur++)
{
if (a[cur] <= key)
{
swap(a + cur, a + prev);
if(cur!=end) prev++;
}//注意当prev已和end置换后,就不能++了,否则返回值有误
}
return prev;
}
----非递归的快速排序:
递归问题改非递归:1.改循环(斐波那契数列求解),一些简单递归才能改循环。
2.栈模拟存储数据非递归,所有的递归问题都能解决。
非递归:1.提高效率。递归建立栈帧还是有一定消耗,但现代计算机优化后这个消耗可以忽略。
2.递归最大的缺陷是,如果栈帧的深度太深,可能会导致栈溢出。因为系统栈空间一般 不大,在MB级别。数据结构模拟非递归,数据是存储在堆上的,堆是G级别的空间。
----快速排序的效率:
时间复杂度:O(N*logN),每一层都进行了N次排序,总共有logN层。
空间复杂度:O(logN),同层的空间会递归重复利用,所以递归了logN层。
7.归并排序:外排序
归并排序的单趟排序可以看作是合并两个有序数组,合并之前要使两个数组有序,将数组递归分解成子问题进行合并,逐层合并直至数组有序。该过程有logN层,每层中要对N个数据操作排序,时间复杂度为O(NlogN)。归并排序特别适合外部排序,即数据量太大,无法全部加载到内存中的情况。归并排序可以分块读取数据,逐块排序,最后合并结果。
7.1归并排序(递归)
可以看作是一个二叉树的后序遍历。
void margeSort(int* a, int left, int right,int* tmp)
{
if (right <= left) return;
int mid = (left + right) / 2;
margeSort(a, left, mid, tmp);
margeSort(a, mid+1, right, tmp);
//排好序的部分给到tmp
int l = left, r = mid + 1, t = 0;
while (l <= mid && r <= right)
{
if (a[l] < a[r])
{
tmp[t++] = a[l++];
}
else
{
tmp[t++] = a[r++];
}
}
while (r <= right)
{
tmp[t++] = a[r++];
}
while (l <= mid)
{
tmp[t++] = a[l++];
}
for (int i = left,j=0; i <= right; i++,j++)
{
a[i] = tmp[j];
}
}
7.2非递归的归并排序
先两两归并有序,再四四归并有序....当gap大于数组长度时以上次gap的长度对数组截断归并左右有序的两部分 ,这样可以确保多余不能被gap整除的部分正确参与排序。
void MergeSort(int* a, int n)
{
assert(a);
int mid = 0;
int* tmp = malloc(sizeof(int) * n);
int gap = 2;
for (int i = 0; i < n; i+=gap)
{
if (i + gap - 1 >= n - 1)
{
//要兼顾长度为整个数组的情况
if (i)
{
margeSort(a, i, n - 1, (i + n - 1) / 2, tmp);
gap *= 2;
i = (-gap);//i初始为0会加gap变成非零 跳过一些元素
}
else
{
margeSort(a, i, n - 1,gap/2-1 , tmp);//添加mid参数以解决最后一次的多余元素
break;
}
}
else margeSort(a, i, i + gap - 1, (2*i+gap-1) / 2, tmp);
}
//margeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
7.3用归并排序处理文件
当给定一个数据量巨大的文件要求排序,由于内存空间有序就不能把这些数据全部放入内存中排序,此时可以将这份数据分成多份,每份大小不超过内存最大处理限度,我们在内存中处理每份分割后的数据进行快速排序,然后将它们分别存入几个小文件中,这几个小文件已然有序,此时使用归并排序将小文件两两归并,最后合并成完全有序的大文件。为了便于演示,我们接下来以100个数据作为大文件,分割成10个小文件能被内存处理的限度。
----第一部分:先将文件数据分割成一段一段内存能够处理的小文件数据
1.先读取文件file,令文件指针fout指向它(判断是否为空验证是否打开失败)。
2.创建变量n作为分割成小文件存储数据的大小,n=10为10个;
3.创建变量i用来递增;
4.创建num用来存每次从大文件中读出来的数据;
5.创建数组变量a[10]用来存小文件的十个数据方便进行排序;创建字符数组变量subfile[10]用来暂时存储每个小文件名。
6.开始进入循环当大文件不为空则一直读取其中数据,每读满十个就进入else进行一次处理;先使用快速排序令其有序,再创建为1~10的文件名(表明是第几个小文件),再对这个小文件进行创建并且写入刚刚排好的数据;再令i=0从头开始,数组a也全部置空。
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
printf("打开文件夹失败!");
exit(-1);
}
//分割成一段段数据,内存排序后写到小文件
int n = 10,i = 0,num = 0,filei = 0;
int a[10];
const char subfile[10];//
while (fscanf(fout, "%d\n", &num) != EOF)//fout文件指针 "%d\n"读的格式 &num存入num
{
if (i < n-1)
{
a[i++] = num;//i
}
else
{
if (i == n - 1) a[i] = num;
QuickSort(a, 0, n - 1);
sprintf(subfile, "%d", filei++);//
FILE* fin = fopen(subfile, "w");//没有该文件 创建该文件然后将排好序的数据写进去
for (int i = 0; i < n; i++)
{
fprintf(fin, "%d\n", a[i]);
}
fclose(fin);
i = 0;
memset(a, 0, sizeof(int) * n);
}
}
----第二部分:归并十个有序的小文件
为了方便对归并之后的中间文件管理(使用传统的两两归并的话会出现很多个中间文件,再对中间文件归并...再归并知道变成一个有序的完整的大文件),此时我们先用第一个小文件和第二个小文件生成的第一个中间文件让它去和第三个文件归并,再让新生成的中间文件和第四个归...直到和最后一个小文件归并,此时我们就得到了完整且有序的大文件,且每次只控制一个中间文件。对小文件的命名是1~10,为了方便管理对归并产生的中间文件的命名就是将归并的文件名合并。
1.我们先创建了三个能够存储文件名的字符数组;初始化file1为文件1,初始化file2为文件2,初始化mfile为中间文件12。
2.进入循环从2到 n (10);先调用合并两个文件的函数将file1、2合并到中间文件mfile12中;然后更新:将中间文件mfile12给到file1、将整型制的i+1即文件3 (i从2开始)重新赋给file2、令中间文件重命名为mfile123。
3.在下次循环中就开始将file1(中间文件12)和file2(小文件3)归并到mfile(中间文件123)......
char file1[100] = "1";
char file2[100] = "2";
char mfile[100] = "12";
for (int i = 2; i <= n; i++)
{
MergeFile(file1, file2, mfile);
strcpy(file1, mfile);
sprintf(file2, "%d", i+1);//从第二个开始读 开始合并下一个
sprintf(mfile, "%s%d",mfile, i+1);//将格式化的数据写到某个字符串中
}//更新mfile的名字
fclose(fout);
----第三部分:归并两个文件
和归并两个数组的逻辑相似,但这里要注意的是fscanf()一旦调用,文件就会自动读取下一个数据,所以我们用两个变量托管读取这两个文件数据的函数的返回值,我们要判断文件是否已经读完,就是检查返回值是否为EOF(-1)。注意在while判断条件中不能直接判断fscanf的返回值,在判断中它会自动读取下一个并判断的是下一个的返回值,假定在之前文件已经读到最后一个数据,在下一个循环中一经判断发现它的下一个为空不能进循环,那么就会导致末尾的这个数据不被处理。
void MergeFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");
FILE* fout2 = fopen(file2, "r");
FILE* fin = fopen(mfile, "w");
int num1, num2;
int f1, f2;
f1 = fscanf(fout1, "%d\n", &num1);
f2 = fscanf(fout2, "%d\n", &num2);
while (f1 != EOF && f2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
f1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
f2 = fscanf(fout2, "%d\n", &num2);
}
while ( f1 != EOF)
{//当继续调用scanf时,并没有判断当前的num1末尾,而是先对它++了才进行的判断
fprintf(fin, "%d\n", num1);
f1 = fscanf(fout1, "%d\n", &num1);
}
while (f2 != EOF)
{
fprintf(fin, "%d\n", num2);
f2 = fscanf(fout2, "%d\n", &num2);
}
}
}
8.计数排序(非比较排序)
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* countArr = (int*)malloc(sizeof(int) * range);
memset(countArr, 0, sizeof(int) * range);
//统计次数
for (int i = 0; i < n; i++)
{
countArr[a[i] - min]++;
}
for (int j = 0,i=0; j < range && i<n; j++)
{
while (countArr[j]--)
{
a[i++] = j+min;
}
}
free(countArr);
}
计数排序只适用于整型,对于字符类型排序不能适用,时间复杂度是O(n+range);空间复杂度为O(range);如果range不大,数据较集中,那么计数排序就是最快最优的排序方式。
8种排序总结
稳定性:数组中相同值,排完序后可以做到相对顺序不变就是稳定的,相对位置发生变化就不稳定。