目录
先单趟在再整体
插入排序
直接插入排序(InsertSort)
思想
- 举例——>摸牌
在有序的数组中摸一张牌插入到前面合适的位置中去
插入排序的逻辑,摸到一张牌后和最后的比较大小,大则插入在后面,使得总体有序。
实现
- 单趟
先排单趟,比较到要插入数据与前一个数据(
end
)比较大小,比该数大则插入到该数后面,直到比较到第一个数据为止插入到首元素位置(循环条件为end>=0
)。
先将要插入元素保存到tmp
中,该位置为end+1
,等到合适位置后插入到end+1
中去。
为处理end
比较完没找到的(tmp
最小)的情况,当tmp>=end
时,跳出while
,在循环外交换。
每次比较完后end--
向前比较
单趟如下![[Pasted image 20230925213125.png]]
- 总体
共有n个数据,至少比较n次,故在用
for
循环,由于end+1
的存在,i<=n-2
/i<n-1
;
拿到一个乱序数组,从第一个数据向后排序,故end=i
,
代码
void InsertSort(int*a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end]; //往后挪动
}
else
{
break;
}
end--;
}
a[end + 1] = tmp;
}
}
int main()
{
int arr[] = { 1,3,5,7,4,1,0,2,6 };
InsertSort(arr,sizeof(arr)/sizeof(arr[0]));
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
![[Pasted image 20230925213339.png]]
复杂度分析
- 时间复杂度
最优:- 有序:O(n)
最劣:- 完全逆序:O(n^2)
- 空间复杂度
O(1)
希尔排序
- 引入
①在插入排序中从第一个数开始向后遍历,②每到一个数(x)就与之前的m个数从后往前进行(比较–交换),若完全逆序,则每个数的要与前n个数交换,时间复杂度提升至O(n^2),完全有序则为O(n)。直到遍历完全后有序。
而希尔在此基础上思考,遍历是少不了的,那能否通过减少从后往前比较时的交换次数进行(蓝色内容)来提高时间效率呢?。
于是希尔排序的核心为先进行分组预排序,再进行直接插入排序来优化。
![[Pasted image 20230927113722.png]]
- 以下是从0到1的希尔排序的过程
1.预排序
单元素
该元素往前比较,如果小于前面数,则交换,并将下标前移。直到n<0时退出循环,完成一个数的位置排序
//单个数据单趟
gap = 3;
int end = 0;
int tmp = a[end + 1];
while (end >= n)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
单趟元素
对组内单元素的排序,每隔gap个数据进行比较,直到单趟元素排序完成。
int gap = 3;
for (int i = 0; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
//单个数据单趟
while (end >= n)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
多组
为使每组元素动起来,在最外层套一层循环,每组元素有gap个,故循环gap次。
void InsertSort(int*a, int n)
{
int gap = 3;
for (int j = 0; j < gap; j++)
{
for (int i = j ; i < n - gap; i += gap)
{
int end = i;
int tmp = a[end + gap];
//单个数据单趟
while (end >= n)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
多组并排
最后一步多组可以按如上分次进行,也可以按如下多组并行排序,效果相同,但代码更加整洁。
对与gap的
void InsertSort(int*a, int n)
{
int gap = 3;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
//单个数据单趟
while (end >= n)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
分析
多组并排
意义:让大的数更快地到后面去,小的数更快的到前面去
gap越大,跳的越快,越不接近有序。
gap越小,跳的越慢,越解决有序,gap=1,直接有序
2.直接插入排序
- 以上逻辑对比直接插入排序我们可得,当gap=1时,该排序为直接插入排序。
- 当数据过多gap=3时预排过多,优化不明显,所以gap随n的变化而变化(gap=gap/2或gap=gap/3+1(效率更高,为保证最后1次为1,故+1))。
- 所以这样定义gap:第一趟gap=n每组分1个元素,之后分2、4、8……n个元素(gap=n/2、n/4……1),到1时相当于预排序后的直接插入排序,实现有序。
void ShellSort(int* a, int n)
{
//多组并排
int gap = n;
while (gap > 1)
{
gap = gap / 2;
//gap = gap / 3 + 1;
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;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
复杂度计算
- 时间复杂度
gap=3,每组n/3个数据,每趟比较3次(1+2),一共比较(n/3)*3=n次
gap=9,每组n/9个数据,每趟比较36(1+2+3+…+9)次,一共比较(n/9)*36=4n次
………………(组数x每组的次数)上一次排序对这次有所增益
目前在严蔚敏教材中时间复杂度约为O(n1.3)<O(nlog2N);该准确数据仍在研究暂未有定论;
- 空间复杂度为1
- 不稳定
选择排序
直接选择排序
遍历一遍直接选出 最大/最小 的值
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//直接选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n-1;
while(end > begin)
{
int mini = begin, maxi = end;
for (int i = begin+1; i <= end ; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[mini], &a[begin]);
if (maxi == begin)
{
maxi = mini;
}
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
堆排序
排升序建小堆,排降序建大堆
交换排序
冒泡排序
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 1; j < n - i; j++)
{
if (a[j-1] > a[j])
{
int tmp = a[j-1];
a[j-1] = a[j];
a[j] = tmp;
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
快速排序
Honre
将第一个数设为key,
找key,左边找比key小,右边找比key大,交换。
比key小的放到左边,比key大的换到右边,最终达到左边比key小、右边比key大的效果。
当左右相遇时,key与右边交换。
至此,key为它要到的最终位置。
单趟
int PartSort(int* a, int left , int right)
{
//*防止局部变量交换无效
int keyi = left;
while(left < right)
{
//*等于号防止陷入死循环的情况
//*left<right防止越界访问
//找小_大就跳过
while (left < right && a[right] >= a[keyi])
{
right--;
}
//找大,小就跳过
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
单趟时间复杂度为O(n);
递归
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return ;
int keyi = PartSort(a, begin, end);
//[begin, keyi-1] keyi [key+1, end]
QuickSort(a, begin, keyi-1);
QuickSort(a, keyi + 1, end);
}
- 为什么相遇位置比key小?
右边先走做到的
- 右边先走找小,找到比key小数- 左边找大
- 找到
- 交换——>右边走
- 未找到
- R=L,相遇位置是右边所找的比key小的数
三数取中
//三数取中
int GetMid(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[right])
{
if (a[left] > a[mid])
return mid;
else if (a[left] < a[mid])
return left;
else
return left;
}
else //right 大
{
if (a[right] > a[mid])
return mid;
else if (a[right] < a[mid])
return right;
else
return right;
}
}
挖坑法
//挖坑法
int PartSort2(int* a, int left, int right)
{
int midi = GetMid(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int hole = left;
while (left < right)
{
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
前后指针法
cur找比key小的数,++prev,交换
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi])
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return keyi;
}
小区间优化
降低递归次数——>直接插入
递归太深可能会产生栈溢出,所以我们来学习非递归(当然快排不太可能会溢出,1000000000个数据30层,)
非递归——栈
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end); //入栈先右后左
STPush(&st, begin);
while(!STEmpty(&st))
{
//出栈先左后右
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort1(a, left, right);
// [left, keyi-1] keyi [keyi+1, right]
//先入右再入左
if (right > keyi + 1) //有位置
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1)
{
STPush(&st, keyi-1);
STPush(&st, left);
}
}
STDestory(&st);
}
二叉树结构的排序
右边找比key小的,左边找比key大的。
归并排序
递归
开空间
- 分左右区间(递进)
直到递进空返回,直到1个元素 - 归并
将 左|右 进行大小比较,大的元素插入到tmp数组中。
判断 左|右 是否已经空,有一个空即停止。
后续将剩下的元素插入到tmp中去。 - 拷贝
将tmp
中数组插入到原数组a
中,顺序与tmp中相同 - (回归)
回归后继续 归并-拷贝- 回归
代码
void _MergeSort(int* a,int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
//递归
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid+1, end);
//归并到tmp中,并拷贝到原数组
//[begin1, end1][begin2, end2]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin;
while (begin1 <= end1 && begin2 <= end2) //有一个递归结束就结束
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//只进一个
while(begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp==NULL)
{
perror("malloc error\n");
return ;
}
_MergeSort(a, tmp, 0, n-1);
free(tmp);
}
空间复杂度:O(n)
时间复杂度:O(n*log2n)
非递归
void MergeSortNonr(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp==NULL)
{
perror("malloc error\n");
return ;
}
int gap = 1;
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//[begin1, end1] [begin2, end2]
int begin1 = i, end1 = gap + i - 1;
int begin2 = i + gap, end2 = 2 * gap + i - 1;
//printf("[%d,%d][%d,%d]", begin1, end1, begin2, end2);
if (begin2 > n)
{
break;
}
if (end2 > n)
{
end2 = n - 1;
}
int index = i;
while (begin1 <= end1 && begin2 <= end2) //有一个结束就结束
{
if (a[begin1] < a[begin2])
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//只进一个
while (begin1 <= end1)
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
memcpy(a + i, tmp + i, (end2-i+1) * sizeof(int));
}
//printf("\n");
gap *= 2;
}
free(tmp);
}
复杂度
空间复杂度:O(n)
时间复杂度:O(n*log2n)
内/外排序
磁盘不支持下标访问
顺序写顺序读
计数排序
统计每个数字出现的次数,每个值是多少就对对应位置++;
局限:数值得是相对集中
相对映射
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 = (int*)malloc(sizeof(int) * range);
//将空间内数据初始化为0
memset(count, 0, sizeof(int) * range);
//统计数据出现的次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//排序(回写)
int j = 0;
for (int i = 0; i < range; i++)
{
while (count[i]--)
{
a[j++] = i + min;
}
}
}
复杂度
时间复杂度:O(n+range)
空间复杂度:O(range)
适合范围集中的排序
本篇内容就到这里了
如有不足之处欢迎在评论区补充交流