目录
一.各大排序算法:
排序算法 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 数据对象稳定性 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 稳定 |
选择排序 | O(n2) | O(n2) | O(1) | 数组不稳定、链表稳定 |
插入排序 | O(n2) | O(n2) | O(1) | 稳定 |
快速排序 | O(n*log2n) | O(n2) | O(log2n) | 不稳定 |
堆排序 | O(n*log2n) | O(n*log2n) | O(1) | 不稳定 |
归并排序 | O(n*log2n) | O(n*log2n) | O(n) | 稳定 |
希尔排序 | O(n*log2n) | O(n2) | O(1) | 不稳定 |
计数排序 | O(n+m) | O(n+m) | O(n+m) | 稳定 |
桶排序 | O(n) | O(n) | O(m) | 稳定 |
基数排序 | O(k*n) | O(n2) | O(k+n) | 稳定 |
1.排序的相关概念
(1)排序
所谓排序,就是使一串记录,
按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
(2)稳定性
待排序数据在排序前后相同元素的相对位置是否发生改变
如果改变:该排序不稳定
反之就是稳定的
(3)内部排序
数据元素全部放在内存中的排序。
(4)外部排序
数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
2.常见排序介绍:
(1)插入类排序:
a.直接插入排序:
算法思想
- 从第一个元素开始,该元素可以认为已经被排序。
- 取出下一个元素,在已经排序的元素序列中从后向前扫描。
- 如果该元素(已排序)大于新元素,该元素移到下一个位置。
- 重复步骤3,直到找到已排序的元素小于或者等于新的元素的位置。
- 将元素插入到对应位置。
- 重复2~5。
[1]主要思想:
{1}给定一组序列,默认第一个元素已经有序,从第二个元素开始遍历数组
{2}对于每一个待排序的元素,与已经排好序的元素进行从后向前的比较,
直到找到待插入元素的位置
{3}数组遍历完毕后排序也就完成
[2]代码实现:
//插入排序(升序)
void InsertSort(int array[], int size)
{
for (int i = 1; i < size; i++)//控制循环趟数
{
int key = array[i];//标记待插入元素
int end = i - 1;//end标记已经排好序的序列的最后一个下标
while (end >= 0 && array[end] > key)
{
array[end + 1] = array[end];
end--;
}
array[end + 1] = key;//找到插入元素的位置
}
}
[2]复杂度分析
时间复杂度O(N^2)
两层循环,外层控制循环趟数,循环size-1次,
内层循环寻找插入位置,每个元素要找到自己的位置最差需要遍历已经排好序的所有元素,
因此要循环N次
综上,时间复杂度为O(N^2)
空间复杂度O(1):未借助辅助空间
[3]稳定性 :稳定
插入排序没有出现跨元素交换的情况,因此相同的元素排序前后相对位置不会发生变化
[4]适用场景:元素基本有序或元素数量比较少
b.希尔排序:
算法思想
- 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk = 1。
- 按照增量序列个数k,对序列进行k趟排序
- 每趟排序根据对应的增量ti,将待排序的序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。
[1]主要思想:希尔排序是对直接插入排序的升级
{1}选定一个基准值,将数组下标%基准值相等的元素分为一组
{2}对每一组采用直接插入排序的方法进行排序
{3}对基准值进行更新,循环执行以上两步,直到基准值减小到1停止
{4}此时序列已经有序
[2]注:基准值如何选取?
采用Kunth提出的gap = gap/3 (向下取整)+1
[3]代码实现
//希尔排序
//定义gap,将数组下标%gap相等的元素分为一组
//对每一组采用插入排序的方式进行排序
//对gap每次执行 gap/3 +1 直到gap减小为1
void ShellSort(int array[],int size)
{
int gap = size;
//gap判断条件应该是大于1,因为gap的值始终是>=1的
while (gap > 1)
{
gap = gap / 3 + 1;//gap等于1时,最后一次排序,
排完之后整个数组已经有序
for (int i = gap; i < size; i++)
{
int key = array[i];
int end = i - gap;
while (end>=0 && array[end]>key)
{
array[end + gap] = array[end];
end -= gap;
}
array[end + gap] = key;
}
}
}
[4]复杂度分析
时间复杂度
希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,
因此在好些树中给出的希尔排序的时间复杂度都不固定
在这里gap的计算采用的是Kunth的方法,这样对gap取值,
最终的时间复杂度位于N1.25~1.6*N1.25
空间复杂度
未借助任何辅助空间,所以是O(1)
[5]稳定性 :不稳定
存在跨区间交换元素
[6]适用场景:数据比较随机 数据量比较大
(2)选择类排序
a.选择排序(优化版本)
算法思想
- 在未排序的序列中找到最小(大)元素,存放到排序序列的起始位置。
- 从剩下未排序元素中继续寻找最小(大)元素,然后放到自己已排序的序列的末尾。
- 以此类推,直到所有元素排序完毕。
[1]主要思想:普通版本的选择排序一次只会从中选出一个元素(最大或最小)
优化后的版本,一次可以从带排序数组中选取出两个元素,分别是最大最小元素
具体步骤如下:
{1}以升序为例,遍历数组,找到数组中最大和最小的两个元素
{2}将最大元素与数组末尾元素交换,将最小元素与数组开头元素交换
{3}循环,直到对数组完成一次遍历后,排序结束
[2]代码实现 原始版本+优化版本
//选择排序
static void Swap(int *left, int *right)
{
int temp = *left;
*left = *right;
*right = temp;
}
void SelectSort(int array[], int size)
{
for (int i = 0; i < size-1; i++)
//控制循环趟数 size个元素,需要循环size-1趟
{
int maxPos = 0;
for (int j = 0; j < size - i; j++)
//每一趟遍历数组中未排序元素,更新maxPos的位置
{
if (array[maxPos] < array[j])
{
maxPos = j;
}
}
if (maxPos != size - 1 - i)
{
Swap(&array[maxPos], &array[size - 1 - i]);
}
}
}
//优化版本
void SelectSortOP(int array[], int size)
{
int left = 0;
int right = size - 1;
while (left < right)
{
int maxPos = left;
int minPos = left;
int index = left + 1;
while (index <= right)
{
if (array[maxPos] < array[index])
{
maxPos = index;
}
if (array[minPos]>array[index])
{
minPos = index;
}
index++;
}
if (maxPos != right)
{
Swap(&array[maxPos],&array[right]);
}
if (minPos == right)
{
minPos = maxPos;
}
if (minPos != left)
{
Swap(&array[minPos],&array[left]);
}
left++;
right--;
}
}
[3]复杂度分析
时间复杂度 O(N2)
空间复杂度 O(1)
[4]稳定性:不稳定
选择排序存在跨区间交换
[5]适用场景:基本情况都可以使用,但都不使用(由于时间复杂度的原因)
b.堆排序:
算法思想
- 如果要从小到大排序,建立大堆,根节点大于左右子树。
- 将根结和最后一个元素交换,并且树的元素个数减1。
- 重复1~2,直到只剩一个元素。
[1]主要思想
{1}建堆:将待排序元素按照要求进行建堆操作
升序:建大堆
降序:建小堆
{2}利用堆删除的思想进行排序
将堆顶元素与最后一个元素交换,将堆元素规模-1,
然后对新的堆顶元素采用向下调整算法是其称为新的堆结构
[2]代码实现:
//堆排序
static void HeapAdjust(int array[], int size, int parent)
{
int child = 2 * parent + 1;
//默认标记左孩子(完全二叉树,必定是先有左孩子,再有右孩子)
while (child < size)
{
if (child + 1 < size && array[child + 1] > array[child])
//更新child,让其指向较大的孩子结点
{
child += 1;
}
if (array[child]>array[parent])
//判断较大孩子结点与父节点的大小关系
{
Swap(&array[child], &array[parent]);
//交换后更新parent与child,因为交换可能会导致子树不符合堆的结构
parent = child;
child = 2 * child + 1;
}
else//满足堆的结构
{
return;
}
}
}
void HeapSort(int array[], int size)
{
//1.建立堆(向下调整) 升序--大堆 降序---小堆
for (int root = (size - 2) / 2; root >= 0; root--)
{
HeapAdjust(array, size, root);
}
//2.利用堆删除的思想进行排序
int end = size - 1;
while (end > 0)
{
Swap(&array[0], &array[end]);
HeapAdjust(array, end, 0);
end--;
}
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(1)
[4]稳定性 : 不稳定
[5]适用场景 : Top-K问题
(3)交换类排序
a.冒泡排序
算法思想
- 比较相邻的元素,如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从第一对开始,一直到最后一对,做完后,最后的元素会是最大的元素。
- 针对所有的元素重复上面的步骤,除排序好的。
- 持续对越来越少的元素重复上述步骤,直到哪次没有任何一对数字需要比较或者是交换。
[1]主要思想
以升序为例:
给定一组数据,相邻元素之间两两进行比较,一趟下来,将最大的元素排到了数组最后,
循环size-1趟后,数组整体将会有序。在每一趟循环过程中,定义一个标志量,
用来标记本趟排序是否发生交换,如果发生,说明数组没有完全有序,继续下一趟循环,
如果没有发生改变,说明数组已经有序,可以提前跳出循环
[2]代码实现
//冒泡排序
//两两比较,一趟遍历找到一个元素的最终位置(最大或最小)
void BubbleSort(int array[],int size)
{
for (int i = 0; i < size - 1; i++)
//控制循环趟数,size个元素循环size-1次即可完成排序
{
int isChanged = 0;
//标记一趟遍历中是否有元素交换,若没有交换,说明数组已经有序。
此时直接返回,无需再进入下面的循环
for (int j = 0; j < size - 1 - i; j++)
//将最大元素搬移至数组最后
{
if (array[j]>array[j + 1])
{
isChanged = 1;
Swap(&array[j], &array[j + 1]);
}
}
if (!isChanged)
{
return;
}
}
}
[3]复杂度分析:
时间复杂度:O(N2)
空间复杂度:O(1)
[4]稳定性 :稳定
[5]适用场景:数据接近有序
b.快速排序
算法思想
- 选第一个数为标准。
- 将比基准小的数据交换到前面,比基准大的交换到后面
- 对左边的空间和右边的空间重复,直到各区间只有一个数字
递归版本:
[1]主要思想:
{1}选定一个基准值,
将该组数据小于基准值的划分子在基准值的左侧,大于基准值的划分在基准值的右侧
{2}递归:
使用快排对基准值的左侧进行排序
使用快排对基准值的右侧进行排序
[2]代码实现:
//递归
void QuickSort(int array[], int left, int right )
{
if (right - left > 1)
{
//根据Partion对数组进行划分--->小于div的位于div的左侧,
大于等于div的位于div的右侧
//主要划分方式有 hoare版本 挖坑法 前后指针法
int div = Partion(array, left, right);
QuickSort(array, left, div);
QuickSort(array, div + 1, right);
}
}
[3]划分方法&基准值确定
上述说法中有两个点值得深究:
{1}如何对数据划分
首先,有三种比较常见的划分方式,分别是hoare版本、挖坑法、前后指针法
a.hoare版本
[1]让begin从前向后遍历,找到大于基准值的元素停下来
[2]让end从后向前遍历,找到小于基准值的元素停下来
[3]将begin和end所指向的元素交换
[4]循环1、2、3步,直到begin与end相遇,划分结束
代码展示:
//hoare版本
static int Partion1(int array[], int left, int right)
{
int begin = left;
int end = right - 1;
int mid = GetMidPos(array, left, right);
if (mid != end)
{
Swap(&array[end], &array[mid]);
}
int key = array[end];
while (begin < end)
{
while (begin < end&& array[begin] <= key)
{
begin++;
}
while (begin<end && array[end]>= key)
{
end--;
}
if (begin < end)
{
Swap(&array[begin], &array[end]);
}
}
if (begin != right-1)
{
Swap(&array[begin], &array[right-1]);
}
return begin;
}
挖坑法:
主要思想与hoare版本相似
[1]前提:将基准值保存到key中,此时数组基准值所在位置相当于可以占的“坑位”,
用end标记
a.begin从前向后遍历,找到大于基准值key的元素后停止,
然后将该元素赋值给空着的坑位end,此时,begin所指的位置称为新的坑位
b.end指针从后向前移动,遇到小于基准值key的元素后,将其赋值给新的坑位begin
c.循环执行上述操作,直到begin与end相遇时,
将key值赋值到begin所在位置,一次划分结束
[4]过分过程中的基准值如何选取:
采用三数取中法,即:取数组左右端点以及中间元素三者中值居中的作为基准值
注:一般情况下找到的基准值要是不在数组末尾,
将基准值与末尾元素交换(确保基准值始终在数组的末尾)
非递归版本:
[1]主要思想
{1}与递归的思路基本一致,只不过是采用栈这一数据结构来存储待处理元素的下标范围
{2}先存储右区间,再存储左区间。每个区间先存储右边界,再存储左边界
[2]代码实现
#include "Stack.h"
//非递归
void QuickSortNor(int array[], int size)
{
int begin = 0;
int end = size;
Stack s;
StackInit(&s);
StackPush(&s, end);
StackPush(&s, begin);
while (!StackEmpty(&s))
{
begin = StackTop(&s);
StackPop(&s);
end = StackTop(&s);
StackPop(&s);
if (end - begin > 1)
{
//div将数组分为左右两部分
int div = Partion(array, begin, end);
//按照先处理左边后处理右边的思想将边界下标入栈
//入栈顺序为先入右后入左
StackPush(&s, end);
StackPush(&s, div + 1);
StackPush(&s, div);
StackPush(&s, begin);
}
}
//栈为空,表示排序完成,将栈销毁
StackDestroy(&s);
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(logN)
[4]稳定性 : 不稳定
[5]适用场景:接近有序 | 求数组中第k大(小)的数据
c.归并排序:
算法思想
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
递归:
[1]主要思想:
{1}均分,将待排序数组不断均分,分为左右两半部分,直到每一部分都是有序
(仅有一个元素)
{2}归并。情形:合并两个有序数组
{3}将存储在临时数组中的元素copy至原数组
[2]代码实现:
//归并排序
static void MergeData(int array[], int left, int mid,
int right, int temp[])
{
int begin1 = left;
int end1 = mid;
int begin2 = mid;
int end2 = right;
int index = left;
while (begin1 < end1 && begin2 < end2)
{
if (array[begin1] <= array[begin2])
{
temp[index++] = array[begin1++];
}
else
{
temp[index++] = array[begin2++];
}
}
while (begin1 < end1)
{
temp[index++] = array[begin1++];
}
while (begin2 < end2)
{
temp[index++] = array[begin2++];
}
}
static void _MergeSort(int array[], int left, int right,int temp[])
{
int mid = left + ((right - left) >> 1);
if (right - left > 1)
{
//分解
_MergeSort(array, left, mid,temp);
_MergeSort(array, mid, right,temp);
//合并
MergeData(array,left,mid,right,temp);
memcpy(array + left, temp + left, (right - left)*sizeof(array[0]));
}
}
//递归
void MergeSort(int array[], int size)
{
int* temp = (int*)malloc(sizeof(int)*size);
if (NULL == temp)
{
assert(0);
return;
}
_MergeSort(array, 0, size, temp);
free(temp);
}
非递归:
[1]主要思想:
{1}将待排序数组的每一个元素看做是一个独立的个体,那么他们每个都是有序的
{2}对每一个部分直接归并,
而直接归并分三步,分别是
①采用gap来标记每次每组参与归并的元素个数
②gap从1开始,每归并完成一次,gap*2
③直到gap的值大于数组元素个数size时,说明排序完成
注:随着gap的不断增大,归并函数中边界变量可能会越界,需要做合法性判断
{3}每归并一次,将临时数组中的元素拷贝至原数组
[2]代码实现:
//非递归
void MergeSortNor(int array[], int size)
{
int* temp = (int*)malloc(sizeof(array[0])*size);
if (NULL == temp)
{
assert(0);
return;
}
int gap = 1;
while (gap < size)
{
for (int i = 0; i < size; i+= 2*gap)
{
int left = i;
int mid = left + gap;
int right = mid + gap;
//mid 与right可能会越界,需要进行合法性检验
if (mid > size)
{
mid = size;
}
if (right > size)
{
right = size;
}
//默认从一个元素为一组进行归并
MergeData(array, left, mid, right, temp);
}
//将临时数组temp中的元素copy至array数组
memcpy(array, temp, sizeof(array[0])*size);
gap *= 2;
}
free(temp);
}
[3]复杂度分析
时间复杂度:O(NlogN)
空间复杂度:O(N)
[4]稳定性:稳定
[5]适用场景 :数据量大,无法一次加载到内存 外部排序
d.计数排序:
算法思想
- 找出待排序的数组最大和最小的元素
- 统计数组中每个值为i的元素出现的个数,存入数组c的第i-min项
- 将下标+min的值根据在数组c中的个数存到原数组中。
[1]主要思想:
{1}统计待排序区间中每个元素出现的次数,
{2}通过铺助数组count来保存,即每出现一个待排序元素array[i],
将对应的count[array[i]]加1
{3}循环遍历数组count,数组中的值count[i]表明数据i出现的次数,
数据出现几次,就输出几个i
[2]代码实现
// 计数排序
void CountSort(int* array, int size)
{
//1、确定辅助数组大小
int maxValue = array[0];
int minValue = array[0];
for (int i = 1; i < size; i++)
{
if (array[i]>maxValue)
{
maxValue = array[i];
}
if (array[i] < minValue)
{
minValue = array[i];
}
}
int len = maxValue - minValue + 1;
int *count = (int*)malloc(sizeof(int)*len);
if (NULL == count)
{
assert(0);
return;
}
memset(count, 0, sizeof(int)*len);
//2、遍历待排序数组,统计每个元素出现的次数
for (int i = 0; i < size; i++)
{
count[array[i] - minValue]++;
}
//3、遍历辅助数组,按照数组值输出对应个数的元素
int index = 0;
for (int i = 0; i < len; i++)
{
while (count[i]--)
{
//printf("%d ", i + minValue);
array[index++] = i + minValue;
}
}
printf("\n");
//4、释放辅助空间
free(count);
}
[3]复杂度:
时间复杂度O(N)
空间复杂度O(M),M表示所给数据的范围,即:maxVale-minval
[4]稳定性:稳定
[5]适用场景:数据集中在菜一个数据段
桶排序
算法思想
- 设置一个定量的数组当作空桶;
- 遍历输入数据,并且把数据一个一个放到对应的桶里去;
- 对每个不是空的桶进行排序;
- 从不是空的桶里把排好序的数据拼接起来。
基数排序
算法思想
- 取得数组中的最大数,并取得位数。
- arr为原始数组,从最低位开始取每个位组成radix数组。
- 对radix进行计数排序(利用计数排序适用于小范围数的特点)。