概念:
插入排序是一种简单的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
算法步骤:
以下是插入排序算法的一般步骤:
- 将第一待排序的记录看作是有序序列,从第二个记录开始进行遍历。
- 假设当前记录的关键字是key,用它与有序序列中的记录从后往前依次进行比较。
- 如果key值小于当前记录的关键字,则将当前记录向右移动一位。
- 重复第3步直到key值大于或等于有序序列中的某个记录的关键字,或者已经到了有序序列的开头。
- 将key插入到找到的位置。
- 重复步骤2到5,直到所有记录都被扫描并插入到有序序列中。
++插入排序的时间复杂度是O(n^2),在数据规模较小或者部分数据已经有序的情况下效率较高。但是,对于大规模数据,插入排序的性能会下降。++
单次插入操作
通俗一点的:
插入排序是一种非常直观的排序方法,就像我们整理扑克牌一样。假设你手中有一副没有排序的扑克牌,你想要将它们从小到大排序。你可以这样操作:
-
选择基准:你先拿出第一张牌,这张牌就是你的"基准",它暂时是有序的。
-
逐个比较:然后你拿出第二张牌,将它与第一张牌比较。如果第二张牌的数字比第一张牌大,你就将它放在第一张牌的后面,这样你就有两张牌的有序序列了。
-
插入位置:如果第二张牌的数字比第一张牌小,你就继续拿出第三张牌,将它与第一张牌比较。如果第三张牌的数字也比第二张牌小,你就将它放在第一张牌的前面,然后继续比较第二张和第三张牌。如果第二张牌比第三张牌大,你就将第二张牌移动到第三张牌的后面。
-
重复操作:你继续拿出剩下的牌,重复上述步骤,直到所有的牌都被比较过并插入到正确的位置。
-
完成排序:当你手中的牌都按照这个方式比较并插入后,你就得到了一副从小到大排序的扑克牌。
将这个过程转换为代码,就是插入排序算法。
代码实现:
cpp
#include <iostream>
using namespace std;
// 插入排序函数
void insertionSort(int arr[], int n) {
int i, key, j;
// 从第二个元素开始遍历数组
for (i = 1; i < n; i++) {
key = arr[i]; // 当前要插入的元素
j = i - 1;
// 将当前元素与已排序部分的元素从后向前比较
while (j >= 0 && arr[j] > key) {
// 如果已排序部分的元素大于key,则将该元素向后移动
arr[j + 1] = arr[j];
j = j - 1;
}
// 将key插入到正确的位置
arr[j + 1] = key;
}
}
效果展示:
cpp
// 打印数组的函数
void printArray(int arr[], int n) {
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << "\n"; // 换行
}
// 主函数
int main() {
int arr[] = {9, 5, 1, 4, 3};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
printArray(arr, n);
// 调用插入排序函数
insertionSort(arr, n);
cout << "Sorted array: ";
printArray(arr, n);
return 0;
}
cpp
Original array: 9 5 1 4 3
Sorted array: 1 3 4 5 9
D:\2024C语言\data-structure\insertion_sort\x64\Debug\insertion_sort.exe (进程 9304)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
insertionSort
函数会遍历数组中的每个元素,并将每个元素插入到前面已排序部分的正确位置。printArray
函数用于打印数组,让我们可以看到排序前后的对比。main
函数中定义了一个未排序的数组,并调用insertionSort
函数对其进行排序。
优化:
以下是插入排序的几种优化方法:
-
二分插入排序:在插入元素前,使用二分查找法找到插入点,这样可以减少比较元素的次数,将比较操作的时间复杂度降低到O(log n)。但是,由于插入操作本身的时间复杂度仍然是O(n),所以整体的时间复杂度仍然是O(n^2)。
-
希尔排序:希尔排序是插入排序的一种改进版本,它通过引入增量序列来对数组进行分组,每组内的元素使用插入排序进行排序。随着增量逐渐减小,直到增量为1,此时整个数组使用插入排序进行排序。这种方法可以减少比较和移动的次数,提高效率。
-
优化数据移动:在找到插入点后,可以一次性将所有大于待插入元素的值向后移动一位,而不是逐个交换。这样可以减少数据的移动次数。
-
使用链表:在链表上实现插入排序可以避免数组元素移动的开销,因为链表的节点可以单独移动。在链表上,插入排序的时间复杂度可以接近O(n log n)。
-
使用循环而不是递归:在某些情况下,使用循环结构代替递归结构可以减少函数调用的开销。
下面是一个C++中实现的插入排序,包括了使用二分查找来优化查找插入点的过程:
cpp
#include <iostream>
#include <vector>
#include <algorithm> // 引入algorithm库以使用std::swap
using namespace std;
// 二分查找找到插入点
int binarySearch(const vector<int>& arr, int key, int start, int end) {
while (start < end) {
int mid = start + (end - start) / 2;
if (arr[mid] >= key) {
end = mid;
} else {
start = mid + 1;
}
}
return start;
}
// 插入排序函数
void insertionSort(vector<int>& arr) {
for (int i = 1; i < arr.size(); ++i) {
int key = arr[i];
int j = binarySearch(arr, key, 0, i);
// 将大于key的元素向后移动
while (j < i) {
arr[j + 1] = arr[j];
++j;
}
arr[j] = key; // 插入key到正确的位置
}
}
// 打印数组的函数
void printArray(const vector<int>& arr) {
for (int num : arr) {
cout << num << " ";
}
cout << endl;
}
// 主函数
int main() {
vector<int> arr = {9, 5, 1, 4, 3};
cout << "Original array: ";
printArray(arr);
insertionSort(arr);
cout << "Sorted array: ";
printArray(arr);
return 0;
}
insertionSort
函数使用二分查找来找到每个元素的插入点,然后一次性将所有大于该元素的值向后移动,以减少数据移动的次数。这种方法在查找插入点时提高了效率,但整体时间复杂度仍然是O(n^2),因为数据移动的时间复杂度仍然是O(n)
特性:
- 时间复杂度为 O(n2)、自适应排序:在最差情况下,每次插入操作分别需要循环 n−1、n−2、...、2、1 次,求和得到 (n−1)n/2 ,因此时间复杂度为 O(n2) 。在遇到有序数据时,插入操作会提前终止。当输入数组完全有序时,插入排序达到最佳时间复杂度 O(n) 。
- 空间复杂度为 O(1)、原地排序:指针 i 和 j 使用常数大小的额外空间。
- 稳定排序:在插入操作过程中,我们会将元素插入到相等元素的右侧,不会改变它们的顺序。