在算法导论一书里第一个教我们实现的算法是插入排序算法以及插入排序算法的优化——归并排序算法,以及简单的对于归并排序算法的优化操作。在本次博客当中我们就着重讲解这两种算法
■插入排序算法
◆插入排序算法的介绍:
插入排序算法是专门对于数组进行排序的一种算法。可以将无序的数组转化成为有序的状态。可以说插入排序算法是我们排序算法当中最简单的算法之一。
其主要进行的操作和我们打扑克当中抓拍的思路很相同。
我们只需要保证我们最开始的数组为有序状态,拿到一个新的数据的时候就可以将该数据从最右边开始向左进行判断我们新数据想要插入的位置。如果小于我们最右边的数据就进行交换操作。直到第一次数据大于左侧的数据即可。实际过程如下图所示:
但是我们可能有的同学会想:要是前面的元素不有序呢?这样不是使用起来很局限吗?我们可以先将第一个元素单独看成是一个数组,因为只有一个元素,一定是自己有序的。(前面不存在比它大的数据)因此我们只需要在第二个元素之后开始向后进行判断就可以满足我们插入排序的前提条件了。
◆将算法思路代码化:
将上述思路转化成为代码就如下所示:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<vector>
using namespace std;
void insertSort(vector<int>& target)
{
//编写一个函数,模拟实现插入排序的操作
for (int i = 1; i < target.size(); i++)
{
//一共从第2个元素开始认为每一次向有序数组当中插入一个元素
int pos = i;
for (int j = i - 1; j >= 0; j--)
{
if (target[pos] < target[j])
{
// 例如: 11 12 21 16 16为需要进行判断的数据,如果小于前面一个数据就进行交换
swap(target[pos], target[j]);
//交换之后需要将pos进行--操作,使得我们的pos对应的元素一直为16
pos--;
}
else
{
break;
}
}
}
}
int main()
{
int arr[] = { 21,12,32,22,1,63,2,73,11 };
vector<int> nums(arr, arr + 9);
insertSort(nums);
//完成排序之后直接进行打印数组当中的内容
for (auto e : nums)
{
cout << e << " ";
}
return 0;
}
运行结果如图所示:
◆时间复杂度分析:
假设一共有n个数据,我们一共需要进行n-1次数据的插入操作,每一次插入操作都需要进行该元素目标位置的查找。通常情况下我们采用最坏时间复杂度作为程序总的时间复杂度。因此我们可以算出:
经过上面的计算可得插入排序的时间复杂度为O(N^2),但是对于一个排序算法来说N^2的时间复杂度太大了,一旦数据规模过大就需要运行好久。因此我们对上述算法使用分治的思想进行优化就得到了我们的归并排序算法。
■归并排序算法:
◆归并排序算法介绍:
对于一个较长的无序数组来说,较大的数据范围不容易进行判断。所以我们可以采用分治的思想,将较大的问题转化为规模较小的一个个子问题进行解决。对于归并排序算法来说,我们每一次会选择数组长度的一般开始进行排序,同样的道理为了便于我们对数据进行排序。我们要求所划分的子数组当中的元素为有序的状态。所以为了满足我们算法执行的前提条件我们需要进行一步步的递归操作,直到数组当中只剩下一个元素为止。因为一个数组会被默认为一个有序的子数组。
将数组拆分成由一个个元素组成的有序子数组的时候,我们就可以将相邻的有序的子数组进行合并,得到一个新的子数组,直到递归结果,最后一次进行合并得到一个完整的有序数组。
算法所要执行的步骤思路如下:
◆将算法思路代码化:
在这个算法导论这个章节当中,我们会下意识的默认大家的编程能力已经足够,只要知道了算法思路就可以实现出相应的代码。所以我们在直到算法思路之后直接向大家展示我们将算法思路转化为代码的成果,之后再为大家分析其时间复杂度以及进一步的优化操作及原因。
#include<iostream>
#include<vector>
using namespace std;
vector<int> mergeDate(const vector<int>& nums1,const vector<int>& nums2)
{
//将两个数组当中的数据进行合并操作
vector<int> ret;
int pos1 = 0;
int pos2 = 0;
while (pos1 < nums1.size() && pos2 < nums2.size())
{
if (nums1[pos1] > nums2[pos2])
{
//将nums2当中的数据加入到数组当中并将pos2++
ret.push_back(nums2[pos2]);
pos2++;
}
else
{
//当nums1当中数据小于等于nums2的时候均取nums1当中的数据加入到数组当中
ret.push_back(nums1[pos1]);
pos1++;
}
}
//当跳出循环的时候就代表有一个数组结束了,需要将另一个没有结束的数组当中的元素全部加入到数组当中
if (pos1 != nums1.size())
{
while (pos1 < nums1.size())
{
ret.push_back(nums1[pos1]);
pos1++;
}
}
if (pos2 != nums2.size())
{
while (pos2 < nums2.size())
{
ret.push_back(nums2[pos2]);
pos2++;
}
}
return ret;
}
//设置一个left和right参数,作为需要进行递归的新区间的范围
vector<int> mergeSort(const vector<int>& target,int left,int right)
{
if (left == right)
{
//当数组分解完毕之后就可以进行合并
vector<int> tmp;
tmp.push_back(target[left]);
return tmp;
}
//之后递归调用函数的左右区间
int mid = left + (right - left) / 2;
vector<int> nums1 = mergeSort(target, left, mid);
vector<int> nums2 = mergeSort(target, mid+1, right);
//之后将相邻的两个数组进行合并操作
return mergeDate(nums1, nums2);
}
int main()
{
int arr[] = { 21,12,32,22,1,63,2,73,11 };
vector<int> nums(arr, arr+9);
int left = 0;
int right = nums.size()-1;
vector<int> tmp=mergeSort(nums,left,right);
for (auto e : tmp)
{
cout << e << " ";
}
return 0;
}
运行结果如图所示:
◆时间复杂度分析:
可以很容易的发现我们的时间复杂度有着很明显的降低。根据分析我们每一次递归都会将数组当中的数据分成两份,所以我们总共需要进行的排序次数为log(N)次,每一次排序的时间复杂度都是O(N),所以我们归并排序总的时间复杂度为O(N*logN)。
◆插入排序和归并排序时间复杂度的比较
看到这里可能有的人回想:一个是N^2一个是N*logN有什么可比的呢?不是N*logN快吗?其实不能单纯的这么进行比较。因为我们使用归并排序的时候还有其他方面的消耗。例如栈帧的开辟,将数组分成进行细小的划分同样也需要时间。我们对于归并排序的时间复杂度的计算并没有计算在内,因为较小项无论是常数还是系数都是可以被忽略的。
但是如果我们数据量较小的时候,由于对数的性质,数据较小的时候经过划分的次数反而越多。因此当数据量较小的时候反而是插入排序的效率更高。这一方面可以减少我们栈帧的开辟一方面也可以优化我们的代码。
●数据量较小的时候优选插入排序,数据较大的时候优选归并排序
◆优化归并排序
因此我们可以对我们的归并排序进行优化操作。我们可以提前结束我们的递归,将之前的递归操作转而调用插入排序进而获得一个有序的数组。优化之后的代码如下:
#include<iostream>
#include<vector>
using namespace std;
void insertSort(vector<int>& target)
{
//编写一个函数,模拟实现插入排序的操作
for (int i = 1; i < target.size(); i++)
{
//一共从第2个元素开始认为每一次向有序数组当中插入一个元素
int pos = i;
for (int j = i - 1; j >= 0; j--)
{
if (target[pos] < target[j])
{
// 例如: 11 12 21 16 16为需要进行判断的数据,如果小于前面一个数据就进行交换
swap(target[pos], target[j]);
//交换之后需要将pos进行--操作,使得我们的pos对应的元素一直为16
pos--;
}
else
{
break;
}
}
}
}
vector<int> mergeDate(const vector<int>& nums1,const vector<int>& nums2)
{
//将两个数组当中的数据进行合并操作
vector<int> ret;
int pos1 = 0;
int pos2 = 0;
while (pos1 < nums1.size() && pos2 < nums2.size())
{
if (nums1[pos1] > nums2[pos2])
{
//将nums2当中的数据加入到数组当中并将pos2++
ret.push_back(nums2[pos2]);
pos2++;
}
else
{
//当nums1当中数据小于等于nums2的时候均取nums1当中的数据加入到数组当中
ret.push_back(nums1[pos1]);
pos1++;
}
}
//当跳出循环的时候就代表有一个数组结束了,需要将另一个没有结束的数组当中的元素全部加入到数组当中
if (pos1 != nums1.size())
{
while (pos1 < nums1.size())
{
ret.push_back(nums1[pos1]);
pos1++;
}
}
if (pos2 != nums2.size())
{
while (pos2 < nums2.size())
{
ret.push_back(nums2[pos2]);
pos2++;
}
}
return ret;
}
//设置一个left和right参数,作为需要进行递归的新区间的范围
vector<int> mergeSort(const vector<int>& target,int left,int right)
{
if (right-left<5)
{
//当数据量小于一定的程度的时候我们直接调用插入排序进行代码的优化操作
vector<int> tmp(target.begin() + left, target.begin() + right+1);
insertSort(tmp);
return tmp;
}
//之后递归调用函数的左右区间
int mid = left + (right - left) / 2;
vector<int> nums1 = mergeSort(target, left, mid);
vector<int> nums2 = mergeSort(target, mid+1, right);
//之后将相邻的两个数组进行合并操作
return mergeDate(nums1, nums2);
}
int main()
{
int arr[] = { 21,12,32,22,1,63,2,73,11,32,33,21 };
vector<int> nums(arr, arr+12);
int left = 0;
int right = nums.size()-1;
vector<int> tmp=mergeSort(nums,left,right);
for (auto e : tmp)
{
cout << e << " ";
}
return 0;
}
我们将递归结束的条件从只有一个元素修改为只有最后5个元素的时候直接调用插入排序。这样就可以减少最后几次递归所产生的大量栈帧的创建以及分割等操作带来的消耗。当然,并不是一定是最后5个元素调用插入排序才是最好。我们可以根据我们的实际判断进行具体的操作。我们只需要知道这个优化的思想即可。
此上就是我们本次博客的全部内容了,感谢您的观看,今后将会持续更新leetcode刷题系列,算法导论系列以及C++学习系列相关的博客,欢迎您的关注,下次再见。