第二章 链表
如何准备
链表是面试中非常常见的问题。问题难度有难有易。无论题目如何,我们都建议你先练熟简单题目。掌握了简单链表题,面对那些难题的时就会轻松一点。所以下面列出了一些链表题中必备的代码。
创建一个链表:
注意:在面试的时候写链表,一定要先问清楚是写单链表还是双链表!
//逆序建立一个链表
void CreateList_L(LinkList &L , int n)
{//逆位序输入n个元素的值,建立带头结点的单链表
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;//先建立一个带头结点的单链表
for(int i=n;i>0;i--)
{
P=(LinkList)malloc(sizeof(LNode));
scanf(p->data);
p->next=L->next;
L->next=p;//插入到表头
}
}
在单链表中删除一个节点
//逆序建立一个链表
Status ListDelete_L(LinkList &L , int i, ElemType &e)
{
LinkList p,q;
int j=0;
while (p->next && j<i-1)
{
p=p->next;
j++;
}
if (!(p->next) || j>i-1) //删除位置不合理
return ERROR;
q=p->next;
p->next=q->next;
e=q->data;
free(q);
return OK;
}
2.1从一个未排序的链表中删除重复元素
进阶:如果不能使用额外的空间怎么办?
2.1解答:
如果可以使用哈希表的话,那么检查每一个元素是否重复,然后移除。
如果不能额外使用空间的话,我们就需要两个指针来遍历数组:current用来正常的遍历;runner则用来检查元素是否重复。runner只为一个元素检查一次是否重复,因为每一次runner会删除和元素的所有重复元素。
//一个带头节点的单链表
void deleteDups(LinkList &L)
{
if (NULL==L->next) return;//空链表
LinkList pre;
pre=L->next;//pre指向第一个结点
if(NULL==pre->next) return;//只有一个结点
LinkList cur;
cur=pre->next;
while (cur!=NULL)
{
LinkList runner=L->next;
while (runner!=cur)
{
if (runner->data==cur->data)//出现重复
{
pre->next=cur->next;
cur=pre->next;
break;
}
if (runner==cur)//没有出现重复,更新指针
{
pre=cur;
cur=cur->next;
}
}
}
}
2.2 实现算法查找单链表中的倒数第n个元素。
注意:这个问题强烈的暗示递归算法,但是这里才采用一种更巧妙的方法。像这样类似的问题就,一定要多思考看看。能不能用非递归的方法代替递归的方法。倒数第一个元素是最后一个元素。
通过一次遍历找到单链表中倒数第n个节点,链表可能相当大,可使用辅助空间,但是辅助空间的数目必须固定,不能和n有关。
单向链表的特点是遍历到末尾后不能反向重数n个节点。因此必须在到达尾部的同时找到倒数第n个节点。
不管是顺数n个还是倒数n个,其实都是距离-标尺问题。标尺是一段距离可以用线段的两个端点来衡量,我们能够判断倒数第一个节点,因为他的next==NULL。如果我们用两个指针,并保持他们的距离为n,那么当这个线段的右端指向末尾节点时,左端节点就指向倒数第n个节点。
2.2解答:
这里我们假设链表的长度至少为n。
算法描述:
(1) 创建两个指针p1和p2,均指向链表第一个结点。
(2) 让p2增加n-1次,使得p2指向第n个结点(使得p1和p2之间距离为n)
(3) 若p2->next为空,则返回p1;不为空则同时增加p1,p2。(如果p2->next为空,则表示p1所指的元素为倒数第n个,因为p1,p2的距离为n。)
(4) 重复(3)
LinkList nthToLast(LinkList L,int n)
{
if (NULL==L->next || n<1) return NULL;//空链表
LinkList pre;
pre=L->next;//pre指向第一个结点
LinkList cur;
cur=pre;
for (int i=0;i<n-1;i++)
{
if (NULL==cur)
{
return NULL;//没有n个结点
}
cur=cur->next;
}
while(cur->next!=NULL)
{
cur=cur->next;
pre=pre->next;
}
return pre;
}
类似题目:一次遍历单链表找到中间结点
思路类似,设置两个指针,一个走2步时,另一个走1步,那么一个走到最后一个结点是,另一个走到中间结点
Code:
LinkListfindMidNote(LinkList L)
{
if (NULL==L->next) return NULL;//空链表
LinkList pre;
pre=L->next;//pre指向第一个结点
LinkList cur;
cur=pre;
while(cur)
{
cur=cur->next;
if(cur)
{
cur=cur->next;
pre=pre->next;
}
}
return pre;
}
2.3实现一个算法,只给你链表中间的一个元素(没有链表头),将其从链表中删除。
例如:
输入:节点 c (原链表为 a->b->c->d->e)
输出:没有任何返回值。但链表变成 a->b->d->e
2.3解答:(编程之美也有同样的题目)
本题的解答只是把输入的下一个元素拷贝到输入的这个元素中以完成删除输入的元素。注意:这样的方法并不能删除链表的最后一个元素。这一点需要和你的面试官说清楚。算法有缺陷没有关系,大胆的告诉你的面试官,他们喜欢看到你提出这些。至于怎么解决可以和你的面试官讨论。
bool deleteNode(LinkList pcur)
{
Assert(pcur!=NULL);//pcur不为空
LinkList pnext=pcur->next;
if (pnext!=NULL)
{
pcur->next=pnext->next;
pcur->data=pnext->data;
delete pnext;
return true;
}
return false;
}
2.4两个用链表表示的数字,最低位在表头。写一个函数对两个这样的链表求和,采用相同的形式返回结果。
例如: (3 -> 1 -> 5) + (5 -> 9 ->2) 返回 8 -> 0 -> 8
2.4解答:
本题可以采用递归的方式逐位的做加法。算法流程:
//不带头结点链表
LinkList LinkListAdd(LinkList L1,LinkList L2,int carry)
{
if(NULL==L1 && NULL==L2 && carry==0) return NULL;
//LinkList addlist=new LinkList(carry,NULL);
LinkList addlist=(LinkList)malloc(sizeof(LinkListNode));
addlist->data=carry;
addlist->next=NULL;
int value=carry;
if (NULL!=L1)
{
value+=L1->data;
}
if (NULL!=L2)
{
value+=L2->data;
}
addlist->data=value %10;
LinkList more=LinkListAdd(L1==NULL?NULL:L1->next,L2==NULL?NULL:L2->next,value>=10?1:0);
addlist->next=more;//more 可能为Null因此不用写也不能写more->next=NULL;
return addlist;
}
//带头结点链表,非递归,
LinkList LinkListAdd(LinkList L1,LinkList L2)
{
if(NULL==L1->next && NULL==L2->next) return NULL;
//LinkList addlist=new LinkList(carry,NULL);
L1=L1->next;
L2=L2->next;
LinkList addlist=(LinkList)malloc(sizeof(LinkListNode));//头结点
addlist->data=0;
addlist->next=NULL;
LinkList head=addlist;
int carry=0;
while(NULL!=L1 || NULL!=L2 || carry!=0)
{
LinkList p=(LinkList)malloc(sizeof(LinkListNode));
p->data=0;
p->next=NULL;
p->data+=carry;
if (NULL!=L1)
{
p->data+=L1->data;
L1=L1->next;
}
if (NULL!=L2)
{
p->data+=L2->data;
L2=L2->next;
}
carry=p->data>=10?1:0;
p->data=p->data%10;
head->next=p;
head=p;
}
return addlist;
}
2.5 给一个带有环的链表,设计算法查找这个环的起点。
环的定义:链表中的某一元素的下一个元素为之前的一个元素。
例如:输入 A -> B -> C -> D -> E -> C 输出 C
2.5 解答:
如果在链表中有两个指针,从表头开始p1以每次一个节点的速度前进,p2每次2个。那么这两个指针一定在链表的环中相遇。想想两辆车以不同的速度在一个环形跑道上运动,那么他们肯定会在跑道上再次相遇。
这个问题最难的部分就在于如何超出这个环开始的节点。假想下如果是在一个圆形的跑道上p2以两倍的速度于p1同时从起点出发,他们将会哪里相遇。还是在起点!
现在我们再假设如果p2超前起跑线k米起跑,他们第一次在哪里相遇呢?在起跑线后面k米处。(为什么?假设p2过了起跑线之后跑了x和p1相遇,那么p2跑了l-k+x,其中l为跑道的长度,至相遇时p1也跑了x。根据相遇时消耗的时间相等得方程 (l-k-x)/2=x/1,解得 x = l-k,也就是在起跑线后k处相遇。)
那重新回到问题本身,当p1指针刚进入环的时候,p2在环中恰好领先p1指正k个节点,k为链表头节点到环开始处的距离。根据之前的结论,当p1、p2相遇的时候,他们都相遇环开始节点k个距离。
那么我们得到如下结论:
(1)表头距离环开始处k节点
(2)p1,p2距离环开始处也是k个节点。
如果当p1、p2相遇之后,将p1至于表头然后两个指针都采用1的速度移动的话。那么相遇处即为环开始处。