思想:
可以想象为打扑克的排序,在摸牌时,假设前面的牌都是排好序的,现在摸到一张新的牌,插入原来排好的序列中。
那么我们在写代码的时候就可以认为第一个是有序的,从第二个开始,把后边的数全部插入到前面
代码实现:
//插入排序
void InSertSort(int* a, int n)
{
int cur = 1;
while (cur < n)
{
int tmp = a[cur];//排cur处的数字
int end = cur - 1;
while (end >= 0)
{
if (a[end] > tmp)//继续比较
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
//通过画图就可知道不管是break还是结束while循环
//都要在end + 1处插入
a[end + 1] = tmp;
cur++;
}
}
最坏的情况就是当整个数组都是逆序的时候此时复杂度为O(N ^ 2)
最好的情况就是当数组接近顺序有序的时候此时复杂度为O(N)
为什么接近就是O(N)?
例如这个接近有序的数组:1 2 3 5 4 7 6 8 9
要挪动的数字只有4、6, 都只挪动(循环)一次所以复杂度为O(N)
从上面的分析可知,如果一个数组接近有序,那么插入排序的复杂度就为O(N)。
那么我们就可以分两个步骤排序:
1、预排序 ——> 接近有序
2、直接插入排序
那么如和预排序呢?
先分组,对分组的数据进行插入排序
间隔为gap的分成一组,对一组进行插入排序
如图:
用绿色为例:
全部排完后:
可以看出这个相比于最开始更接近有序,并且大的数被更快的挪到后边,小的更快的挪到前面。
并且当gap越小,越接近有序,当gap为1时,就是插入排序
那么现在就要确定gap的值:
刚开始把gap值设的较大一点,慢慢减小,最后为1时就排好了(多组预排序)
void ShellSort(int* a, int n)
{
//一趟预排序
int gap = n;
while (gap > 1)
{
//gap不能等于0
//gap == 1时直接插入排序
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很大,for循环走2/3N ->N次,但下面的while就是常数次,所以时间复杂度为O(N)
当gap很小时,本来是O(N^2),但是前面经过预排序,已经变得有序,所以还是O(N)
可以看出来while循环里复杂度为O(N)
那么while循环了多少次呢?log3(N)
所以整体的时间复杂度:O(long3(N) * N)
平均的时间复杂度为O(N^1.3)
思路:
本质上就是选出最小(最大),插入到左右两端,在选出次小(大)的
为了提高效率,我们可以一次把最大和最小的选出来.
void SelectSort(int* a, int n)
{
int left = 0, right = n - 1;
while (left < right)
{
int minIndex = left, maxIndex = left;//记录下标
for (int i = left; i <= right; i++)
{
if (a[minIndex] > a[i])
{
minIndex = i;
}
if (a[maxIndex] < a[i])
{
maxIndex = i;
}
}
Swap(&a[minIndex], &a[left]);
//当最大的在左边时候,最大的会被交换走
if (left == maxIndex)
{
maxIndex = minIndex;
}
Swap(&a[maxIndex], &a[right]);
left++;
right--;
}
}
注意把最小的放到左边时有可能会影响最大值(有可能最大值在左边被交换走了)。
选出最小的N次,选N次
时间复杂度为O(N^2)
堆排序在以前写过的文章有详细讲解:
一万字学会堆和二叉树
这里就不过多介绍
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
//大堆
void AdjustDown(int* a, int n, int parent)
{
int child = 2 * parent + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
//大的放后面
int end = n - 1;
while (end)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
end--;
}
}
建堆的时间复杂度为O(N),向下调整的时间复杂度为O(log^N)(高度)
O(N + N*log^N)——> O(N * log(N))
大致思想就是两个指针遍历数字,每次都把最大的给挪到最后
要注意的是控制趟数
假如有n个元素,则走n - 1趟
每走一趟,俩指针需要走的趟数就少一次(n - 1 - i)
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
//用来判断是否交换过,提高效率
int flag = 1;
for (int cur = 0; cur < n - 1 - i; cur++)
{
if (a[cur + 1] < a[cur])
{
Swap(&a[cur], &a[cur + 1]);
flag = 0;
}
}
if (flag)
{
break;
}
}
}
最坏情况:
第一趟:N
第二趟:N - 1
……
O(N^2)
最好情况(有序)
O(N)
可以发现冒泡和插入的时间复杂度相似
那么两种排序算法哪个更好呢?
1️⃣顺序有序时两个一样好
2️⃣接近有序时插入排序好(比如有一段有序,插入就不用挪动,而冒泡还得走完)
思想:选出一个key(左),把key放入正确位置,使key的左边全是小于key的,右边全是大于key的,走一遍叫单趟排序
然后递归下去就能排好全部
这里单趟排序有三种方法:
1️⃣Hoare(左右指针法)
2️⃣挖坑法
3️⃣前后指针法
注意一定要让右边先走
这样每次相遇点一定是小的
左边只有一个就不用递归了,把右边在进行单趟排序,直到都只剩下一个元素
//hoare法
int PartSort(int* a, int begin, int end)
{
int keyi = begin;
int left = begin, right = end;
while (left < right)
{
//右找小
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
if (left < right)
{
Swap(&a[left], &a[right]);
}
}
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
//中间没有元素就返回
if (begin >= end)
{
return;
}
int keyi = PartSort(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
//挖坑法
int PartSort1(int* a, int begin, int end)
{
int tmp = a[begin];
int left = begin, right = end;
while (left < right)
{
//右边先找小
while (left < right && a[right] >= tmp)
{
right--;
}
a[left] = a[right];
//左边找大的
while (left < right && a[left] <= tmp)
{
left++;
}
a[right] = a[left];
}
a[left] = tmp;
return left;
}
void QuickSort(int* a, int begin, int end)
{
//中间没有元素就返回
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
//前后指针法
int PartSort2(int* a, int left, int right)
{
int key = left;
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[key])
{
prev++;
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
//中间没有元素就返回
if (begin >= end)
{
return;
}
int keyi = PartSort2(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
当改成非递归算法时要用到栈,用栈的特性达到递归的效果,用栈只是为了获取单趟排序的区间,单趟排序不变
//非递归快排
void QuickSortNonR(int* a, int begin, int end)
{
Stack 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 key = PartSort(a, left, right);
//key左边有区间就入栈
if (left < key - 1)
{
StackPush(&st, left);
StackPush(&st, key - 1);
}
//key右边有数据就入栈
if (right > key + 1)
{
StackPush(&st, key + 1);
StackPush(&st, right);
}
}
StackDestroy(&st);
}
我们知道快排的理想情况就是每次key放在中间的位置,那么理想状态下时间复杂度为O(N*log(N))
但是这毕竟是理想状况,如果数组变成逆序有序,时间复杂度直接变成O(N^2)
针对O(N^2)这类情况就有了三数取中的方法
大致意思就是比较最左、最右、中间的三个数,把大小居中的数当key
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;
if (a[left] < a[right])
{
if (a[mid] < a[left])
{
return left;
}
else if (a[mid] > a[right])
{
return right;
}
else
{
return mid;
}
}
else
{
if (a[mid] < a[right])
{
return right;
}
else if (a[mid] > a[left])
{
return left;
}
else
{
return mid;
}
}
}
我们可以发现当我们往下递归时,随着深度增加,每一层的递归次数都以平方的速度增加,越往下就递归越多次。
为了减少最后几层的递归次数,当要递归数组长度小于某个值的时候,就采取非快排算法,小区间优化若是使用得当的话,会在一定程度上加快快速排序的效率,而且待排序列的长度越长,该效果越明显。
void QuickSort(int* a, int begin, int end)
{
//中间没有元素就返回
if (begin >= end)
{
return;
}
if(end - begin > 10)
{
int keyi = PartSort2(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else
{
//a + begin 是要插入排序的开始位置
//end - begin 是长度 +1就是元素个数
InSertSort(a + begin, end - begin + 1);
}
}
思想:
将两个有序数组合并,得到一个整体有序的数组
但是归并的前提是有序数组,得到有序数组的方法就是当数组只有一个元素就认为是有序
创建一个等长的临时数组,在临时数组归并,归并好后再拷贝回原数组
void _MergeSort(int* a, int* tmp, int left, int right)
{
if (left >= right)
{
return;
}
//注意中间下标求法
int mid = (left + right) >> 1;
//对左序列归并
_MergeSort(a, tmp, left, mid);
//对右序列归并
_MergeSort(a, tmp, mid + 1, right);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
//用i记录放入tmp的位置
int i = left;
//归并两个序列放入tmp中
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
//把tmp拷回数组
int j = left;
for (j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
//归并排序
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
}
思想:
先让两两归并,再慢慢往上加,最后归并成有序数组
1️⃣最后一组归并时,最后一个小区间存在,但不满gap个
2️⃣最后一组归并时,最后一个小区间不存在
3️⃣最后一组归并时,最后一个小区间不存在,并且第一个小区间不完整
第一种情况控制第二区间的边界,第二、三种情况则不需要处理最后一组。
void _MergeSort(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
int i = begin1, 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++];
}
//拷贝回原数组
memcpy(a + j, tmp + j, sizeof(int) * (end2 - j + 1));
}
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
int gap = 1;
while (gap < n)
{
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;
//控制边界
if (end1 >= n)
{
break;
}
if (end2 >= n)
{
end2 = n - 1;
}
_MergeSort(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2;
}
free(tmp);
}
每一层都有N个数归并,因为是树形结构,所以有log(N)层,时间复杂度为O(N * log(N))
计数排序也叫非比较排序,是通过统计数组中相同元素出现的次数,然后通过统计的结果将数组回收到原来的数组中。
这种映射方法叫绝对映射,如果最小值都很大,那么count数组会很大,势必会造成空间浪费。
所以应该用到相对映射:a数组的最小值就对应count数组的下标0,a数组的最大值就是count数组的最后一个下标。
要注意的是计数排序只适合排数据范围相对集中的数组,不然会造成空间浪费。也不能排浮点数。
//计数排序
void CountSort(int* a, int n)
{
//记录最小元素
int min = a[0];
//记录最大元素
int max = a[0];
for (int i = 0; i < n; i++)
{
if (min > a[i])
{
min = a[i];
}
if (max < a[i])
{
max = a[i];
}
}
int range = max - min + 1;
//不能用malloc,会让count数组有随机值
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
printf("realloc fail\n");
exit(-1);
}
//统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//返回原来数组a
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
从代码分析,三层循环都是O(N),一层是O(range),所以时间复杂度为O(N + range)。