目录
1 排序的概念及其应用
1.1 排序的概念
1.2 排序的应用
1.3 常见的排序算法
2 直接插入排序
2.1 基本思想
2.2 基本思路
2.3 代码实现
2.4 时间复杂度
3 冒泡排序(回顾)
3.1 思路分析
3.2 时间复杂度
4 比较
1 排序的概念及其应用
1.1 排序的概念
- 排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 内部排序:数据元素全部放在内存中的排序。
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2 排序的应用
排序可以应用在各种排名中(大学、品牌、成绩等),
1.3 常见的排序算法
我们现在已经学了冒泡排序O(N*N)和堆排序O(N*logN)。
接下来我们先讲直接插入排序。
2 直接插入排序
2.1 基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:
把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
- end表示一个下标,它的范围是[0,n-2],也就是最多只能指向倒数第二个数。
- 下标[0,end]是有序区间,end+1的下标插入这段有序区间仍是有序的。
直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定
2.2 基本思路
- 先单趟再多趟,先局部再整体。
单趟
如上只是简单的一趟排序,并不确定是哪一趟。而我们要实现的是多趟排序,这样才能把一组数据排好。那么如何进行多趟呢?
我们只需要再加一个循环即可,让end从0++到n-2。
- end不能是n-1,因为n-1的值是要和前面比较的,n个数的下标范围是[0,n-1]。
2.3 代码实现
//升序
void InsertSort(int* a, int n)
{
//[0,end]end+1插入
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;
}
}
打印代码
void PrintSort(int* a, int n)
{
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
- 提问:如果让i从1开始循环,可以吗?
不可以,如果i=1的话,那么就是从下标为2的那个数开始向前插入,也就是第三个数。但是我们并不确定第一个数和第二个数是否有序,所以不可以!
- 提问:这个位置可以存储数据个数吗?
不可以,哨兵位的头节点也不可以存储数据个数。因为数据类型不一样。数据类型可能会是char/double等,无法记录个数。
2.4 时间复杂度
- 时间复杂度: O(N^2)
最好情况——顺序有序:O(N)
最差情况——逆序:O(N^2)
3 冒泡排序(回顾)
回顾戳👉冒泡排序法代码
3.1 思路分析
单趟
- 每一趟都是从第一个数开始,然后依次从前往后两两比较,这里我们用升序。只要前面的数大于后面的数,我们就交换两个数的位置。比较完一组以后++i,再依次后面的两个数。
void BubbleSort(int* a, int n)
{
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
}
}
}
//或者i从0开始,要注意范围的改变
for (int i = 0; i < n-1; i++)
{
if (a[i] > a[i+1])
{
Swap(&a[i], &a[i+1]);
}
}
多趟
- 这里我们要控制的是结束位置。经过一趟排序后,最大的那个数已经排到最后面了,所以下一趟就不需要再与那个数比较了(改变i)。所以我们可以再写一个循环来控制多趟,j<n表示一共要实行n趟。
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
//一趟
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
}
}
}
}
3.2 时间复杂度
- 时间复杂度:O(N^2)
- 最好情况——已经有序:O(N)。
最好的情况就是不进里面的循环,只进入外面的循环,循环一圈出来发现没有数进行交换。
那我们怎么才能看出来有没有进循环呢?
- 代码这样改
- 如果进循环了,exchange就改为true,没有进就仍旧为false。
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
bool exchange = false;
for (int i = 1; i < n - j; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = true;
}
}
if (exchange == false)
break;
}
}
4 比较
冒泡排序和插入排序谁更优?
- 直接插入排序和冒泡排序的时间复杂度是一个等级的,但是直接插入排序效率更好。
- 因为冒泡排序一直都是等差数列,而插入排序只有在逆序的情况下才会使等差数列,如果后面的数比前面的数大,那么就不用交换了,直接看后面的那个数就可以。(排升序)