题目
编码实现:将链表B借在链表A的后面。要求时间复杂度为O(1)。
题意分析
题目只说链表,并未说什么链表。言外之意,需要你来定义一种合适的链表结构。撇开代码的具体实现细节,这个考察你设计数据结构的能力。
数据结构的设计
这题用的是:循环单链表!别问我怎么想到用循环单链表,我想不到(?),是老师说的。
【单链表】
我们肯定先想到我们学过的最简单的:单链表!
我们学过的单链表结构如下图。设置单链表的结构时,增设了一个数据域为空的头节点(原因看书或者百度)。first指向头节点,故first标识一个单链表。
假设使用这种结构来做题目的操作,显然时间复杂度为O(n)。要将单链表B接到单链表A的后面,得先找到单链表A的终端节点!这需要从头节点开始遍历整个单链表A。
【循环单链表】
先来看这种循环单链表的结构
这种结构是在单链表的基础上稍加改造得来的,仍用头指针first去标识,这样有什么缺点呢?(见图中文字)
为了规避这种缺点,只需要再稍加改造即可。
编码实现
【连接前】
【连接后】
对比前后的示意图,想一下如何连?
尝试一下
rb->next = ra->next;
ra->next = ? //找不到单链表B的头节点
所以在“连”之前必须:先保存下单链表B的头指针(有点类似交换两个数的操作)。保存单链表A的头指针行不行?不行!原因后面说。
p = rb->next; //保存下单链表B的头指针
rb->next = ra->next; //是不是有点像:学习C语言时交换两个数的操作?
ra->next = p->next; //注意单链表B的头节点是不要的!
对照上述代码,自己画一下示意图。执行完上述3行代码,似乎已经完成连接操作了,但其实不然。如果只是到这里,那么一开始保存单链表A的头指针也是可以的。那为什么我之前说一开始只能保存单链表B的头指针呢?
先上这道题的完整代码(若有错误大神轻喷),边看代码边思考会好一点
【完整代码】
#include<iostream>
#include<cstdio>
using namespace std;
//节点的定义。为了防止权限的限制导致使用不便,就直接将节点定义成结构体了
template<typename T>
struct Node
{
T data;
Node<T>* next;
};
template<typename T>
class CLinkList
{
public:
CLinkList(); //无参构造
CLinkList(T a[], int n); //建立n个元素的单链表
void connect(CLinkList<T> &B);
//其他方法由于本题不需要,就略掉
void print();
private:
Node<T>* rear; //头指针。标识一个单链表
};
template<typename T>
CLinkList<T>::CLinkList()
{
rear=new Node<T>;
rear->next=rear;
}
template<typename T>
CLinkList<T>::CLinkList(T a[], int n)
{
rear=new Node<T>;
rear->next=rear;
//尾插法
Node<T>* p=NULL; //指向要插入的节点d。初始化为NULL
for(int i=0;i<n;i++)
{
p=new Node<T>;
p->data=a[i];
p->next=rear->next;
rear->next=p;
rear=p;
}
}
template<typename T>
void CLinkList<T>::connect(CLinkList<T> &B)
{
Node<T>* p=B.rear->next; //保存下链表B的表头地址
B.rear->next=rear->next; //链表B表尾节点的指针域指向当前链表(相当于A)的表头
rear->next=p->next; //当前链表表尾节点的指针域指向链表B的起始元素(注意不是头节点)
rear=B.rear; //连接完成,修改新链表的表尾指针
B.rear=p->next=p; //将链表B置为空的循环单链表(跟无参构造一样),缺少这一步,那么对象B和对象A都标识同一条链表
}
template<typename T>
void CLinkList<T>::print()
{
Node<T>* first=rear->next;
Node<T>* p=first->next;
while(p!=first)
{
cout<<p->data<<" ";
p=p->next;
}
cout<<endl;
}
int main()
{
int a[]={1,2,3,4,5};
int b[]={6,7,8,9,10};
//CLinkList<int> A=CLinkList<int>();
CLinkList<int> A=CLinkList<int>(a, 5);
A.print();
CLinkList<int> B=CLinkList<int>(b, 5);
B.print();
A.connect(B);
A.print();
B.print();
return 0;
}
假设现在将循环单链表定义成类,由于尾指针可以标识一个单循环链表,故成员参数只有一个尾指针rear。这个类有一个方法:传递一个该类对象B,可以将B接在当前对象的后面。
那么以A为主体,调用该方法,传递参数B。执行完上面3行代码所表示的操作,得到一个新的循环单链表A,这新的A是将B接到原本的A上得到的(不妨将新的A表示为A+B)。问题来了。链表B还存在吗?它的内存并没有释放,它在内存中是存在的,但另一方面B又跟A融为一体从而成为了新的A。如果对象B里面的rear不改动的话,那么对象B里面的rear和对象A里面的rear标识的是同一个单链表(A+B)。所以对象B里面的rear就需要改动!不能让别人通过对象B里面的rear访问到原来链表B的内容。
总结一下,指针这种东西,尽管总体思路有了,但在代码的具体实现细节还是很容易出错的,特别是一些循环的边界处理和特殊情况。