1. hoare法
- 每次确定一个key【首元素】位置,从此分了两个区间:【0,key-1】,【key+1,end】。
- 每个内部排序是双循环:取第一个为key,R先走,R找小,L找大,注意不要越界。每次循环内部做交换,为交换大小值。最后外面再交换的是相遇位置和keyi做交换。
- 分析:L、R相遇位置是否永远能keyi做交换。即是否相遇位置永小于key。
如果L先遇R:因为R先起步,R位置停在小于key处,可以交换。
如果R先遇L:因为上次L交换完R,L处已经是小于key的值了。- 总之,以R先找小的快排,最后直接交换相遇位置和keyi。
代码分析:
- 局部Sort的参数:需要给L、R区间范围,已经数组名。
递归或非递归总体:需要给L、R、数组名,所以两个函数的参数一样。- 出错:递归的出口应该在快排本身上
- 每个partSort()中没有递归,只是在不断交换做key的定位。
- R先行,找小,(等于也略过。) 先写越界判断再比较值,因C语言错误读不报错。
- 递归出口:快排是不断在以key为间隔的两个区间做排序。递归出口是:L>=R。什么时候可能:L>=R:如果经过一轮快排,key定在末尾,那么右区间【key+1, end】,此时key+1>end。,且左区间是:【L,keyi-1】,**不小心当然多写一个成【L,keyi】也没事。
int partSort_hoare(int* a, int L, int R)
{
int keyi = L;
int key = a[L];
while (L < R)
{
while (L< R && a[R] >= key)
R--;
while (L < R && a[L] <= key)
L++;
Swap(&a[L], &a[R]);
}
// 此时,两个应该相遇了,直接交换
int meeti = L;
Swap(&a[keyi], &a[L]);
return meeti;
}
void quickSortBy_hoare(int* a, int L, int R)
{
if (L >= R)
return;
int keyi = partSort_hoare(a, L, R);
quickSortBy_hoare(a, L, keyi-1);
quickSortBy_hoare(a, keyi + 1, R);
}
int main()
{
int a[] = { 3, 2, 2, 11, 3, 44, 0, 7, 66};
quickSortBy_hoare(a, 0, sizeof(a) / sizeof(int)-1);
PrintArray(a, sizeof(a) / sizeof(int));
return 0;
}
每次分为两个区间,时间复杂度是:树的深度次局部sort(),而每层接近堆n个数做排序移动,所以总体是O(NlogN)。
因为做栈递归,需要参数使用函数栈帧,也就是上面说的递归树的深度次,O(logN)。
不稳定。比如 5, 5 ,6 ,7 ,1,2, 3 做快排,第一个5因为选为k,最后会在中间,和第二个5相对位置发生了改变。
- 要交换的是&a[keyi],而不是key值,我为了比较方便写了key变量。
- 递归是在Quick函数中,每次的partSort不做递归。
2. 三数取中的挖坑法
int getMidIndex(int* a, int l, int r)
{
int mid = (l + r) / 2;
if(a[l]>a[r])
{
if (a[r] > a[mid])
return r;
// mid > r
else if (a[l] < a[mid])
return l;
else
return mid;
}
// l
else
{
if (a[mid] > a[r])
return r;
// l < r , mid < r 比mid 和l
else if (a[mid] < a[l])
return l;
else
return mid;
}
}
int getMidIndex(int* a, int l, int r)
{
int mid = (l + r) / 2;
if(a[l]>a[r])
{
if (a[r] > a[mid])
return r;
// mid > r
else if (a[l] < a[mid])
return l;
else
return mid;
}
// l
else
{
if (a[mid] > a[r])
return r;
// l < r , mid < r 比mid 和l
else if (a[mid] < a[l])
return l;
else
return mid;
}
}
int partSort_hole(int* a, int L, int R)
{
int mid = getMidIndex(a, L, R);
Swap(&a[mid], &a[L]);
int keyi = L;
int key = a[L];
int hole = keyi;
while (L < R)
{
while (L < R && key <= a[R])
R--;
a[hole] = a[R];
hole = R;
while (L < R && a[L] <= key)
L++;
a[hole] = a[L];
hole = L;
}
a[L] = key;
int meeti = L;
return meeti;
}
void quickSortBy_hole(int* a, int L, int R)
{
if (L >= R)
return;
int keyi = partSort_hole(a, L, R);
quickSortBy_hole(a, L, keyi-1);
quickSortBy_hole(a, keyi+1, R);
}
挖坑法和霍尔方法非常相似,但是记得要等号也要跳过,递归给出口,时刻要更新坑位。找到一个值后,把这个值填入上一个坑,随后坑位做更新。
且先取中间。
R找小,找到小,就填坑。
L找大 填坑。
最外面,相遇位置,填入key。
因为一开始keyi位置空出来了,把R找到小的填过来,小的前去了,之后L找到的大的,填到之前R找到的位置,R在后面,所以使得大的后去了
2. 还是卡着不动
递归没有出口:太阳你个麻麻,找了半个小时。
快排是递归,要有出口,所有递归都要有出口
快排是递归,要有出口,所有递归都要有出口
快排是递归,要有出口,所有递归都要有出口
3. 前后指针法【也利用三数取中】
思路:【也先做三数取中】
描述:prev和cur一开始挨着,且prev处是key值,cr找到大于等于key的值,就继续往后走,prev不动。【因为希望prev和cur之间夹着大于或等于key的值】cur找到小于,就和prev的下一个交换。总之,cur找到小就让prev++做交换。
- 使用指针:prev、cur。
- prev一开始是keyi,cur是prev的下一个
- cur遍历【1,R】,寻找所有小于key的位置。
- 当a【cur】>=key,就略过,做加加:cur++,因为cur要找小。此时,prev和cur中间夹着大于和等于的值。
- 如果a【cur】
- 最后做keyi与prev的交换。且返回prev,因为位置固定了。
代码(普通版本)
int partSort_frontAback(int* a, int L, int R)
{
int key = a[L];
int keyi = L;
int cur = L + 1;
int prev = L;
while (cur <= R)
{
while (cur<=R && a[cur]>=key)
{
cur++;
}
if (cur <= R && a[cur] < key)
{
prev++;
Swap(&a[cur], &a[prev]);
cur++;
}
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void quickSortBy_frontAback(int* a, int L, int R)
{
if (L >= R)
return;
int keyi = partSort_frontAback(a, L, R);
quickSortBy_frontAback(a, L, keyi-1);
quickSortBy_frontAback(a, keyi+1, R);
}
代码(升级版)
改进思路:
- cur处不管大于还是小于key都要往后走。
- 如果cur到了小于位置,但prev和cur挨着【prev下一位是cur】,那就prev只往后走【因为prev和cur之间只能夹着大于等于,小于要略过】,prev加加就行。
// 不管咋样,cur++。内部只有在cur小于时,才有prev的操作
// 最后prev是小于位置
int partSort_frontAback(int* a, int L, int R)
{
int mid = getMidIndex(a, L, R);
Swap(&a[mid], &a[L]);
int key = a[L];
int keyi = L;
int prev = L;
int cur = L + 1;
while (cur<=R)
{
if (a[cur] < key)
{
prev++;
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
总之,改进就是cur一直++,只有cur在小于key位置,才动prev,让prev++,再交换。最后再prev一定是小于位置,与开头交换。再返回prev。
将序列拆到每个数字一组(每个数字看作已有序的数组),然后两两合并,不断两两合并。
每个数字一组时的归并:第一组:比较两个数组的头进行比较,选取较小的,放到头部,发现左边已经空了,把右边依次放进去。第二组:同第一组。
两个数字一组时的归并:
第一组:左序列和右序列头部左比较,右边头更小则挪到新数组中。如下挪9后,右序列头部变成了17,此时还是右边头更小,当一个序列(这里是右序列)空了后,把左边依次挪到到新数组中。
但是有多余剩下了两组数据。
最后做两组的归并,还是比两个有序序列的头部,然后挪动到新数组。
递归思路:
所有子区间有序才可以做归并,所以递归拆到每个数组只剩下一个数
做法:每次取中,然后从拆开。借助第三方数组做归并,归并到第三方数组,再把归并结果拷贝回原数组。再对原数组做归并。
什么时候是大于等于。
void _mergeSort(int* a, int* tmp, int left, int right)
{
if (left >= right)
{
if(left>right)
printf("存在大于情况\n");
return;
}
int mid = (left + right) / 2;
_mergeSort(a, tmp, left, mid);
_mergeSort(a, tmp, mid+1, right);
int begin1 = left;
int end1 = mid;
int begin2 = mid+1;
int end2 = right;
int i = left;
while (begin1 <= end1 && begin2 <= end2)
if (a[begin1] < a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while(begin2<=end2)
tmp[i++] = a[begin2++];
for (int j = left; j <= right; j++)
a[j] = tmp[j];
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
printf("malloc fail\n");
_mergeSort(a, tmp, 0, n-1);
free(tmp);
}
// 归并的核心:需要一个临时数组上做归并,再把结果返回给原始数组。
// 归并需要两个有序的区间,且两个区间都有序所以一开始拆到单个数字【因为算有序】才行
// 以mid拆,拆两个区间。
// 递归不能一直这样下去,需要出口:left>=right,=时,是只有一个数字,大于不存在。
// 第一次下到这里来说明两个区间有序了。
// 两个区间开始比较,比较需要两个都存在。
// 在tmp上做归并:给tmp赋值,需要从left开始。
// 赋值过程中已经做了++。所以while就够了
// 最后再把归并结果给a:
最后注意参数:给的是n
MergeSort(a, sizeof(a) / sizeof(int))。
此外,所有递归且需要分区间去做的这种,都需要写一个核心递归子函数。
void merge_NoR(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int i = begin1;
int j = begin1;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
tmp[i++] = a[begin1++];
else
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)
tmp[i++] = a[begin1++];
while (begin2 <= end2)
tmp[i++] = a[begin2++];
for (; j <= end2; j++)
a[j] = tmp[j];
}
// 思路是:对一个数组,gap个为1组,每两个gap,做归并。
// 归并区间:0组:【0,0】和【1,1】,1组:【2,2】【3,3】
// 和下面对应起来了 i=0,【0,0】,【1,1】之后,第二组,i=1时,【2,2】【3,3】
// gap 一开始是1,第0组,区间1:[i, i+gap-1], 区间2[i+gap, i+2*gap-1],
// 做第二组归并,i+=2gap:原因如下:
// 而i的范围:i不可以超过n-1,现在我不确定,i只小于n-1,那加上gap会不会超,
// 所以用i
// 剩余情况,都在以下,第一个不够或第二个没有或不够。
// 一组两个区间共有2*gap数,下一组的起始是:i+2*gap,所以不是i++,是i+=2gap
// 如果此时,起始不存在,即 第一个区间数量不够
// 第二个区间数量不够
// 第二个区间不存在
// 如果第一个区间不够,不用归并,因为这个区间已经有序了,后序gap增大,一定会归并上的,它可能算第二个小区间不存在情况或不够情况或第一个不够的情况
// 第二个区间不存在,也不用归并,
// 第二个不够,改右区间为end2
// ```所以第一个不够和第二个不存在,是同一种情况,直接判断第二个不存在,不做归并
// ```所以只在意第二个不够的情况,改end
// 如果第二个不存在就不归并 ,这个情况也包括了第一个不完整。或者第一个不完整的条件也一样
// 判断第二个区间不存在,begin2>=n,说明2区间越界了。就停止。不做merge
// begin2>=n和end1>=n,都做break。左区间不够或第二个不存在,都不做归并。
// ```但是也判断第二个区间数量不够,end2>=n,说明gap个数量的区间2,超了。
// ```修正end2,所以把变量都取出来,
// 使得作为begin和end变量都能变。
//
// ```
// gap需要变化。gap是每个区间中数的数量,一开始是1。对gap做循环
//
void MergeSort_NoR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail \n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i+=2*gap)
{
int begin1 = i;
int end1 = i + gap - 1;
int begin2 = i + gap;
int end2 = i + 2 * gap - 1;
if (end1 >= n)
break;
if (end2 >= n)
end2 = n - 1;
merge_NoR(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2;
}
free(tmp);
}
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1;
int* count = malloc(sizeof(int)*range);
memset(count , 0, sizeof(int)*range);
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j+min;
}
}
}
归并排序:
宏观上,虽然每一层的区间数量不同,但是每层总共对N个数字做归并,高度是LogN,所以总时间复杂度:O(NlogN)。
希尔排序:
时间复杂度O(n^1.3)
缺点:需要O(N)时间复杂度
注意快排和归并排序传入的参数大小不一样。
冒泡、插入、归并是稳定的,剩下都不稳定
void BubbleSort(int*a, int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = n-1; j >= i ; j--)
{
if (a[j] < a[j - 1])
Swap(&a[j], &a[j-1]);
}
}
}
思路:
每次让最大或最小的下去或上去,且遍历范围每次要少1。所以必须双层循环。
- 双层循环外层控制每次固定的位置,范围:i:【0, n-2】,因为n个数,做n-1次排序即可。剩余一个绝对有序。
- 内层循环,j = i,相当于依次固定【0,n-1】中第一个,让小于上浮,让j减减,所以j>=i,不必>= 0,因为每次固定最上面一个。
稳定性分析:
冒泡排序是稳定的,因为遇到等于情况,可以不做交换。