目录
直接插入排序简介
● 插入排序是一种简单直观的排序算法,它也是基于比较的排序算法。它的工作原理是通过不断扩张有序序列的范围,对于未排序的数据,在已排序中从后向前扫描,找到相应的位置并插入。插入排序在实现上通常采用就地排序(不占用额外内存或占用常数的内存,当需要大量数据排序时,占用内存非常少。),因而空间复杂度为O(1)。在从后向前扫描的过程中,需要反复把已排序元素逐步向后移动,为新元素提供插入空间,因此插入排序的时间复杂度为O(n^2);
● 插入排序是基于比较的排序。所谓的基于比较,就是通过比较数组中的元素,看谁大谁小,根据结果来调整元素的位置。
因此,对于这类排序,就有两种基本的操作:①比较操作; ②交换操作
其中,对于交换操作,可以优化成移动操作,即不直接进行两个元素的交换,还是用一个枢轴元素(temp)将当前元素先保存起来,然后执行移动操作,待确定了最终位置后,再将当前元素放入合适的位置。(下面的插入排序就用到了这个技巧)–因为,交换操作需要三次赋值,而移动操作只需要一次赋值!
有些排序算法,比较次数比较多,而移动次数比较少,而有些则相反。比如,归并排序和快速排序,前者移动次数比较多,而后者比较次数比较多。
这里主要介绍插入排序
算法步骤
从第一个元素开始,该元素可以认为已经被排序
取出下一个元素,在已经排序的元素序列中从后向前扫描
如果该元素(已排序)大于新元素,将该元素移到下一位置
重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
将新元素插入到该位置后
重复步骤2~5
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。
最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。
最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数减去(n-1)次。平均来说插入排序算法复杂度为O(n^2)。
算法演示
假设我们要对数组{12,4,5,2,6,14}进行插入排序,排序过程为:
算法动态示意图:
下面看代码示例:
#include<iostream>
#include<cassert>
using namespace std;
class SqList
{
public:
SqList(size_t sizeElem);
~SqList();
void printElem();
void swapElem(int &a, int &b);
void insertSort();
void create(const size_t length);
private:
int *m_base; //指向数组
int m_length; //记录数组中的个数
};
SqList::SqList(size_t sizeElem)
{
m_base = new int[sizeElem];
assert(m_base != nullptr);
m_length = 0;
}
SqList::~SqList()
{
delete m_base;
m_base = nullptr;
}
void SqList::create(const size_t length)
{
m_length = length;
cout << "请分别输入你想排序的这" << length << "个元素,中间以回车键隔开:\n";
for (size_t i = 0; i != length; ++i)
{
cin >> m_base[i];
}
cout << endl;
}
void SqList::printElem()
{
for (size_t i = 0; i != m_length; ++i)
{
cout << m_base[i] << " ";
}
cout << endl;
}
void SqList::swapElem(int &a, int &b)
{
int temp = a;
a = b;
b = temp;
}
void SqList::insertSort()
{
/* 这个代码是把要无须数组的第一个数据用temp保存起来,
然后用循环来移动数据,最后把该数据放到有序数组的相应位置*/
int count = 0;
for (int i = 1; i < m_length ; ++i) //从第2个数据开始插入
{
int j = i - 1; // j记录的是有序数组的最后一个位置,从后面往前面找
int temp = m_base[i]; //记录要插入的数据 ,temp 每次循环记录无须数组的第一个数字
while (0 <= j && temp < m_base[j] ) //从后向前,找到比其小的数的位置
{
m_base[j + 1] = m_base[j]; //向后挪动
--j;
}
if (j != i - 1) //存在比其小的数(其实这个if语句去掉,直接后面的语句也是可以的)
{ // 这里的j 就是表示要插入位置之前一个位置的下标
m_base[j + 1] = temp; //把要插入的数据插入合适的位置
}
++count;
}
cout << "直接插入排序花了" << count << "次完成了排序!" << endl;
/* 这块代码用的是交换数据, 意思就是说 把无须数组的第一个元素直接交换到有序数组的相应位置,没有移动元素
for (int i = 1; i < m_length; ++i)
{
for (int j = i; 0 < j; --j)
{
if (m_base[j] < m_base[j - 1])
{
swapElem(m_base[j], m_base[j - 1]);
}
else
break;
}
}*/
}
int main()
{
{
int sizeCapacity(0);
cout << "输入数组的最大容量:";
cin >> sizeCapacity;
SqList mySqList(sizeCapacity);
while (true)
{
{
cout << "\n************************ 欢迎来到来到直接插入排序的世界!**********************\n" << endl
<< "输入0,退出程序!" << endl
<< "输入1,进行直接插入排序!" << endl
<< "输入2,清屏!" << endl;
}
cout << "************************* 请输入你想要使用的功能的序号 **********************" << endl;
int select(0);
cout << "请输入你的选择:";
cin >> select;
if (!select)
{
cout << "程序已退出,感谢你的使用!" << endl;
break;
}
switch (select)
{
case 1:
{
cout << "请输入你想排序数组元素的个数:";
int arraySize(0);
cin >> arraySize;
assert(arraySize != 0);
mySqList.create(arraySize);
cout << "先输出排序前的元素:";
mySqList.printElem();
mySqList.insertSort();
cout << "再输出排序后的元素:";
mySqList.printElem();
break;
}
case 2:
system("cls");
cout << "程序已清屏!可以重新输入!" << endl;
break;
default:
cout << "输入的序号不正确,请重新输入!" << endl;
}
}
}
system("pause");
return 0;
}
复杂度分析
● 插入排序的时间复杂度 就是判断比较次数有多少,而比较次数与 待排数组的初始顺序有关。 最好情况下,排序前对象已经按照要求的有序。比较次数(n−1) ; 移动次数(0)次。则对应的时间复杂度为O(n)。
● 最坏情况是数组逆序排序,第 i 趟时第 i 个对象必须与前面 i 个对象都做排序码比较,并且每做1次比较就要做1次数据移动,此时需要进行 (n +2)*(n-1) / 2次比较; 而记录的移动次数也达到最大值 (n+4)*(n-1)/2 次。 则对应的时间复杂度为
● 如果排序记录是随机的,那么根据概率相同的原则,在平均情况下的排序码比较次数和对象移动次数约为,因此,直接插入排序的时间复杂度为。 同样的 时间复杂度,直接插入排序比冒泡和简单选择排序的性能好一些。
插入排序不适合对大量数据进行排序应用,但排序数量级小于千时插入排序的效率还不错,可以考虑使用。
直接插入排序采用就地排序,空间复杂度为O(1).
其实,插入排序的比较次数与数组的逆序数相关,因为插入排序在将某个元素插入到合适位置时,其实就是消除这个元素的逆序数。
稳定性
直接插入排序是稳定的,不会改变相同元素的相对顺序。
直接插入排序算法的特点
● 它是稳定排序,不改变相同元素原来的顺序。
● 它是就地排序,只需要O(1)的额外内存空间。
● 它是在线排序,可以边接收数据边排序。
● 它跟我们牌扑克牌的方式相似。
● 对小数据集是有效的。