🚀 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
🐌 个人主页:蜗牛牛啊
🔥 系列专栏:🛹数据结构、🛴C++
📕 学习格言:博观而约取,厚积而薄发
🌹 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! 🌹
文章目录
排序的相关概念
排序就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前(相同的数据,保证排序前后它们的相对位置不变),则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
插入排序
直接插入排序
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
如当我们向一个有序序列中插入元素时,可能有下面几种情况:
单次插入数据的代码如下:
int end = n - 1;//n - 1为插入数据的前一个位置的下标
int tmp = a[end + 1];//tmp就是插入数据
//循环找到比tmp小的数据
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
//将end + 1位置值覆盖为tmp
a[end + 1] = tmp;
对数组中元素进行排序,我们可以理解为end是从数组下标为0的位置开始的,tmp = a[end + 1]便是插入的数据:
在单次插入数据的外面加上一层for循环来控制即可:
代码(排升序):
//直接插入排序
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;
}
}
//将tmp的值放到合适位置
a[end + 1] = tmp;
}
}
注意:for循环中的判断条件为i < n - 1,因为如果判断条件为i < n时,访问下标end所在的位置时,虽然没有问题,但是tmp = a[end + 1],访问end+ 1下标时将会发生越界访问问题。
直接插入排序特性总结:
-
时间复杂度:最坏情况是逆序,每次都要遍历到while循环结束,将数据插入到下标为0的位置,此时每个单趟排序的时间复杂度为end,一共要挪动的次数为1 + 2 + 3 + 4 + ……+ (n - 1) ,所以最坏情况下时间复杂度为O(N2);最好情况就是每次插入的数据都能保证数组有序,时间复杂度为O(N)。
-
空间复杂度:不需要开辟额外的空间,所以空间复杂度为O(1)。
-
通过上面时间复杂度可以知道:元素集合越接近有序,直接插入排序算法的时间效率越高
当数据完全有序时,直接插入排序就是O(N),只需比较N次,不需要交换
- 直接插入排序是一种具有稳定性的排序算法(当有两个相同的数据时,可以保证它们之间的相对顺序不变)。
希尔排序
希尔排序也可以叫做缩小增量排序,我们通过上面对直接插入排序的特性总结可以知道元素集合越接近有序,直接插入排序算法的时间效率越高,希尔排序就是对直接插入排序的优化。
希尔排序的算法思想:
1、先对初始数据进行预排序;
2、对预排序之后的数据再进行直接插入排序
通过上述两步进行排序就会比直接对原始数据进行直接插入排序的效率高,从而达到优化的效果。
预排序就是选定一个gap作为间距,将要排序的数据分为gap组,先分别对每组数据进行直接插入排序,再缩小gap值并再次对每组数据进行排序。然后当gap == 1时对预排序之后的数据再进行直接插入排序就完成整体数据的排序了。
我们先对上面的数据进行一次预排序,代码实现:
void ShellSort(int* a, int n)
{
//假设gap为3
int gap = 3;
//将gap组数据都排好序
for (int i = 0; i < gap; i++)
{
//将本组数据排好序
for (int j = i; j < n - gap; j = j + gap)
{
int end = j;
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;
}
}
}
代码运行结果:
实现过程:
所以此次预排序的起始和最后结果为:
上面的单次预排序和直接插入排序很像,如果gap等于1就是直接插入排序了。直接插入排序在for循环中的条件为i < n- 1; i++
,而这次预排序在循环中的条件是i = n - gap; i += gap
。
上面的程序中,假设数据个数为N,有gap组,每组有N/gap个数据,每组最坏情况下挪动次数:1+2+3+……+gap/n(和直接插入有点类似),所以时间复杂度最坏的情况:(1 + 2+ 3 + …… +gap/n) gap(乘以gap因为有gap组)。*
对于上面的代码我们可以简化使其具有相同的效果:
void ShellSort(int* a, int n)
{
//假设gap为3
int gap = 3;
for (int j = 0; j < n - gap; ++j)
{
int end = j;
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;
}
}
当gap大于1的时候是预排序,但是当gap等于1的时候就是直接插入排序。
思考:当数据个数N = 100000时,如果gap还是等于3是否合适?肯定是不合适的,所以我们可以改变gap的值,因为预排序可以走很多次,只要最后一次gap == 1便能实现数据有序,所以我们可以改变gap的值,有多种写法:
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//保证最后一次gap等于1,进行预排序之后的插入排序
//gap = gap / 2;
//假设gap为3
gap = gap / 3 + 1;//一定要加1保证最后结果为1,除2一定能保证gap大于1时结果为1,但是除3
//不能,gap = 2 时除3等于0,所以要加1
//int gap = 3;
for (int j = 0; j < n - gap; ++j)
{
int end = j;
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、gap越大,大的数可以更快到后面,小的可以更快到前面;但是gap越大越不接近有序。
2、gap越小,数据跳动越慢,但是越接近有序。
希尔排序的时间复杂度:
希尔排序的时间复杂度并不好计算,我们可以记一个结论:希尔排序的时间复杂度为O(N1.3)。
对比直接插入排序和希尔排序的性能:
clock()函数获取的是毫秒。
void TestOp()
{
srand(time(0));
const int N = 10000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
for (int i = 0; i < N; i++)
{
a1[i] = rand();
a2[i] = a1[i];
}
//直接插入排序
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
//希尔排序
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
}
运行结果:
要注意C语言的rand()函数有一定的局限性,随机数最多只能产生3万多个,我们可以通过+i的方式让随机数不那么重复。
希尔排序特性总结:
-
希尔排序是对直接插入排序的优化。
-
当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序。这样整体而言,可以达到优化的效果。
-
希尔排序的时间复杂度不好计算,我们可以记一个结论:希尔排序的时间复杂度为O(N1.3)。
-
**稳定性:不稳定。**因为可能将相同的数据分到不同组中,可能导致排完之后相对顺序就发生改变。
选择排序
选择排序的基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序
直接选择排序就是在一组数据中通过遍历找到最小的数据,然后放到数组的前边(未更新数据的位置),并且重复遍历查找交换,直到全部数据排完。
(升序为例)代码实现:
//直接选择排序
void OptionSort(int* a, int n)
{
int begin = 0;
//确保遍历完全部数据
while (begin < n)
{
int min = begin;//记录最小元素的下标
//找到最小的元素
for (int i = begin; i < n; i++)
{
if (a[min] > a[i])
{
min = i;//更新min下标,使其是较小元素的下标
}
}
Swap(&a[min], &a[begin]);//交换
begin++;//从下一个位置开始遍历
}
}
上述代码在最坏情况下,第一次要比较N-1次,第二次要比较N-2次……最后一次比较一次,所以最坏情况下的时间复杂度为O(N2),空间复杂度为O(1)。
上述思路可以优化:每趟遍历的时候找到最大值和最小值的位置,然后把最大值放在该序列的尾部,最小值放在该序列的头部。这样可以使排序的效率提升一倍。
//直接选择排序
void OptionSort(int* a, int n)
{
int left = 0;//第一个元素的下标
int right = n - 1;//最后一个元素的下标
while (left < right)
{
int min = left;//记录最小元素的下标
int max = right;//记录最大元素的下标
//找到最小的元素和最大的元素
for (int i = left;