一.插入排序
1.直接插入排序
动图演示
图片演示:
- 每次将数据插入到有序的序列,因为第一个数可以视为有序,所以直接将第二个数插入
- 之后再将第三个数与前两个数排序
- 以此类推
- 当我们将最后一位数字插入后,整组数据就有序了
- 代码如下
//直接插入排序
void InsertSort(int* a, int n)
{
//每次循环都会插入一个新数据
for (int i = 0; i < n - 1; i++)
{
int end = i;//end为被插入数组的最后一位的下标
int tmp = a[end + 1];//tmp用来保存要插入的数据
while (end >= 0)
{
//如果数字大于要插入的数据就要往后移一位
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
//如果数字小于要插入的数据就要跳出循环
else
{
break;
}
}
//将数据插如到end下标的后一位
a[end + 1] = tmp;
}
}
排序特性
1.时间复杂度 O(n2)
最好情况:当序列本身有序时,每次插入数据都是直接插入,只需遍历一次数据,时间复杂度O(n)。
最坏情况:当每次插入新数据时都要遍历一次被插入的序列,时间复杂度为O(n2)。2.空间复杂度:O(1)
空间复杂度为常数值,所以为O(1)。
3.稳定性:稳定
遇到相同的数据直接插入到相同数据后面,不会改变相同数据的前后位置。
2.希尔排序
动图演示
1.希尔排序是直接插入排序的优化。
2.希尔排序分为两部分1.预排序2.直接插入排序
3.希尔排序通过预排序
先将序列中的数据进行较大幅移动,让大数字往后移,小数字往前移。这样最后的直接插入排序
部分就避免了当插入新数据时会出现遍历较多数据的情况
- 先设定一个gap,gap有两层含义,第一层为每组数据的间隔,第二层为要组数的个数。
- 当gap不为1时就是
预排序
,当gap为1时就是直接插入排序
。- 预排序时对每组序列运用
直接插入排序的逻辑
进行排序
假设gap=5
图片演示:
-
缩小gap
-
当gap变为1时,就是
直接插入排序
-
代码如下
//希尔排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap /= 2;//每次缩小gap,当gap为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;
}
}
}
排序特性
1.时间复杂度 O(n1.3)
希尔排序的时间复杂度与gap的大小选择有关,目前还无法准确推算出希尔排序的时间复杂度,只能大概推算出为O(n1.3)。
2.空间复杂度:O(1)
空间复杂度为常数值,所以为O(1)。
3.稳定性:不稳定
在预排序阶段有可能改变相同数值的前后位置,所以不稳定
二.选择排序
1.直接选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)
的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
动图演示:
图片演示:
- 第一次遍历数组找出最大值和最小值,将最大值和最小值和第1位和倒数第1位交换
- 第二次遍历数组,找出最大值和最小值,将最大值和最小值和第二位和倒数第二位交换,之后的以此类推
- 代码如下:
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//直接选择排序
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[mini] > a[i])
{
mini = i;//找出最小值的下标
}
if (a[maxi] < a[i])
{
maxi = i;//找出最大值的下标
}
}
Swap(&a[begin], &a[mini]);
//如果最大值下标在第一位,那么最小值和第一位交换完之后要将maxi赋值给最大值的下标
if (maxi == begin)
{
maxi = mini;//最大值的下标和mini位置
}
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
排序特性
1.时间复杂度 O(n2)
直接选择排序每次都要遍历大量数据
2.空间复杂度:O(1)
空间复杂度为常数值,所以为O(1)。
3.稳定性:不稳定
如果序列两端中有和其他位置相同的值,那么交换时有可能会改变相同值的前后位置,比如
[4,5,6,4,2,8,3] 当2和4交换后,两个4的前后位置就发生了改变,所以不稳定。
2.堆排序
动图演示:
图片来源于网络,侵删
堆排序是指利用堆这种数据结构
所设计的一种排序算法,这里我们以升序为例
堆排序分为两步:1.建堆 2.利用堆进行排序
我们先来看第一步:建堆
-
对于一个数组我们可以将其看成一个
完全二叉树
。
-
因为我们要排升序,所以我们需要将这颗树改为
大根堆
。大根堆就是每个节点的值都大于或等于其子节点的值。要将树改为大根堆可以用向下调整
,并且从最后一个节点的父亲开始。
我们再来看第二步:利用堆进行排序
- 将
堆顶的元素
和最后一位
进行交换,这样最大的数就跑到了最后一位,然后通过对堆顶
向下调整保持剩下元素
为大堆
- 代码如下:
//交换函数
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整函数
void AdjustDown(int* a, int n, int parent)//parent为要向下调整元素的下标
{
int child = parent * 2 + 1;//先设定child为左孩子节点
while (child < n)
{
//如果右孩子节点比左孩子节点大,则将child改为右孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
// 如果孩子节点大于父亲节点,则和父亲交换
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
// 如果孩子节点小于父亲节点,则向下调整结束
else
{
break;
}
}
}
//堆排序 O(N*logN)
void HeapSort(int* a, int n)
{
//第一步:先建堆,因为排升序,所以建大根堆。O(n)
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//第二步:利用堆进行排序。O(N*logN)
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);//交换堆的首尾
AdjustDown(a, end, 0);//堆顶向下调整
--end;//将已经排好的元素排除在堆外
}
}
排序特性
1.时间复杂度 O(n*logn)
第一步建堆过程的时间复杂度为O(n),第二步用堆排序过程的时间复杂度为 O(nlogn),所以总和可以看作 O(nlogn)
2.空间复杂度:O(1)
空间复杂度为常数值,所以为O(1)。
3.稳定性:不稳定
建堆过程和堆排序过程都有可能改变相同大小元素的前后位置,所以不稳定。
三.交换排序
1.冒泡排序
动图演示:
基本思想:通过不断比较相邻元素,让最大的数往后移。
- 代码如下:
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
int exchange = 0;
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;//这里通过一个变量,可以优化冒泡
}
}
//如果exchange==0就说明序列已经全部有序,退出循环
if (exchange == 0)
{
break;
}
}
}
2.快速排序
快速排序有三种方法:1.霍尔法,2.挖坑法,3.前后指针法
霍尔法:
动图演示:
基本思想:
-
先设定第一位数为key
-
然后先右指针往左,碰到比key值小的停下,再左指针往右走,碰到比key大的值停下
-
将他们交换
-
然后先右指针继续往左,碰到比key值小的停下,再左指针往右走,碰到比key大的值停下,当他们相遇时停下
-
将相遇位置与key值交换。
交换完后,key值左边的数都比key小,右边的数都比key大
-
然后对key的左区间和右区间重复上面的步骤,直到有序
-
代码如下:
//直接插入排序
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 (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
//三个数中取中等大小的值
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[end] > a[begin])
{
return begin;
}
else
{
return end;
}
}
}
//霍尔法
int PartSort1(int* a, int begin, int end)
{
//通过三数取中可以减少对于已经有序的序列过多的递归调用
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin;
int right = end;
int keyi = left;//keyi为序列第一位的下标
while (left < right)
{
//右指针先走,找比a[keyi]小的数
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左指针再走,找比a[keyi]大的数
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//将指针相遇位置与a[keyi]交换
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
//如果区间只有一个数或不存在那就不用排序,直接返回
if (begin >= end)
{
return;
}
//当左右区间小于一定值时可以直接用插入排序,避免过多的递归调用
if ((end - begin + 1) < 15)//小区间优化
{
//用插入排序减少递归调用
InsertSort(a + begin, end - begin + 1);
}
else
{
//对当前序列使用霍尔法,并且返回key的下标
int keyi = PartSort1(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end],对左右区间递归排序
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
挖坑法:
动图演示:
基本思想:
-
先将第一位保存到key,并且形成坑位
-
然后右指针先往左走直到碰到比key值小的数停下
-
将右指针指向的数填到坑位上,并且坑位转移到右指针所在位置
-
左指针再往右走直到碰到比key值大的数停下
-
将左指针指向的数填到坑位上,并且坑位转移到左指针所在位置
-
重复2到5步骤,直到左右指针相遇时
-
将key值填到相遇位置
-
分别对相遇位置的
左右区间
重复上面步骤1到7
- 代码如下:
//直接插入排序
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 (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
//三个数中取中等大小的值
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[end] > a[begin])
{
return begin;
}
else
{
return end;
}
}
}
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin, right = end;
int key = a[left];//将第一个数保存到key
int hole = left;//第一个位置为坑位
while (left < right)
{
//右指针先走,找小于key的值
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];//将右指针指向的值填到坑位
hole = right;//坑位转移到右指针所在位置
//左指针再走,找大于key的值
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];//将左指针指向的值填到坑位
hole = left;//坑位转移到左指针所在位置
}
a[hole] = key;//将key值填到指针相遇的地方,指针一定在坑位相遇
return hole;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
//如果区间只有一个数或不存在那就不用排序,直接返回
if (begin >= end)
{
return;
}
//当左右区间小于一定值时可以直接用插入排序,避免过多的递归调用
if ((end - begin + 1) < 15)//小区间优化
{
//用插入排序减少递归调用
InsertSort(a + begin, end - begin + 1);
}
else
{
//对当前序列使用挖坑法,并且返回key的下标
int keyi = PartSort2(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end],对左右区间递归排序
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
前后指针法:
动图演示:
基本思想:
-
让key等于第一个位的下标 ,prev指向第一位,cur指向第二位
-
cur从第二位开始往后走,直到碰到比key对应值小的数
-
prev往前走一步
-
交换prev和cur对应值
-
重复上面2到4步骤
-
当cur超出范围时,交换key和prev所对应值,这时prev对应值的左区间都比它小,右区间都比它大
- 代码如下:
//直接插入排序
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 (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
//三个数中取中等大小的值
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[end] > a[begin])
{
return begin;
}
else
{
return end;
}
}
}
//前后指针法
int PartSort3(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int prev = begin;//prev指向第一位
int cur = begin + 1;//cur指向第二位
int keyi = begin;//key指向第一位
while (cur <= end)
{
// 找到比key小的值时,跟++prev位置交换
// 当++prev==cur时可以不交换
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);//交换key和prev
keyi = prev;
return keyi;
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
//如果区间只有一个数或不存在那就不用排序,直接返回
if (begin >= end)
{
return;
}
//当左右区间小于一定值时可以直接用插入排序,避免过多的递归调用
if ((end - begin + 1) < 15)//小区间优化
{
//用插入排序减少递归调用
InsertSort(a + begin, end - begin + 1);
}
else
{
//对当前序列使用挖坑法,并且返回key的下标
int keyi = PartSort2(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end],对左右区间递归排序
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
快速排序的非递归代码如下:
这里使用栈
来模拟二叉树的前序遍历
//三个数中取中等大小的值
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[end] > a[begin])
{
return begin;
}
else
{
return end;
}
}
}
//霍尔法
int PartSort1(int* a, int begin, int end)
{
//通过三数取中可以减少对于已经有序的序列过多的递归调用
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin;
int right = end;
int keyi = left;//keyi为序列第一位的下标
while (left < right)
{
//右指针先走,找比a[keyi]小的数
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左指针再走,找比a[keyi]大的数
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//将指针相遇位置与a[keyi]交换
Swap(&a[keyi], &a[left]);
keyi = left;
return keyi;
}
//快速排序的非递归
void QuickSortNonR(int* a, 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 = PartSort1(a, left, right);//对当前序列使用霍尔排序
// [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.时间复杂度 O(n*logn)
2.空间复杂度:O(logn)
由于递归会消耗函数栈帧,函数栈帧最多同时存在O(logn)。
3.稳定性:不稳定
不管那种排序都有可能改变相同大小元素的前后位置,所以不稳定。
四.归并排序
动图演示:
基本思想:先让子序列有序,再将有序的子序列
合并成有序的序列
- 递归代码如下:
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//递归截止条件,如果区间只有一个数或不存在就返回
if (begin >= end)
{
return;
}
int mid = (begin + end) / 2;
//类似二叉树后续遍历
// [begin, mid] [mid+1, end] 递归让子区间有序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
// 归并[begin, mid] [mid+1, end]
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin;//额外空间下标
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] > a[begin2])
{
tmp[i++] = a[begin2++];
}
else
{
tmp[i++] = a[begin1++];
}
}
//哪个序列没归并完直接全部归并
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//拷贝返回原数组
memcpy(a + begin, tmp + begin, sizeof(int)*(end-begin+1));
}
//归并排序
void MergeSort(int* a, int n)
{
//用一个额外空间暂时存储合并数据
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("mallco fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
- 非递归代码如下:
//归并排序非递归
void MergeSortNonR(int* a, int n)
{
//用一个额外空间暂时存储合并数据
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("mallco fail\n");
exit(-1);
}
int rangeN = 1;//归并两组中每组的数据个数
while (rangeN<n)
{
for (int i = 0; i < n; i += rangeN * 2)
{
// [begin1,end1][begin2,end2] 归并
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + rangeN * 2 - 1;
int j = i;
// end1 begin2 end2 越界问题
// 修正区间 归并完了整体拷贝 or 归并每组拷贝都可以
if (end1 >= n)//end1越界,begin2和end2肯定也越界
{
end1 = n - 1;
// 改为不存在区间,为了下面归并逻辑的进行,否则数组会越界访问
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)//begin2越界,end2肯定也越界
{
// 改为不存在区间,为了下面归并逻辑的进行
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)//end2越界
{
end2 = n - 1;
}
// 归并[begin1, end1] [begin2, end2]
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] > a[begin2])
{
tmp[j++] = a[begin2++];
}
else
{
tmp[j++] = a[begin1++];
}
}
//哪个序列没归并完直接全部归并
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
}
//整体归并再拷贝
memcpy(a, tmp, sizeof(int) * n);
rangeN *= 2;//增加每组归并个数
}
free(tmp);
tmp = NULL;
}
排序特性
1.时间复杂度 O(n*logn)
类似二叉树的后续遍历
2.空间复杂度:O(n)
需要额外空间暂时存储合并数据
3.稳定性:稳定
归并不会改变相同大小元素的前后位置,所以稳定。
五.计数排序
动图演示:
图片来源网络,侵删。
基本思想:
-
先将要排序序列的
最大值和最小值
找出,用max-min+1
开辟出需要的计数数组count的大小。
-
遍历序列,将
序列的值减去序列中的最小值
之后找到对应的count数组的下标然后++。
-
将count数组中大小不为0的下标加上最小值后依次填到原数组中。
-
代码如下:
//计数排序
void CountSort(int* a, int n)
{
//找出最大和最小值
int max = a[0];
int min = 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;//计算要开辟的计数数组countA大小
int* countA = (int*)calloc(range, sizeof(int));
if (countA == NULL)
{
perror("calloc fail");
exit(-1);
}
//遍历序列,将序列的值减去序列中的最小值之后找到对应的count数组的下标然后++。
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
//将count数组中大小不为0的下标加上最小值后依次填到原数组中。
int j = 0;
for (int i = 0; i < range; i++)
{
while (countA[i]--)
{
a[j++] = i + min;
}
}
free(countA);
}
排序特性
1.时间复杂度 O(n+最大值与最小值之间范围)
数据越集中,计数排序效率越高
2.空间复杂度:O(最大值与最小值之间范围)
需要开辟数组来计数
3.稳定性:稳定
计数不会改变相同大小元素的前后位置,所以稳定。
六.排序总结对比
通过了解不同排序的时间和空间复杂度已经稳定性,我们可以依据不同情况选择可以满足我们需求的排序
最后觉的本文好的话记得点赞加收藏哦!