插入排序的改进
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
插入排序
插入排序是通过若干(一般是N-1)轮的插入得到所有数据组成的有序列的过程。
每轮插入都是向已有的(不完整的)有序列中插入新的数据元素,使原来的有序列得到扩充。第一轮插入前只有包含一个数据元素的有序列。
插入排序所花费的操作主要是用于查找新元素的插入位置(查)和将新元素插入到该位置(插)。因此,降低时间复杂度的努力也主要围绕查和插来进行。
1 直接交换的插入排序
#include
using namespace std;
ifstream fin("count4.in");
ofstream fout("count.out");
main()
{int n,a[200001],i,j,t=clock();
fin>>n>>a[1];
for(i=2;i<=n;i++)
{fin>>a[i];
for(j=i;j>1;j--)
if(a[j]// else break;
}
fout<<(clock()-t)/1000.0<
fout<fout<
单看swap(a[j],a[j-1])一句且不理会注释行,此代码倒象是气泡法,把注释行的“//”去掉,更容易理解for j的循环目的在于将a[i]插入到原有的从a[1]到a[i-1]的有序子列中,而且还能明显提速。
2 借助监视哨查找、利用循环交换进行插入
我们知道交换两个下标变量的操作较费时间,大范围的连续交换改为循环交换将会减少数据移动量。把自身当作“监视哨”还可以把限制循环范围的判断与对关键字大小的判断合为一个判断,这就节省了一些判断操作。
#include
using namespace std;
ifstream fin("count4.in");
ofstream fout("count.out");
main()
{int n,m,a[200001],i,j,k,t=clock();
fin>>n>>a[1];
for(i=2;i<=n;i++)
{fin>>a[i];
for(j=1;a[j]for(m=a[k=i];k>j;k--) a[k]=a[k-1];
a[j]=m;
}
fout<<(clock()-t)/1000.0<
fout<fout<
有了这两种改进,速度将可能提高一倍。
3 合并循环、减少判断、循环交换
例题:NOIP提高组合并果子
二、合并果子 (fruit.pas/dpr/c/cpp)
【问题描述】
在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。
每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。
例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费
体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。
【输入文件】
输入文件fruit.in包括两行,第一行是一个整数n(1<=n<=10000),表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数ai(1<=ai<=20000)是第i种果子的数目。
【输出文件】
输出文件fruit.out包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于231。
【样例输入】
3
1 2 9
【样例输出】
15
【数据规模】
对于30%的数据,保证有n<=1000:
对于50%的数据,保证有n<=5000;
对于全部的数据,保证有n<=10000。
此题用哈夫曼算法,每次找两堆最少的果子进行合并。可以先排序,这相当于删除掉两堆最少的果子并将其合为一堆新的果子。然后再把这堆新果子插入到有序列中。重复此操作,直到所有果子合为一堆。
参考代码
#include
using namespace std;
int a[10001]={2147483647};
void insert(int x,int y)
{
while(a[x]
}
int main()
{
int n,s=0;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
insert(i-1,a[i]);
}
for(int i=n-1;i>0;i--)
{
s+=a[i]+=a[i+1];
insert(i-1,a[i]);
}
cout<system("pause");
return 0;
}
4 二路插入
上面3个描述都是平方级计算量的描述。如同选择排序时的二路处理,如果我们对这种顺序查找的排序也进行二路处理,计算量也一定会降低。
课堂练习:编程完成二路插入升序排序。输入的第一航是数据量N及分界值M,第二行是N个待排序数据。对于小于分界值的数据进行原来的插入排序,而不小于分界值的数据从a[n]向a[1]反向插入。
这里的分界值如果将N个数据正好平分,就会得到最好的二路处理效果,但分配不够平均时,效果将打折扣。请评估把100个数据分配为下面三种格局时的计算量:
50 50
30 70
10 90
能否按最平均的分配来描述二路插入排序呢?下面以COUNT竞赛题为例,
NOIP 提高组2007
第一题:count 统计数字
输入一个数n(n<=200000)和n个自然数(每个数都不超过1.5*10^9),请统计出这些自然数各自出现的次数,按顺序从小到大输出。输入数据保证不相同的数不超过10000个。
样例输入:
8
2
4
2
4
5
100
2
100
样例输出:
2 3
4 2
5 1
100 2
下面用双向链来描述二路插入
#include
#include
using namespace std;
ifstream fin("count.in");
ofstream fout("count.out");
struct node
{
int Data,Count;
node *previous,*next;
};
void ins(node *q,int x)
{
node *p=new node;
p->Data=x;
p->Count=1;
p->previous=
q;
q->next->previous=p;
p->next=q->next;
q->next=p;
return;
}
int main()
{
int clockt=clock();
node l,m,r,*z,*t;
z=&m;
int x,n,i,b=0;
fin>>n>>m.Data;
m.Count=1;
l.Data=-1;
r.Data=1500000001;
l.next=r.previous=z;
m.previous=&l;
m.next=&r;
for(i=1;i
fin>>x;
if(x==z->Data)
z->Count++;
else if(x
{t=z;
do t=t->previous;
while(x
if(x==t->Data)
t->Count++;
else
{
ins(t,x);
b--;
}
}
else
{t=z;
do t=t->next;
while(x>t->Data);
if(x==t->Data)
t->Count++;
else
{
ins(t->previous,x);
b++;
}
}
if(b<-1)
{
z=z->previous;
b=0;
}
else if(b>1)
{
z=z->next;
b=0;
}
}
t=l.next;
while(t!=&r)
{
fout<
}
cout<<(clock()-clockt)/1000.0;
system("pause");
return 0;
}
5 用链式插入辅助哈希解决COUNT
如果把COUNT中的数据范围由0
using namespace std;
ifstream fin("count.in");
ofstream fout("count.out");
int b[15000000];
main()
{int n,a,i,t=clock();
fin>>n;
for(i=0;i
for(i=1;i<15000000;i++)
if(b[i]>0) fout<fout<<(clock()-t)/1000.0;
}
面对大范围的数据,自映射哈希法的地址空间就不够了,压缩地址空间很可能造成地址冲突,采用链地址法就是解决地址冲突的常用方法之一。
#include
#include
using namespace std;
ifstream fin("count6.in");
ofstream fout("count.out");
struct node
{
int Data,Count;
node *next;
node()
{
next=0;
}
};
node *h[150001];
int main()
{
int n,i,j,temp;
fin>>n;
node *p,*q;
for(i=0;i<=150000;i++)
h[i]=new node;
for(i=1;i<=n;i++)
{
fin>>temp;
p=h[temp/10000];
while(p->next!=0&&temp>p->next->Data)
p=p->next;
if(p->next!=0&&p->next->Data==temp)
p->next->Count++;
else
{
q=new node;
q->next=p->next;
p->next=q;
q->Data=temp;
q->Count=1;
}
}
for(i=0;i<=150000;i++)
if(h[i]->next!=0)
{
p=h[i]->next;
while(p!=0)
{
fout<
}
}
system("pause");
return 0;
}
在4和5中,插入都避免了成片搬,把“插”的代价降低到了极点,但“查”的代价仍然很大。为了降低“查”的代价,可以考虑折半查找。
6 折半查找的插入排序
仍然以COUNT为例,下面的折半插入描述时间复杂度较低。
#include
#include
using namespace
std;
ifstream fin("count.in");
ofstream fout("count.out");
struct node
{int data,count;
};
node a[10001];
int se(int l,int r,int x)
{int m;
while(r-l>1)
{m=(l+r)/2;
if (xr=m;
else
l=m;
}
return l;
}
int main(int argc, char *argv[])
{a[0].data=-1;
a[1].data=2147483647;
int n,i,x,t,d=1;
t=sizeof(node);
fin>>n;int y;
for(i=1;i<=n;i++)
{
fin>>x;
y=se(0,d,x);
if(a[y].data==x)
a[y].count++;
else
{memcpy(a+y+2,a+y+1,t*(d-y));
d++;
a[y+1].data=x;
a[y+1].count=1;
}
}
for(i=1;i
}
能否在线性结构“查”优化到log2(n)同时“插”也优化到o(1)呢?为什么?
7 在直接交换的插入排序的基础上进行多路和多轮的改进——希尔排序
原来(5 2 7 4 1 3 8 6)
看成(5)(2)(7)(4)(1)(3)(8)(6)
第一轮子序列下标差4:12745386
第二轮下标差2:12537486
第三轮下标差1:(12345678)
用它解决COUNT的参考代码
#include
using namespace std;
ifstream cin("count.in");
ofstream cout("count.out");
int a[200001];
int main()
{
int n, m, i, j,t;
cin >> n;
m = n;
for (i = 0; i < n; i++)
cin >> a[i];
while ((m /= 2) > 0)
for (i = m; i < n;a[j]=t, i++)
for (t=a[j=i]; j>=m&& ta[j]=a[j - m];
for (m=i=1;i<=n;i++)
if(a[i]==a[i-1])
m++;
else
{
cout<m=1;
}
return 0;
}
时间复杂度是o(n*log2(n))。整个过程的最后一轮就是本节1的处理过程,只是把直接交换改为循环交换而已。
为什么在一个时间复杂度较高的过程前增加了更多的操作还能降低时间复杂度呢?大跨度的反序与小跨度的反序有什么关系?
此算法的空间复杂度为N,也具有很高的实用性。
8 利用查找二叉树进行插入排序
在查找二叉树基本均匀的情况下,查找的代价接近于log2(n),而插入的代价就是o(1)。据此算法也可以用插入排序解决COUNT问题,下面给出两种递归描述的参考代码
先是有返回值的插入函数
#include
using namespace std;
typedef struct tt{
int data,sum;
struct tt *l,*r;
}node;
node* ins(node *t,int x)
{
if(t==NULL)
{
t=new node;
t->data=x;
t->sum=1;
t->l=t->r=NULL;
return t;
}
else if((t->data)>x)
{
t->l=ins(t->l,x);
return t;
}
else if((t->data)
t->r=ins(t->r,x);
return t;
}
else
{
(t->sum)++;
return t;
}
}
void output(node* t)
{
if(t!=NULL)
{
output(t->l);
cout<<(t->data)<<' '<<(t->sum)<
}
}
int main()
{
int n,i,x;
cin>>n;
node
*tree;
tree=NULL;
for(i=0;i
tree=ins(tree,x);
}
output(tree);
system("pause");
return 0;
}
再是无返回值的插入函数
#include
#include
using namespace std;
ifstream fin ("count3.in");
typedef struct tt{
int data,sum;
struct tt *l,*r;
}node;
void ins(node **t,int x)
{
if((*t)==NULL)
{
(*t)=new node;
(*t)->data=x;
(*t)->sum=1;
(*t)->l=NULL;
(*t)->r=NULL;
}
else if(((*t)->data)>x)
ins((&((*t)->l)),x);
else if(((*t)->data)
else
((**t).sum)++;
}
void output(node* t)
{
if(t!=NULL)
{
output(t->l);
cout<<(t->data)<<' '<<(t->sum)<
}
}
int main()
{
int n,i,x;
fin>>n;
node *tree;
tree=NULL;
for(i=0;i
fin>>x;
ins(&tree,x);
}
output(tree);
system("pause");
return 0;
}
课堂练习 把最后这个算法的插入函数和中序遍历函数都用非递归描述
预习
复习C++的位处理,进一步熟悉链表的常用操作
多关键字排序
合并排序
哈希法的“再散列”技术