目录
1、插入排序
1.1 基本思想
1.2 直接插入排序
1.2.1 思路
1.2.2 代码实现
1.2.3 性质
1.3 希尔排序
1.3.1 思路
1.3.2 代码实践
1.3.3 性质
2、选择排序
2.1 基本思想
2.2 直接选择排序
2.2.1 思路
2.2.2 代码实践
2.2.3 性质
2.3 堆排序
2.3.1 思路
2.3.2 代码实践
2.3.3 性质
3、交换排序
3.1 基本思想
3.2 冒泡排序
3.2.1 思路
3.2.2 代码实践
3.2.3 性质
3.3 快速排序
3.3.1 思路
3.3.2 代码实践
3.3.2.1 hoare版本
3.3.2.2 挖坑法
3.3.2.3 前后指针法
3.3.2.4 快排优化
3.3.2.4.1 三数取中
3.3.2.4.2 小区间优化
3.3.2.4.3 代码实践
3.3.2.5 三路划分法和自省排序(用于重复值较多的)
3.2.2.6 非递归快排(利用栈)
3.3.3 性质
4、归并排序
4.1 基本思想
4.2 代码实践
4.2.1 递归版本
4.2.2 非递归版本
4.3 性质
5、计数排序
5.1 基本思想
5.2 代码实践
5.3 性质
排序的稳定性:如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。
相同的数无所谓相对次序,但在实际应用中,比的是结构体,需要保持相对次序不变
如:学生成绩这个结构体,要求总分相同时,语文成绩高的排在前面
那么先按语文成绩排,再用稳定性的排序排总分(整体是按总分排,那么总分最后排)
排序的代码实践,建议先单趟再整体
以下默认升序
1、插入排序
1.1 基本思想
将一个数 插入到 有序的序列中(类似于玩扑克牌时的插入)
注意:挪动数据是进行覆盖
1.2 直接插入排序
1.2.1 思路
开始时,认为arr[0]是有序的,插入arr[1]……最后是arr[n-1]插入到前[0,n-2]中
1.2.2 代码实现
// 直接插入排序
// O(N^2)
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{ // [0,end]是有序的,插入end+1位置的值
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])// <,为了稳定性,在相等的值的后面插入
{
a[end + 1] = a[end];
--end;
}
else
break;
}
a[end + 1] = tmp;
}
}
1.2.3 性质
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)(逆序的时候),但通常不是逆序,所以还行,有实践意义
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
1.3 希尔排序
1.3.1 思路
先进行预排序(让其接近有序):
以gap为一个间隔,分成n(总元素个数)/gap个组,分别进行直接插入排序
如:下面以 gap = 3 为例,10/3,分成3组
代码如下:
三层循环是 先完成一组,再完成下一组
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 >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
三层循环看着别扭,下面改为两层循环(书上都是两层循环),但效率没变,都是完成了多组排序,
两层循环是 三个组(n/gap个组),分别交替进行排序
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
此时gap = 1时,为直接插入排序,
1.3.2 代码实践
gap取法很多,目前没有最优取法,下面这种取法较好
// 希尔排序
// O(N^1.3)
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
// +1,使最后一次执行gap为1
// gap > 1,预排序
// 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 (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
break;
}
a[end + gap] = tmp;
}
}
}
1.3.3 性质
1. 希尔排序是对直接插入排序的优化
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
3. 希尔排序的时间复杂度不好计算,可以估计为O(N^1.3),但还是比快排弱一点
4. 稳定性:不稳定
2、选择排序
2.1 基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完
即 选最值 与相应位置进行交换
2.2 直接选择排序
2.2.1 思路
遍历找最值,与起始位置进行交换
遍历该数组,把最大(小)的数与该组的第一个位置交换,
这里优化一下,遍历该数组,把最小的数的下标和最大的数的下标找到(因为要交换,要找下标),分别与第一个位置的数,最后一个的数交换
2.2.2 代码实践
// 直接选择排序
// O(N^2)
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin<end)
{
int maxi = begin;
int mini = begin;
for (int i = begin + 1; i <= end; ++i)//begin+1,不用和自己比
{
if (a[maxi] < a[i])
maxi = i;
if (a[mini] > a[i])
mini = i;
}
Swap(&a[mini], &a[begin]);
if (maxi == begin)
maxi = mini;
Swap(&a[maxi], &a[end]);
++begin;
--end;
}
}
2.2.3 性质
1. 直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用
2. 时间复杂度:O(N^2),教学意义
3. 空间复杂度:O(1)
4. 稳定性:不稳定
2.3 堆排序
2.3.1 思路
建堆找最值,与结尾位置进行交换
堆排序(Heapsort)是指利用堆,这种数据结构所设计的一种排序算法,它是选择排序的一种。它是 通过堆来进行选择数据。
注意:升序建大堆,降序建小堆
想要详细了解堆+堆排序+堆的应用,可以看看我这篇博客
堆+堆排序+topK问题_大顶堆小顶堆java-CSDN博客
2.3.2 代码实践
/// 堆排序
// O(N*logN)
void AdjustDown(int* a, int n, int root)
{
int child = 2 * root + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])// child+1防止越界
++child;
if (a[child] > a[root])
{
Swap(&a[root], &a[child]);
root = child;
child = 2 * child + 1;
}
else
return;// 不发生交换时,已经成堆了,直接结束
}
}
void HeapSort(int* a, int n)
{
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
i = n - 1;
while (i > 0)
{
Swap(&a[0], &a[i]);
--i;
AdjustDown(a, i+1, 0);
}
}
2.3.3 性质
1. 堆排序使用堆来选数,效率就高了很多
2. 时间复杂度:O(N*logN),但还是比快排弱一点
3. 空间复杂度:O(1)
4. 稳定性:不稳定
3、交换排序
3.1 基本思想
所谓交换,就是根据序列中两个数的比较结果 来对换这两个数在序列中的位置
通过交换,完成排序
3.2 冒泡排序
3.2.1 思路
相邻的数两两比较,进行交换,把最值交换到结尾位置
3.2.2 代码实践
// 冒泡排序
// O(N^2)
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; ++j)
{
int flag = 0;
for (int i = 0; i < n - 1 - j; ++i)
{
if (a[i] > a[i + 1])// > 为了稳定性,相等的值不交换
{
Swap(&a[i], &a[i + 1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
3.2.3 性质
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2),教学意义
3. 空间复杂度:O(1)
4. 稳定性:稳定
3.3 快速排序
3.3.1 思路
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:
任取待排序元素序列中的某元素作为基准值(key),一般取最左边的值为key,通过某种方法,将待排序集合分割成左右子序列,左子序列中所有元素均 < key,右子序列中所有元素均 > key,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止
3.3.2 代码实践
left和right分别为 指向对应数组首尾元素的下标,是闭的
3.3.2.1 hoare版本
keyi = left,begin = left,end = right(分别为L和R的起始位置)
右边先走,左找大,右找小,进行交换
当begin == end(即相遇),循环结束,再Swap(&a[keyi],&a[begin]),keyi = begin(让keyi指向key)
左边做key,右边先走,相遇时可以保证相遇位置的值 < key
证明:
L遇R(R停L动):R停的位置的值 < key
R遇L(L停R动):若之前发生了交换,L位置下的值 < key,
若没有发生交换,key自己交换(有序情况下,都是自己交换,时间复杂度为O(N^2),后面三数取中,会进行优化)
反之,右边做key,左边先走,相遇时可以保证相遇位置的值 > key
// 快速排序hoare版本
int PartSort1(int* a, int left, int right)
{
int keyi = left;
int begin = left, end = right;// 初始的范围,后面要用,不能改变
while (begin < end)// begin == end 相遇,不用找了
{
// 先 右找小
while (begin < end && a[end] >= a[keyi])
--end;
// 再 左找大
while (begin < end && a[begin] <= a[keyi])
++begin;
Swap(&a[begin], &a[end]);
}
Swap(&a[keyi], &a[begin]);
return begin;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort1(a,left,right);
// [left,keyi-1] keyi [keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
3.3.2.2 挖坑法
(并没有用交换,而是覆盖坑位)
key存储左边的值,左边形成坑位
右边先走,右找小,找到后,该值填入坑位,当前位置形成新坑
左找大,找到后,该值填入坑位,当前位置形成新坑……
当相遇时,把key的值填入坑位
注意:左边是坑,左边先走,大的值就填到左边了
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int key = a[left];
int hole = left;
int begin = left, end = right;
while (begin < end)
{
// 先 右找小,找到后填坑,甩坑
// 相遇时,坑自己填自己的值,把坑甩给自己,没关系
while (begin<end && a[end]>=key)
--end;
a[hole] = a[end];
hole = end;
// 再 左找大,找到后填坑,甩坑
while (begin < end && a[begin] <= key)
++begin;
a[hole] = a[begin];
hole = begin;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort2(a,left,right);
// [left,keyi-1] keyi [keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
3.3.2.3 前后指针法
prev = left,cur = left + 1 (prev和cur的起始位置)
若cur的值 < key,++prev,prev与cur进行交换,++cur
若cur的值 >= key,++cur,当cur > right循环结束,
再Swap(&a[keyi],&a[prev]),keyi = prev(让keyi指向key)
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyi = PartSort3(a,left,right);
// [left,keyi-1] keyi [keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
3.3.2.4 快排优化
3.3.2.4.1 三数取中
决定快排性能的关键点是每次单趟排序后,key对数组的分割,
如果每次选key基本二分居中,那么快排的递归树就是颗均匀的满⼆叉树,性能最佳。但是实践中虽然不可能每次都是二分居中,但是性能也还是可控的。
但是如果出现每次选到最小值/最大值,划分为0个和N-1的子问题时,时间复杂度为 O(N^2),数组序列有序时就会出现这样的问题,可以用三数取中或者随机选key解决了这个问题。
3.3.2.4.2 小区间优化
不再递归分割排序,减少递归的次数
3.3.2.4.3 代码实践
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
int GetMidi(int* a, int left, int right)
{
int midi = (left + right) / 2;
// left midi right
if (a[left] < a[midi])
{
if (a[midi] < a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[midi]
{
if (a[midi] > a[right])
{
return midi;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化,不再递归分割排序,减少递归的次数
if ((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = PartSort3(a, left, right);
// [left,keyi-1] keyi [keyi+1,right]
QuickSort(a, left, keyi - 1);
QuickSort(a, keyi + 1, right);
}
}
3.3.2.5 三路划分法和自省排序(用于重复值较多的)
虽然用了三数取中,但是现在还是有一些场景没解决(数组中有大量重复数据时),如:
// 数组中有多个跟key相等的值
int a[] = { 6,1,7,6,6,6,4,9 };
int a[] = { 3,2,3,3,3,3,2,3 };
// 数组中全是相同的值
int a[] = { 2,2,2,2,2,2,2,2 };
三路划分:用于重复值较多的,但重复值没那么多时,效率会比前面的方法低一些
三路划分核心思想:把数组中的数据分为三段【<key的值】 【=key的值】【>key的值】
实现思想:
1. key默认取left位置的值
2. left指向区间最左边,right指向区间最后边,cur指向left+1位置
3. cur遇到 < key的值,跟left位置的值交换,left++,cur++
4. cur遇到 > key的值,跟right位置交换,换到右边,right--,无脑与right换,cur都会判断
5. cur遇到 = key的值,cur++
6. 直到cur > right结束
// 快速排序三路划分法
void PartSort3Way(int* a, int left, int right)
{
if (left >= right)
return;
// 小区间优化,不再递归分割排序,减少递归的次数
if ((right - left + 1) < 10)
{
InsertSort(a + left, right - left + 1);
}
else
{
// 三数取中
int midi = GetMidi(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int cur = left + 1;
int begin = left, end = right;//保存之前的left和right
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur], &a[left]);
++left;
++cur;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
--right;
}
else
++cur;
}
// [begin,left-1] [left,right] [right+1,end]
PartSort3Way(a, begin, left - 1);
PartSort3Way(a, right + 1, end);
}
}
自省排序:重复值多与不多,效率都还行(C++STL库里的 sort 就是用这种方法)
当发现递归层数过多时(可能就是重复值导致),就停止使用快排,改用堆排
递归函数多了两个参数(每次递归层数++,使用堆排的层数),和一个使用堆排的判断,
3.2.2.6 非递归快排(利用栈)
我对递归的理解是,自己调用自己,自己会给自己传参
那么改非递归,可以把参数存储到栈里,通过循环取参
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)// prev+1 == cur不交换
Swap(&a[prev], &a[cur]);
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right)
{
Stack stack;
StackInit(&stack);
StackPush(&stack, right);
StackPush(&stack, left);
int begin, end, keyi;
while (!StackEmpty(&stack))
{
begin = StackTop(&stack);
StackPop(&stack);
end = StackTop(&stack);
StackPop(&stack);
keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
if (begin < keyi - 1)
{
StackPush(&stack, keyi-1);
StackPush(&stack, begin);
}
if (keyi + 1 < end)
{
StackPush(&stack, end);
StackPush(&stack, keyi+1);
}
}
StackDestroy(&stack);// 非常容易忘记
}
stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
stack.c
#include "Stack.h"
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_top = ps->_capacity = 0;
}
//动态申请
void Stackcapacity(Stack* ps)
{
if (ps->_top == ps->_capacity)
{
ps->_capacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
STDataType* tmp = (STDataType*)realloc(ps->_a, ps->_capacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("Stackcapacity()::realloc()");
return;
}
ps->_a = tmp;
}
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
Stackcapacity(ps);//动态申请
ps->_a[ps->_top] = data;
ps->_top++;
}
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
if (ps->_top == 0)
{
printf("栈已空\n");
return;
}
ps->_top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->_top != 0);
return ps->_a[ps->_top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps)
{
assert(ps);
if (ps->_top == 0)
return 1;
else
return 0;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->_a);
ps->_a = NULL;
ps->_top = ps->_capacity = 0;
}
3.3.3 性质
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN) 平均情况下
3. 空间复杂度:O(logN) 递归调用的层数(每层要O(1)的额外空间,存储划分点)
4. 稳定性:不稳定
4、归并排序
4.1 基本思想
将已有序的序列合并,得到完全有序的序列,单个元素可以认为是有序的
若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:
4.2 代码实践
begin和end分别为 指向对应数组首尾元素的下标,是闭的
4.2.1 递归版本
类似于后序,
先"递",分到单个元素(认为单个元素是有序序列),然后归并两个有序序列,memcpy,再"归"
void _MergeSort(int* a, int* tmp, int begin, int end)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin,mid] [mid+1,end]
_MergeSort(a, tmp, begin, mid);
_MergeSort(a, tmp, mid + 1, end);
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;// memcpy要用begin算个数,begin不能变
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++];
// 注意+begin,下标指向的位置的地址 等于 数组名+下标
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
// 归并排序递归实现
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort()::malloc()");
return;
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
return;
}
两个为什么:
为什么要这样分[begin,mid] [mid+1,end],[begin,mid-1],[mid,end]不行吗
两个元素的区间为,[奇数,奇数+1],或者,[偶数,偶数+1],
两种情况一样,下面以[奇数,奇数+1]再分为例
用[begin,mid-1],[mid,end]再分[奇数,奇数+1]
mid = (奇数 + 奇数 + 1)/2 = 奇数,分为两个区间,[奇数,奇数-1]和[奇数,奇数+1],
[奇数,奇数],begin > end,直接return;
[奇数,奇数+1],又会分出,[奇数,奇数-1]和[奇数,奇数+1]……所以一直在"递",最后栈溢出
用[begin,mid] [mid+1,end]再分[奇数,奇数+1]
mid = (奇数 + 奇数 + 1)/2 = 奇数,分为两个区间,[奇数,奇数]和[奇数+1,奇数+1],
[奇数,奇数],begin == end,return;
[奇数+1,奇数+1],begin == end,return;就结束了
为什么每次归并后就要memcpy
因为归并的序列必须是有序序列,单个元素可以认为是有序序列
tmp是把a中的序列进行归并,每次归并成有序序列就copy到a中,就保证下次取a中的序列是有序的
4.2.2 非递归版本
用一个栈模拟,从栈里取出两个区间([begin1,end1],[begin2,end2]),进行归并,可是begin1,end1和begin2,end2已经取出来了,要归并到哪里呢,栈里面没有与之相关的数据了
那么直接在数组上进行操作,类似于层序
引入gap(所要归并的有序序列的元素个数)
归并的时候会出现的情况
// 归并排序非递归实现
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("MergeSort()::malloc()");
return;
}
int gap = 1;// 所要归并的有序序列的元素个数
while (gap < n)
{
// i = 0,指向第一个有序序列的首元素
// i+=2*gap,i跳过了两个gap大小的有序序列,指向下一个有序列的首元素
for (int i = 0; i < n; i += 2 * gap)
{
// 与递归相比,非递归里的i就是递归里的begin
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + gap + gap - 1;
if (begin2 >= n)
break;// [begin2,end2]都越界了,不用归并,也不用memcpy了
if (end2 >= n)
end2 = n - 1;// end2越界了,进行修正
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])// = 为了稳定性,相等的值左边先插入
tmp[j++] = a[begin1++];
else
tmp[j++] = a[begin2++];
}
while (begin1 <= end1)
tmp[j++] = a[begin1++];
while (begin2 <= end2)
tmp[j++] = a[begin2++];
// 注意+begin,下标指向的位置的地址 等于 数组名+下标
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
gap *= 2;
}
free(tmp);
tmp = NULL;
return;
}
4.3 性质
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定
5、计数排序
5.1 基本思想
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
如:
5.2 代码实践
使用相对映射:count[a[i] - min]++;
// 计数排序
// 时间复杂度:O(N+range)
// 只适合整数/适合范围集中,可以排负数,-min,下标不会是负数
// 空间范围度:O(range)
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 1; i < n; ++i)
{
if (a[i] > max)
max = a[i];
if (min > a[i])
min = a[i];
}
int range = max - min + 1;
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("CountSort()::calloc()");
return;
}
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; ++i)
{
while (count[i]--)// 执行count[i]次
{
a[j++] = i + min;
}
}
free(count);
count = NULL;
return;
}
5.3 性质
1.只适合整数/适合范围集中,(可以排负数,因为-min,下标不会是负数)
在某些方面非常快,如排集中的整数
2. 时间复杂度:O(N+range)
3. 空间范围度:O(range)