排序,这个看似“平平无奇”的操作。在人面对这些一串又一串的枯燥无味的数据时,只能各凭经验和运气,且但数据量过大时,人力就显得如此可笑,此时人们想到了计算机,但计算机面对时,对于不同性质的数据(例如数据个数的量级、原本的数据的顺序更加接近升序、降序、无序)、不同需求、不同环境时的排序,也需要程序员们写出相对于更好的排序算法。
排序定义:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,ri=rj,且ri在rj之前,而在排序后的序列中,ri仍在rj之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。(一般来说内排序,就已经满足大部分的需求)
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。(例如在磁盘(文件)内排序)
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
举一个生活的例子:打牌,在玩家拿牌时,拿一张插一张,直到拿完,也就排完序了。
插入排序图解
// 插入排序(升序排)
void InsertSort(int* a, int n)
{
for (int i = 0;i < n - 1;i++)
{
//在end + 1处插入
int end = i;
int tmpe = a[end + 1];
while (end >= 0)
{
//插入值小于当前值,前移
if (tmpe < a[end])
{
a[end + 1] = a[end];
end--;
}
//插入值大于当前值
else
{
break;
}
}
a[end + 1] = tmpe;
}
}
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)gap
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
希尔排序法又称缩小增量法。可以理解为插入排序的优化
思想是:先选定一个整数n,所有距离为n的记录分在同一组内,把待排序文件中所有记录分成多个个组,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达n=1时,所有记录在统一组内排好序。
希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。 2. 当gap > 1时都是预排序,目的是让数组更接近于有序(本质上希尔排序也是插入排序,所以越有序,排的也越快)。当gap == 1时(不分组了此时),数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。 3. 希尔排序的时间复杂度不好计算,因为gap的取值方法(一般gap = gap/3 + 1,或者取 2 也行)很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:
《数据结构(C语言版)》--- 严蔚敏:
《数据结构-用面相对象方法与C++描述》--- 殷人昆:
省流小结:时间复杂度约等于n^1.3
4.稳定性:不稳定。
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
2.2.2 直接选择排序
1.在元素集合arrayi--arrayn-1中选择关键码最大(小)的数据元素
2.若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
3.在剩余的arrayi--arrayn-2(arrayi+1--arrayn-1)集合中,重复上述步骤,直到集合剩余1个元素
直接选择排序
void Swap(int* a, int* b)
{
int tmpe = *a;
*a = *b;
*a = tmpe;
}
//选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
int maxi = begin;
int mini = n - 1;
while (begin < end)
{
for (int i = begin + 1; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
if (maxi == begin)
maxi = mini;
Swap(&a[end], &a[maxi]);
}
begin++;
end--;
}
直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用(太捞了) 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:不稳定
https://blog.csdn.net/qq_68140277/article/details/126678992
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。
思想:由于堆的本身性质,一次排堆,只能选出数据的最值所以我们采用递归来多次排堆。在每次排堆中,将排好的堆的根(最值)与末位交换(堆排时是先确定末尾的数),在下一次排堆时,并将尾上的已经排好堆的数,不列入下一次排堆的范围内,即堆的大小减一。如此递归,直到所有数排好。
特别的:
1.排升序要建大堆,排降序建小堆。升序顺序中,越后数越大,堆排时是先确定末尾的数,大堆的根,降序同理。
2.在建堆和递归调整堆时,采用向下调整法更好(向上也可跑,但是效率低点),向下调整时,是以一颗树为单位来调整的,即包含左右子树和父节点,这样的排序直接导致叶子节点的数据可以不用排,而是从倒数其第二排(这个时候才有父节点)开始排的,且由于堆为完全二叉树,由于二叉树本身的结构,每一层节点个数是以指数增加,所以导致最后一排节点个数接近总节点数的一半。向上调整则需要全部排序。
详情解释可查找上文链接处查看
void AdjustDown(int* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出小的那个孩子
if (minChild + 1 < n && a[minChild + 1] > a[minChild])
{
minChild++;
}
if (a[minChild] > a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = 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);
}
// 选数 N*logN
int i = 1;
while (i < n)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
++i;
}
}
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,
交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
典中典的它来了,冒泡是我们最早接触的排序算法
思想:从头开始,依次两两对比并交换,依次向后。单趟,能确定末尾的数。
void BubbleSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int flag = 1;
for (int j = 0; j < n - 1 - i; j++)
{
if (a[j] < a[j + 1])
{
int tmpe = a[j];
a[j] = a[j + 1];
a[j + 1] = tmpe;
flag = 0;
}
}
if (flag)
break;
}
}
冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序 2. 时间复杂度:O(N^2) 3. 空间复杂度:O(1) 4. 稳定性:稳定
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法
基本思想:任取待排序元素序列中的某元素作为基准值(key),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
1. hoare版本
// 快速排序hoare版本(最初版本:头尾查找,左找大,右找小,左先走)
int PartSort1(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], a[left]);
int keyi = left;
while (left < right)
{
//右找大,由于是循环且在循环时定位(right)在改变,所以需判断定位值是否在范围内
while (right > left && a[right] >= a[keyi])
{
right--;
}
//左找小,同上
while (right >left && a[left] <= a[keyi])
{
left++;
}
//该条件为去除 left = right时的情况(由于前面的 right > left 所以不存在left > right情况)(没有也可以)
if(left < right)
Swap(&a[left], &a[right]);
}
//此时left与righ相遇(left = right)
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
//[begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
2. 挖坑法
// 快速排序挖坑法
int PartSort2(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], a[left]);
int key = a[left];
int hole = left;
while (left < right)
{
//先右,找小
while (key >= a[right] && left < right)
{
right--;
}
a[hole] = a[right];
hole = right;
//左找大
while (key <= a[left] && left < right)
{
left++;
}
a[hole] = key;
return hole;
}
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort2(a, begin, end);
//[begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
3. 前后指针版本
// 快速排序前后指针法
int PartSort3(int* a, int left, int right)
{
int mid = GetMidIndex(a, left, right);
Swap(&a[mid], a[left]);
int keyi = left;
int prev = left;
int cur = prev + 1;
while (cur <= right)
{
//该if条件(2个条件不可交换位置,只能在a[cur] < a[keyi]下,prve自加)包含2钟情况
// 一般情况:cur 与 prve 相邻,即中间无 大于key的元素(prve正常自加,但不进入内部)
//遇见大于key的之后:cur 与 prve不相邻时,即需要cur找小,去为cur的前进铺路(将右边小的搬到左边)
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[cur], &a[prev]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
return prev;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort3(a, begin, end);
//[begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
2.3.2 快速排序优化
快排原有的问题:
由于快排是采用的类似于二叉树的结构,所以选择key时,最好接近中间数,这样才能将排完序key的位置才能接近中间位置,使得下次递归时的2个子空间大小才更加的相近,这样更接近二分,递归次数更少,算法更优。也由于这个原因如果采用常规的key直接取第一个或者最后一个,
当原数据相对有序时,此时由于取key的原因,反而效率最低(是不是很神奇)
当递归到较小区间(数据较少)时,快排由于本身的复杂性,此时再用快排就有杀鸡用牛刀了的感觉了,且由于小区间的数量众多(递归到了最后大区间都被分成众多小区间)也会影响效率。
优化方法:
1. 三数取中法(随机也可以)选key 2. 递归到小的子区间时,可以考虑使用插入排序
在排序时,可能由于数据量的庞大,可能会导致内存不够函数的递归使用(每一次函数的调用递归,都会开辟新的函数栈帧,即消耗新的内存空间),此时我们就需要又快捷又不使用递归的排序。
思想:在快排算法递归时,使用了left,right,key,key-1,key+1,来定位当前正在进行排序的区间,和需要排序的key值。我们可以将该递归过程,使用栈的数据结构来模拟该过程。
栈https://blog.csdn.net/qq_68140277/article/details/126381722
void QuickSortNonR(int* a, int left, int right)
{
Stack st;
StackInit(&st);
StackPush(&st, left);
StackPush(&st, right);
while (StackEmpty(&st) != 0)
{
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
//判断此时,区间内是否有有效数据
if(right - left <= 1)
continue;
int div = PartSort1(a, left, right);
// 以基准值为分割点,形成左右两部分:[left, div) 和 [div+1, right)
StackPush(&st, div+1);
StackPush(&st, right);
StackPush(&st, left);
StackPush(&st, div);
}
StackDestroy(&s);
}
快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(logN)
4. 稳定性:不稳定
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
本人理解:有点类似于希尔排序的,分类排序,但是思想是不同的。希尔排序是,采用将gap(分的组数)越来越细,达到螺旋式的逐步有序,中间的过程比较未知。而归并排序是,采用固定式的分组加合并排序(本质上,只有合并排序,在组成员只有一个时才开始合并),是阶梯式的,更加可推测其中间过程。
归并排序
//单趟归并排序
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (end + begin) / 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[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
// 拷贝回原数组 -- 归并哪部分就拷贝哪部分回去
memcpy(a+begin, tmp+begin, (end-begin+1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
return;
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
思想:计数排序又称为鸽巢原理,直接开辟新的空间,使用空间自带的位置顺序数字,来对原数据的成员进行统计并排序
操作步骤:
1. 统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
// 时间复杂度:O(N+range)
// 空间复杂度:O(range)
// 适合数据范围集中,也就是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 (a[i] < min)
{
min = a[i];
}
}
int range = max - min + 1;
// 统计次数
int* countA = (int*)malloc(sizeof(int)* range);
if (countA == NULL)
{
perror("malloc fail");
return;
}
memset(countA, 0, sizeof(int)*range);
for (int i = 0; i < n; ++i)
{
countA[a[i] - min]++;
}
// 排序
int j = 0;
for (int i = 0; i < range; ++i)
{
while (countA[i]--)
{
a[j] = i + min;
++j;
}
}
free(countA);
}
计数排序的特性总结:
1. 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。 2. 时间复杂度:O(MAX(N,范围)) 3. 空间复杂度:O(范围)