目录
2024.10.22
前言
插入排序是排序的入门算法
一、以顺序结构下为存储条件的插入排序
1.算法思想以及代码实现
这里我们应用C++中的数组来实现,其逻辑比较简单类似于我们“摸牌”的一个过程,即将当前元素插入到已经有序的序列中即可。
#include <iostream>
#include <vector>
void insertionSort(std::vector<int>& arr) {
int n = arr.size();
for (int i = 1; i < n; i++) {
//默认第一个元素为有序的元素,这样i便从第二个元素出发即下标为1
int temp = arr[i];
int j = i;
// 将arr[i]插入到已排序部分的正确位置
while (j >= 0 && arr[j] > temp) {//如果当前元素小于有序序列里最后一个元素时候
arr[j] = arr[j-1];//后移有序序列的元素
j--;
}//当不在小于有序序列当前所指示的元素时,停止循环
arr[j] = temp;//将待插入元素插入到合适位置
}//for
}
int main() {
std::vector<int> arr = {12, 11, 13, 5, 6};
std::cout << "原始数组: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
insertionSort(arr);
std::cout << "排序后数组: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
二、链表中的插入排序
1.算法思想
我们可以简练的概括为两个任务:第一找到正确的插入位置,第二将当前结点插入到该位置即可
(1).找到插入位置:
这里与数组中不同,数组可以通过对下标的加减来实现向前向后的遍历操作,而对于链表而言,中可以访问后继结点,我们给出解决方案:
首先引入哑巴结点的概念,通过在链表前加入一个结点,存放位置在数据节点之前。这样的好处就是我们人为的使所有数据节点都具有了前驱结点。现在我们进入正题,首先我们要设置三个指针,curr、prev、以及p。其中curr指示当前待插入元素,其初始值为头结点的下一个结点。prev指示curr的前一个元素(同时也是有序序列的最后一个元素,也就是有序序列最大的元素)其初始值指向头节点。而对于p指针而言,p.next为当前结点应该插入的位置,起初始值为哑巴结点。
首先我们应该进行一个比较如果curr.val大于等于prev.val我们只需要让这俩指针同时后移一位即可否则就要从有序序列里寻找了下面给出示意图
注意:我们比较的条件不是指针p所指向的结点值的大小,而是下一个结点值的大小!我们利用一个While循环:开始的时候:p指向dummynode,p.next.val<curr.val(2<4),随后p后移,p指向第二个结点,p.next.val<curr.val(3<4),p指向第三个结点,p.next.val<curr.val不成立了。退出循环,此时指针p的下一个位置就是curr应该插入的位置。
(2).如何实现插入的过程
经典的链表算法题目:
我们需要三步骤来实现:
prev.next=curr.next;//对应操作①
curr.next=p.next;//对应操作②
p.next=curr;//对应操作③
这样一来我们就解决了第二个问题了。
2.代码实现
#include<iostream>
using namespace std;
struct ListNode {
int val; // 节点的值
ListNode* next; // 指向下一个节点的指针
// 构造函数
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* INSERTIONSORT(ListNode* head) {
if (!head || !head->next) return head; // 如果链表为空或只有一个元素,直接返回
ListNode* dummynode = new ListNode(0);//创建dummynode其值不重要,以为不参与比较
dummynode->next = head;//将dummynode与头节点链接
ListNode* prev = head;//prev指针
ListNode* curr = head->next;//curr指针
while (curr) {//只要curr不为null
if (curr->val >= prev->val) {//最好的情况,curr的val大于等于prev的val
prev = curr;//指针后移
curr = curr->next;//指针后移
}
else {//注意是每一次p指针都是从dummy出发
ListNode* p = dummynode;//创建p指针
while (p->next->val < curr->val) {//开始寻找合适的插入位置
p = p->next;
}//找到
prev->next = curr->next;
curr->next = p->next;
p->next = curr;//以上三行代表了我们的第二个任务插入curr
curr = prev->next;//curr指针后移
}
}
return dummynode->next;//最后返回dummynode.next,而非头节点。
}
void printList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
cout << current->val << " -> ";
current = current->next;
}
cout << "nullptr" << endl;
}
int main() {
// 创建链表 4 -> 2 -> 1 -> 3
ListNode* head = new ListNode(4);
head->next = new ListNode(2);
head->next->next = new ListNode(1);
head->next->next->next = new ListNode(3);
cout << "Original list: ";
printList(head);
// 对链表进行插入排序
head = INSERTIONSORT(head);
cout << "Sorted list: ";
printList(head);
return 0;
}
三、递归版本的插入排序
在IIT的<算法导论>课程中的期中考试,出现了这样的问题:请用递归思想改写插入排序。我在之前是没有遇到过的。
1.算法思想以及代码实现
提及递归算法:我们知道递归算法的关键在于找到递归边界以及递归式,如果数组长度 n
小于等于1,则直接返回,因为数组已经有序。否则,首先递归地对前 n-1
元素进行排序。然后,将第 n
个元素插入到已排序部分的适当位置。通过比较并移动元素,在合适的位置插入当前元素。
#include <iostream>
using namespace std;
// 递归函数,用于将元素插入到已排序部分的适当位置
void recursiveInsert(int arr[], int n) {
// 基准情况:如果数组长度为1或0,则返回
if (n <= 1) return;
// 递归地对前 n-1 个元素进行排序
recursiveInsert(arr, n - 1);
// 将第 n 个元素插入到已排序部分的适当位置
int last = arr[n - 1];
int j = n - 2;
// 移动元素,找到适当的位置插入 last
while (j >= 0 && arr[j] > last) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = last;
}
// 主函数,用于测试递归插入排序
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
// 调用递归插入排序函数
recursiveInsert(arr, n);
cout << "Sorted array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
四、折半插入排序
1.算法思想
在最开始我们提到过我们实现插入排序的过程就是循环的实现两个任务:找到插入位置和将待插入元素插入其中。在这个过程中我们,需要比较元素和移动元素,给我们带来了时间开销。当我们引入折半插入排序这个策略即可减小比较的过程(即寻找插入位置的过程)带来的时间消耗。简单来讲我们都知道二分查找的效率很高,但是也有局限性(只可以在有序序列中使用)而在插入排序中我们前面一部分的序列就是有序序列,我们可以利用折半查找(二分查找)来确定插入的位置。
(1)找的设计(二分查找的实现)
上图为寻找正确插入位置的具体流程,显而易见减少了比较的次数,同时注意到可以使用递归来实现该功能。
(2)插入元素
非常简单,即给定一个位置,我们将待插入元素插入即可,不再过多阐述
2.代码实现
#include <iostream>
using namespace std;
// 二分查找函数,用于找到插入位置
int binarySearch(int arr[], int item, int right, int left) {//其中参数item为待插入元素
if (left <= right)//递归的出口
return (item > arr[right]) ? (right + 1) : right;
int mid = (right + left) / 2;
if (item == arr[mid])//恰好找到的情况
return mid + 1;
if (item > arr[mid])
return binarySearch(arr, item, mid + 1, left);
return binarySearch(arr, item, right, mid - 1);
}
// 折半插入排序函数
void binaryInsertionSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
int temp = arr[i];
int j = i;
// 找到插入位置
int loc = binarySearch(arr, key, 0, j);
// 移动元素,为插入位置腾出空间
while (j >= loc) {
arr[j] = arr[j-1];
j--;
}
arr[j] = temp;
}
}
// 主函数,用于测试折半插入排序
int main() {
int arr[] = {5, 2, 9, 1, 5, 6};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "Original array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
// 调用折半插入排序函数
binaryInsertionSort(arr, n);
cout << "Sorted array: ";
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
五、二路插入排序
在前面我们了解了折半插入排序算法,其通过对比较次数的减少来提高代码的效率,现在我们引入二路插入排序,通过减少移动的次数来提高效率。
1.算法思想:
我们采用一个大小与待排序数组相同的数组作为辅助空间结构:简言之就是设计两个指针,一个head,一个tail。在该结构中可以向后面进行数据插入,也可以由前面插入(所以比较的时候先与头指针和尾指针进行比较)。对于指针的移动采用模运算的方式。(这样子讲比较抽象不易理解)下面给出示意图。
首先给出辅助结构以及指针的示意图
然后对第一种情况进行分析(即当前元素小于头指针的情况下,也就是判定当前元素为最小元素)
其实现比较简单,先确定指针head的位置,再进行插入。注意head指针前移的逻辑为模运算。也就是head=(head-1+n)%n。
接下来我们讨论当待插入元素的大小比目前辅助结构中最大的元素(同时也是尾指针所指向的元素)还要大的情况:tail=tail+1。
这种情况也是很简单不需要移动元素,并且尾指针移动的逻辑非常简单,简单的后移即可
接下来我们看第三种情况:即待插入元素小于尾指针所指的元素,同时还大于头指针所指向的元素,我们此时不得不移动元素了,下面是具体的移动方法。
第一步,将尾指针后移一步(注意包括最大的元素一起移动,为即将插入的元素留出一个位置。然后从这个空位置开始向前遍历、比较,直到遇到合适的位置。在这个过程中我们使用模运算来实现。(代码中会清晰的理解)
最后一步,我们将辅助结构中的元素一一安置到原数组即可。
2.代码实现
#include <iostream>
using namespace std;
void TWayINSERTION(int* l, int n) {
int* arr = new int[n]; // 动态分配辅助数组
int head, tail;
head = 0;
tail = 0;
arr[0] = l[0];
for (int i = 1; i < n; i++) {
if (l[i] <= arr[head]) {
head = (head - 1 + n) % n;
arr[head] = l[i];
}
else if (l[i] > arr[tail]) {
tail = (tail + 1) % n;
arr[tail] = l[i];
}
else {
tail++; // 更新尾指针位置
arr[tail] = arr[(tail - 1 + n) % n]; // 将最后一个元素移到新的位置
int j = tail - 1;
for (; l[i] < arr[(j - 1 + n) % n]; j = (j - 1 + n) % n) {
arr[j] = arr[(j + n - 1) % n]; // 移动元素
}
arr[j] = l[i]; // 插入新元素
}
}
for (int i = 0; i < n; i++) {
l[i] = arr[head];
head = (head + 1) % n;
}
delete[] arr; // 释放动态分配的内存
}
int main() {
int arr[] = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3 };
int n = sizeof(arr) / sizeof(arr[0]);
TWayINSERTION(arr, n);
for (int i = 0; i < n; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
总结
介绍了插入排序的算法,并且在数组和链表中进行了实现。此外又介绍了递归版本的插入排序,最后分别通过减少比较次数和移动次数引入了折半插入以及二路插入排序算法。