本文将以排序算法中的插入排序为例,介绍优化算法,编制高效程序的方法。
人们通常用于排序手中桥牌的方法是一次考虑一张牌,将它插入到已经排序过的牌的适当位置中(时刻让它们保持有序)。在计算机实现中,我们需要将较大的元素移到右边,为插入的元素准备空间,然后再在空位置上插入该元素。该算法的通常的一个实现如下。
#include <stdio.h>
#include <stdlib.h>
typedef int Item;
#define key(A) (A)
#define less(A,B) (key(A)<key(B))
#define exch(A,B) {Item t=A;A=B;B=t;}
#define compexch(A,B) if(less(B,A)) exch(A,B)
void sort(Item a[],int l,int r)
{
int i,j;
for(i=l+1;i<=r;i++)
for(j=i;j>l;j--)
compexch(a[j-1],a[j]);
}
int main(int argc,char *argv[])
{
using namespace std;
//int i,N=atoi(argv[1]),sw=atoi(argv[2]);
int i,N=5000,sw=1;
int *a=(int *)malloc(N*sizeof(int));
if(sw)
for(i=0;i<N;i++)
a[i]=1000*(1.0*rand()/RAND_MAX);
/*else
while(scanf("%d",&a[N])==1)
N++;*/
sort(a,0,N-1);
for(i=0;i<N;i++)
printf("%3d ",a[i]);
printf("/n");
return 0;
}
上面插入排序的实现简单,但有效。我们将考虑三种方式来改进它,说明我们对许多实现的循环研究方式:我们希望代码简洁,清晰而且高效,但这些目标有时是矛盾的,所以,我们必须经常寻求一种折衷的方案。我们通过开发一个自然对应的实现,然后通过一系列转换,检查每种算法的有效性(和正确性)以寻求对它的改进。
首先,当我们碰到的键不大于正被插入项的键时,停止compexch运算,因为左边的子数组已经排序好了。特别是,在上面的程序中,当条件less(a[j-1],a[j])为真时,我们可以跳出for循环。这种修改将实现变成一种适应性排序,而且对于随即排序的键,程序加速约2倍。
通过前一段中描述的改进,中止内循环的条件有两个----可以将它重新用while循环来编码,以明确反映这一事实。实现的一个较细微的改进的根据是j>l的测试通常是多余的。实际上,仅在插入元素是目前看到的最小元素,而且到达了数组的起始处时,它才成功。一种常用的替换方案是让键在a[1]到a[N]中保持有序,并在a[0]中放入一个标记键(sentinel key),让它至少与数组中最小键相同。然后,测试是否碰到了较小键的同时测试这两个条件,让内循环更小,让程序更快。
标记有时不方便实用:也许最小的可能键不容易定义,或者调用程序没有空间包含额外的键。下面的改进版本演示了解决插入排序中这两个问题的一种方式。第一遍遍历数组时,将具有最小键的项放在第一个位置。然后,排序数组其余部分,让首位且最小的项作为标记。我们一般在代码中避免使用标记,因为使用明确的测试通常更容易理解。但我们也要注意到标记可以发挥作用,让程序更简单,更有效。
我们将要进行的第三个改进也包含删除内循环的过多指令。它的依据是对同一个元素的连续交换效率不高。如果进行两次或更多次的交换:
t=a[j];a[j]=a[j-1];a[j-1]=t;
接下来是
t=a[j-1];a[j-1]=a[j-2];a[j-2]=t;
依此类推。t的值在这两个序列间没有改变,在下一次交换时先保存,再重新载入t值是浪费时间。我们可以将较大元素右移一个位置,而不使用交换,避免上述方式的时间浪费。
改进的插入排序实现如下:
void insertion(Item a[],int l,int r)
{
int i;
for(i=l+1;i<=r;i++)
compexch(a[l],a[i]);
for(i=l+2;i<=r;i++)
{
int j=i;
Item v=a[i];
while(less(v,a[j-1]))
{
a[j]=a[j-1];
j--;
}
a[j]=v;
}
}
在本文中,我们对雅致,高效的算法以及他们的雅致,高效的实现感兴趣。充分理解算法的性质是编制可以在应用中高效运用的实现的最好向导。
为了测试两个算法的效果,我们使用如下代码:
LARGE_INTEGER liTemp;
LONGLONG QPart1,QPart2;
double dfMinus,dfFreq,dfTim;
::QueryPerformanceFrequency(&liTemp);
dfFreq=(double)liTemp.QuadPart;
::QueryPerformanceCounter(&liTemp);
QPart1=(double)liTemp.QuadPart;
// do the task
sort(a,0,N-1);
// the task has end
::QueryPerformanceCounter(&liTemp);
QPart2=(double)liTemp.QuadPart;
dfMinus=(double)QPart2-QPart1;
dfTim=dfMinus/dfFreq;
在两个注释之间的是我们要测试的函数,当我们用两个算法实现对5000个随机产生的int型数进行排序时,第一个实现用时0.115930秒,第二个实现用时0.042093秒。测试平台为:赛扬2.66GHz,512MB内存,Windows XP操作系统,VC++ 6.0开发环境。
人们通常用于排序手中桥牌的方法是一次考虑一张牌,将它插入到已经排序过的牌的适当位置中(时刻让它们保持有序)。在计算机实现中,我们需要将较大的元素移到右边,为插入的元素准备空间,然后再在空位置上插入该元素。该算法的通常的一个实现如下。
#include <stdio.h>
#include <stdlib.h>
typedef int Item;
#define key(A) (A)
#define less(A,B) (key(A)<key(B))
#define exch(A,B) {Item t=A;A=B;B=t;}
#define compexch(A,B) if(less(B,A)) exch(A,B)
void sort(Item a[],int l,int r)
{
int i,j;
for(i=l+1;i<=r;i++)
for(j=i;j>l;j--)
compexch(a[j-1],a[j]);
}
int main(int argc,char *argv[])
{
using namespace std;
//int i,N=atoi(argv[1]),sw=atoi(argv[2]);
int i,N=5000,sw=1;
int *a=(int *)malloc(N*sizeof(int));
if(sw)
for(i=0;i<N;i++)
a[i]=1000*(1.0*rand()/RAND_MAX);
/*else
while(scanf("%d",&a[N])==1)
N++;*/
sort(a,0,N-1);
for(i=0;i<N;i++)
printf("%3d ",a[i]);
printf("/n");
return 0;
}
上面插入排序的实现简单,但有效。我们将考虑三种方式来改进它,说明我们对许多实现的循环研究方式:我们希望代码简洁,清晰而且高效,但这些目标有时是矛盾的,所以,我们必须经常寻求一种折衷的方案。我们通过开发一个自然对应的实现,然后通过一系列转换,检查每种算法的有效性(和正确性)以寻求对它的改进。
首先,当我们碰到的键不大于正被插入项的键时,停止compexch运算,因为左边的子数组已经排序好了。特别是,在上面的程序中,当条件less(a[j-1],a[j])为真时,我们可以跳出for循环。这种修改将实现变成一种适应性排序,而且对于随即排序的键,程序加速约2倍。
通过前一段中描述的改进,中止内循环的条件有两个----可以将它重新用while循环来编码,以明确反映这一事实。实现的一个较细微的改进的根据是j>l的测试通常是多余的。实际上,仅在插入元素是目前看到的最小元素,而且到达了数组的起始处时,它才成功。一种常用的替换方案是让键在a[1]到a[N]中保持有序,并在a[0]中放入一个标记键(sentinel key),让它至少与数组中最小键相同。然后,测试是否碰到了较小键的同时测试这两个条件,让内循环更小,让程序更快。
标记有时不方便实用:也许最小的可能键不容易定义,或者调用程序没有空间包含额外的键。下面的改进版本演示了解决插入排序中这两个问题的一种方式。第一遍遍历数组时,将具有最小键的项放在第一个位置。然后,排序数组其余部分,让首位且最小的项作为标记。我们一般在代码中避免使用标记,因为使用明确的测试通常更容易理解。但我们也要注意到标记可以发挥作用,让程序更简单,更有效。
我们将要进行的第三个改进也包含删除内循环的过多指令。它的依据是对同一个元素的连续交换效率不高。如果进行两次或更多次的交换:
t=a[j];a[j]=a[j-1];a[j-1]=t;
接下来是
t=a[j-1];a[j-1]=a[j-2];a[j-2]=t;
依此类推。t的值在这两个序列间没有改变,在下一次交换时先保存,再重新载入t值是浪费时间。我们可以将较大元素右移一个位置,而不使用交换,避免上述方式的时间浪费。
改进的插入排序实现如下:
void insertion(Item a[],int l,int r)
{
int i;
for(i=l+1;i<=r;i++)
compexch(a[l],a[i]);
for(i=l+2;i<=r;i++)
{
int j=i;
Item v=a[i];
while(less(v,a[j-1]))
{
a[j]=a[j-1];
j--;
}
a[j]=v;
}
}
在本文中,我们对雅致,高效的算法以及他们的雅致,高效的实现感兴趣。充分理解算法的性质是编制可以在应用中高效运用的实现的最好向导。
为了测试两个算法的效果,我们使用如下代码:
LARGE_INTEGER liTemp;
LONGLONG QPart1,QPart2;
double dfMinus,dfFreq,dfTim;
::QueryPerformanceFrequency(&liTemp);
dfFreq=(double)liTemp.QuadPart;
::QueryPerformanceCounter(&liTemp);
QPart1=(double)liTemp.QuadPart;
// do the task
sort(a,0,N-1);
// the task has end
::QueryPerformanceCounter(&liTemp);
QPart2=(double)liTemp.QuadPart;
dfMinus=(double)QPart2-QPart1;
dfTim=dfMinus/dfFreq;
在两个注释之间的是我们要测试的函数,当我们用两个算法实现对5000个随机产生的int型数进行排序时,第一个实现用时0.115930秒,第二个实现用时0.042093秒。测试平台为:赛扬2.66GHz,512MB内存,Windows XP操作系统,VC++ 6.0开发环境。