一道关于链表的面试题:时间复杂度O(1)内将链表B借在链表A的后面

题目

编码实现:将链表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的内容。

总结一下,指针这种东西,尽管总体思路有了,但在代码的具体实现细节还是很容易出错的,特别是一些循环的边界处理和特殊情况。

 

 

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值