A. 关于有序表合并操作
线性结构合并操作可以分为两类,一类是顺序表(顺序存储),另一类是链表(链式存储),二者在有序表合并时的操作并不完全相同。详细分析如下:
①顺序表:由于顺序表属于顺序存储结构,当需要插入或删除表中元素时需要移动该位置后续的各元素,所以按通常方法,应开辟一个临时的存储表,用于存放合并后的数据。如不开辟一个临时的顺序表用于存储合并后的内容,则需在每次插入时移动后续元素,时间复杂度更大。
基本操作:
i. 开辟临时存储空间,数组长度为A表长度+B表长度;
ii. 取A,B中第一个元素a0,b0进行比较:如a0小于b0,将a0放入临时数组,a0变为a1继续比较;如a0小于等于b0,将b0放入临时数组,b0变为b1继续比较。重复操作。
iii. 当A或B表到达数组尾部时结束循环。
iv. 当A未到数组尾部,则将剩余部分放入临时数组;当B未到数组尾部,则将剩余部分放入临时数组。上述二个操作最后只会执行其一。
v. 将临时数组的值通过循环赋值给A表并返回。
具体的算法略。
②单链表:单链表的优点在于插入和删除结点时无需移动元素,因此不用开辟额外的临时存储空间用于存放最终结果,可直接修改其中一个链表以得到结果并返回。
基本操作:
i. 声明指针变量pa指向A的头结点,声明pb指向B的首元结点(这里默认链表有头结点Lnode)。
ii. 比较pa->next->data和pb->data,如pa->next->data>pb->data。那么将pb结点放在pa之后,并删除pb所指结点;如pa->next->data<pb->data,pa = pa->next。
iii. 当pa->next或pb为NULL时结束循环。
iv. 如pb不为空,则把pb剩余部分链接到pa之后,返回。
具体算法见下:
void Merger(Lnode * L, Lnode * T){
Lnode *p = L;
Lnode *psave;
Lnode *q = T->next;
Lnode *qsave;
while (p->next && q){
if (p->next->data < q->data){
p = p->next;
}
else{
qsave = q->next;
q->next = p->next;
p->next = q;
q = qsave;
p = p->next;
}
}
if (q) p->next = q;
return;
}③不同的合并规则:有时合并规则会有变化,总结后基本有两类。
i. 合并A,B并删除A,B中都存在的元素
基本思想:对于ai, bj不相等的情况操作同上;当ai == bj时则删除ai, bj,ai->ai+1,bj->bj+1。
ii. 合并有序链表A,B且仅保留A,B中都存在的元素
基本思想:对于ai, bj不相等的情况,删除二者中较小者;当ai == bj时则删除bj,ai->ai+1。
④合并要求:
应注意合并要求,比如不允许使用malloc申请存储空间, 不含相同元素。不同要求需要对算法进行一定程度上的修改。
⑤多表合并:
多表合并时类似于两表合并操作,只是每次需要判断三表中ai,bj,ck中最小者进行相应的操作。
这里放上一段算法供参考(将A,B,C表合并为A,仅保留A,B,C中都包含的元素,不允许malloc申请空间,同时释放无用的结点):
Lnode * ChooseSame3(Lnode * L, Lnode * &T, Lnode * &Q){
if (!L || !L->next || !T || !T->next || !Q || !Q->next) return NULL;
Lnode * pl = L;
Lnode * pt = T;
Lnode * pq = Q;
Lnode * plsave = NULL;
Lnode * ptsave = NULL;
Lnode * pqsave = NULL;
while (pl->next && pt->next && pq->next){
if (pl->next->data == pt->next->data && pl->next->data == pq->next->data && pl->next->next->data != pl->next->data){
pl = pl->next;
ptsave = pt->next;
pt->next = ptsave->next;
free(ptsave);
ptsave = NULL;
pqsave = pq->next;
pq->next = pqsave->next;
free(pqsave);
pqsave = NULL;
}
else if (pl->next->data == pt->next->data && pl->next->data == pq->next->data){
plsave = pl->next;
pl->next = plsave->next;
free(plsave);
plsave = NULL;
}
else{
DeleteMinData(pl, pt, pq);
}
}
while (pl->next){
plsave = pl->next;
pl->next = plsave->next;
free(plsave);
}
while (pt->next){
ptsave = pt->next;
pt->next = ptsave->next;
free(ptsave);
}
while (pq->next){
pqsave = pq->next;
pq->next = pqsave->next;
free(pqsave);
}
free(T);
free(Q);
T = Q = NULL;
return L;
}
void DeleteMinData(Lnode * &pl, Lnode * &pt, Lnode * &pq){
if (pl->next->data <= pt->next->data && pl->next->data <= pq->next->data){
Lnode * plsave = pl->next;
pl->next = plsave->next;
free(plsave);
}
else if (pt->next->data <= pl->next->data && pt->next->data <= pq->next->data){
Lnode * ptsave = pt->next;
pt->next = ptsave->next;
free(ptsave);
}
else if (pq->next->data <= ql->next->data && pq->next->data <= qt->next->data){
Lnode *pqsave = pq->next;
pq->next = pqsave->next;
free(pqsave);
}
}B. 关于链表清空操作
链表清空操作主要就是采用头插法类似的思想。
含有头结点的链表:
Lnode *p = L->next;
while (p){
L->next = p->next;
free(p);
p = L->next;
}
free(L);
L = NULL;//释放头结点关于逆置操作
逆置操作无需用malloc申请空间,直接从L->next->next处断开,采用头插法逆置。
Lnode * p = L->next->next;
L->next->next = NULL;
Lnode *psave;
while (p){
psave = p->next;
p->next = L->next;
L->next = p;
p = psave;
}C. 关于插入和删除操作
如在p的下一节点插入或删除,则按常用方法;
如删除p或在p的位置插入节点,则:
i. 删除:把p的下一节点的数据域拷贝到p节点,然后删除下一节点;
ii. 插入:在p后插入一个节点,把p节点数据域拷贝到后一节点中,再把新的数据赋给p节点。
上述两种方法只是在结果上等同于插入或删除本节点,但实际上也是插入或删除下一节点。
如在p的上一节点出插入或删除。则必须从头搜索或用双向链表。
D. 关于头结点的利弊
这里讨论三种情况。
i. 不含头结点,只有头指针。
这种情况,头指针直接指向第一个结点,最为简单,但在某些操作时需要区别对待,如尾插法插入节点时,需要判断head是否为NULL。
ii. 含头结点,且头结点与普通链表节点相同。
这种情况,头结点仅作一个空节点,带来的便利就是统一操作,尾插法或判断空表时,无需区别对待。
iii. 含头结点,且与普通链表节点不同。
这种头结点一般用于存储链表信息,如链表长度等。但在某些操作时需区别对待,同第一种情况。
E. 补充:
传参时*p和*&p的区别:
前者可以修改p所指节点的内容(包括指针域和数据域,即p->next和p->data都可修改)。
后者除了能够修改p所指节点内容外还能修改p所指的节点地址,因此当采用*&p时,从被调函数返回调用函数时,p所指结点是被调函数中p最终所指向的结点,可能被修改。如不希望修改可采用Lnode * const &p。
由于&为引用传参,可以免去一些时空开销,设计算法时应斟酌考虑。
本文详细介绍了链表的各种操作,包括有序链表的合并,无需额外空间的合并策略,链表清空操作,链表逆置,以及链表的插入和删除操作。此外,还探讨了头结点在链表中的作用和不同情况。对于合并,文章提供了针对不同合并规则的算法实现,如合并并删除重复元素和保留共同元素。
2708

被折叠的 条评论
为什么被折叠?



