需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。
目录
一、常见排序算法
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 | ||
平均 | 最好 | 最坏 | |||||
插入排序 | 直接插入排序 | O( | O(N)有序场景 | O( | O(1) | 稳定 | 数据有序或接近有序, 时间O( |
希尔排序 | O( | O(N)有序场景 | O( | O(1) | 不稳定 | 插入排序怕大量无序不重复数据,希尔是插入的改良 | |
选择排序 | 直接选择排序 | O( | ~~ | ~~ | O(1) | 不稳定例如89855 | 最差排序 |
堆排序 | O(N*logN) | ~~ | ~~ | O(1) | 不稳定 | 1、首次建堆可以用于找最大/最小。2、top k问题 | |
交换排序 | 冒泡排序 | O( | ~~ | ~~ | O(1) | 稳定 | ”那你先写个冒泡排序吧“ |
快速排序 | O(N*logN) | ~~ | O( | O(logN) | 不稳定 | 官方库采用的排序算法,遇事不决用快排 | |
归并 | 归并排序 | O(N*logN) | ~~ | ~~ | O(N) | 稳定 | 外排序 |
非比较排序 | 计数排序 | O(N+range) | ~~ | ~~ | O(range) | 稳定 | 适合排序数据范围小的数据,不能排序浮点数 |
二、直接插入排序
1、直接插入排序思想
扑克牌玩过吧,扑克牌抽牌,边抽边排的思想。
虽然直接插入排序是一个的算法,但可以认为它是所有算法中最优的。当直接插入排序遇到重复数据较多、有序、接近有序的数据时,时间复杂度将会降为O(
)。
直接插入排序的缺点就是时间复杂度过大,针对大量无序不重复数据,排序吃力,所以就有了希尔排序。
2、直接插入排序代码
void InsertSort(int* arr, int size)//插入排序O(N^2)
{
for (int i = 0; i < size - 1; ++i)
{
int end = i;//end是最后一个有序数字的下标
int tmp = arr[end + 1];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + 1] = arr[end];
--end;
}
else
break;
}
arr[end + 1] = tmp;//最后把tmp放到数组end+1位置处
}
}
二、希尔排序
1、希尔排序思想
希尔排序是直接插入排序的改进,gap可以除三加一,也可以除二。每次循环gap是变化的。
gap大于1,称为预排序。希尔排序的预排序是将gap间隔的数据分为一组,进行直接插入排序,让数组接近有序。
gap越大,数组内的数字跳的越快,但排完还不是有序。gap越小,数字跳的越慢,但排完越接近有序。希尔排序gap先大后小。
在数据均不相等的情况下,数据较少,希尔排序优于O(),反之,希尔排序劣于O(
)
2、希尔排序代码
void ShellSort(int* arr, int size)//希尔排序(N^1.3)
{
int gap = size;
while (gap > 1)//gap等于1时说明已经有序,退出循环
{
gap = gap / 3 + 1;
for (int i = 0; i < size - gap; ++i)
{
int end=i;//end是数组最后一个元素的下标
int tmp = arr[end + gap];
while (end >= 0)
{
if (arr[end] > tmp)
{
arr[end + gap] = arr[end];
end -= gap;
}
else
break;
}
arr[end + gap] = tmp;
}
}
}
三、选择排序
1、选择排序思想
以升序为例,选择排序每次遍历数组,选出最大的那个数。可以认为它是最菜的排序算法。
以下代码为选择排序的优化,遍历一次选出最大和最小的数字。
2、选择排序的代码
void CelectSort(int* arr, int size)//选择排序
{
int begin = 0, end = size - 1;
while (begin < end)
{
int min = begin, max = begin;
for (int i = begin+1; i <=end; ++i)
{
if (arr[i] > arr[max])
max = i;
if (arr[i] < arr[min])
min = i;
}
if (max == begin)
{
max = min;
}
Swap(&arr[begin], &arr[min]);
Swap(&arr[end], &arr[max]);
++begin;
--end;
}
}
注意:一趟遍历选出最大和最小的写法,需要考虑max==begin的情况。防止max在初始位置,但该点的值被min换走的情况。
四、堆排序
1、堆排序思想
1、以升序为例:先找到最后一个非叶节点,采用向下调整算法,再找到倒数第二个非叶节点,重复向下调整算法,把一个数组建成堆。这个时候,堆顶就是数组中最大的数。该步时间复杂度O(N)
动图如下:
2、将数组调整成大堆后,将堆顶数据(最大的数)和堆底数据进行互换,锁定这个最大的数。这个时候原有的堆形态被破坏,需要重新使用向下调整算法对这个数组重建堆。迭代锁定。该步时间复杂度O(N*logN)
动图如下:
2、堆排序代码
void Adjustdown(int* arr, int size, int parent)//向下调整算法
{
int maxChild = 2 * parent + 1;
while (maxChild<size)
{
if (maxChild + 1 < size && arr[maxChild] < arr[maxChild + 1])
{
++maxChild;
}
if (arr[maxChild] > arr[parent])
{
Swap(&arr[maxChild], &arr[parent]);
}
parent = maxChild;
maxChild = 2 * parent + 1;
}
}
void HeapSort(int* arr, int size)//堆排序O(N*logN)
{
//初始化建堆O(N)
for (int i = (size - 1 - 1) / 2; i >= 0; --i)
{
Adjustdown(arr, size, i);
}
//重建堆O(N*logN)
for (int i = 0; i < size - 1; ++i)
{
Swap(&arr[0], &arr[size - 1 - i]);
Adjustdown(arr, size-1-i, 0);
}
}
五、冒泡排序
1、冒泡排序思想
一对一对地交换
2、冒泡排序代码
void BubbleSort(int* arr, int size)//冒泡排序(N^2)
{
for (int i = 0; i < size - 1; ++i)//趟数
{
int flag = 0;
for (int j = 0; j < size - 1 - i; ++j)
{
if (arr[j] > arr[j + 1])
{
Swap(&arr[j], &arr[j + 1]);
flag = 1;
}
}
if (flag == 0)
break;
}
}
立个flag,当flag没变,说明一趟走完没有进行两两交换,数组已经有序,跳出循环结束排序。
六、快速排序
1、hoare版
1.1hoare版单趟排序动图
1.2代码
//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
int keyi = left;//选left做key,keyi是下标
while (left < right)
{
//我们选了left做key,那么右边先走。反之左边先走。
while (left < right && arr[right] >= arr[keyi])//R找小
{
--right;
}
while (left < right && arr[left] <= arr[keyi])//L找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
int meet = left;//meet是left和right的相遇点下标
Swap(&arr[meet], &arr[keyi]);
return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
if (begin >= end)
return;
int keyi = PartSort(arr, begin, end);
//[begin,keyi-1] key [keyi+1,end]
QuickSort(arr, begin, keyi - 1);//快排
QuickSort(arr, keyi + 1, end);//快排
}
1.3思想
1、PartSort是单趟排序,我们先在数组中选择一个位置做key,一般是数组开头或末尾那个位置。
上面代码中,是用数组首元素做key,为了保证小人相遇位置的值小于key,必须让右边的小人先行动。如果用数组末尾做key,那么需要左边的小人先走。
右边小人一直走,遇到比key小的数则停下,左边小人再走,遇到比key大的数停下。再交换左右小人脚下的数字。交换完成后一直重复此步骤,直到左右小人相遇。
将相遇点与keyi位置交换(注意我们是要改变数组,所以需要交换相遇点与keyi位置的值,而不是交换相遇点与key的值)
单趟排序完成后,keyi左边的值都比key小,keyi右边的值都比key大。
2、QuickSort中利用二叉树的递归,完成排序。
该方法存在缺陷:
1、递归层数过多有爆栈风险 2、面对有序或者接近有序的待排序数据,时间复杂度就变成了O()
所以需要作如下优化:
1.4三数取中,优化选key
1、随机选key(听着就很随机,虽然不靠谱,但有的场景还是可以使用随即选key的方法)
2、针对有序情况,选正中间数据做key(前提是知道有序)
3、三数取中(选出左中右三数中间大小的做key)(三数取中后,对于缺陷2,直接由最坏情况变成最好情况)
三数取中代码实现:
int GetMidIndex(int* arr, int left, int right)//三数取中
{
int mid = left + (right-left) / 2;
if (arr[left] >= arr[right])
{
if (arr[left] > arr[mid])
{
if (arr[mid] >= arr[right])
return mid;
else
return right;
}
else
return left;
}
else
{
if (arr[right] > arr[mid])
{
if (arr[mid] >= arr[left])
return mid;
else
return left;
}
else
return right;
}
}
//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[left]);
int keyi = left;//选left做key,keyi是下标
while (left < right)
{
//我们选了left做key,那么右边先走。反之左边先走。
while (left < right && arr[right] >= arr[keyi])//R找小
{
--right;
}
while (left < right && arr[left] <= arr[keyi])//L找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
int meet = left;//meet是left和right的相遇点下标
Swap(&arr[meet], &arr[keyi]);
return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
if (begin >= end)
return;
int keyi = PartSort(arr, begin, end);
//[begin,keyi-1] key [keyi+1,end]
QuickSort(arr, begin, keyi - 1);//快排
QuickSort(arr, keyi + 1, end);//快排
}
三数取中后,我们继续进行下一个优化:
1.5小区间优化,减少递归次数
我们知道。二叉树的最后一层节点个数近乎占整棵树节点的一半,倒数第二层占25%,倒数第三层占12.5%。也就是说,后三层就占了整颗二叉树节点的87.5%。
那么,我们可以在递归时加一个限制条件:当递归时,区间剩余元素少于8个时(可自定义剩余个数),改为使用插入排序来排序这些小区间。
hoare版本最终代码:
int GetMidIndex(int* arr, int left, int right)//三数取中
{
int mid = left + (right-left) / 2;
if (arr[left] >= arr[right])
{
if (arr[left] > arr[mid])
{
if (arr[mid] >= arr[right])
return mid;
else
return right;
}
else
return left;
}
else
{
if (arr[right] > arr[mid])
{
if (arr[mid] >= arr[left])
return mid;
else
return left;
}
else
return right;
}
}
//[left,right]
int PartSort(int* arr, int left, int right)//单趟排完不是有序,只是key左边比key小,右边比key大
{
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[left]);
int keyi = left;//选left做key,keyi是下标
while (left < right)
{
//我们选了left做key,那么右边先走。反之左边先走。
while (left < right && arr[right] >= arr[keyi])//R找小
{
--right;
}
while (left < right && arr[left] <= arr[keyi])//L找大
{
++left;
}
Swap(&arr[left], &arr[right]);
}
int meet = left;//meet是left和right的相遇点下标
Swap(&arr[meet], &arr[keyi]);
return meet;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//递归快排
{
if (begin >= end)
return;
if (end - begin <= 8)//小区间优化
{
InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
return;
}
else
{
int keyi = PartSort3(arr, begin, end);
//[begin,keyi-1] key [keyi+1,end]
QuickSort(arr, begin, keyi - 1);//快排
QuickSort(arr, keyi + 1, end);//快排
}
}
2、挖坑法
2.1挖坑法单趟排序动图
2.2挖坑法思想
三数取中后,我们把数组最左边当成坑,并记录这个位置的值key(动态中坑的初始下标是0,key是6)
同样的,右边小人先走,找到比key小的数字停下来,将脚下位置的数字填入坑中,同时,脚下变成了新的坑。
左边小人再行动,找到比key大的数字停下来,将脚下位置的数字填入坑中,同时,脚下变成新的坑。
循环上述过程,直到两小人相遇。相遇时,脚下必定是坑,那么把刚开始的key填入这个坑中。完成了单趟排序。
挖坑法同样可以适用于三数取中和小区间优化。
2.3挖坑法代码
//挖坑法
int PartSort(int* arr, int left, int right)
{
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[left]);
int key = arr[left];//选left做key,key是值
int hole = left;//坑
while (left < right)
{
while (left < right && arr[right] >= key)//右边找小,填到左边坑
{
--right;
}
arr[hole] = arr[right];
hole = right;
while (left < right && arr[left] <= key)//左边找小,填到右边坑
{
++left;
}
arr[hole] = arr[left];
hole = left;
}
arr[hole] = key;//最后把key放到坑里
return hole;//返回最后的坑位
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//递归快排
{
if (begin >= end)
return;
if (end - begin <= 8)//小区间优化
{
InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
return;
}
else
{
int keyi = PartSort3(arr, begin, end);
//[begin,keyi-1] key [keyi+1,end]
QuickSort(arr, begin, keyi - 1);//快排
QuickSort(arr, keyi + 1, end);//快排
}
}
3、前后指针法
3.1前后指针法单趟排序动图
3.2前后指针法思想
prev指向初始位置,cur指向prev的下一个位置
prev先走,cur后走,当cur遇到比key大的值,prev停下,cur继续走,直到cur遇到比key小的数,prev再往前走,prev向前走完后,交换cur与prev的值
循环上述过程,直到cur>right,交换keyi和prev的值
动图看起来就像是prev在后面推着key大的数字往前走,当然该方法也要用三数取中和小区间优化。
3.3前后指针法代码
int PartSort(int* arr, int left, int right)
{
int mid = GetMidIndex(arr, left, right);
Swap(&arr[mid], &arr[left]);
int keyi = left;//选left做key,keyi是下标
int prev = left, cur = prev + 1;
while (cur <= right)
{
if (arr[cur] < arr[keyi] && ++prev!=cur)
{
Swap(&arr[cur], &arr[prev]);
}
++cur;
}
Swap(&arr[prev], &arr[keyi]);
return prev;
}
//[begin,end]
void QuickSort(int* arr, int begin,int end)//快排
{
if (begin >= end)
return;
int keyi = PartSort(arr, begin, end);
//[begin,keyi-1] key [keyi+1,end]
if (end - begin <= 8)//小区间优化
{
InsertSort(arr+begin, end -begin + 1);//插入排序O(N^2)
return;
}
else
{
QuickSort(arr, begin, keyi - 1);//快排
QuickSort(arr, keyi + 1, end);//快排
}
}
4、快排的非递归方法
4.1非递归的思想
从递归的方法可以看出,在第一次选出keyi时,可以对左右区间进行递归操作,重复选出keyi,不断对小区间进行排序,直至有序。同样的,非递归方法使用栈来模拟递归的过程,每次将左右区间的下标压入栈中,就执行了一次该区间的单趟排序。直到栈空,说明数组已经有序。
4.2非递归代码
void NonFQuickSort(int* arr, int begin, int end)//非递归快排,使用栈
{
ST 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 keyi = PartSort3(arr, left, right);//keyi是下标,单趟排序
//[left,keyi-1] keyi [keyi+1,right]
if (keyi + 1 < right)
{
StackPush(&st, keyi + 1);//压栈
StackPush(&st, right);//压栈
}
if (left < keyi - 1)
{
StackPush(&st, left);//压栈
StackPush(&st, keyi - 1);//压栈
}
}
StackDestroy(&st);//销毁
}
七、归并排序
1、归并排序的递归方法
1.1归并排序的思想
现在想对一组无序数组排序,那么将这个数组均分成左右两个部分,只要左区间有序,右区间有序,那就可以借助额外的空间,将左右两个数组中小的数不断尾插,形成有序,最后将额外空间中的数据拷贝回原数组即可。那么左右区间怎么才能有序呢?通过递归的方法,对左右区间不断均分至区间内仅剩一个数,开始归并。
归并的缺点在于需要额外O(N)的空间,常用于解决磁盘的外排序。
1.2归并排序代码
void _MergeSort(int* arr, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = begin + (end - begin) / 2;//将左右区间二分
//[begin,mid][mid+1,end]
_MergeSort(arr, begin, mid, tmp);
_MergeSort(arr, mid+1, end, tmp);
//升序归并,取小的尾插,重新定义起始位置,防止begin和end被改变
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//左右区间必定有一个还没有尾插完
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//将tmp部分区域数据拷贝回原数组
memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
void MergeSort(int* arr, int size)
{
int* tmp = (int*)malloc(sizeof(int) * size);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(arr, 0, size - 1, tmp);
free(tmp);
}
2、归并排序的非递归
2.1归并排序的非递归思想
因为递归版本是将数组内元素不断均分,直至区间内仅剩一个元素时,开始归并。那么非递归版本直接一一归并···两两归并····四四归并······直至整体有序。注意分类讨论边界问题:begin1是不可能越界的,当end1和begin2越界时,直接break即可;当end2越界时,修正end2=size-1继续进行归并。
2.2归并排序的非递归代码
void MergeSortNonR(int* arr, int size)
{
int* tmp = (int*)malloc(sizeof(int) * size);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
int gap = 1;
while (gap < size)
{
for (int j = 0; j < size; j += 2 * gap)
{
int begin1 = j, end1 = j + gap - 1;
int begin2 = j + gap, end2 = j + 2 * gap - 1;
//判断边界
if (end1 >= size)
{
break;
}
if (begin2 >= size)
{
break;
}
if (end2 >= size)//修正end2,继续归并
{
end2 = size - 1;
}
int i = j;
while (begin1 <= end1 && begin2 <= end2)
{
if (arr[begin1] <= arr[begin2])
{
tmp[i++] = arr[begin1++];
}
else
{
tmp[i++] = arr[begin2++];
}
}
//还剩一个区间没有尾插完
while (begin1 <= end1)
{
tmp[i++] = arr[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = arr[begin2++];
}
//将tmp数组中的数据拷贝回原数组
memcpy(arr + j, tmp + j, sizeof(int) *(end2-j+1));//end2-j-1千万不可以写成2*gap,因为当end2被修正时没救不等于2*gap了
}
gap *= 2;;
}
free(tmp);
tmp = NULL;
}
八、计数排序
1、计数排序的思想
计数排序适用于排序数组极差小的数据。此时range忽略,时间复杂度O(N)的话,也是一个非常强大的排序。
1、找出数组中的最大值和最小值,求出range=max-min+1
2、开辟range个空间并对这块区域初始化为0
3、通过相对映射对每个数出现的频次计数(同时数据已经被排序)
4、根据频次覆盖写入数据至原数组
5、free临时空间
2、计数排序代码
void CountSort(int* arr, int size)//计数排序,时间O(N+range),空间O(range)
{
int min = arr[0], max = arr[0];
//遍历找出最大、最小
for (int i = 0; i < size; ++i)
{
if (arr[i] < min)
min = arr[i];
if (arr[i] > max)
max = arr[i];
}
//malloc一个相对映射数组
int range = max - min + 1;//malloc数组的元素个数
int* Count = (int*)malloc(sizeof(int) * range);
if (Count == NULL)
{
perror("malloc fail:");
return;
}
memset(Count,0, sizeof(int) * range);
//相对映射计数
for (int i = 0; i < size; ++i)
{
++Count[arr[i] - min];
}
//将映射数据覆盖至原数组
int j = 0;
for (int i = 0; i < range; ++i)
{
while (Count[i]--)
{
arr[j++] = i + min;
}
}
//释放malloc的空间
free(Count);
}