冒泡排序:
冒泡排序在正常使用中并不多,主要是用来帮助初学者去开拓和实现编程思想的理解;
图像理解:
通过每次比较两个数之间更大的,将这个大的数不断地与后面的数值相比较,最终将大的数值移动到最后;
通过这个数据个数-1次的循环来不断比较以实现排序的目的;
-1是因为其他数值比完后有一个数值自动是确定他的位置的不需要比较;
代码:
//冒泡Bubble //排序Sort//amount传递过来的是数量
void BubbleSort(int* pa, int amount)
{
//版本1
//amount个数次比较
for (int j = 0; j < amount; j++)
{
//每一次两两比较
for (int i = 0; i < amount - j - 1; i++)
{
//升序
if (pa[i]>pa[i+1])
{
Swap(&pa[i], &pa[i + 1]);//这里要取地址是因为pa[]是个数值
}
}
}
//以上默认是无序的,对于本来就是升序的效率过低
//版本2
for (int j = 0; j < amount; j++)
{
bool exchange = false;
for (int i = 0; i < amount - j - 1; i++)
{
if (pa[i] > pa[i + 1])
{
Swap(&pa[i], &pa[i + 1]);
exchange = true;
}
}
//如果进行交换了,那外层循环继续执行
//如果没交换,那就是第一遍检查是满足升序,eg.4 5 6 7,就直接从大循环跳出
if (exchange == true)
break;
}
}
重点理解:
代码1:程序外循环,j<amount是执行一共多少个数就要比多少次
代码2:程序内循环,假设一共5个数
直接插入排序:
时间复杂度最坏O(n^2)--逆序
最好O(n)--顺序有序
引入:在日常中我们打扑克牌整理牌时就用到了插入排序的思想;
第一张接手的排随便放(默认),后面接手的会和第一张进行比较,之后每接手一张就会比较之前已经有序的牌并将其插入;
代码:
//插入
void InsertSort(int* pa, int amount)
{
//方法一:翻滚式
//走数组每个数除第一张(手里已经有一张,继续拿牌)
for (int i = 1; i < amount; i++)
{
int endsub = i - 1;
int tmp = pa[i];
while (endsub >= 0)
{
if (pa[endsub] > tmp)
{
//两张牌不断翻滚式插入
Swap(&pa[endsub], &pa[endsub + 1]);
endsub--;
}
else
break;
}
}
//方法2:挖坑式
for (int i = 1; i < amount; i++)
{
int endsub = i - 1;
int tmp = pa[i];
while (endsub>=0)
{
if (pa[endsub] > tmp)
{
pa[endsub + 1] = pa[endsub];
//endsub--后,会将最开始的end位置空出
endsub--;
}
else
break;
}
//当break说明手里拿的牌大于endsub的位置
//而endsub+1刚好是空位,并且endsub+2已经确定大于手里的牌
pa[endsub + 1] = tmp;
}
}
动图配合代码分析:
方法一:翻滚式
方法二:挖坑式
选择排序(效率不高):
选择排序有两种形式:1.单向。2.双向
时间复杂度O(N^2)
空间复杂度O(1)
稳定性:不稳定,实际中不常用
这里我们直接写双向的就包括单向了。
总体思想:
1.找到数组里面最大于最小的数值,并确定其下标
2.通过不断地交换使得最大和最小的值不断交换到数组两边
可能会遇见的情况:
因为是要进行数值交换,我们需要4个下标,最大最小和开始结束
beginsub,endsub,maxsub,minsub,这里sub代表下标subscript
并且连续的交换害怕出现两个下标在一起,更换数据时将这个位置的数值换掉,会导致后面再去找时,不再是原来的内容而导致出错
如图:
更换后end就不能找到最大值,因为被上一步的交换将值覆盖了;
代码:
//选择
void SelectSort(int* pa, int amount)
{
//用于缩小排序数值的范围下标subscript
int beginsub = 0, endsub = amount - 1;
while (beginsub<endsub)
{
int maxsub = beginsub, minsub = beginsub;
//寻找每次缩减完范围后的最大与最小的下标
for (int i=beginsub;i<=endsub;i++)
{
if (pa[i] > pa[maxsub])
maxsub = i;
if (pa[i] < pa[minsub])
minsub = i;
}
//排升序
Swap(&pa[beginsub], pa[minsub]);
//避免后面maxsub在上一步被替换
//如果替换了,那就是大小位置换了,再更换一下位置就恢复了
if (beginsub == maxsub)
maxsub = minsub;
Swap(&pa[endsub], &pa[maxsub]);
beginsub++;
endsub--;
}
}
希尔排序:
时间复杂度:O[n^(3/2)]左右,但更偏向于1.3次方
希尔排序思路:
总体:分成小组同时进行排序(类似CPU多核同时处理)
1.预排序->使得数组接近有序
2.插入排序;
引入思想:
你现在需要出一套卷子,如果一个人全负责,有些多,那你一定会想到将不同模块分配给不同人去一起出一套卷子;
于是我们的希尔排序思路就被引出;
我们对于很长的一组数据进行排序,一个人也是无法快速排序,那就将这组数据拆分为多个小的数据,但是总体上又不打乱他们所处的位置;
如图:
预排序代码:
图解代码思路:
由以上内容总结出:
gap越大,大的数越快到后面,小的数越快到前面
gap越小,大的小的挪动越慢,但更接近有序
gap==1,就是直接插入排序
完整版代码:
//希尔
void ShellSort(int* pa, int amount)
{
//完整版:
//通过不断地缩减gap使预排序更接近有序
int gap = amount;
//知道gap==1时,完成整个希尔排序
while (gap>1)
{
//+1确保gap最后一定为1
//gap==1后,插入排序完成,退出while循环。条件必须gap>1
gap = gap / 3 + 1;
for (int i = 0; i < amount - gap; i++)
{
int endsub = i;
int tmp = pa[endsub + gap];
while (endsub >= 0)
{
if (pa[endsub] > tmp)
{
pa[endsub + gap] = pa[endsub];
endsub -= gap;
}
else
{
break;
}
pa[endsub + gap] = tmp;
}
}
}
}
堆排序:
如果是一开始,你一定认为降序是大堆而升序应该是小堆,那就大错特错了。
例如小堆是如图的但是数组并不是一个有序的所以可以肯定的是简单地理解是错误的需要进一步操作
升序建大堆
降序建小堆
向上调整建堆(插入过程)
向下调整建堆
前提:向下调整建堆前提必须是左右子树必须都为大/小堆,但是左右子树又不一定是。所以我们要创造出左右子树的条件。
叶子节点就不需要搞,天生就是大/小堆
n为下标(数据的总个数),n-1-1,,第一个-1是为了让n总个数-1能让n变成对应数据下标进去,第二个-1是为了找到倒数第一个非子叶节点(最后一个节点的父亲)调整
另一种(n-1-1)/2的理解:
Eg.降序建小堆
例如:我们建立一个小堆最后出来的结果却是降序因为我们在中间交换了一次
代码:
//堆排
void AdjustUp(int* pa, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (pa[child] > pa[parent])//大于==大堆,小于==小堆
{
int tmp = pa[child];
pa[child] = pa[parent];
pa[parent] = tmp;
//初始位置内容上面交换已经完成,然后再比较上一位的父亲和孩子
//才能不断向上调整,所以改变p和c下标的内容
child = parent;
parent = (child - 1) / 2;
}
else//如果不是上面if的情况,说明已经满足,大堆或者小堆的要求
{
break;
}
}
}
void AdjustDown(int* pa, int sub, int parent)//subscript下标sub
{
assert(pa);
int child = parent * 2 + 1;
while (child < sub)//当孩子出去整个数组范围了那在逻辑上父亲已经到最底层了
{
if (child + 1 < sub && pa[child + 1] > pa[child])
//这里为什么还要加child+1<sub?
//通过计算,下标为child的一定是有数值的,但是child+1有可能刚好出数组边界
//会造成越界访问,为了避免,所以增加新的约束条件
//放前面是只有满足这个条件才能让他两比较,放后面还是可能会越界访问
//这里我们是看左孩子和右孩子谁小,换成大于号就是谁大
//当有1和2时,新增3我们只需要和1和2中的小的比(或者大)
{
child++;
}
if (pa[child] > pa[parent])
{
int tmp = pa[child];//交换
pa[child] = pa[parent];
pa[parent] = tmp;
parent = child;//更换下一个比较的下标
child = parent * 2 + 1;
}
else//不在需要交换就跳出while循环
{
break;
}
}
}
void HeapSort(int* pa, int amount)
{
for (int i = 1; i < amount; i++)
//向上调整默认根已经有数据所以从1开始
{
AdjustUp(pa, i);//建大堆
}
//测试
//SortPrintf(pa,amount);
int end = amount - 1;//指向最后一个数的下标
int i = 0;
while (end > 0)
{
int tmp = pa[i];
pa[i] = pa[end];
pa[end] = tmp;
AdjustDown(pa, end, 0);
//测试
//SortPrintf(pa, amount);
//printf("\n");
--end;
}
}
结果显示的小堆是降序
这里我们通过不断地找到堆顶最小的数,然后通过交换将已经确定的数放在一边,去找次一位小的数,再将他拿出来排在刚刚最小的数后,重复此过程我们反而是将这个“类”从小到大的顺序变成了从大到小的顺序
快速排序:
思路引入:快排是一种将整个数组利用递归不断切小的一种排序
引入keysub,通过读取kuysub对应的数值,并与之作比较,大的放后面小的放前面,那他的值就是在他应该在的地方;
核心:逐步确定keysub实现排序
方法1.hoare版本
代码:
void QuickSort(int* pa, int beginsub, int endsub)
{
if (beginsub >= endsub)
{
return;
}
int keysub = PartSort1(pa, beginsub, endsub);
//[beginsub,keysub-1] keysub [keysub+1,end]
QuickSort(pa, beginsub, keysub - 1);
QuickSort(pa, keysub + 1, endsub);
}
//hoare
int PartSort1(int* pa, int leftsub,int rightsub)
{
int keysub = leftsub;
while (leftsub < rightsub)
{
//右边找小
while (leftsub<rightsub)
{
if (pa[rightsub] >= pa[keysub])
rightsub--;
else
break;
}
//左边找大
while (leftsub < rightsub)
{
if (pa[leftsub] <= pa[keysub])
leftsub++;
else
break;
}
Swap(&pa[rightsub], &pa[leftsub]);
}
Swap(&pa[keysub], &pa[leftsub]);//左右都可以
//上一步交换了值但是没交换下标
return leftsub;
}
简化:
测试结果:
注意事项:
1.出现死循环情况;
如果判断中无=(等号)
遇见两个相同的数,不管怎么交换之后,leftsub和rightsub永远都不会改变,就造成死循环了
2.出现越界情况;
leftsub满足<=情况,那leftsub就会越界,所以加leftsub<rightsub为限制条件
图解程序运行:
这里是逻辑理解,但实际操作的还是数组
注意:
if (beginsub >= endsub)
{
return;
}
两种情况:
1.区间只有一个值,无需排序
2.区间不存在
那方先走问题:
左边是keysub,右边先走,保障相遇位置的值比key小;
右边是keysub,左边先走,保障相遇位置的值比key大;
方法2:挖坑法
思路讲解:
通过在数组中挖去一个数值,让其他数据可以通过这个“坑”进行挪动(大的放右边,小的放左边)
通过逐渐寻找挖去数据最合适的坑位以达到排序目的
代码:
//挖坑法
int PartSort2(int* pa, int leftsub, int rightsub)
{
int key = pa[leftsub];
int holesub = leftsub;
while (leftsub < rightsub)
{
//右边找小
while (leftsub < rightsub && pa[rightsub] >= key)
{
rightsub--;
}
pa[holesub] = pa[rightsub];
holesub = rightsub;
//左边找大
while (leftsub < rightsub && pa[leftsub] <= key)
{
leftsub++;
}
pa[holesub] = pa[leftsub];
holesub = leftsub;
}
pa[holesub] = key;
return holesub;
}
图解代码运行过程:
方法3:前后指针法(有别的叫法)
图解思路:
代码:
//前后指针法
int PartSort3(int* pa, int leftsub, int rightsub)
{
int prevsub = leftsub;
int cursub = leftsub + 1;
int keysub = leftsub;
while (cursub <= rightsub)
{
if (pa[cursub] < pa[keysub] && ++prevsub != cursub)
{
Swap(&pa[prevsub], &pa[cursub]);
}
++cursub;
}
Swap(&pa[prevsub], &pa[keysub]);
return prevsub;
}
测试结果:
复杂度:
时间复杂度O(N*logN)-O(n^2)
解决方案:
1.随机数选key
2.三数取中
空间复杂度:O(logN);
测试结果:
单位ms
三数取中(挖坑法):
中
//挖坑法--三数取中
int PartSort2(int* pa, int leftsub, int rightsub)
{
//三数取中后与最左边交换,提高效率
int midsub = GetMidIndex(pa, leftsub, rightsub);
Swap(&pa[leftsub], &pa[midsub]);
int key = pa[leftsub];
int holesub = leftsub;
while (leftsub < rightsub)
{
//右边找小
while (leftsub < rightsub && pa[rightsub] >= key)
{
rightsub--;
}
pa[holesub] = pa[rightsub];
holesub = rightsub;
//左边找大
while (leftsub < rightsub && pa[leftsub] <= key)
{
leftsub++;
}
pa[holesub] = pa[leftsub];
holesub = leftsub;
}
pa[holesub] = key;
return holesub;
}
//三数取中
int GetMidIndex(int* pa, int leftsub, int rightsub)
{
int midsub = (leftsub + rightsub) / 2;
if (pa[leftsub] < pa[midsub])
{
if (pa[midsub] < pa[rightsub])
{
return midsub;
}
else if (pa[leftsub] < pa[rightsub])
{
return rightsub;
}
else
{
return leftsub;
}
}
else//pa[leftsub]>pa[midsub]
{
if (pa[midsub] > pa[rightsub])
{
return midsub;
}
else if (pa[leftsub] > pa[rightsub])
{
return rightsub;
}
else
{
return leftsub;
}
}
}
三数取中后的测试结果:
非递归快排:
栈->深度优先
队列->广度优先
栈是后进先出,
栈的具体详解:栈的理解和运用-CSDN博客
//非递归快排
void QuickSortNonR(int* pa, int beginsub, int endsub)
{
//栈的操作
ST st;
STInit(&st);
//将首位下标压入栈中
STPush(&st, endsub);
STPush(&st, beginsub);
//如果栈里不为空,继续
while (!STEmpty(&st))
{
//上面压入时时尾,首
//下面出来时,是首尾,对应左右
int leftsub = STTop(&st);
STPop(&st);
//STPop将栈里数据清除
int rightsub = STTop(&st);
STPop(&st);
int keysub = PartSort3(pa, leftsub, rightsub);
//[leftsub,keysub-1] keyusb [keysub+1,rightsub]
//0--4 5 6--9
//压栈,还是先尾后首
if (keysub + 1 < rightsub)
{
STPush(&st, rightsub);
STPush(&st, keysub + 1);
}
if (leftsub < keysub + 1)
{
STPush(&st, keysub - 1);
STPush(&st, leftsub);
}
}
STDestroy(&st);
}
代码图解:
三数取中本质是针对有序的情况
针对快排:
下面就成最坏的的情况只能一个一个去走
对以上问题解决方案:
三路划分:
代码:
//快排--三路划分
void QuickSort(int* pa, int beginsub, int endsub)
{
if (beginsub >= endsub)
{
return;
}
int leftsub = beginsub;
int rightsub = endsub;
int cursub = leftsub + 1;
int midsub = GetMidIndex(pa, leftsub, rightsub);
Swap(&pa[leftsub], &pa[midsub]);
int key = pa[leftsub];
while (cursub <= rightsub)
{
if (pa[cursub <= key])
{
Swap(&pa[leftsub], &pa[cursub]);
}
else if (pa[cursub] > key)
{
Swap(&pa[rightsub], &pa[cursub]);
--rightsub;
}
else
{
++cursub;
}
}
//小-l l--r r--大
//[beginsub,leftsub-1] [leftsub,rightsub] [rightsub+1,endsub]
QuickSort(pa, beginsub, leftsub - 1);
QuickSort(pa, rightsub + 1, endsub);
}
破解专门针对三数取中:
代码:
左边加一个随机数就是一个随机的中间值,避免专门的测试用例来针对我,并且取摸不会大于内个摸的数,就不会越界
个人写快排时候不用写三路划分;
归并排序:
核心思想:将一组数据逐层拆分成不同组,每组内数据排序,再逐层递归回去;
逻辑理解如图:
但实际还是在原数组操作递归
空间复杂度O(N),需要一个tmp的数组
递归:
核心:先分割在递归
时间复杂度:O( N*logN)
每一层归并回去是N,有logN层,一共是N*logN,6层货架,每层2两东西
空间复杂度:N(malloc的空间)+logN(递归的深度(最深)就是空间)但是在N面前logN太小所以是O(N)
//归并--递归
void MergeSort(int* pa, int amount)
{
int* tmp = (int*)malloc(sizeof(int) * amount);
//让函数子程序递归,避免每次都要一个tmp
_MergeSort(pa, 0, amount - 1, tmp);
free(tmp);
}
void _MergeSort(int* pa, int beginsub, int endsub, int* tmp)
{
//最后才写的返回条件
//beginsub==endsub相等说明没有数据或者只有一个数据
if (beginsub == endsub)
{
return;
}
int midsub = (beginsub + endsub) / 2;
//[beginsub,midsub] [mid+1,end]
_MergeSort(pa, beginsub, midsub, tmp);
_MergeSort(pa, midsub + 1, endsub, tmp);
//重新将区间值赋给变量,以便后序操作
int beginsub1 = beginsub, endsub1 = midsub;
int beginsub2 = midsub + 1, endsub2 = endsub;
//tmp下标
int i = beginsub;
//进行合并
while (beginsub1<=endsub1&&beginsub2<=endsub2)
{
//比较每个数组,从开头比较
if (pa[beginsub1] < pa[beginsub2])
{
tmp[i++] = pa[beginsub1++];
}
else
{
tmp[i++] = pa[beginsub2++];
}
}
//可能遇见一组已经归并完但另一组有很多残留
//归并前:1 2 4 3 8 9
//归并后:1 2 3 4 - -
//8 9 没进入tmp情况
//不知道beginsub1和beginsub2谁先完成,那就选择性进一个
while (beginsub1 <= endsub1)
{
tmp[i++] = pa[beginsub1++];
}
while (beginsub2 <= endsub2)
{
tmp[i++] = pa[beginsub2++];
}
//将tmp数组内容放回原数组
memcpy(pa + beginsub, tmp + beginsub, sizeof(int) * (endsub - beginsub + 1));
//+beginsub因为可能数组复制从中间开始
//1 2 3 4 endsub-beginsub==3 所以+1
}
递归优化:(小区间优化)
a+begin有可能开始位是数组的中间某部分
优化调用次数,从后三层比较好
类似二叉树,后三成调用的资源多,但此时没必要在浪费栈资源,所以采用一般的排序方法
非递归:
gap:每组内有gap个数据;
注意:此时我们细心观察不难发现我们给的例子是2的次方个,但如果不是那还能两个组之间互相比较吗?
回答:不能,此时可能会遇见的情况:
1.一开始是有一组不能比
2.开始两两能互相比较但是下一轮就不行了
初步测试代码:
根据上面我们提到的问题,测试
没有问题,但是给数组多家一个数,他的边界会越界;
//归并--非递归
void MergeSortNonR(int* pa, int amount)
{
int* tmp = (int*)malloc(sizeof(int) * amount);
//按照图片中gap = 1 2 4走
int gap = 1;
while (gap < amount)
{
int j = 0;
for (int i = 0; i < amount; i += 2 * gap)
{
//每组的合并数据
//为什么是这样的加减乘除不懂看上图gap==2理解
int beginsub1 = i, endsub1 = i + gap - 1;
int beginsub2 = i + gap, endsub2 = i + 2 * gap - 1;
//测试
printf("修改前:[%d,%d][%d,%d]\n", beginsub1, endsub1, beginsub2, endsub2);
while (beginsub1 <= endsub1 && beginsub2 <= endsub2)
{
if (pa[beginsub1] < pa[beginsub2])
{
tmp[j++] = pa[beginsub1++];
}
else
{
tmp[j++] = pa[beginsub2++];
}
}
while (beginsub1 <= endsub1)
{
tmp[j++] = pa[beginsub1++];
}
while (beginsub2 <= endsub2)
{
tmp[j++] = pa[beginsub2++];
}
}
printf("\n");
//全部一次性复制回去
memcpy(pa, tmp, sizeof(int) * amount);
gap *= 2;
}
free(tmp);
}
代码结果:
通过上面验证,
不管是遇见哪一种情况都会造成越界问题:
1.endsub1,beginsub2,endsub2全部越界;
2.beginsub2 endsub2越界
3.endsub2越界
解决:那就按照这三种情况分别判断一下
同时我们发现一次性复制会有很大麻烦,我们采用一段一段时复制
修改后的代码:
//归并--非递归
void MergeSortNonR(int* pa, int amount)
{
int* tmp = (int*)malloc(sizeof(int) * amount);
//按照图片中gap = 1 2 4走
int gap = 1;
while (gap < amount)
{
int j = 0;
for (int i = 0; i < amount; i += 2 * gap)
{
//每组的合并数据
//为什么是这样的加减乘除不懂看上图gap==2理解
int beginsub1 = i, endsub1 = i + gap - 1;
int beginsub2 = i + gap, endsub2 = i + 2 * gap - 1;
//测试
printf("修改前:[%d,%d][%d,%d]\n", beginsub1, endsub1, beginsub2, endsub2);
//解决方法
if (endsub1 >= amount)
{
endsub1 = amount - 1;
//不存在区间-给相反下面默认不存在,1 2范围给成2 1
beginsub2 = amount;
endsub2 = amount - 1;
}
else if (beginsub2 >= amount)
{
//不存区间
beginsub2 = amount;
endsub2 = amount - 1;
}
else if (endsub2 >= amount)
{
endsub2 = amount - 1;
}
printf("修改后:[%d,%d][%d,%d]\n", beginsub1, endsub1, beginsub2, endsub2);
while (beginsub1 <= endsub1 && beginsub2 <= endsub2)
{
if (pa[beginsub1] < pa[beginsub2])
{
tmp[j++] = pa[beginsub1++];
}
else
{
tmp[j++] = pa[beginsub2++];
}
}
while (beginsub1 <= endsub1)
{
tmp[j++] = pa[beginsub1++];
}
while (beginsub2 <= endsub2)
{
tmp[j++] = pa[beginsub2++];
}
//归并一组,拷贝一组
memcpy(pa + i, tmp + i, sizeof(int) * (endsub2 - i + 1));
}
printf("\n");
gap *= 2;
}
free(tmp);
}
测试结果:
通过对比我们发现有用!!
计数排序:
时间复杂度:O(N+Range)
空间复杂度:O(Range)
缺陷:
缺陷1:依赖数据范围,适合用于范围集中的数组;
缺陷2:只能用于整形;
核心思路:
1.统计每个数据出现的次数
2.根据统计次数一次排序
代码:
//计数排序
void CountSort(int* pa, int amount)
{
//假设最小最大值
int min = pa[0], max = pa[0];
//找正确的最小最大值
for (int i = 0; i < amount; i++)
{
if (pa[i] < min)
{
min = pa[i];
}
if (pa[i] > max)
{
max = pa[i];
}
}
//计算数据中的数据跨度--eg.从1到9,跨度一共有9个数
int range = max - min + 1;
int* countA = (int*)calloc(range,sizeof(int) );
if (countA == NULL)
{
perror("CountSort_calloc");
return;
}
//统计计数
for (int i = 0; i < amount; i++)
{
countA[pa[i] - min]++;
}
//排序
int k = 0;
for (int j = 0; j < range; j++)
{
while (countA[j]--)
{
pa[k++] = j + min;
}
}
}
代码详解:相对映射:
大文件排序(思想):
利用递归思想,将大文件分为多个小文件,在小文件里快排后在两个两个递归回去;
小文件归并内存里操作
文件与文件归并,在磁盘操作
内排序:在内存中对数据排序-->数据量小
外排序:在外存(硬盘)中对数据排序-->数据量大
稳定性:
稳不稳定主要在意相等时候他是用的排序是否要交换,如果交换使得他们的顺序变换了那就不稳定,如果没有就稳定
归并排序稳定需要在之前的加个等于号