一、插入排序算法概述
插入排序算法是一种简单直观的排序算法,在排序算法中具有重要地位。它适用于小型数据集或数据部分有序的情况。
其主要特点如下:
- 实现简单:代码相对简洁,易于理解和实现。
- 空间复杂度低:只需要常数级别的额外空间来存储临时变量,空间复杂度为 O (1)。
- 稳定性:相同元素的相对顺序在排序后保持不变。
适用场景包括:
- 当数据量较小,其他复杂排序算法的额外开销可能超过其优势时,插入排序是一个不错的选择。
- 对于已经部分有序的数据,插入排序能够更快地完成排序,因为它可以利用数据的已有序性,减少比较和移动操作的次数
二、插入排序算法原理
(一)基本思路
插入排序的基本思路是将待排序序列分为已排序和未排序两部分。初始时,将第一个元素视为已排序部分,其余元素为未排序部分。然后,从第二个元素开始,依次将未排序元素插入到已排序部分的合适位置,使已排序部分始终保持有序。
在插入过程中,通过与已排序部分的元素逐个比较,确定插入位置,并将大于待插入元素的元素向后移动,为待插入元素腾出空间。
(二)具体过程
例如,对于数组 [3, 5, 1, 4, 2] 进行插入排序。
首先,3 被视为已排序部分,[3] 。
然后处理 5 ,因为 5 > 3 ,直接将 5 插入已排序部分,得到 [3, 5] 。
接着处理 1 ,将 1 与 5 比较,1 < 5 ,5 向后移动,得到 [3, 5, 5] ;再将 1 与 3 比较,1 < 3 ,3 向后移动,得到 [3, 3, 5] ;最后将 1 插入合适位置,得到 [1, 3, 5] 。
处理 4 时,与 5 比较,4 < 5 ,5 向后移动,得到 [1, 3, 5, 5] ;再与 3 比较,3 < 4 ,将 4 插入合适位置,得到 [1, 3, 4, 5] 。
最后处理 2 ,依次与 5 、4 、3 比较并移动,将 2 插入合适位置,最终得到排序后的数组 [1, 2, 3, 4, 5] 。
通过这样逐步比较、移动和插入,完成整个插入排序的过程。
三、插入排序算法的 C 语言实现
#include<stdio.h>
// 插入排序函数
void insertionSort(int arr[], int n) {
int i, key, j;
for (i = 1; i < n; i++)
// i 从 1 开始,因为单个元素的数组已经是有序的
{
key = arr[i]; // 保存当前要插入到已排序部分的元素
j = i - 1;
while (j >= 0 && arr[j] > key) // 将 arr[i] 插入到已排序部分
{
// 如果已排序部分的元素大于 key,则将该元素向后移动一位
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key; // 找到了合适的位置,将 key 插入
}
}
// 打印数组函数
void printArray(int arr[], int size)
{
for (int i = 0; i < size; i++) // 逐个打印数组中的元素
printf("%d ", arr[i]);
printf("\n"); // 打印完一行数组元素后换行
}
// 主函数
int main() {
int arr[] = {12, 11, 13, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
printf("未排序的数组: \n");
printArray(arr, n); // 打印提示信息和未排序的数组
insertionSort(arr, n); // 调用插入排序函数对数组进行排序
printf("排序后的数组: \n");// 打印提示信息和排序后的数组
printArray(arr, n);
return 0;
}
在上述代码中,insertionSort函数是插入排序的核心实现。外层的for循环从数组的第二个元素开始,将当前元素作为key。内层的while循环从已排序部分的末尾开始,将key插入到已排序部分的正确位置。通过不断比较和移动元素,最终完成排序。
(二)优化方法
1.减少交换操作
void insertionSort(int arr[], int n)
{
int i, key, j;
for (i = 1; i < n; i++) // i 从 1 开始,因为单个元素的数组本身就是有序的
{
key = arr[i]; // 将当前要处理的元素保存为 key
j = i - 1;
while (j >= 0 && arr[j] > key) // 使用赋值操作代替交换操作
{
// 如果前一个元素比 key 大,就将前一个元素向后移动一位
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;// 将 key 插入到合适的位置
}
}
在内层循环中,使用赋值操作代替交换操作可以减少不必要的开销。原本的交换操作需要进行三次赋值,而赋值操作只需要一次。
2.二分查找优化
// 二分查找函数,用于在已排序数组中找到插入位置
int binarySearch(int arr[], int item, int low, int high)
{
if (high <= low) // 如果 high 小于等于 low,表示未找到插入位置
return (item > arr[low])? (low + 1) : low;
int mid = (low + high) / 2;
if (item == arr[mid]) // 如果找到了目标元素,返回其位置
return mid + 1;
if (item > arr[mid]) // 如果目标元素大于中间元素,在右半部分继续查找
return binarySearch(arr, item, mid + 1, high);
if (item < arr[mid]) // 如果目标元素小于中间元素,在左半部分继续查找
return binarySearch(arr, item, low, mid - 1);
}
// 插入排序函数,使用二分查找优化插入位置的确定
void insertionSort(int arr[], int n)
{
int i, key, j, loc;
for (i = 1; i < n; i++)
{
key = arr[i];
j = i - 1;
loc = binarySearch(arr, key, 0, j);// 使用二分查找找到插入位置
while (j >= loc) // 移动元素以腾出插入位置
{
arr[j + 1] = arr[j];
j--;
}
arr[loc] = key;
}
}
使用二分查找来找到插入位置,从而减少比较次数。二分查找通过不断将区间缩小一半,快速定位插入位置,提高了算法的效率。
四、插入排序算法的性能分析
(一)时间复杂度
插入排序的时间复杂度取决于待排序序列的初始状态。
- 最坏情况:当待排序序列是完全逆序时,每次插入一个元素都需要与已排序部分的所有元素进行比较和移动,时间复杂度为 。
- 最好情况:当待排序序列已经是有序的,每次插入元素都不需要移动已排序部分的元素,时间复杂度为 。
- 平均情况:插入排序的平均时间复杂度也为 。
(二)空间复杂度
插入排序是一种原地排序算法,它只需要常数级别的额外空间来存储临时变量,例如用于存储当前待插入的元素和循环变量等,空间复杂度为 。
(三)稳定性
插入排序是一种稳定的排序算法。在排序过程中,如果两个元素相等,后面的元素不会移动到前面元素的前面,而是直接插入到与它相等的元素之后,从而保持了原有的相对顺序。这使得插入排序在需要保持相同元素相对位置不变的场景中非常有用。
(四)与其他排序算法的比较
与冒泡排序相比,插入排序在平均情况下性能更优。冒泡排序在每次迭代中都可能进行多次交换操作,而插入排序通常交换操作较少。
与快速排序相比,插入排序在小规模数据集中表现较好,因为快速排序的分治策略在处理小规模数据时可能产生额外的开销。但对于大规模数据集,快速排序的平均时间复杂度为 ,性能优于插入排序。
与归并排序相比,归并排序的性能较为稳定,但需要额外的空间来存储临时数组。插入排序则是原地排序,空间复杂度更低,但时间复杂度在大规模数据集中相对较高。
总之,在选择排序算法时,需要根据数据规模、对空间和时间的要求等因素综合考虑,以确定最适合的算法。