否则,low=m+1;//插入位置在高半区
转②
⑤high+1即为待插入位置,从j-1到high+1的记录,逐个后移,r[high+1]=r[0];放置待插入记录。
【算法10.3】
voidInsertSort(S_TBL*s)
{/*对顺序表s作折半插入排序*/
for(i=2;i<=s->length;i++)
{s->elem[0]=s->elem[i];/*保存待插入元素*/
low=i;high=i-1;/*设置初始区间*/
while(low<=high)/*该循环语句完成确定插入位置*/
{mid=(low+high)/2;
if(s->elem[0].key>s->elem[mid].key)
low=mid+1;/*插入位置在高半区中*/
elsehigh=mid-1;/*插入位置在低半区中*/
}/*while*/
for(j=i-1;j>=high+1;j--)/*high+1为插入位置*/
s->elem[j+1]=s->elem[j];/*后移元素,留出插入空位*/
s->elem[high+1]=s->elem[0];/*将元素插入*/
}/*for*/
}/*InsertSort*/
【时间效率】
确定插入位置所进行的折半查找,关键码的比较次数至多为,次,移动记录的次数和直接插入排序相同,故时间复杂度仍为O(n2)。
是一个稳定的排序方法。
10.2.3表插入排序
直接插入排序、折半插入排序均要大量移动记录,时间开销大。
若要不移动记录完成排序,需要改变存储结构,进行表插入排序。
所谓表插入排序,就是通过链接指针,按关键码的大小,实现从小到大的链接过程,为此需增设一个指针项。
操作方法与直接插入排序类似,所不同的是直接插入排序要移动记录,而表插入排序是修改链接指针。
用静态链表来说明。
#defineSIZE200
typedefstruct{
ElemTypeelem;/*元素类型*/
intnext;/*指针项*/
}NodeType;/*表结点类型*/
typedefstruct{
NodeTyper[SIZE];/*静态链表*/
intlength;/*表长度*/
}L_TBL;/*静态链表类型*/
假设数据元素已存储在链表中,且0号单元作为头结点,不移动记录而只是改变链指针域,将记录按关键码建为一个有序链表。
首先,设置空的循环链表,即头结点指针域置0,并在头结点数据域中存放比所有记录关键码都大的整数。
接下来,逐个结点向链表中插入即可。
【例10.2】表插入排序示例
012345678
key域
next域
初始状态
MAXINT
49
38
65
97
76
13
27
49
0
-
-
-
-
-
-
-
-
i=1
MAXINT
49
38
65
97
76
13
27
49
1
0
-
-
-
-
-
-
-
i=2
MAXINT
49
38
65
97
76
13
27
49
2
0
1
-
-
-
-
-
-
i=3
MAXINT
49
38
65
97
76
13
27
49
2
3
1
0
-
-
-
-
-
i=4
MAXINT
49
38
65
97
76
13
27
49
2
3
1
4
0
-
-
-
-
i=5
MAXINT
49
38
65
97
76
13
27
49
2
3
1
5
0
4
-
-
-
i=6
MAXINT
49
38
65
97
76
13
27
49
6
3
1
5
0
4
2
-
-
i=7
MAXINT
49
38
65
97
76
13
27
49
6
3
1
5
0
4
7
2
-
i=8
MAXINT
49
38
65
97
76
13
27
49
6
8
1
5
0
4
7
2
3
图10.1
表插入排序得到一个有序的链表,查找则只能进行顺序查找,而不能进行随机查找,如折半查找。
为此,还需要对记录进行重排。
重排记录方法:
按链表顺序扫描各结点,将第i个结点中的数据元素调整到数组的第i个分量数据域。
因为第i个结点可能是数组的第j个分量,数据元素调整仅需将两个数组分量中数据元素交换即可,但为了能对所有数据元素进行正常调整,指针域也需处理。
【算法10.3】
1.j=l->r[0].next;i=1;//指向第一个记录位置,从第一个记录开始调整
2.若i=l->length时,调整结束;否则,
a.若i=j,j=l->r[j].next;i++;转
(2)//数据元素应在这分量中,不用调整,处理下一个结点
b.若j>i,l->r[i].elem<-->l->r[j].elem;//交换数据元素
p=l->r[j].next;//保存下一个结点地址
l->r[j].next=l->[i].next;l->[i].next=j;//保持后续链表不被中断
j=p;i++;转
(2)//指向下一个处理的结点
c.若jr[j].next;//j分量中原记录已移走,沿j的指针域找寻原记录的位置
转到(a)
【例10.3】对表插入排序结果进行重排示例
012345678
key域
next域
初始状态
MAXINT
49
38
65
97
76
13
27
49
6
8
1
5
0
4
7
2
3
i=1
j=6
MAXINT
13
38
65
97
76
49
27
49
6
(6)
1
5
0
4
8
2
3
i=2
j=7
MAXINT
13
27
65
97
76
49
38
49
6
(6)
(7)
5
0
4
8
1
3
i=3
j=
(2),7
MAXINT
13
27
38
97
76
49
65
49
6
(6)
(7)
(7)
0
4
8
5
3
i=4
j=
(1),6
MAXINT
13
27
38
49
76
97
65
49
6
(6)
(7)
(7)
(6)
4
0
5
3
i=5
j=8
MAXINT
13
27
38
49
49
97
65
76
6
(6)
(7)
(7)
(6)
(8)
0
5
4
i=6
j=(3),7
MAXINT
13
27
38
49
49
65
97
76
6
(6)
(7)
(7)
(6)
(8)
(7)
0
4
i=7
j=(5),8
MAXINT
13
27
38
49
49
65
76
97
6
(6)
(7)
(7)
(6)
(8)
(7)
(8)
0
图10.2
【时效分析】
表插入排序的基本操作是将一个记录插入到已排好序的有序链表中,设有序表长度为i,则需要比较至多i+1次,修改指针两次。
因此,总比较次数与直接插入排序相同,修改指针总次数为2n次。
所以,时间复杂度仍为O(n2)
10.2.4希尔排序(Shell’sSort)
希尔排序又称缩小增量排序,是1959年由D.L.Shell提出来的,较前述几种插入排序方法有较大的改进。
直接插入排序算法简单,在n值较小时,效率比较高,在n值很大时,若序列按关键码基本有序,效率依然较高,其时间效率可提高到O(n)。
希尔排序即是从这两点出发,给出插入排序的改进方法。
希尔排序方法:
1.选择一个步长序列t1,t2,…,tk,其中ti>tj,tk=1;
2.按步长序列个数k,对序列进行k趟排序;
3.每趟排序,根据对应的步长ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。
仅步长因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。
【例10.4】待排序列为39,80,76,41,13,29,50,78,30,11,100,7,41,86。
步长因子分别取5、3、1,则排序过程如下:
p=53980764113295078301110074186
└─────────┴─────────┘
└─────────┴──────────┘
└─────────┴──────────┘
└─────────┴──────────┘
└─────────┘
子序列分别为{39,29,100},{80,50,7},{76,78,41},{41,30,86},{13,11}。
第一趟排序结果:
p=32974130113950764113100807886
└─────┴─────┴─────┴──────┘
└─────┴─────┴─────┴──────┘
└─────┴─────┴──────┘
子序列分别为{29,30,50,13,78},{7,11,76,100,86},{41,39,41,80}。
第二趟排序结果:
p=11373929114130764150868078100
此时,序列基本“有序”,对其进行直接插入排序,得到最终结果:
7111329303941415076788086100
图10.3
【算法10.5】
voidShellInsert(S_TBL&p,intdk)
{/*一趟增量为dk的插入排序,dk为步长因子*/
for(i=dk+1;i<=p->length;i++)
if(p->elem[i].keyelem[i-dk].key)/*小于时,需elem[i]将插入有序表*/
{p->elem[0]=p->elem[i];/*为统一算法设置监测*/
for(j=i-dk;j>0&&p->elem[0].keyelem[j].key;j=j-dk)
p->elem[j+dk]=p->elem[j];/*记录后移*/
p->elem[j+dk]=p->elem[0];/*插入到正确位置*/
}
}
voidShellSort(S_TBL*p,intdlta[],intt)
{/*按增量序列dlta[0,1…,t-1]对顺序表*p作希尔排序*/
for(k=0;kShellSort(p,dlta[k]);/*一趟增量为dlta[k]的插入排序*/
}
【时效分析】
希尔排序时效分析很难,关键码的比较次数与记录移动次数依赖于步长因子序列的选取,特定情况下可以准确估算出关键码的比较次数和记录的移动次数。
目前还没有人给出选取最好的步长因子序列的方法。
步长因子序列可以有各种取法,有取奇数的,也有取质数的,但需要注意:
步长因子中除1外没有公因子,且最后一个步长因子必须为1。
希尔排序方法是一个不稳定的排序方法。
10.3交换排序
交换排序主要是通过两两比较待排记录的关键码,若发生与排序要求相逆,则交换之。
10.3.1冒泡排序(BubbleSort)
先来看看待排序列一趟冒泡的过程:
设1通过两两比较、交换,重新安排存放顺序,使得r[j]是序列中关键码最大的记录。
一趟冒泡方法为:
①i=1;//设置从第一个记录开始进行两两比较
②若i≥j,一趟冒泡结束。
③比较r[i].key与r[i+1].key,若r[i].key≤r[i+1].key,不交换,转⑤
④当r[i].key>r[i+1].key时,r[0]=r[i];r[i]=r[i+1];r[i+1]=r[0];
将r[i]与r[i+1]交换
⑤i=i+1;调整对下两个记录进行两两比较,转②
冒泡排序方法:
对n个记录的表,第一趟冒泡得到一个关键码最大的记录r[n],第二趟冒泡对n-1个记录的表,再得到一个关键码最大的记录r[n-1],如此重复,直到n个记录按关键码有序的表。
【算法10.6】
①j=n;//从n记录的表开始
②若j<2,排序结束
③i=1;//一趟冒泡,设置从第一个记录开始进行两两比较,
④若i≥j,一趟冒泡结束,j=j-1;冒泡表的记录数-1,转②
⑤比较r[i].key与r[i+1].key,若r[i].key≤r[i+1].key,不交换,转⑤
⑥当r[i].key>r[i+1].key时,r[i]<-->r[i+1];将r[i]与r[i+1]交换
⑦i=i+1;调整对下两个记录进行两两比较,转④
【效率分析】
空间效率:
仅用了一个辅助单元。
时间效率:
总共要进行n-1趟冒泡,对j个记录的表进行一趟冒泡需要j-1次关键码比较。
移动次数:
最好情况下:
待排序列已有序,不需移动。
10.3.2快速排序
快速排序是通过比较关键码、交换记录,以某个记录为界(该记录称为支点),将待排序列分成两部分。
其中,一部分所有记录的关键码大于等于支点记录的关键码,另一部分所有记录的关键码小于支点记录的关键码。
我们将待排序列按关键码以支点记录分成两部分的过程,称为一次划分。
对各部分不断划分,直到整个序列按关键码有序。
一次划分方法:
设1≤p①low=p;high=q;//设置两个搜索指针,low是向后搜索指针,high是向前搜索指针
r[0]=r[low];//取第一个记录为支点记录,low位置暂设为支点空位
②若low=high,支点空位确定,即为low。
r[low]=r[0];//填入支点记录,一次划分结束
否则,low③若lowhigh=high-1;转③//寻找r[high].keyr[low]=r[high];//找到r[high].key//小于支点记录关键码的记录前移。
④若lowlow=low+1;转④//寻找r[low].key≥r[0].key
r[high]=r[low];//找到r[low].key≥r[0].key,设置low为新支点位置,
//大于等于支点记录关键码的记录后移。
转②//继续寻找支点空位
【算法10.7】
intPartition(S_TBL*tbl,intlow,inthigh)/*一趟快排序*/
{/*交换顺序表tbl中子表tbl->[low…high]的记录,使支点记录到位,并反回其所在位置*/
/*此时,在它之前(后)的记录均不大(小)于它*/
tbl->r[0]=tbl->r[low];/*以子表的第一个记录作为支点记录*/
pivotkey=tbl->r[low].key;/*取支点记录关键码*/
while(low{while(lowr[high].key>=pivotkey)high--;
tbl->r[low]=tbl->r[high];/*将比支点记录小的交换到低端*/
while(lowr[high].key<=pivotkey)low++;
tbl->r[low]=tbl->r[high];/*将比支点记录大的交换到低端*/
}
tbl->r[low]=tbl->r[0];/*支点记录到位*/
returnlow;/*反回支点记录所在位置*/
}
【例1