插入排序
零.排序的基本概念
1.定义
假设含n个记录的序列:{R1,R2,R3,···,Rn}
其相应的关键字
序列为:
{K1,K2,K3,···,Kn}。
需确定1,2,3,···,n的一种排列p1,p2,p3,···,pn,使其相应的关键字满足如下的非递减(或非递增)关系
Kp1 ≤ Kp2 ≤K p3 ≤ ··· ≤ Kpn
即,使最上面的那个序列成为一个按关键字有序的序列:
{Rp1,Rp2,Rp3,···,Rpn}
以上中,如果排序的关键字是主关键字(唯一),则排序的结果一定是唯一的,如果次关键字,则排序的结构不唯一。
2.稳定性
针对于多关键字的排序方法,假设两个关键字相等。Ki = Kj,而且Ri 领先于Rj 也就是i < j 。若排序后:
- Ri 任然领先于Rj ,则该排序方法是稳定的。
- 反之,则不稳定。
如果证明一个排序是不稳定的,只需要找出一对K即可。
3.内外部排序
根据待排序记录的数量不同,使得排序过程中设计的储存器不同,可将排序分为两种:
- 内部排序:数据较少(仅相较于外部排序),放在计算机随机储存器(内存条)中,进行排序的过程
- 外部排序:就是数据太多,内存条存不下,在内部排序中,尚需要对外进行访问的过程。
一.直接插入排序
1.算法原理
插入排序的思想是:将一个表分成有序
和无序
两个部分。我们每次将无序
部分中的某个元素插入到有序
部分中合适的位置。这就是基本插入排序的思想。
这种算法的时间复杂度为:
O ( n 2 ) O(n^2) O(n2)
2.排序过程
3.代码实现
void InsertSort(int a[], int n)
{
for(int i= 1; i<n; i++)
{
if(a[i] < a[i-1])
{
//若第 i 个元素大于 i-1 元素则直接插入;反之,需要找到适当的插入位置后在插入。
int j= i-1;
int x = a[i];
while(j>-1 && x < a[j])
{
//采用顺序查找方式找到插入的位置,在查找的同时,将数组中的元素进行后移操作,给插入元素腾出空间
a[j+1] = a[j];
j--;
}
a[j+1] = x; //插入到正确位置
}
}
}
二.折半插入排序
1.算法原理
这种算法的思想和基本插入排序相同,也是有序
和无序
两个部分。不过,通过二分查找
的方式,查找我们的插入到有序
部分中的下标。
折半插入排序算法相比较于直接插入排序算法,只是减少了关键字间的比较次数,而记录的移动次数没有进行优化,所以该算法的时间复杂度
仍是 O ( n 2 ) O(n^2) O(n2)
2.排序过程
这里展示最后一次的排序过程:
3.代码
void BInsertSort(int a[],int size){
int i,j,low = 0,high = 0,mid;
int temp = 0;
for (i=1; i<size; i++) {
low=0;
high=i-1;
temp=a[i];
//采用折半查找法判断插入位置,最终变量 low 表示插入位置
while (low<=high) {
mid=(low+high)/2;
if (a[mid]>temp) {
high=mid-1;
}else{
low=mid+1;
}
}
//有序表中插入位置后的元素统一后移
for (j=i; j>low; j--) {
a[j]=a[j-1];
}
a[low]=temp;//插入元素
}
三.2路插入排序
1.算法原理
这个算法和直接插入排序都差不多,只不过将插入的方向变成了两个,然后把链模拟成环链,然后就可以向前插,也可以向后插,这就是说的,两路。
2-路插入排序相比于折半插入排序,只是减少了移动记录的次数,没有根本上避免,所以其时间复杂度仍为 O ( n 2 ) O(n^2) O(n2)
2.排序过程
3.代码
void insert(int arr[], int temp[], int n)
{
int i,first,final,k;
first = final = 0;//分别记录temp数组中最大值和最小值的位置
temp[0] = arr[0];
for (i = 1; i < n; i ++){
// 待插入元素比最小的元素小
if (arr[i] < temp[first]){
first = (first - 1 + n) % n;
temp[first] = arr[i];
}
// 待插入元素比最大元素大
else if (arr[i] > temp[final]){
final = (final + 1 + n) % n;
temp[final] = arr[i];
}
// 插入元素比最小大,比最大小
else {
k = (final + 1 + n) % n;
//当插入值比当前值小时,需要移动当前值的位置
while (temp[((k - 1) + n) % n] > arr[i]) {
temp[(k + n) % n] =temp[(k - 1 + n) % n];
k = (k - 1 + n) % n;
}
//插入该值
temp[(k + n) % n] = arr[i];
//因为最大值的位置改变,所以需要实时更新final的位置
final = (final + 1 + n) % n;
}
}
// 将排序记录复制到原来的顺序表里
for (k = 0; k < n; k ++) {
arr[k] = temp[(first + k) % n];
}
}
四.表插入排序
1.算法原理
我们通过指向
来表示一个元素的下一个结点来表示插入,有点像模拟链表的方式。这样,我们的插入并需不要移动,只需要更新next指针。缺点就是耗内存。
我们从MAX的next开始,末尾指向MAX,也就是链表头尾结点公用Max。
最后再重新遍历一遍Next数组,然后修改原排序数组即可。
从表插入排序的实现过程上分析,与直接插入排序相比只是避免了移动记录的过程(修改各记录结点中的指针域即可),而插入过程中同其它关键字的比较次数并没有改变,所以表插入排序算法的时间复杂度仍是 O ( n 2 ) O(n^2) O(n2)
2.排序过程
3.代码实现
#include <stdio.h>
#include <stdlib.h>
#define MAX 0xffff
void insert(int arry[], int size)
{
int *next = (int*)malloc(sizeof(int) * (size+1)); // 这是next数组
int *key = (int*)malloc(sizeof(int) * (size+1)); // 这是模拟表。表头默认为最大。
int i, pre, insert, temp;
next[0] = 0; // 把表头指向表尾
key[0] = MAX; // 把表头值设为为最大
for (i = 0; i < size; ++i)
{
// 构造新的key表,并且将next数组初始化(指向表尾)
key[i+1] = arry[i];
next[i+1] = 0;
}
insert = 1; // 从新的模拟表非表头开始
while(insert < size+1) // 把原表全部插入
{
pre = 0; // 初始化表结点指针(前驱)指向表头,当然是0.
// 循环终止的条件: 当前表结点的下一个是表尾 或者 当前待插入的值小于当前表结点的下一个(前驱的后继才是我们要比较的值),
while(next[pre] != 0 && *(key+next[pre]) <= key[insert])
{
pre = next[pre]; // 更新表指针
}
// 此时,我们表结点的后继就是我们要插入的值。该过程和链表的插入一模一样
temp = next[pre]; // 把表结点的指向的值存起来
next[pre] = insert; // 把表结点指向插入的地址
next[insert] = temp; // 把插入的结点指向刚才表结点的后继
++insert; // 更新待插入的指针
}
// 将排好序的结果 赋值给原数组
pre = next[0];
for (i = 0; i < size; ++i)
{
arry[i] = key[pre];
pre = next[pre];
}
}
int main()
{
int a[8] = {
7,6,4,2,5,1,3};
int i = 0;
insert(a, 7);
for (i = 0; i < 7; ++i)
{
printf("%d ", a[i]);
}
return 0;
}
五.希尔排序
1.算法原理
先将整个带排序记录序列分成若干个子序列,分别进行插入排序。待整个序列中的记录“基本有序”时,再对全体进行一次直接插入排序。
换句话说,就是将一个大的序列,分成小组进行直接排序,在小组内容够少的情况下,插入并不需要移动较多元素。然后逐渐扩大规模,让小组渐渐变大,因为刚才进行过一次交换排序,所以该序列的有序度会提高,所以插入可能
并不需要移动太多,直到最后对整体进行直接插入,保证全部排好序。虽然如此,在最坏情况下,它的时间复杂度任然为: O ( n 2 ) O(n^2) O(n2)。
2.分组数目与时间复杂度
希尔排序的时间复杂度,是取决于增量
序列的函数,这个涉及一些数学上还未解决的难题。所以,目前没有人能得到最好的增量序列
。但是已经研究出一些局部结论。当增量序列等于 d l t a [ k ] = 2 t − k + 1 − 1 dlta[k]=2^{t-k+1}-1 dlta[k]=2t−k+1−1 时,时间复杂度大约在 O ( n 1.5 ) O(n^{1.5}) O(n1.5)。
增量序列有各种取法,但是必须遵守:增量序列中的值没有除1以外的公因子,且最后一次的增量一定是1.
3.排序过程
为了程序方便,我们可以自定义增量序列,也可以用长度÷2的方式,比如长度是10,那么第一次增量为10÷2=5;然后就是增量5÷2=2,然后就是增量为2÷2=1,完成。
这个例子为了直观,我们采用自定义增量序列的方式,序列dlta={3,2,1}
相同颜色块进行直接插入排序,图中是dlta为x时,已排好的状态。
4.代码实现
和上面一模一样
#include <stdio.h>
#include <stdlib.h>
void ArryShow(int arry[], int size);
void Sinsert(int arr[], int size, int dlat);
void ShellSort(int arry[], int arrysize, int dlat[], int dlatsize);
void Sinsert(int arr[], int size, int dlat)
{
int i, pre, pptr, this, tptr, temp;
for(int i=dlat;i<size;i++)
{
int j = i;
int temp = arr[j];
if(arr[j]<arr[j-dlat]){
while(j-dlat>=0 && temp<arr[j-dlat])
{
//移动法
arr[j] = arr[j-dlat];
j-=dlat;
}
arr[j] = temp;
}
}
}
void ShellSort(int arry[], int arrysize, int dlat[], int dlatsize)
{
int i = 0;
for(; i < dlatsize; ++i)
{
Sinsert(arry, arrysize, dlat[i]);
ArryShow(arry, arrysize);
}
}
void ArryShow(int arry[], int size)
{
int i = 0;
printf("\n");
for (; i < size; ++i)
{
printf("%d ", arry[i]);
}
}
int main()
{
int a[8] = {
7,6,4,2,5,1,3};
int dlat[3] = {
3,2,1};
ShellSort(a, 7, dlat, 3);
return 0;
}