链表的归并排序
简述
想一想有什么可说的呢,由于链表不支持随机访问,所以很多针对连续内存的内部排序方法在链表上不是不能使用就是效率大大降低。这里介绍的是关于动态联表的归并排序。由于归并排序的递归色彩以及合并两有序链表的算法效率高,从而使得归并排序得以在处理单链表排序上被使用。
其思想大致如下:类比于数组或者连续内存的归并排序。其递归的思路是,将整个链表一直二分,二分成一个个单记录,再反过来对他们进行执行合并有序链表算法,并层层向上,最终完成对整个链表的排序。
合并有序链表的算法与合并两个有序数组还有一个不同,即后者需要总长度的辅助空间“复制”进来。但是对于链表,只需要改变他们的指针即可。即在空间复杂度上降低了。这也是在链表排序上选择归并排序的优势之一。
另外需要提到的一个技巧,即快慢指针法,此方法利用不同遍历速度的两个指针,可以按照一个比例定位整体链表的位置。这个思想方法可以用在很多地方。
本文后面的代码一次给出如下基本函数:创建动态联表函数,合并两有序链表函数,归并单链表排序函数。
代码实例
#include<iostream>
using namespace std;
typedef struct DyNode{
int key;
DyNode *next;
}DyNode;
DyNode *creatDynamicList(int raw[], int);//创建动态链表函数
void display_Dylist(DyNode *p);//打印动态链表函数
DyNode *Dylist_Join(DyNode *, DyNode *);//合并有序链表函数
DyNode *MergeList(DyNode *plist);//归并排序单链表函数
void main(){
int list[8] = { 0, 49, 38, 65, 97, 76, 13, 27};
DyNode *dplist,*dlist;
dplist = creatDynamicList(list, 8);
display_Dylist(dplist);
cout << endl;
dlist = MergeList(dplist);
display_Dylist(dlist);
}
DyNode *creatDynamicList(int raw[], int n){
//创建动态链表
if (n == 0) return NULL;
DyNode *ptemp, *phead;
DyNode *p = new DyNode;
p->key = raw[0];
phead = p;
for (int i = 1; i<n; i++){
if (!(ptemp = new DyNode)) return NULL;//考虑下分配失败的问题。
p->next = ptemp;
ptemp->key = raw[i];
p = ptemp;
}
p->next = NULL;
return phead;
}
void display_Dylist(DyNode *p){
//打印动态链表
while (p){
cout << p->key << ' ';
p = p->next;
}
}
DyNode *Dylist_Join(DyNode *pa, DyNode *pb){
//合并两个有序链表函数。可以单独使用。
if (pa == NULL && pb == NULL) return NULL;
DyNode *nlist = new DyNode;//合并链表,不需要等长的存储空间。只需要开辟一个临时的节点。
DyNode *pc = nlist;//使临时指针指向所创建的节点。
//如下是经典的三段有序合并过程
while (pa != NULL && pb != NULL){
if (pa->key < pb->key){
pc->next = pa;
pc = pa;//不同于数组,不需要复制和移动,只需要改动指针即可
pa = pa->next;
}
else{
pc->next = pb;
pc = pb;
pb = pb->next;
}
}
if (pa != NULL) pc->next = pa;
if (pb != NULL) pc->next = pb;
pc = nlist->next;//创建后,将pc作为新有序链表的头,跳过临时的节点。
delete nlist;//回收临时节点。
nlist = NULL;//释放内存后需要将指针置空,好习惯。
return pc;
}
DyNode *MergeList(DyNode *plist){
//归并有序链表。原理利用快慢指针来找到中间节点。并且利用此节点将链表分成两部分。进行合并。
//如下是跳出递归循环的条件。
if (plist == NULL || plist->next == NULL) return plist;
//如下是利用快慢指针找到中间指针。
DyNode *plow, *pfast;
plow = plist;
pfast = plist->next;
while (pfast->next != NULL && pfast->next->next != NULL){
plow = plow->next;
pfast = pfast->next->next;
}
//找到后,plow->next是第二部分的第一个节点。然后开始递归。
pfast = MergeList(plow->next);
plow->next = NULL;//这一步非常关键,是对第一部分的终点设定。
plow = MergeList(plist);
return Dylist_Join(plow, pfast);//将合并好的有序链表返回。
}
复杂度分析
遵循归并排序方式,其复杂度依旧在O(nlogn)量级上,但是与数组的归并排序不同,其空间复杂度不需要O(n).
总结
说一些关于代码中的问题。
1,快慢指针技巧。
2, 在创建链表的时候,如果需要,要分析一下是否能创建成功的问题,如果内存分配失败后如何处理呢?我这里只是使用了if将其情况列出来,但是未做处理。
3,避免野指针,尽可能在new出来的内存,不用之后记得收回。对于指针收回,可以delete之前判空。另外在delete后,记得将指针置空。这是一个好习惯。因为delete只是释放掉了指向的那块内存内容,但是这个指针本身的值还是有的,如果后面在不知情的情况下使用了这个指针,那么程序必然就会不可控制。